This commit is contained in:
lyswhut
2022-10-29 11:40:36 +08:00
parent e45cee6596
commit 8841951fe3
34 changed files with 0 additions and 0 deletions
@@ -0,0 +1,149 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close @close="handleCloseModal" @after-enter="$refs.dom_input.focus()" teleport="#view")
main(:class="$style.main")
h2 {{$t('play_timeout')}}
div(:class="$style.content")
div(:class="[$style.row, $style.inputGroup]")
base-input(:class="$style.input" ref="dom_input" v-model="time" type="number")
p(:class="$style.inputLabel") {{$t('play_timeout_unit')}}
div(:class="$style.row")
base-checkbox(id="play_timeout_end" :modelValue="appSetting['player.waitPlayEndStop']" @update:modelValue="updateSetting({'player.waitPlayEndStop': $event})" :label="$t('play_timeout_end')")
div(:class="[$style.row, $style.tip, { [$style.show]: !!timeLabel }]")
p {{$t('play_timeout_tip', { time: timeLabel })}}
div(:class="$style.footer")
base-btn(:class="$style.footerBtn" @click="handleCancel") {{$t(timeLabel ? 'play_timeout_stop' : 'play_timeout_close')}}
base-btn(:class="$style.footerBtn" @click="handleConfirm") {{$t(timeLabel ? 'play_timeout_update' : 'play_timeout_confirm')}}
</template>
<script>
import { useTimeout, startTimeoutStop, stopTimeoutStop } from '@renderer/core/player/timeoutStop'
import { ref } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
const MAX_MIN = 1440
const rxp = /([1-9]\d*)/
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props, { emit }) {
const { timeLabel } = useTimeout()
const time = ref(appSetting['player.waitPlayEndStopTime'])
const handleCloseModal = () => {
emit('update:modelValue', false)
}
const handleCancel = () => {
if (timeLabel.value) {
stopTimeoutStop()
}
handleCloseModal()
}
const verify = () => {
const orgText = time.value
let text = time.value
if (rxp.test(text)) {
text = RegExp.$1
if (parseInt(text) > MAX_MIN) {
text = text.substring(0, text.length - 1)
}
} else {
text = ''
}
time.value = text
return text && orgText == text ? parseInt(text) : ''
}
const handleConfirm = () => {
let time = verify()
if (time == '') return
if (appSetting['player.waitPlayEndStopTime'] != time) updateSetting({ 'player.waitPlayEndStopTime': time })
startTimeoutStop(time * 60)
handleCloseModal()
}
return {
appSetting,
updateSetting,
timeLabel,
time,
handleCloseModal,
handleCancel,
handleConfirm,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 530px;
min-width: 280px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
}
}
.content {
padding-top: 15px;
font-size: 14px;
}
.row {
padding-top: 5px;
}
.inputGroup {
display: flex;
align-items: center;
}
.input {
flex: auto;
}
.inputLabel {
flex: none;
margin-left: 10px;
}
.tip {
visibility: hidden;
&.show {
visibility: visible;
}
}
.footer {
margin-top: 20px;
display: flex;
flex-flow: row nowrap;
}
.footerBtn {
flex: auto;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 150px;
.mixin-ellipsis-1;
+ .footerBtn {
margin-left: 15px;
}
}
.ruleLink {
.mixin-ellipsis-1;
}
</style>
@@ -0,0 +1,73 @@
<template lang="pug">
dt#about {{$t('setting__about')}}
dd
p.small
| 本软件完全免费,代码已开源,开源地址:
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#readme')") https://github.com/lyswhut/lx-music-desktop
p.small
| 最新版网盘下载地址(网盘内有Windows、MAC版):
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘地址
| &nbsp;&nbsp;密码:
span.hover(:aria-label="$t('setting__click_copy')" @click="clipboardWriteText('glqw')") glqw
p.small
| 软件的常见问题可转至:
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq')") 常见问题
p.small
strong 本软件没有客服
| ,但我们整理了一些常见的使用问题,
strong 仔细 仔细 仔细
| 地阅读常见问题后,
p.small
| 仍有问题可加企鹅群&nbsp;
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
| &nbsp;反馈
strong (为免满人,无事勿加,入群先看群公告)
| ,或到 GitHub 提交&nbsp;
span.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/issues')") issue
br
p.small
| 如果你喜欢并经常使用洛雪音乐,并想要第一时间尝鲜洛雪的新功能&nbsp;
span(style="text-decoration: line-through;") (当小白鼠)
| ,
p
| 可以加入测试企鹅群&nbsp;
span.hover(:aria-label="$t('setting__click_open')" @click="openUrl('https://qm.qq.com/cgi-bin/qm/qr?k=zR6aYosQoKb07g4FGFZdO9n9zL1dhFpE&jump_from=webapi')") 768786588
| &nbsp;,注意:测试版的功能可能会不稳定,
strong 打算潜水的勿加
br
p.small 由于软件开发的初衷仅是为了对新技术的学习与研究,因此软件直至停止维护都将会一直保持纯净。
p.small
| 你已签署本软件的&nbsp;
base-btn(min @click="handleShowPact") 许可协议
| ,协议的在线版本在&nbsp;
strong.hover.underline(:aria-label="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop#%E9%A1%B9%E7%9B%AE%E5%8D%8F%E8%AE%AE')") 这里
| &nbsp;。
br
p
small By:
| 落雪无痕
</template>
<script>
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { isShowPact } from '@renderer/store'
import { openUrl, clipboardWriteText } from '@common/utils/electron'
export default {
name: 'SettingAbout',
setup() {
const handleShowPact = () => {
isShowPact.value = true
}
return {
openUrl,
clipboardWriteText,
handleShowPact,
}
},
}
</script>
@@ -0,0 +1,380 @@
<template lang="pug">
dt#backup {{$t('setting__backup')}}
dd
h3#backup_part {{$t('setting__backup_part')}}
div
base-btn.btn.gap-left(min @click="handleImportPlayList") {{$t('setting__backup_part_import_list')}}
base-btn.btn.gap-left(min @click="handleExportPlayList") {{$t('setting__backup_part_export_list')}}
base-btn.btn.gap-left(min @click="handleImportSetting") {{$t('setting__backup_part_import_setting')}}
base-btn.btn.gap-left(min @click="handleExportSetting") {{$t('setting__backup_part_export_setting')}}
dd
h3#backup_all {{$t('setting__backup_all')}}
div
base-btn.btn.gap-left(min @click="handleImportAllData") {{$t('setting__backup_all_import')}}
base-btn.btn.gap-left(min @click="handleExportAllData") {{$t('setting__backup_all_export')}}
dd
h3#backup_other {{$t('setting__backup_other')}}
div
base-btn.btn.gap-left(min @click="handleExportPlayListToText") {{$t('setting__backup_other_export_list_text')}}
base-btn.btn.gap-left(min @click="handleExportPlayListToCsv") {{$t('setting__backup_other_export_list_csv')}}
</template>
<script>
import { toRaw } from '@common/utils/vueTools'
// import { mergeSetting } from '@common/utils'
// import { base as eventBaseName } from '@renderer/event/names'
// import { defaultList, loveList, userLists } from '@renderer/core/share/list'
import {
toNewMusicInfo,
// toOldMusicInfo,
fixNewMusicInfoQuality,
} from '@renderer/utils'
import {
showSelectDialog,
openSaveDir,
} from '@renderer/utils/ipc'
// import { currentStting } from '../setting'
import { dialog } from '@renderer/plugins/Dialog'
import useImportTip from '@renderer/utils/compositions/useImportTip'
import { useI18n } from '@renderer/plugins/i18n'
import { createUserList, getListMusics, overwriteListMusics } from '@renderer/store/list/action'
import { LIST_IDS } from '@common/constants'
import { defaultList, loveList, userLists } from '@renderer/store/list/state'
import { appSetting, updateSetting } from '@renderer/store/setting'
import migrateSetting from '@common/utils/migrateSetting'
export default {
name: 'SettingUpdate',
setup() {
const t = useI18n()
// const setting = useRefGetter('setting')
// const settingVersion = useRefGetter('settingVersion')
// const setSettingVersion = useCommit('setSettingVersion')
// const setList = useCommit('list', 'setList')
const showImportTip = useImportTip()
const getAllLists = async() => {
const lists = []
lists.push(await getListMusics(defaultList.id).then(musics => ({ ...defaultList, list: toRaw(musics) })))
lists.push(await getListMusics(loveList.id).then(musics => ({ ...loveList, list: toRaw(musics) })))
for await (const list of userLists) {
lists.push(await getListMusics(list.id).then(musics => ({ ...toRaw(list), list: toRaw(musics) })))
}
return lists
}
const importOldListData = async(lists) => {
for await (const list of lists) {
try {
if (userLists.some(l => l.id == list.id) || list.id == LIST_IDS.DEFAULT || list.id == LIST_IDS.LOVE) {
await overwriteListMusics({ listId: list.id, musicInfos: list.list.map(m => toNewMusicInfo(m)) })
} else {
await createUserList({
name: list.name,
id: list.id,
list: list.list.map(m => toNewMusicInfo(m)),
position: list.position,
source: list.source,
sourceListId: list.sourceListId,
})
}
} catch (err) {
console.log(err)
}
}
}
const importNewListData = async(lists) => {
for await (const list of lists) {
try {
if (userLists.some(l => l.id == list.id) || list.id == LIST_IDS.DEFAULT || list.id == LIST_IDS.LOVE) {
await overwriteListMusics({ listId: list.id, musicInfos: list.list.map(m => fixNewMusicInfoQuality(m)) })
} else {
await createUserList({
name: list.name,
id: list.id,
list: list.list.map(m => fixNewMusicInfoQuality(m)),
position: list.position,
source: list.source,
sourceListId: list.sourceListId,
})
}
} catch (err) {
console.log(err)
}
}
}
const importOldSettingData = (setting) => {
console.log(setting)
setting = migrateSetting(setting)
setting['common.isAgreePact'] = false
updateSetting(setting)
}
const importNewSettingData = (setting) => {
setting['common.isAgreePact'] = false
updateSetting(setting)
}
const importAllData = async(path) => {
let allData
try {
allData = await window.lx.worker.main.readLxConfigFile(path)
} catch (error) {
return
}
switch (allData.type) {
case 'allData':
// 兼容0.6.2及以前版本的列表数据
if (allData.defaultList) await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: allData.defaultList.list.map(m => toNewMusicInfo(m)) })
else await importOldListData(allData.playList)
importOldSettingData(allData.setting)
break
case 'allData_v2':
await importNewListData(allData.playList)
importNewSettingData(allData.setting)
break
default: return showImportTip(allData.type)
}
}
const handleImportAllData = () => {
showSelectDialog({
title: t('setting__backup_all_import_desc'),
properties: ['openFile'],
filters: [
{ name: 'Setting', extensions: ['json', 'lxmc'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
dialog.confirm({
message: t('setting__backup_part_import_list_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
}).then(confirm => {
if (!confirm) return
importAllData(result.filePaths[0])
})
})
}
const exportAllData = async(path) => {
let allData = {
type: 'allData_v2',
setting: { ...appSetting },
playList: await getAllLists(),
}
window.lx.worker.main.saveLxConfigFile(path, allData)
}
const handleExportAllData = () => {
openSaveDir({
title: t('setting__backup_all_export_desc'),
defaultPath: 'lx_datas_v2.lxmc',
}).then(result => {
if (result.canceled) return
exportAllData(result.filePath)
})
}
const exportSetting = (path) => {
const data = {
type: 'setting_v2',
data: { ...appSetting },
}
window.lx.worker.main.saveLxConfigFile(path, data)
}
const handleExportSetting = () => {
openSaveDir({
title: t('setting__backup_part_export_setting_desc'),
defaultPath: 'lx_setting_v2.lxmc',
}).then(result => {
if (result.canceled) return
exportSetting(result.filePath)
})
}
const importSetting = async(path) => {
let settingData
try {
settingData = await window.lx.worker.main.readLxConfigFile(path)
} catch (error) {
return
}
switch (settingData.type) {
case 'setting':
importOldSettingData(settingData.data)
break
case 'setting_v2':
importNewSettingData(settingData.data)
break
default: return showImportTip(settingData.type)
}
}
const handleImportSetting = () => {
showSelectDialog({
title: t('setting__backup_part_import_setting_desc'),
properties: ['openFile'],
filters: [
{ name: 'Setting', extensions: ['json', 'lxmc'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
importSetting(result.filePaths[0])
})
}
const exportPlayList = async(path) => {
const data = {
type: 'playList_v2',
data: await getAllLists(),
}
window.lx.worker.main.saveLxConfigFile(path, data)
}
const handleExportPlayList = () => {
openSaveDir({
title: t('setting__backup_part_export_list_desc'),
defaultPath: 'lx_list.lxmc',
}).then(result => {
if (result.canceled) return
exportPlayList(result.filePath)
})
}
const importPlayList = async(path) => {
let listData
try {
listData = await window.lx.worker.main.readLxConfigFile(path)
} catch (error) {
return
}
console.log(listData.type)
switch (listData.type) {
case 'defautlList': // 兼容0.6.2及以前版本的列表数据
await overwriteListMusics({ listId: LIST_IDS.DEFAULT, musicInfos: listData.data.list.map(m => toNewMusicInfo(m)) })
break
case 'playList':
await importOldListData(listData.data)
break
case 'playList_v2':
await importNewListData(listData.data)
break
default: return showImportTip(listData.type)
}
}
const handleImportPlayList = () => {
showSelectDialog({
title: t('setting__backup_part_import_list_desc'),
properties: ['openFile'],
filters: [
{ name: 'Play List', extensions: ['json', 'lxmc'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
dialog.confirm({
message: t('setting__backup_part_import_list_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
}).then(confirm => {
if (!confirm) return
importPlayList(result.filePaths[0])
})
})
}
const exportPlayListToText = async(savePath, isMerge) => {
const lists = await getAllLists()
await window.lx.worker.main.exportPlayListToText(savePath, lists, isMerge)
}
const handleExportPlayListToText = async() => {
const confirm = await dialog.confirm({
message: t('setting__backup_other_export_list_text_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
})
if (confirm) {
openSaveDir({
title: t('setting__backup_other_export_dir'),
defaultPath: 'lx_list_all.txt',
}).then(result => {
if (result.canceled) return
let path = result.filePath
if (!path.endsWith('.txt')) path += '.txt'
exportPlayListToText(path, true)
})
} else {
showSelectDialog({
title: t('setting__backup_other_export_dir'),
// defaultPath: currentStting.value.download.savePath,
properties: ['openDirectory'],
}).then(result => {
if (result.canceled) return
exportPlayListToText(result.filePaths[0], false)
})
}
}
const exportPlayListToCsv = async(savePath, isMerge) => {
const lists = await getAllLists()
await window.lx.worker.main.exportPlayListToCSV(savePath, lists, isMerge, `${t('music_name')},${t('music_singer')},${t('music_album')}\n`)
}
const handleExportPlayListToCsv = async() => {
const confirm = await dialog.confirm({
message: t('setting__backup_other_export_list_text_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
})
if (confirm) {
openSaveDir({
title: t('setting__backup_other_export_dir'),
defaultPath: 'lx_list_all.csv',
}).then(result => {
if (result.canceled) return
let path = result.filePath
if (!path.endsWith('.csv')) path += '.csv'
exportPlayListToCsv(path, true)
})
} else {
showSelectDialog({
title: t('setting__backup_other_export_dir'),
// defaultPath: currentStting.value.download.savePath,
properties: ['openDirectory'],
}).then(result => {
if (result.canceled) return
exportPlayListToCsv(result.filePaths[0], false)
})
}
}
// window.eventHub.on(eventBaseName.set_config, handleUpdateSetting)
// onBeforeUnmount(() => {
// window.eventHub.off(eventBaseName.set_config, handleUpdateSetting)
// })
return {
// currentStting,
handleExportPlayList,
handleImportPlayList,
handleExportSetting,
handleImportSetting,
handleExportAllData,
handleImportAllData,
handleExportPlayListToText,
handleExportPlayListToCsv,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,471 @@
<template lang="pug">
dt#basic {{$t('setting__basic')}}
dd
h3#basic_theme {{$t('setting__basic_theme')}}
div
ul(:class="$style.theme")
li(v-for="theme in defaultThemes" :key="theme.id" :aria-label="$t('theme_' + theme.id)" @click="toggleTheme(theme)" :style="theme.styles" :class="[$style.themeItem, {[$style.active]: themeId == theme.id}]")
div(:class="$style.bg")
span(:class="$style.label") {{$t('theme_' + theme.id)}}
li(v-for="theme in userThemes" :key="theme.id" :aria-label="theme.name" @click="toggleTheme(theme)" @contextmenu="handleEditTheme(theme)" :style="theme.styles" :class="[$style.themeItem, {[$style.active]: themeId == theme.id}]")
div(:class="$style.bg")
span(:class="$style.label") {{theme.name}}
li(:aria-label="$t('theme_auto_tip')" @click="handleSetThemeAuto" @contextmenu="isShowThemeSelectorModal = true" :style="autoTheme" :class="[$style.themeItem, $style.auto, {[$style.active]: themeId == 'auto'}]")
div(:class="$style.bg")
div(:class="$style.bgContent")
div(:class="$style.light")
div(:class="$style.dark")
span(:class="$style.label") {{$t('theme_auto')}}
li(:aria-label="$t('theme_add')" @click="handleEditTheme()" :class="[$style.themeItem, $style.add]")
div(:class="$style.bg")
div(:class="$style.bgContent")
svg-icon(:class="$style.icon" name="plus")
span(:class="$style.label") {{$t('theme_add')}}
dd
div
.gap-top.top
base-checkbox(id="setting_show_animate" :modelValue="appSetting['common.isShowAnimation']" @update:modelValue="updateSetting({'common.isShowAnimation': $event})" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" :modelValue="appSetting['common.randomAnimate']" @update:modelValue="updateSetting({'common.randomAnimate': $event})" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_start_in_fullscreen" :modelValue="appSetting['common.startInFullscreen']" @update:modelValue="updateSetting({'common.startInFullscreen': $event})" :label="$t('setting__basic_start_in_fullscreen')")
.gap-top
base-checkbox(id="setting_to_tray" :modelValue="appSetting['tray.enable']" @update:modelValue="updateSetting({'tray.enable': $event})" :label="$t('setting__basic_to_tray')")
p.gap-top
base-btn.btn(min @click="isShowPlayTimeoutModal = true") {{$t('setting__play_timeout')}} {{ timeLabel ? ` (${timeLabel})` : '' }}
dd
h3#basic_source {{$t('setting__basic_source')}}
div
.gap-top(v-for="item in apiSources" :key="item.id")
base-checkbox(:id="`setting_api_source_${item.id}`" name="setting_api_source"
need :modelValue="appSetting['common.apiSource']" @update:modelValue="updateSetting({'common.apiSource': $event})" :disabled="item.disabled" :value="item.id" :label="item.label")
p.gap-top
base-btn.btn(min @click="isShowUserApiModal = true") {{$t('setting__basic_source_user_api_btn')}}
dd
h3#basic_window_size {{$t('setting__basic_window_size')}}
div
base-checkbox.gap-left(v-for="(item, index) in windowSizeList" :id="`setting_window_size_${item.id}`" name="setting_window_size"
need :modelValue="appSetting['common.windowSizeId']" @update:modelValue="updateSetting({'common.windowSizeId': $event})" :disabled="isFullscreen" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
dd
h3#basic_font_size {{$t('setting__basic_font_size')}}
div
//- base-selection.gap-teft(:list="fontSizeList" :modelValue="appSetting['common.fontSize']" @update:modelValue="updateSetting({'common.fontSize': $event})")
base-checkbox.gap-left(v-for="item in fontSizeList" :key="item.id" :id="`setting_basic_font_size_${item.id}`"
name="setting_basic_font_size" need :modelValue="appSetting['common.fontSize']" @update:modelValue="updateSetting({'common.fontSize': $event})"
:value="item.id" :label="item.label" :disabled="isFullscreen")
dd
h3#basic_font {{$t('setting__basic_font')}}
div
base-selection.gap-teft(:list="fontList" :modelValue="appSetting['common.font']" @update:modelValue="updateSetting({'common.font': $event})" item-key="id" item-name="label")
dd
h3#basic_lang {{$t('setting__basic_lang')}}
div
base-checkbox.gap-left(v-for="item in langList" :key="item.locale" :id="`setting_lang_${item.locale}`" name="setting_lang"
need :modelValue="appSetting['common.langId']" @update:modelValue="updateSetting({'common.langId': $event})" :value="item.locale" :label="item.name")
dd
h3#basic_sourcename {{$t('setting__basic_sourcename')}}
div
base-checkbox.gap-left(v-for="item in sourceNameTypes" :key="item.id" :id="`setting_abasic_sourcename_${item.id}`"
name="setting_basic_sourcename" need :modelValue="appSetting['common.sourceNameType']" @update:modelValue="updateSetting({'common.sourceNameType': $event})" :value="item.id" :label="item.label")
dd
h3#basic_control_btn_position {{$t('setting__basic_control_btn_position')}}
div
base-checkbox.gap-left(v-for="item in controlBtnPositionList" :key="item.id" :id="`setting_basic_control_btn_position_${item.id}`"
name="setting_basic_control_btn_position" need :modelValue="appSetting['common.controlBtnPosition']" @update:modelValue="updateSetting({'common.controlBtnPosition': $event})" :value="item.id" :label="item.name")
ThemeSelectorModal(v-model="isShowThemeSelectorModal")
ThemeEditModal(v-model="isShowThemeEditModal" :theme-id="editThemeId" @submit="handleRefreshTheme")
play-timeout-modal(v-model="isShowPlayTimeoutModal")
user-api-modal(v-model="isShowUserApiModal")
</template>
<script>
import { computed, ref, watch, reactive, shallowReactive } from '@common/utils/vueTools'
import { windowSizeList, userApi, isFullscreen, themeId } from '@renderer/store'
import { langList, useI18n } from '@/lang'
import { getSystemFonts } from '@renderer/utils/ipc'
import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
import { useTimeout } from '@renderer/core/player/timeoutStop'
import { dialog } from '@renderer/plugins/Dialog'
import ThemeSelectorModal from './ThemeSelectorModal'
import ThemeEditModal from './ThemeEditModal'
import PlayTimeoutModal from './PlayTimeoutModal'
import UserApiModal from './UserApiModal'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { getThemes, applyTheme, findTheme } from '@renderer/store/utils'
import { joinPath } from '@common/utils/nodejs'
export default {
name: 'SettingBasic',
components: {
ThemeSelectorModal,
ThemeEditModal,
PlayTimeoutModal,
UserApiModal,
},
setup() {
const t = useI18n()
const defaultThemes = shallowReactive([])
const userThemes = shallowReactive([])
const autoTheme = reactive({})
const updateAutoTheme = (info) => {
let light = findTheme(info, appSetting['theme.lightId'])
if (!light) light = info.themes.find(theme => theme.id == 'green')
let dark = findTheme(info, appSetting['theme.darkId'])
if (!dark) dark = info.themes.find(theme => theme.id == 'black')
autoTheme['--color-primary-theme-light'] = light.config.themeColors['--color-theme']
autoTheme['--background-image-theme-light'] = light.isCustom
? light.config.extInfo['--background-image'] == 'none'
? 'none'
: `url(file:///${joinPath(info.dataPath, light.config.extInfo['--background-image']).replaceAll('\\', '/')})`
: light.config.extInfo['--background-image']
autoTheme['--color-primary-theme-dark'] = dark.config.themeColors['--color-theme']
autoTheme['--background-image-theme-dark'] = dark.isCustom
? dark.config.extInfo['--background-image'] == 'none'
? 'none'
: `url(file:///${joinPath(info.dataPath, dark.config.extInfo['--background-image']).replaceAll('\\', '/')})`
: dark.config.extInfo['--background-image']
}
let dataPath = ''
const init = () => {
getThemes((info) => {
// console.log(info)
dataPath = info.dataPath
defaultThemes.splice(0, defaultThemes.length, ...info.themes.map(t => {
return {
id: t.id,
styles: {
'--color-primary-theme': t.config.themeColors['--color-theme'],
'--background-image-theme': t.config.extInfo['--background-image'],
},
}
}))
userThemes.splice(0, userThemes.length, ...info.userThemes.map(t => {
return {
id: t.id,
name: t.name,
styles: {
'--color-primary-theme': t.config.themeColors['--color-theme'],
'--background-image-theme': t.config.extInfo['--background-image'] == 'none'
? 'none'
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`,
},
}
}))
updateAutoTheme(info)
})
}
const editThemeId = ref('')
const handleEditTheme = (theme) => {
// console.log(theme)
if (!theme && userThemes.length >= 10) {
dialog({
message: t('theme_max_tip'),
confirmButtonText: t('alert_button_text'),
})
return
}
editThemeId.value = theme ? theme.id : ''
isShowThemeEditModal.value = true
}
const handleRefreshTheme = () => {
init()
}
init()
const toggleTheme = (theme) => {
if (themeId.value == theme.id) return
themeId.value = theme.id
applyTheme(theme.id, appSetting['theme.lightId'], appSetting['theme.darkId'], dataPath)
updateSetting({ 'theme.id': theme.id })
}
watch(() => [appSetting['theme.lightId'], appSetting['theme.darkId']], () => {
getThemes(updateAutoTheme)
})
const isShowThemeSelectorModal = ref(false)
const handleSetThemeAuto = () => {
if (themeId.value == 'auto') return
if (window.localStorage.getItem('theme-auto-tip') != 'true') {
window.localStorage.setItem('theme-auto-tip', 'true')
dialog({
message: t('setting__basic_theme_auto_tip'),
confirmButtonText: t('ok'),
})
}
toggleTheme({ id: 'auto' })
}
const isShowThemeEditModal = ref(false)
const isShowPlayTimeoutModal = ref(false)
const { timeLabel } = useTimeout()
const isShowUserApiModal = ref(false)
const getApiStatus = () => {
let status
if (userApi.status) status = t('setting__basic_source_status_success')
else if (userApi.message == 'initing') status = t('setting__basic_source_status_initing')
else status = `${t('setting__basic_source_status_failed')} - ${userApi.message}`
return status
}
const apiSources = computed(() => {
return [
...apiSourceInfo.map(api => ({
id: api.id,
label: t('setting__basic_source_' + api.id) || api.name,
disabled: api.disabled,
})),
...userApi.list.map(api => ({
id: api.id,
label: `${api.name}${api.description ? `(${api.description})` : ''}${api.id == appSetting['common.apiSource'] ? `[${getApiStatus()}]` : ''}`,
status: api.status,
message: api.message,
disabled: false,
})),
]
})
const sourceNameTypes = computed(() => {
return [
{ id: 'real', label: t('setting__basic_sourcename_real') },
{ id: 'alias', label: t('setting__basic_sourcename_alias') },
]
})
const controlBtnPositionList = computed(() => {
return [
{ id: 'left', name: t('setting__basic_control_btn_position_left') },
{ id: 'right', name: t('setting__basic_control_btn_position_right') },
]
})
const systemFontList = ref([])
const fontList = computed(() => {
return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]
})
getSystemFonts().then(fonts => {
systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^"|"$)/g, '') }))
})
const fontSizeList = computed(() => {
return [
{ id: 14, label: t('setting__basic_font_size_14px') },
{ id: 15, label: t('setting__basic_font_size_15px') },
{ id: 16, label: t('setting__basic_font_size_16px') },
{ id: 17, label: t('setting__basic_font_size_17px') },
{ id: 18, label: t('setting__basic_font_size_18px') },
{ id: 19, label: t('setting__basic_font_size_19px') },
]
})
return {
appSetting,
updateSetting,
defaultThemes,
userThemes,
autoTheme,
// currentStting,
// themes,
// themeClassName,
isShowThemeSelectorModal,
isShowThemeEditModal,
handleSetThemeAuto,
isShowPlayTimeoutModal,
timeLabel,
apiSources,
isShowUserApiModal,
windowSizeList,
langList,
sourceNameTypes,
controlBtnPositionList,
fontList,
isFullscreen,
toggleTheme,
themeId,
handleRefreshTheme,
editThemeId,
handleEditTheme,
fontSizeList,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.theme {
display: flex;
flex-flow: row wrap;
// padding: 0 15px;
margin-bottom: -20px;
.themeItem {
display: flex;
flex-flow: column nowrap;
align-items: center;
cursor: pointer;
// color: var(--color-primary);
margin-right: 30px;
transition: .3s ease;
transition-property: color, opacity;
margin-bottom: 18px;
width: 56px;
&:hover {
opacity: .7;
}
&:last-child {
margin-right: 0;
}
&.active {
color: var(--color-primary-font-active);
.bg {
border-color: var(--color-primary-font-active);
}
&:hover {
opacity: 1;
}
}
.bg {
display: block;
width: 36px;
height: 36px;
margin-bottom: 5px;
border: 2Px solid transparent;
padding: 2Px;
transition: border-color .3s ease;
border-radius: 5px;
&:after {
display: block;
content: ' ';
width: 100%;
height: 100%;
border-radius: @radius-border;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--color-primary-theme);
background-image: var(--background-image-theme);
}
}
.label {
width: 100%;
text-align: center;
height: 1.2em;
.mixin-ellipsis-1;
}
&.auto {
&.active {
color: var(--color-primary-font-active);
.bg {
border-color: var(--color-primary-font-active);
}
}
>.bg {
&:after {
content: none;
}
}
.bgContent {
position: relative;
height: 100%;
overflow: hidden;
border-radius: 5px;
}
.light, .dark {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
&:after {
display: block;
content: ' ';
width: 100%;
height: 100%;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
}
}
.light {
&:after {
clip-path: polygon(0 0, 100% 0, 0 100%);
}
svg {
fill: var(--color-primary-theme-light);
}
&:after {
background-color: var(--color-primary-theme-light);
background-image: var(--background-image-theme-light);
}
}
.dark {
&:after {
clip-path: polygon(0 100%, 100% 0, 100% 100%);
}
svg {
fill: var(--color-primary-theme-dark);
}
&:after {
background-color: var(--color-primary-theme-dark);
background-image: var(--background-image-theme-dark);
}
}
}
&.add {
>.bg {
&:after {
content: none;
}
.bgContent {
transition: .3s ease;
transition-property: border, color;
box-sizing: border-box;
border: 1Px dashed var(--color-primary-light-100-alpha-300);
color: var(--color-primary-light-100-alpha-300);
position: relative;
height: 100%;
overflow: hidden;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
}
.icon {
// position: absolute;
// font-size: 16px;
width: 66%;
height: auto;
}
}
.label {
color: var(--color-primary-dark-100-alpha-300);
}
}
}
}
</style>
@@ -0,0 +1,270 @@
<template lang="pug">
dt#desktop_lyric {{$t('setting__desktop_lyric')}}
dd
.gap-top
base-checkbox(id="setting_desktop_lyric_enable" :modelValue="appSetting['desktopLyric.enable']" @update:modelValue="updateSetting({ 'desktopLyric.enable': $event })" :label="$t('setting__desktop_lyric_enable')")
.gap-top
base-checkbox(id="setting_desktop_lyric_lock" :modelValue="appSetting['desktopLyric.isLock']" @update:modelValue="updateSetting({ 'desktopLyric.isLock': $event })" :label="$t('setting__desktop_lyric_lock')")
.gap-top
base-checkbox(id="setting_desktop_lyric_audio_visualization" :modelValue="appSetting['desktopLyric.audioVisualization']" @update:modelValue="updateSetting({ 'desktopLyric.audioVisualization': $event })" :label="$t('setting__desktop_lyric_audio_visualization')")
.gap-top
base-checkbox(id="setting_desktop_lyric_delayScroll" :modelValue="appSetting['desktopLyric.isDelayScroll']" @update:modelValue="updateSetting({ 'desktopLyric.isDelayScroll': $event })" :label="$t('setting__desktop_lyric_delay_scroll')")
.gap-top
base-checkbox(id="setting_desktop_lyric_alwaysOnTop" :modelValue="appSetting['desktopLyric.isAlwaysOnTop']" @update:modelValue="updateSetting({ 'desktopLyric.isAlwaysOnTop': $event })" :label="$t('setting__desktop_lyric_always_on_top')")
.gap-top
base-checkbox(id="setting_desktop_lyric_alwaysOnTopLoop" :modelValue="appSetting['desktopLyric.isAlwaysOnTopLoop']" @update:modelValue="updateSetting({ 'desktopLyric.isAlwaysOnTopLoop': $event })" :label="$t('setting__desktop_lyric_always_on_top_loop')")
.gap-top
base-checkbox(id="setting_desktop_lyric_lockScreen" :modelValue="appSetting['desktopLyric.isLockScreen']" @update:modelValue="updateSetting({ 'desktopLyric.isLockScreen': $event })" :label="$t('setting__desktop_lyric_lock_screen')")
.gap-top(v-if="!isLinux")
base-checkbox(id="setting_desktop_lyric_hoverHide" :modelValue="appSetting['desktopLyric.isHoverHide']" @update:modelValue="updateSetting({ 'desktopLyric.isHoverHide': $event })" :label="$t('setting__desktop_lyric_hover_hide')")
.gap-top
base-checkbox(id="setting_desktop_lyric_ellipsis" :modelValue="appSetting['desktopLyric.style.ellipsis']" @update:modelValue="updateSetting({ 'desktopLyric.style.ellipsis': $event })" :label="$t('setting__desktop_lyric_ellipsis')")
.gap-top
base-checkbox(id="setting_desktop_lyric_zoom" :modelValue="appSetting['desktopLyric.style.isZoomActiveLrc']" @update:modelValue="updateSetting({ 'desktopLyric.style.isZoomActiveLrc': $event })" :label="$t('desktop_lyric__lrc_active_zoom_on')")
//- .gap-top
base-checkbox(id="setting_desktop_lyric_fontWeight" :modelValue="appSetting['desktopLyric.style.fontWeight']" @update:modelValue="updateSetting({ 'desktopLyric.style.fontWeight': $event })" :label="$t('setting__desktop_lyric_font_weight')")
dd
h3#desktop_lyric_direction {{$t('setting__desktop_lyric_direction')}}
div
base-checkbox.gap-left(id="setting_desktop_lyric_direction_horizontal" :modelValue="appSetting['desktopLyric.direction']" @update:modelValue="updateSetting({ 'desktopLyric.direction': $event })" need value="horizontal" :label="$t('setting__desktop_lyric_direction_horizontal')")
base-checkbox.gap-left(id="setting_desktop_lyric_direction_vertical" :modelValue="appSetting['desktopLyric.direction']" @update:modelValue="updateSetting({ 'desktopLyric.direction': $event })" need value="vertical" :label="$t('setting__desktop_lyric_direction_vertical')")
dd
h3#desktop_lyric_align {{$t('setting__desktop_lyric_align')}}
div
base-checkbox.gap-left(id="setting_desktop_lyric_align_left" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="left" :label="$t('setting__desktop_lyric_align_left')")
base-checkbox.gap-left(id="setting_desktop_lyric_align_center" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="center" :label="$t('setting__desktop_lyric_align_center')")
base-checkbox.gap-left(id="setting_desktop_lyric_align_right" :modelValue="appSetting['desktopLyric.style.align']" @update:modelValue="updateSetting({ 'desktopLyric.style.align': $event })" need value="right" :label="$t('setting__desktop_lyric_align_right')")
dd
h3#desktop_lyric_color {{$t('setting__desktop_lyric_color')}}
div
p.gap-top
div(:class="$style.groupContent")
div(:class="$style.item")
div(ref="lyric_unplay_color_ref", :class="$style.color")
div(:class="$style.label") {{ $t('setting__desktop_lyric_unplay_color') }}
div(:class="$style.item")
div(ref="lyric_played_color_ref", :class="$style.color")
div(:class="$style.label") {{ $t('setting__desktop_lyric_played_color') }}
div(:class="$style.item")
div(ref="lyric_shadow_color_ref", :class="$style.color")
div(:class="$style.label") {{ $t('setting__desktop_lyric_shadow_color') }}
p.gap-top
base-btn.btn(min @click="resetColor") {{$t('setting__desktop_lyric_color_reset')}}
dd
h3#desktop_lyric_font {{$t('setting__desktop_lyric_font')}}
div
base-selection.gap-teft(:list="fontList" :modelValue="appSetting['desktopLyric.style.font']" @update:modelValue="updateSetting({ 'desktopLyric.style.font': $event })" item-key="id" item-name="label")
</template>
<script>
import { ref, computed, onMounted, onBeforeUnmount } from '@common/utils/vueTools'
import { getSystemFonts } from '@renderer/utils/ipc'
import { isLinux } from '@common/utils'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { useI18n } from '@renderer/plugins/i18n'
import { pickrTools } from '@renderer/utils/pickrTools'
const useLyricUnplayColor = () => {
const lyric_unplay_color_ref = ref(null)
let tools
const initLyricUnplayColor = (color, changed, reset) => {
if (!lyric_unplay_color_ref.value) return
tools = pickrTools.create(lyric_unplay_color_ref.value, color, null, changed, reset)
}
const destroyLyricUnplayColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setLyricUnplayColor = (color) => {
tools?.setColor(color)
}
return {
lyric_unplay_color_ref,
initLyricUnplayColor,
destroyLyricUnplayColor,
setLyricUnplayColor,
}
}
const useLyricPlayedColor = () => {
const lyric_played_color_ref = ref(null)
let tools
const initLyricPlayedColor = (color, changed, reset) => {
if (!lyric_played_color_ref.value) return
tools = pickrTools.create(lyric_played_color_ref.value, color, [
'rgba(7, 197, 86, 1)',
'rgba(255, 250, 18, 1)',
'rgba(25, 181, 254, 1)',
'rgba(255, 18, 34, 1)',
], changed, reset)
}
const destroyLyricPlayedColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setLyricPlayedColor = (color) => {
tools?.setColor(color)
}
return {
lyric_played_color_ref,
initLyricPlayedColor,
destroyLyricPlayedColor,
setLyricPlayedColor,
}
}
const useLyricShadowColor = () => {
const lyric_shadow_color_ref = ref(null)
let tools
const initLyricShadowColor = (color, changed, reset) => {
if (!lyric_shadow_color_ref.value) return
tools = pickrTools.create(lyric_shadow_color_ref.value, color, null, changed, reset)
}
const destroyLyricShadowColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setLyricShadowColor = (color) => {
tools?.setColor(color)
}
return {
lyric_shadow_color_ref,
initLyricShadowColor,
destroyLyricShadowColor,
setLyricShadowColor,
}
}
const useLyricColor = () => {
const { lyric_unplay_color_ref, initLyricUnplayColor, destroyLyricUnplayColor, setLyricUnplayColor } = useLyricUnplayColor()
const { lyric_played_color_ref, initLyricPlayedColor, destroyLyricPlayedColor, setLyricPlayedColor } = useLyricPlayedColor()
const { lyric_shadow_color_ref, initLyricShadowColor, destroyLyricShadowColor, setLyricShadowColor } = useLyricShadowColor()
const initColors = () => {
initLyricUnplayColor(appSetting['desktopLyric.style.lyricUnplayColor'], (color) => {
updateSetting({ 'desktopLyric.style.lyricUnplayColor': color })
})
initLyricPlayedColor(appSetting['desktopLyric.style.lyricPlayedColor'], (color) => {
updateSetting({ 'desktopLyric.style.lyricPlayedColor': color })
})
initLyricShadowColor(appSetting['desktopLyric.style.lyricShadowColor'], (color) => {
updateSetting({ 'desktopLyric.style.lyricShadowColor': color })
})
}
const destroyColors = () => {
destroyLyricUnplayColor()
destroyLyricPlayedColor()
destroyLyricShadowColor()
}
const resetColor = () => {
const defaultSetting = {
'desktopLyric.style.lyricUnplayColor': 'rgba(255, 255, 255, 1)',
'desktopLyric.style.lyricPlayedColor': 'rgba(7, 197, 86, 1)',
'desktopLyric.style.lyricShadowColor': 'rgba(0, 0, 0, 0.14)',
}
updateSetting(defaultSetting)
setLyricUnplayColor(defaultSetting['desktopLyric.style.lyricUnplayColor'])
setLyricPlayedColor(defaultSetting['desktopLyric.style.lyricPlayedColor'])
setLyricShadowColor(defaultSetting['desktopLyric.style.lyricShadowColor'])
}
onMounted(() => {
initColors()
})
onBeforeUnmount(() => {
destroyColors()
})
return {
lyric_unplay_color_ref,
lyric_played_color_ref,
lyric_shadow_color_ref,
resetColor,
}
}
export default {
name: 'SettingDesktopLyric',
setup() {
const t = useI18n()
const {
lyric_unplay_color_ref,
lyric_played_color_ref,
lyric_shadow_color_ref,
resetColor,
} = useLyricColor()
const systemFontList = ref([])
const fontList = computed(() => {
return [{ id: '', label: t('setting__desktop_lyric_font_default') }, ...systemFontList.value]
})
getSystemFonts().then(fonts => {
systemFontList.value = fonts.map(f => ({ id: f, label: f.replace(/(^"|"$)/g, '') }))
})
return {
appSetting,
updateSetting,
lyric_unplay_color_ref,
lyric_played_color_ref,
lyric_shadow_color_ref,
resetColor,
fontList,
isLinux,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.groupContent {
display: flex;
flex-flow: row wrap;
}
.item {
padding-right: 40px;
width: 70px;
display: flex;
flex-flow: column nowrap;
align-items: center;
}
.color {
width: 80%;
aspect-ratio: 1 / 1;
background-color: var(--pcr-color);
border-radius: @radius-border;
cursor: pointer;
transition: @transition-fast !important;
transition-property: background-color, opacity !important;
box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);
&:hover {
opacity: .7;
}
}
.label {
.mixin-ellipsis-2;
padding-top: 10px;
text-align: center;
line-height: 1.1;
}
</style>
@@ -0,0 +1,101 @@
<template lang="pug">
dt#download {{$t('setting__download')}}
dd
base-checkbox(id="setting_download_enable" :modelValue="appSetting['download.enable']" @update:modelValue="updateSetting({'download.enable': $event})" :label="$t('setting__download_enable')")
dd(:aria-label="$t('setting__download_path_title')")
h3#download_path {{$t('setting__download_path')}}
div
p
| {{$t('setting__download_path_label')}}
span.auto-hidden.hover(:class="$style.savePath" @click="openDirInExplorer(appSetting['download.savePath'])" :aria-label="$t('setting__download_path_open_label')") {{appSetting['download.savePath']}}
p
base-btn.btn(min @click="handleChangeSavePath") {{$t('setting__download_path_change_btn')}}
dd
h3#download_use_other_source
| {{$t('setting__download_use_other_source')}}
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__download_use_other_source_tip')")
div
base-checkbox(id="setting_download_isUseOtherSource" :modelValue="appSetting['download.isUseOtherSource']" @update:modelValue="updateSetting({'download.isUseOtherSource': $event})" :label="$t('setting__is_enable')")
div
dd(:aria-label="$t('setting__download_name_title')")
h3#download_name {{$t('setting__download_name')}}
div
base-checkbox.gap-left(:id="`setting_download_musicName_${item.value}`" name="setting_download_musicName" :value="item.value" :key="item.value" need
:modelValue="appSetting['download.fileName']" @update:modelValue="updateSetting({'download.fileName': $event})" v-for="item in musicNames" :label="item.name")
dd
h3#download_data_embed {{$t('setting__download_data_embed')}}
.gap-top
base-checkbox(id="setting_download_isEmbedPic" :modelValue="appSetting['download.isEmbedPic']" @update:modelValue="updateSetting({'download.isEmbedPic': $event})" :label="$t('setting__download_embed_pic')")
.gap-top
base-checkbox(id="setting_download_isEmbedLyric" :modelValue="appSetting['download.isEmbedLyric']" @update:modelValue="updateSetting({'download.isEmbedLyric': $event})" :label="$t('setting__download_embed_lyric')")
dd(:aria-label="$t('setting__download_lyric_title')")
h3#download_lyric {{$t('setting__download_lyric')}}
.gap-top
base-checkbox(id="setting_download_isDownloadLrc" :modelValue="appSetting['download.isDownloadLrc']" @update:modelValue="updateSetting({'download.isDownloadLrc': $event})" :label="$t('setting__is_enable')")
.gap-top
base-checkbox(id="setting_download_isDownloadTLrc" :disabled="!appSetting['download.isDownloadLrc']" :modelValue="appSetting['download.isDownloadTLrc']" @update:modelValue="updateSetting({'download.isDownloadTLrc': $event})" :label="$t('setting__download_tlyric')")
.gap-top
base-checkbox(id="setting_download_isDownloadRLrc" :disabled="!appSetting['download.isDownloadLrc']" :modelValue="appSetting['download.isDownloadRLrc']" @update:modelValue="updateSetting({'download.isDownloadRLrc': $event})" :label="$t('setting__download_rlyric')")
dd
h3#download_lyric_format {{$t('setting__download_lyric_format')}}
div
base-checkbox.gap-left(v-for="item in lrcFormatList" :key="item.id" :id="`setting_download_lrcFormat_${item.id}`"
name="setting_download_lrcFormat" need :modelValue="appSetting['download.lrcFormat']" @update:modelValue="updateSetting({'download.lrcFormat': $event})" :value="item.id" :label="item.name")
</template>
<script>
import { computed } from '@common/utils/vueTools'
// import { getSystemFonts } from '@renderer/utils/tools'
import { openDirInExplorer } from '@common/utils/electron'
import { showSelectDialog } from '@renderer/utils/ipc'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingDownload',
setup() {
const t = useI18n()
const handleChangeSavePath = () => {
showSelectDialog({
title: t('setting__download_select_save_path'),
defaultPath: appSetting['download.savePath'],
properties: ['openDirectory'],
}).then(result => {
if (result.canceled) return
updateSetting({ 'download.savePath': result.filePaths[0] })
})
}
const musicNames = computed(() => {
return [
{ value: '歌名 - 歌手', name: t('setting__download_name1') },
{ value: '歌手 - 歌名', name: t('setting__download_name2') },
{ value: '歌名', name: t('setting__download_name3') },
]
})
const lrcFormatList = computed(() => {
return [
{ id: 'utf8', name: t('setting__download_lyric_format_utf8') },
{ id: 'gbk', name: t('setting__download_lyric_format_gbk') },
]
})
return {
appSetting,
updateSetting,
openDirInExplorer,
handleChangeSavePath,
musicNames,
lrcFormatList,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,305 @@
<template lang="pug">
dt#hot_key {{$t('setting__hot_key')}}
dd
h3#hot_key_local_title {{$t('setting__hot_key_local_title')}}
div
base-checkbox(id="setting_download_hotKeyLocal" v-model="current_hot_key.local.enable" :label="$t('setting__is_enable')" @change="handleHotKeySaveConfig")
div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.local.enable ? 1 : .6 }")
div(:class="$style.hotKeyItem" v-for="item in allHotKeys.local")
h4(:class="$style.hotKeyItemTitle") {{$t('setting__hot_key_' + item.name)}}
base-input(:class="$style.hotKeyItemInput" readonly @keyup.prevent :placeholder="$t('setting__hot_key_unset_input')"
:value="hotKeyConfig.local[item.name] && formatHotKeyName(hotKeyConfig.local[item.name].key)"
@focus="handleHotKeyFocus($event, item, 'local')"
@blur="handleHotKeyBlur($event, item, 'local')")
dd
h3#hot_key_global_title {{$t('setting__hot_key_global_title')}}
div
base-checkbox(id="setting_download_hotKeyGlobal" v-model="current_hot_key.global.enable" :label="$t('setting__is_enable')" @change="handleEnableHotKey")
div(:class="$style.hotKeyContainer" :style="{ opacity: current_hot_key.global.enable ? 1 : .6 }")
div(:class="$style.hotKeyItem" v-for="item in allHotKeys.global")
h4(:class="$style.hotKeyItemTitle") {{$t('setting__hot_key_' + item.name)}}
base-input(:class="[$style.hotKeyItemInput, hotKeyConfig.global[item.name] && hotKeyStatus[hotKeyConfig.global[item.name].key] && hotKeyStatus[hotKeyConfig.global[item.name].key].status === false ? $style.hotKeyFailed : null]"
:value="hotKeyConfig.global[item.name] && formatHotKeyName(hotKeyConfig.global[item.name].key)" @input.prevent readonly :placeholder="$t('setting__hot_key_unset_input')"
@focus="handleHotKeyFocus($event, item, 'global')"
@blur="handleHotKeyBlur($event, item, 'global')")
</template>
<script>
import { ref, onBeforeUnmount, toRaw, shallowReactive } from '@common/utils/vueTools'
import { allHotKeys, hotKeySetEnable, hotKeySetConfig, hotKeyGetStatus } from '@renderer/utils/ipc'
import { isMac } from '@common/utils'
import { useI18n } from '@renderer/plugins/i18n'
const formatHotKeyName = (name) => {
if (name.includes('arrow')) {
name = name.replace(/arrow(left|right|up|down)/, s => {
switch (s) {
case 'arrowleft': return '←'
case 'arrowright': return '→'
case 'arrowup': return '↑'
case 'arrowdown': return '↓'
}
})
}
if (name.includes('mod')) name = name.replace('mod', isMac ? 'Command' : 'Ctrl')
name = name.replace(/(\+|^)[a-z]/g, l => l.toUpperCase())
if (name.length > 1) name = name.replace(/\+/g, ' + ')
return name
}
export default {
name: 'SettingHotKey',
setup() {
const t = useI18n()
const current_hot_key = ref({
local: {
enable: false,
keys: {},
},
global: {
enable: false,
keys: {},
},
})
const hotKeyConfig = ref({
local: {},
global: {},
})
const hotKeyStatus = ref({})
let isEditHotKey = false
let hotKeyTargetInput
let newHotKey
let tip
const initHotKeyConfig = () => {
let config = {}
for (const [type, typeInfo] of Object.entries(current_hot_key.value)) {
let configInfo = config[type] = {}
for (const [key, info] of Object.entries(typeInfo.keys)) {
if (!info.name) continue
configInfo[info.name] = shallowReactive({
key,
info,
})
}
}
hotKeyConfig.value = config
}
const handleHotKeyFocus = (event, info, type) => {
setTimeout(async() => {
await hotKeySetEnable(false)
window.lx.isEditingHotKey = true
isEditHotKey = true
let config = hotKeyConfig.value[type][info.name]
newHotKey = config?.key
hotKeyTargetInput = event.target
event.target.value = tip = t('setting__hot_key_tip_input')
})
}
const handleHotKeyBlur = (event, info, type) => {
setTimeout(async() => {
await hotKeySetEnable(true)
window.lx.isEditingHotKey = false
isEditHotKey = false
const prevInput = hotKeyTargetInput
hotKeyTargetInput = null
if (prevInput?.value == tip) {
prevInput.value = newHotKey ? formatHotKeyName(newHotKey) : ''
return
}
let config = hotKeyConfig.value[type][info.name]
let originKey
if (type == 'global' && newHotKey && current_hot_key.value.global.enable) {
try {
await hotKeySetConfig({
action: 'register',
data: {
key: newHotKey,
info,
},
})
} catch (error) {
console.log(error)
return
}
}
if (config) {
if (config.key == newHotKey) return
originKey = config.key
delete current_hot_key.value[type].keys[config.key]
} else if (!newHotKey) return
if (newHotKey) {
for (const [tempType, tempInfo] of Object.entries(current_hot_key.value)) {
if (tempType == type) continue
config = tempInfo.keys[newHotKey]
if (config) {
console.log(newHotKey, info, config, info.name, config.name)
delete current_hot_key.value[tempType].keys[newHotKey]
break
}
}
current_hot_key.value[type].keys[newHotKey] = info
}
initHotKeyConfig()
// console.log(this.current_hot_key.global.keys)
if (originKey && current_hot_key.value.global.enable) {
try {
await hotKeySetConfig({
action: 'unregister',
data: originKey,
})
} catch (error) {
console.log(error)
}
}
await handleHotKeySaveConfig()
await getHotKeyStatus()
})
}
const handleKeyDown = ({ event, keys, key, type }) => {
// if (!event || event.repeat) return
if (!event || event.repeat || type == 'up' || !isEditHotKey) return
event.preventDefault()
// console.log(event, key)
switch (key) {
case 'delete':
case 'backspace':
key = ''
break
}
hotKeyTargetInput.value = formatHotKeyName(key)
// console.log(keys, key, type)
newHotKey = key
}
// const handleUpdateHotKeyConfig = (config) => {
// // console.log(config)
// for (const [type, info] of Object.entries(config)) {
// current_hot_key.value[type] = info
// }
// }
const handleHotKeySaveConfig = async() => {
// console.log(this.current_hot_key)
await hotKeySetConfig({
action: 'config',
data: toRaw(current_hot_key.value),
})
}
const handleEnableHotKey = async() => {
await hotKeySetConfig({
action: 'enable',
data: current_hot_key.value.global.enable,
})
await handleHotKeySaveConfig()
await getHotKeyStatus()
}
const getHotKeyStatus = () => {
return hotKeyGetStatus().then(status => {
// console.log(status)
hotKeyStatus.value = status
return status
})
}
current_hot_key.value = window.lx.appHotKeyConfig
initHotKeyConfig()
getHotKeyStatus()
window.app_event.on('keyDown', handleKeyDown)
onBeforeUnmount(() => {
window.app_event.off('keyDown', handleKeyDown)
})
return {
// appSetting,
allHotKeys,
current_hot_key,
hotKeyConfig,
hotKeyStatus,
handleHotKeyFocus,
handleHotKeyBlur,
handleEnableHotKey,
formatHotKeyName,
handleHotKeySaveConfig,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.hotKeyContainer {
display: flex;
flex-flow: row wrap;
// margin-top: -15px;
margin-bottom: 15px;
transition: opacity @transition-normal;
}
.hotKeyItem {
width: 30%;
padding-right: 35px;
margin-top: 15px;
box-sizing: border-box;
}
.hotKeyItemTitle {
.mixin-ellipsis-1;
padding-bottom: 5px;
color: var(--color-font-label);
font-size: 12px;
}
.hotKeyItemInput {
width: 100%;
box-sizing: border-box;
// font-family: monospace;
&:focus {
background-color: var(--color-primary-background-active);
text-decoration: none;
}
&::placeholder {
color: rgba(197, 197, 197, 0.7)!important;
}
}
.hotKeyFailed {
text-decoration: line-through;
}
.del-line {
position: relative;
&:before {
display: block;
height: 1px;
position: absolute;
width: 110%;
content: ' ';
left: 0;
background-color: #000;
transform: rotate(-24deg);
transform-origin: 0;
top: 83%;
z-index: 1;
}
&:after {
display: block;
height: 1px;
position: absolute;
width: 110%;
content: ' ';
left: 0;
background-color: #000;
transform: rotate(23deg);
transform-origin: 0px;
top: 2px;
z-index: 1;
}
}
</style>
@@ -0,0 +1,35 @@
<template lang="pug">
dt#list {{$t('setting__list')}}
dd
.gap-top
base-checkbox(id="setting_list_actionButtonsVisible_enable" :modelValue="appSetting['list.actionButtonsVisible']" @update:modelValue="updateSetting({'list.actionButtonsVisible': $event})" :label="$t('setting__list_action_btn')")
.gap-top
base-checkbox(id="setting_list_showSource_enable" :modelValue="appSetting['list.isShowSource']" @update:modelValue="updateSetting({'list.isShowSource': $event})" :label="$t('setting__list_source')")
.gap-top
base-checkbox(id="setting_list_scroll_enable" :modelValue="appSetting['list.isSaveScrollLocation']" @update:modelValue="updateSetting({'list.isSaveScrollLocation': $event})" :label="$t('setting__list_scroll')")
.gap-top
base-checkbox(id="setting_list_clickAction_enable" :modelValue="appSetting['list.isClickPlayList']" @update:modelValue="updateSetting({'list.isClickPlayList': $event})" :label="$t('setting__list_click_action')")
dd(:aria-label="$t('setting__basic_sourcename_title')")
h3#list_addMusicLocationType {{$t('setting__list_add_music_location_type')}}
div
base-checkbox.gap-left(id="setting_list_add_music_location_type_top"
name="setting_list_add_music_location_type" need :modelValue="appSetting['list.addMusicLocationType']" @update:modelValue="updateSetting({'list.addMusicLocationType': $event})" value="top" :label="$t('setting__list_add_music_location_type_top')")
base-checkbox.gap-left(id="setting_list_add_music_location_type_bottom"
name="setting_list_add_music_location_type" need :modelValue="appSetting['list.addMusicLocationType']" @update:modelValue="updateSetting({'list.addMusicLocationType': $event})" value="bottom" :label="$t('setting__list_add_music_location_type_bottom')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingList',
setup() {
return {
appSetting,
updateSetting,
}
},
}
</script>
@@ -0,0 +1,55 @@
<template lang="pug">
dt#network {{$t('setting__network')}}
dd
h3#network_proxy_title {{$t('setting__network_proxy_title')}}
div
p
base-checkbox(id="setting_network_proxy_enable" :modelValue="appSetting['network.proxy.enable']" @update:modelValue="updateSetting({'network.proxy.enable': $event})" :label="$t('setting__is_enable')")
p
base-input.gap-left(:modelValue="appSetting['network.proxy.host']" @update:modelValue="setHost" :placeholder="proxy.envProxy ? proxy.envProxy.host : $t('setting__network_proxy_host')")
base-input.gap-left(:modelValue="appSetting['network.proxy.port']" @update:modelValue="setPort" :placeholder="proxy.envProxy ? proxy.envProxy.port : $t('setting__network_proxy_port')")
p
base-input.gap-left(:modelValue="appSetting['network.proxy.username']" @update:modelValue="setUserName" :placeholder="$t('setting__network_proxy_username')")
base-input.gap-left(:modelValue="appSetting['network.proxy.password']" @update:modelValue="setPassword" type="password" :placeholder="$t('setting__network_proxy_password')")
</template>
<script>
import { onBeforeUnmount } from '@common/utils/vueTools'
import { proxy } from '@renderer/store'
import { debounce } from '@common/utils'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingNetwork',
setup() {
const setHost = debounce(host => {
updateSetting({ 'network.proxy.host': host.trim() })
}, 500)
const setPort = debounce(port => {
updateSetting({ 'network.proxy.port': port.trim() })
}, 500)
const setUserName = debounce(username => {
updateSetting({ 'network.proxy.username': username.trim() })
}, 500)
const setPassword = debounce(password => {
updateSetting({ 'network.proxy.password': password.trim() })
}, 500)
onBeforeUnmount(() => {
if (appSetting['network.proxy.enable'] && !appSetting['network.proxy.host']) proxy.enable = false
})
return {
appSetting,
updateSetting,
setHost,
setPort,
setUserName,
setPassword,
proxy,
}
},
}
</script>
@@ -0,0 +1,23 @@
<template lang="pug">
dt#odc {{$t('setting__odc')}}
dd
.gap-top
base-checkbox(id="setting_odc_isAutoClearSearchInput" :modelValue="appSetting['odc.isAutoClearSearchInput']" @update:modelValue="updateSetting({'odc.isAutoClearSearchInput': $event})" :label="$t('setting__odc_clear_search_input')")
.gap-top
base-checkbox(id="setting_odc_isAutoClearSearchList" :modelValue="appSetting['odc.isAutoClearSearchList']" @update:modelValue="updateSetting({'odc.isAutoClearSearchList': $event})" :label="$t('setting__odc_clear_search_list')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingOdc',
setup() {
return {
appSetting,
updateSetting,
}
},
}
</script>
@@ -0,0 +1,195 @@
<template lang="pug">
dt#other {{$t('setting__other')}}
dd
h3#other_tray_theme {{$t('setting__other_tray_theme')}}
div
base-checkbox.gap-left(:id="'setting_tray_theme_' + item.id" :modelValue="appSetting['tray.themeId']" @update:modelValue="updateSetting({'tray.themeId': $event})" name="setting_tray_theme" need
:label="item.label" :key="item.id" :value="item.id" v-for="item in trayThemeList")
dd
h3#other_resource_cache
| {{$t('setting__other_resource_cache')}}
svg-icon(class="help-icon" name="help-circle-outline" :aria-label="$t('setting__other_resource_cache_tip')")
div
p
| {{$t('setting__other_resource_cache_label')}}
span.auto-hidden {{cacheSize}}
p
base-btn.btn(min :disabled="isDisabledResourceCacheClear" @click="clearResourceCache") {{$t('setting__other_resource_cache_clear_btn')}}
dd
h3#other_other_source {{$t('setting__other_other_cache')}}
div
p
| {{$t('setting__other_other_source_label')}}
span.auto-hidden {{otherSourceCount}}
p
| {{$t('setting__other_music_url_label')}}
span.auto-hidden {{musicUrlCount}}
p
| {{$t('setting__other_lyric_raw_label')}}
span.auto-hidden {{lyricRawCount}}
p
base-btn.btn(min :disabled="isDisabledOtherSourceCacheClear" @click="handleClearOtherSourceCache") {{$t('setting__other_other_source_clear_btn')}}
base-btn.btn(min :disabled="isDisabledMusicUrlCacheClear" @click="handleClearMusicUrlCache") {{$t('setting__other_music_url_clear_btn')}}
base-btn.btn(min :disabled="isDisabledLyricRawCacheClear" @click="handleClearLyricRawCache") {{$t('setting__other_lyric_raw_clear_btn')}}
dd
h3#other_lyric_edited {{$t('setting__other_lyric_edited_cache')}}
div
p
| {{$t('setting__other_lyric_edited_label')}}
span.auto-hidden {{lyricEditedCount}}
p
base-btn.btn(min :disabled="isDisabledLyricEditedCacheClear" @click="handleClearLyricEditedCache") {{$t('setting__other_lyric_edited_clear_btn')}}
</template>
<script>
import { ref, computed } from '@common/utils/vueTools'
import {
clearCache, getCacheSize,
getOtherSourceCount, clearOtherSource,
getMusicUrlCount, clearMusicUrl,
getLyricRawCount, clearLyricRaw,
getLyricEditedCount, clearLyricEdited,
} from '@renderer/utils/ipc'
import { sizeFormate } from '@common/utils/common'
import { dialog } from '@renderer/plugins/Dialog'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingOther',
setup() {
const t = useI18n()
const trayThemeList = computed(() => {
return [
{ id: 0, name: 'native', label: t('setting__other_tray_theme_native') },
{ id: 2, name: 'black', label: t('setting__other_tray_theme_black') },
{ id: 1, name: 'origin', label: t('setting__other_tray_theme_origin') },
]
})
const cacheSize = ref('0 B')
const isDisabledResourceCacheClear = ref(false)
// const isDisabledListCacheClear = ref(false)
const refreshCacheSize = () => {
getCacheSize().then(size => {
cacheSize.value = sizeFormate(size)
})
}
const clearResourceCache = async() => {
if (!await dialog.confirm({
message: t('setting__other_resource_cache_tip_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('setting__other_resource_cache_confirm'),
})) return
isDisabledResourceCacheClear.value = true
clearCache().then(() => {
refreshCacheSize()
isDisabledResourceCacheClear.value = false
})
}
refreshCacheSize()
const otherSourceCount = ref(0)
const isDisabledOtherSourceCacheClear = ref(false)
const refreshOtherSourceCount = () => {
getOtherSourceCount().then(count => {
otherSourceCount.value = count
})
}
const handleClearOtherSourceCache = async() => {
isDisabledOtherSourceCacheClear.value = true
clearOtherSource().then(() => {
refreshOtherSourceCount()
isDisabledOtherSourceCacheClear.value = false
})
}
refreshOtherSourceCount()
const musicUrlCount = ref(0)
const isDisabledMusicUrlCacheClear = ref(false)
const refreshMusicUrlCount = () => {
getMusicUrlCount().then(count => {
musicUrlCount.value = count
})
}
const handleClearMusicUrlCache = async() => {
isDisabledMusicUrlCacheClear.value = true
clearMusicUrl().then(() => {
refreshMusicUrlCount()
isDisabledMusicUrlCacheClear.value = false
})
}
refreshMusicUrlCount()
const lyricRawCount = ref(0)
const isDisabledLyricRawCacheClear = ref(false)
const refreshLyricRawCount = () => {
getLyricRawCount().then(count => {
lyricRawCount.value = count
})
}
const handleClearLyricRawCache = async() => {
isDisabledLyricRawCacheClear.value = true
clearLyricRaw().then(() => {
refreshLyricRawCount()
isDisabledLyricRawCacheClear.value = false
})
}
refreshLyricRawCount()
const lyricEditedCount = ref(0)
const isDisabledLyricEditedCacheClear = ref(false)
const refreshLyricEditedCount = () => {
getLyricEditedCount().then(count => {
lyricEditedCount.value = count
})
}
const handleClearLyricEditedCache = async() => {
if (!await dialog.confirm({
message: t('setting__other_lyric_edited_clear_tip_confirm'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('setting__other_resource_cache_confirm'),
})) return
isDisabledLyricEditedCacheClear.value = true
clearLyricEdited().then(() => {
refreshLyricEditedCount()
isDisabledLyricEditedCacheClear.value = false
})
}
refreshLyricEditedCount()
return {
appSetting,
updateSetting,
trayThemeList,
cacheSize,
isDisabledResourceCacheClear,
clearResourceCache,
otherSourceCount,
isDisabledOtherSourceCacheClear,
handleClearOtherSourceCache,
musicUrlCount,
isDisabledMusicUrlCacheClear,
handleClearMusicUrlCache,
lyricRawCount,
isDisabledLyricRawCacheClear,
handleClearLyricRawCache,
lyricEditedCount,
isDisabledLyricEditedCacheClear,
handleClearLyricEditedCache,
}
},
}
</script>
@@ -0,0 +1,95 @@
<template lang="pug">
dt#play {{$t('setting__play')}}
dd
.gap-top
base-checkbox(id="setting_player_startup_auto_play" :modelValue="appSetting['player.startupAutoPlay']" @update:modelValue="updateSetting({'player.startupAutoPlay': $event})" :label="$t('setting__play_startup_auto_play')")
.gap-top
base-checkbox(id="setting_player_save_play_time" :modelValue="appSetting['player.isSavePlayTime']" @update:modelValue="updateSetting({'player.isSavePlayTime': $event})" :label="$t('setting__play_save_play_time')")
.gap-top
base-checkbox(id="setting_player_lyric_transition" :modelValue="appSetting['player.isShowLyricTranslation']" @update:modelValue="updateSetting({'player.isShowLyricTranslation': $event})" :label="$t('setting__play_lyric_transition')")
.gap-top
base-checkbox(id="setting_player_lyric_roma" :modelValue="appSetting['player.isShowLyricRoma']" @update:modelValue="updateSetting({'player.isShowLyricRoma': $event})" :label="$t('setting__play_lyric_roma')")
.gap-top
base-checkbox(id="setting_player_auto_skip_on_error" :modelValue="appSetting['player.autoSkipOnError']" @update:modelValue="updateSetting({'player.autoSkipOnError': $event})" :label="$t('setting__play_auto_skip_on_error')")
.gap-top
base-checkbox(id="setting_player_lyric_s2t" :modelValue="appSetting['player.isS2t']" @update:modelValue="updateSetting({'player.isS2t': $event})" :label="$t('setting__play_lyric_s2t')")
.gap-top
base-checkbox(id="setting_player_lyric_play_lxlrc" :modelValue="appSetting['player.isPlayLxlrc']" @update:modelValue="updateSetting({'player.isPlayLxlrc': $event})" :label="$t('setting__play_lyric_lxlrc')")
.gap-top
base-checkbox(id="setting_player_highQuality" :modelValue="appSetting['player.highQuality']" @update:modelValue="updateSetting({'player.highQuality': $event})" :label="$t('setting__play_quality')")
.gap-top
base-checkbox(id="setting_player_showTaskProgess" :modelValue="appSetting['player.isShowTaskProgess']" @update:modelValue="updateSetting({'player.isShowTaskProgess': $event})" :label="$t('setting__play_task_bar')")
.gap-top
base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" :modelValue="appSetting['player.isMediaDeviceRemovedStopPlay']" @update:modelValue="updateSetting({'player.isMediaDeviceRemovedStopPlay': $event})" :label="$t('setting__play_mediaDevice_remove_stop_play')")
dd(:aria-label="$t('setting__play_mediaDevice_title')")
h3#play_mediaDevice {{$t('setting__play_mediaDevice')}}
div
base-selection.gap-left(:list="mediaDevices" v-model="mediaDeviceId" @change="handleMediaDeviceIdChnage" item-key="deviceId" item-name="label")
</template>
<script>
import { ref, onBeforeUnmount, watch } from '@common/utils/vueTools'
import { hasInitedAnalyser } from '@renderer/plugins/player'
import { dialog } from '@renderer/plugins/Dialog'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingPlay',
setup() {
const t = useI18n()
const mediaDevices = ref([])
const getMediaDevice = async() => {
const devices = await navigator.mediaDevices.enumerateDevices()
let audioDevices = devices.filter(device => device.kind === 'audiooutput')
mediaDevices.value = audioDevices
// console.log(this.mediaDevices)
}
getMediaDevice()
navigator.mediaDevices.addEventListener('devicechange', getMediaDevice)
onBeforeUnmount(() => {
navigator.mediaDevices.removeEventListener('devicechange', getMediaDevice)
})
const mediaDeviceId = ref(appSetting['player.mediaDeviceId'])
const handleMediaDeviceIdChnage = async() => {
if (hasInitedAnalyser()) {
await dialog({
message: t('setting__play_media_device_error_tip'),
confirmButtonText: t('alert_button_text'),
})
mediaDeviceId.value = appSetting['player.mediaDeviceId']
} else if (appSetting['player.audioVisualization']) {
const confirm = await dialog.confirm({
message: t('setting__play_media_device_tip'),
cancelButtonText: t('cancel_button_text'),
confirmButtonText: t('confirm_button_text'),
})
if (confirm) {
appSetting['player.audioVisualization'] = false
appSetting['player.mediaDeviceId'] = mediaDeviceId.value
} else {
mediaDeviceId.value = appSetting['player.mediaDeviceId']
}
} else {
appSetting['player.mediaDeviceId'] = mediaDeviceId.value
}
}
watch(() => appSetting['player.mediaDeviceId'], val => {
mediaDeviceId.value = val
})
return {
appSetting,
updateSetting,
mediaDevices,
mediaDeviceId,
handleMediaDeviceIdChnage,
}
},
}
</script>
@@ -0,0 +1,31 @@
<template lang="pug">
dt#play_detail {{$t('setting__play_detail')}}
dd
.gap-top
base-checkbox(id="setting_play_detail_font_zoom_enable" :modelValue="appSetting['playDetail.isZoomActiveLrc']" @update:modelValue="updateSetting({'playDetail.isZoomActiveLrc': $event})" :label="$t('setting__play_detail_font_zoom')")
.gap-top
base-checkbox(id="setting_play_detail_lyric_progress_enable" :modelValue="appSetting['playDetail.isShowLyricProgressSetting']" @update:modelValue="updateSetting({'playDetail.isShowLyricProgressSetting': $event})" :label="$t('setting__play_detail_lyric_progress')")
dd
h3#play_detail_align {{$t('setting__play_detail_align')}}
div
base-checkbox.gap-left(id="setting_play_detail_align_left" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="left" :label="$t('setting__play_detail_align_left')")
base-checkbox.gap-left(id="setting_play_detail_align_center" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="center" :label="$t('setting__play_detail_align_center')")
base-checkbox.gap-left(id="setting_play_detail_align_right" :modelValue="appSetting['playDetail.style.align']" @update:modelValue="updateSetting({ 'playDetail.style.align': $event })" need value="right" :label="$t('setting__play_detail_align_right')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingPlayDetail',
setup() {
return {
appSetting,
updateSetting,
}
},
}
</script>
@@ -0,0 +1,26 @@
<template lang="pug">
dt#search {{$t('setting__search')}}
dd
.gap-top
base-checkbox(id="setting_search_showHot_enable" :modelValue="appSetting['search.isShowHotSearch']" @update:modelValue="updateSetting({'search.isShowHotSearch': $event})" :label="$t('setting__search_hot')")
.gap-top
base-checkbox(id="setting_search_showHistory_enable" :modelValue="appSetting['search.isShowHistorySearch']" @update:modelValue="updateSetting({'search.isShowHistorySearch': $event})" :label="$t('setting__search_history')")
.gap-top
base-checkbox(id="setting_search_focusSearchBox_enable" :modelValue="appSetting['search.isFocusSearchBox']" @update:modelValue="updateSetting({'search.isFocusSearchBox': $event})" :label="$t('setting__search_focus_search_box')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
name: 'SettingSearch',
setup() {
return {
appSetting,
updateSetting,
}
},
}
</script>
@@ -0,0 +1,79 @@
<template lang="pug">
dt#sync
| {{$t('setting__sync')}}
button(class="help-btn" @click="openUrl('https://lyswhut.github.io/lx-music-doc/desktop/faq/sync')" :aria-label="$t('setting__sync_tip')")
svg-icon(name="help-circle-outline")
dd
base-checkbox(id="setting_sync_enable" :modelValue="appSetting['sync.enable']" @update:modelValue="updateSetting({ 'sync.enable': $event })" :label="syncEnableTitle")
div
p.small {{$t('setting__sync_auth_code', { code: sync.status.code || '' })}}
p.small {{$t('setting__sync_address', { address: sync.status.address.join(', ') || '' })}}
p.small {{$t('setting__sync_device', { devices: syncDevices })}}
p
base-btn.btn(min :disabled="!sync.status.status" @click="refreshSyncCode") {{$t('setting__sync_refresh_code')}}
dd
h3#sync_port {{$t('setting__sync_port')}}
div
p
base-input.gap-left(:modelValue="appSetting['sync.port']" @update:modelValue="setSyncPort" :placeholder="$t('setting__sync_port_tip')")
</template>
<script>
import { computed } from '@common/utils/vueTools'
import { sync } from '@renderer/store'
import { sendSyncAction } from '@renderer/utils/ipc'
import { openUrl } from '@common/utils/electron'
import { useI18n } from '@renderer/plugins/i18n'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { debounce } from '@common/utils/common'
export default {
name: 'SettingSync',
setup() {
const t = useI18n()
const syncEnableTitle = computed(() => {
let title = t('setting__sync_enable')
if (sync.status.message) {
title += ` [${sync.status.message}]`
}
// else if (this.sync.status.address.length) {
// // title += ` [${this.sync.status.address.join(', ')}]`
// }
return title
})
const syncDevices = computed(() => {
return sync.status.devices.length
? sync.status.devices.map(d => `${d.deviceName} (${d.clientId.substring(0, 5)})`).join(', ')
: ''
})
const refreshSyncCode = () => {
sendSyncAction({ action: 'generate_code' })
}
const setSyncPort = debounce(port => {
updateSetting({ 'sync.port': port.trim() })
}, 500)
return {
appSetting,
updateSetting,
sync,
syncEnableTitle,
setSyncPort,
syncDevices,
refreshSyncCode,
openUrl,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,71 @@
<template lang="pug">
dt#update {{$t('setting__update')}}
dd
p.small
| {{$t('setting__update_latest_label')}}{{versionInfo.newVersion ? versionInfo.newVersion.version : $t('setting__update_unknown')}}
p.small(@click="handleOPenDevTools") {{$t('setting__update_current_label')}}{{versionInfo.version}}
p.small(v-if="versionInfo.downloadProgress" style="line-height: 1.5;")
| {{$t('setting__update_downloading')}}
br
| {{$t('setting__update_progress')}}{{downloadProgress}}
p(v-if="versionInfo.newVersion")
span(v-if="versionInfo.isLatestVer") {{$t('setting__update_latest')}}
base-btn.btn.gap-left(v-else min @click="showUpdateModal") {{$t('setting__update_open_version_modal_btn')}}
p.small(v-else) {{$t('setting__update_checking')}}
</template>
<script>
import { computed } from '@common/utils/vueTools'
import { versionInfo } from '@renderer/store'
import { sizeFormate } from '@common/utils/common'
// import { openDirInExplorer, selectDir } from '@renderer/utils'
import { openDevTools } from '@renderer/utils/ipc'
import { useI18n } from '@renderer/plugins/i18n'
export default {
name: 'SettingUpdate',
setup() {
let lastClickTime = 0
let clickNum = 0
const t = useI18n()
const handleOPenDevTools = () => {
if (window.performance.now() - lastClickTime > 1000) {
if (clickNum > 0) clickNum = 0
} else {
if (clickNum > 4) {
openDevTools()
clickNum = 0
return
}
}
clickNum++
lastClickTime = window.performance.now()
}
const downloadProgress = computed(() => {
return versionInfo.downloadProgress
? `${versionInfo.downloadProgress.percent.toFixed(2)}% - ${sizeFormate(versionInfo.downloadProgress.transferred)}/${sizeFormate(versionInfo.downloadProgress.total)} - ${sizeFormate(versionInfo.downloadProgress.bytesPerSecond)}/s`
: t('setting__update_init')
})
const showUpdateModal = () => {
versionInfo.showModal = true
}
return {
versionInfo,
downloadProgress,
handleOPenDevTools,
showUpdateModal,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,768 @@
<template>
<material-modal :show="modelValue" max-height="90%" teleport="#view" @close="handleCancel">
<main :class="$style.main">
<h2>{{ $t('theme_edit_modal__title') }}</h2>
<div class="scroll" :class="$style.content">
<div :class="[$style.group, $style.base]">
<div :class="$style.groupContent">
<div :class="$style.item">
<div ref="primary_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__primary') }}</div>
</div>
<div :class="$style.item">
<div ref="app_bg_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__app_bg') }}</div>
</div>
<div :class="$style.item">
<div ref="aside_font_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__aside_color') }}</div>
</div>
<div :class="$style.item">
<div ref="main_bg_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__main_bg') }}</div>
</div>
<div :class="[$style.item, $style.bg]">
<div :class="[$style.bgImg, {[$style.hasBg]: !!bgImg}]" @click="selectBgImg">
<img v-if="bgImg" loading="lazy" decoding="async" :class="$style.img" :src="bgImg" alt="Background Image">
<svg-icon v-else :class="$style.icon" name="plus" />
<button :class="$style.removeBtn" type="button" @click.stop="removeBgImg">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" height="100%" viewBox="0 0 212.982 212.982" space="preserve">
<use xlink:href="#icon-delete" />
</svg>
</button>
</div>
<div :class="$style.label">{{ $t('theme_edit_modal__bg_image') }}</div>
</div>
</div>
</div>
<div :class="$style.groupContent">
<div :class="$style.group">
<div :class="$style.groupTitle">
<span :class="$style.title">{{ $t('theme_edit_modal__badge') }}</span>
<span class="badge badge-theme-primary">{{ $t('tag__lossless') }}</span>
<span class="badge badge-theme-secondary">{{ $t('tag__high_quality') }}</span>
<span class="badge badge-theme-tertiary">kw</span>
</div>
<div :class="$style.groupContent">
<div :class="$style.item">
<div ref="badge_primary_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__badge_primary') }}</div>
</div>
<div :class="$style.item">
<div ref="badge_secondary_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__badge_secondary') }}</div>
</div>
<div :class="$style.item">
<div ref="badge_tertiary_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__badge_tertiary') }}</div>
</div>
</div>
</div>
<div :class="$style.group">
<div :class="$style.groupTitle">
<span>{{ $t('theme_edit_modal__control_btn') }}</span>
<div :class="$style.controlBtn">
<button type="button" :class="$style.hide">
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="80%" viewBox="0 0 30.727 30.727" space="preserve">
<use xlink:href="#icon-window-hide" />
</svg>
</button>
<button type="button" :class="$style.min">
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-minimize" />
</svg>
</button>
<button type="button" :class="$style.close">
<svg :class="$style.controlBtnIcon" version="1.1" xmlns="http://www.w3.org/2000/svg" xlink="http://www.w3.org/1999/xlink" width="100%" viewBox="0 0 24 24" space="preserve">
<use xlink:href="#icon-window-close" />
</svg>
</button>
</div>
</div>
<div :class="$style.groupContent">
<div :class="$style.item">
<div ref="close_btn_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__close_btn') }}</div>
</div>
<div :class="$style.item">
<div ref="min_btn_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__min_btn') }}</div>
</div>
<div :class="$style.item">
<div ref="hide_btn_color_ref" :class="$style.color" />
<div :class="$style.label">{{ $t('theme_edit_modal__hide_btn') }}</div>
</div>
</div>
</div>
</div>
</div>
<!-- <div :class="$style.note">
<p>{{ $t('theme_selector_modal__title_tip') }}</p>
</div> -->
<div :class="$style.footer">
<div :class="$style.subContent">
<base-input v-model="themeName" :class="$style.input" :placeholder="$t('theme_selector_modal__theme_name')" />
<base-checkbox id="theme_edit_modal__dark" v-model="isDark" :class="$style.checkbox" :label="$t('theme_edit_modal__dark')" @change="handleDark" />
<base-checkbox id="theme_edit_modal__preview" v-model="preview" :class="$style.checkbox" :label="$t('theme_edit_modal__preview')" @change="handlePreview" />
</div>
<div :class="$style.subContent">
<base-btn v-if="themeId" :class="$style.btn" @click="handleRemove">{{ $t('theme_edit_modal__remove') }}</base-btn>
<base-btn v-if="themeId" :class="$style.btn" @click="handleSaveNew">{{ $t('theme_edit_modal__save_new') }}</base-btn>
<!-- <base-btn :class="$style.btn" @click="handleCancel">{{ $t('btn_cancel') }}</base-btn> -->
<base-btn :class="$style.btn" @click="handleSubmit">{{ $t('btn_save') }}</base-btn>
</div>
</div>
</main>
</material-modal>
</template>
<script>
import { joinPath, extname, copyFile, checkPath, createDir, removeFile, moveFile, basename } from '@common/utils/nodejs'
import { nextTick, ref, watch } from '@common/utils/vueTools'
import { applyTheme, buildThemeColors, getThemes, copyTheme } from '@renderer/store/utils'
// import { appSetting, updateSetting } from '@renderer/store/setting'
// import { applyTheme, getThemes } from '@renderer/store/utils'
import { createThemeColors } from '@common/theme/utils'
import useMainCoolor from './useMainCoolor'
import useAppBgColor from './useAppBgColor'
import useMainBgColor from './useMainBgColor'
import useAsideFontColor from './useAsideFontColor'
import useBadgePrimaryColor from './useBadgePrimaryColor'
import useBadgeSecondaryColor from './useBadgeSecondaryColor'
import useBadgeTertiaryColor from './useBadgeTertiaryColor'
import useCloseBtnColor from './useCloseBtnColor'
import useMinBtnColor from './useMinBtnColor'
import useHideBtnColor from './useHideBtnColor'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { removeTheme, saveTheme, showSelectDialog } from '@renderer/utils/ipc'
import { dialog } from '@renderer/plugins/Dialog'
import { themeInfo } from '@renderer/store'
export default {
name: 'ThemeSelectorModal',
props: {
modelValue: {
type: Boolean,
default: false,
},
themeId: {
type: String,
default: '',
},
},
emits: ['update:modelValue', 'submit'],
setup(props, { emit }) {
const themeName = ref('')
const isDark = ref(false)
const preview = ref(false)
const bgImg = ref('')
let originBgName = ''
let currentBgPath = ''
let theme
const getColor = (color, theme) => {
return color.startsWith('var')
? theme.config.themeColors[color.replace(/var\((.+)\)/, '$1')]
: color
}
const createPreview = () => {
if (!preview.value) return
window.setTheme(buildThemeColors(theme, themeInfo.dataPath))
}
// '--color-app-background': string
// '--color-main-background': string
// '--color-nav-font': string
// '--background-image': string
// '--background-image-position': string
// '--background-image-size': string
// // 关闭按钮颜色
// '--color-btn-hide': string
// '--color-btn-min': string
// '--color-btn-close': string
// // 徽章颜色
// '--color-badge-primary': string
// '--color-badge-secondary': string
// '--color-badge-tertiary': string
const { primary_color_ref, initMainColor, destroyMainColor } = useMainCoolor()
const { app_bg_color_ref, initAppBgColor, destroyAppBgColor, setAppBgColor } = useAppBgColor()
const { aside_font_color_ref, initAsideFontColor, destroyAsideFontColor, setAsideFontColor } = useAsideFontColor()
const { main_bg_color_ref, initMainBgColor, destroyMainBgColor, setMainBgColor } = useMainBgColor()
const { badge_primary_color_ref, initBadgePrimaryColor, destroyBadgePrimaryColor, setBadgePrimaryColor } = useBadgePrimaryColor()
const { badge_secondary_color_ref, initBadgeSecondaryColor, destroyBadgeSecondaryColor, setBadgeSecondaryColor } = useBadgeSecondaryColor()
const { badge_tertiary_color_ref, initBadgeTertiaryColor, destroyBadgeTertiaryColor, setBadgeTertiaryColor } = useBadgeTertiaryColor()
const { close_btn_color_ref, initCloseBtnColor, destroyCloseBtnColor, setCloseBtnColor } = useCloseBtnColor()
const { min_btn_color_ref, initMinBtnColor, destroyMinBtnColor, setMinBtnColor } = useMinBtnColor()
const { hide_btn_color_ref, initHideBtnColor, destroyHideBtnColor, setHideBtnColor } = useHideBtnColor()
let appBgColorOrigin
let appBgColor
let asideFontColorOrigin
let asideFontColor
let mainBgColorOrigin
let mainBgColor
let badgePrimaryColorOrigin
let badgePrimaryColor
let badgeSecondaryColorOrigin
let badgeSecondaryColor
let badgeTertiaryColorOrigin
let badgeTertiaryColor
let closeBtnColorOrigin
let closeBtnColor
let minBtnColorOrigin
let minBtnColor
let hideBtnColorOrigin
let hideBtnColor
const applyPrimaryColor = (color, isDark) => {
theme.config.themeColors = createThemeColors(color, isDark)
if (theme.config.extInfo['--color-app-background'].startsWith('var')) setAppBgColor(getColor(appBgColorOrigin, theme))
if (theme.config.extInfo['--color-nav-font'].startsWith('var')) setAsideFontColor(getColor(asideFontColorOrigin, theme))
if (theme.config.extInfo['--color-main-background'].startsWith('var')) setMainBgColor(getColor(mainBgColorOrigin, theme))
if (theme.config.extInfo['--color-badge-primary'].startsWith('var')) setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme))
if (theme.config.extInfo['--color-badge-secondary'].startsWith('var')) setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme))
if (theme.config.extInfo['--color-badge-tertiary'].startsWith('var')) setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme))
if (theme.config.extInfo['--color-btn-close'].startsWith('var')) setCloseBtnColor(getColor(closeBtnColorOrigin, theme))
if (theme.config.extInfo['--color-btn-min'].startsWith('var')) setMinBtnColor(getColor(minBtnColorOrigin, theme))
if (theme.config.extInfo['--color-btn-hide'].startsWith('var')) setHideBtnColor(getColor(hideBtnColorOrigin, theme))
createPreview()
}
const initColors = (_theme) => {
theme = _theme
// console.log(theme)
themeName.value = theme.name
isDark.value = theme.isDark
currentBgPath = ''
if (theme.config.extInfo['--background-image'] == 'none') {
bgImg.value = ''
originBgName = ''
} else {
bgImg.value = joinPath(themeInfo.dataPath, theme.config.extInfo['--background-image'])
originBgName = theme.config.extInfo['--background-image']
}
appBgColorOrigin = theme.config.extInfo['--color-app-background']
appBgColor = getColor(appBgColorOrigin, theme)
asideFontColorOrigin = theme.config.extInfo['--color-nav-font']
asideFontColor = getColor(asideFontColorOrigin, theme)
mainBgColorOrigin = theme.config.extInfo['--color-main-background']
mainBgColor = getColor(mainBgColorOrigin, theme)
badgePrimaryColorOrigin = theme.config.extInfo['--color-badge-primary']
badgePrimaryColor = getColor(badgePrimaryColorOrigin, theme)
badgeSecondaryColorOrigin = theme.config.extInfo['--color-badge-secondary']
badgeSecondaryColor = getColor(badgeSecondaryColorOrigin, theme)
badgeTertiaryColorOrigin = theme.config.extInfo['--color-badge-tertiary']
badgeTertiaryColor = getColor(badgeTertiaryColorOrigin, theme)
closeBtnColorOrigin = theme.config.extInfo['--color-btn-close']
closeBtnColor = getColor(closeBtnColorOrigin, theme)
minBtnColorOrigin = theme.config.extInfo['--color-btn-min']
minBtnColor = getColor(minBtnColorOrigin, theme)
hideBtnColorOrigin = theme.config.extInfo['--color-btn-hide']
hideBtnColor = getColor(hideBtnColorOrigin, theme)
initMainColor(theme.config.themeColors['--color-primary'], (color) => {
// console.log(color)
applyPrimaryColor(color, theme.isDark)
})
initAppBgColor(appBgColor, (color) => {
// console.log('appBgColor', color)
theme.config.extInfo['--color-app-background'] = color == appBgColor ? appBgColorOrigin : color
createPreview()
}, () => setAppBgColor(getColor(appBgColorOrigin, theme)))
initAsideFontColor(asideFontColor, (color) => {
theme.config.extInfo['--color-nav-font'] = color == asideFontColor ? asideFontColorOrigin : color
createPreview()
}, () => setAsideFontColor(getColor(asideFontColorOrigin, theme)))
initMainBgColor(mainBgColor, (color) => {
theme.config.extInfo['--color-main-background'] = color == mainBgColor ? mainBgColorOrigin : color
createPreview()
}, () => setMainBgColor(getColor(mainBgColorOrigin, theme)))
initBadgePrimaryColor(badgePrimaryColor, (color) => {
theme.config.extInfo['--color-badge-primary'] = color == badgePrimaryColor ? badgePrimaryColorOrigin : color
createPreview()
}, () => setBadgePrimaryColor(getColor(badgePrimaryColorOrigin, theme)))
initBadgeSecondaryColor(badgeSecondaryColor, (color) => {
theme.config.extInfo['--color-badge-secondary'] = color == badgeSecondaryColor ? badgeSecondaryColorOrigin : color
createPreview()
}, () => setBadgeSecondaryColor(getColor(badgeSecondaryColorOrigin, theme)))
initBadgeTertiaryColor(badgeTertiaryColor, (color) => {
theme.config.extInfo['--color-badge-tertiary'] = color == badgeTertiaryColor ? badgeTertiaryColorOrigin : color
createPreview()
}, () => setBadgeTertiaryColor(getColor(badgeTertiaryColorOrigin, theme)))
initCloseBtnColor(closeBtnColor, (color) => {
theme.config.extInfo['--color-btn-close'] = color == closeBtnColor ? closeBtnColorOrigin : color
createPreview()
}, () => setCloseBtnColor(getColor(closeBtnColorOrigin, theme)))
initMinBtnColor(minBtnColor, (color) => {
theme.config.extInfo['--color-btn-min'] = color == minBtnColor ? minBtnColorOrigin : color
createPreview()
}, () => setMinBtnColor(getColor(minBtnColorOrigin, theme)))
initHideBtnColor(hideBtnColor, (color) => {
theme.config.extInfo['--color-btn-hide'] = color == hideBtnColor ? hideBtnColorOrigin : color
createPreview()
}, () => setHideBtnColor(getColor(hideBtnColorOrigin, theme)))
createPreview()
}
const destroyColors = () => {
destroyMainColor()
destroyAppBgColor()
destroyAsideFontColor()
destroyMainBgColor()
destroyBadgePrimaryColor()
destroyBadgeSecondaryColor()
destroyBadgeTertiaryColor()
destroyCloseBtnColor()
destroyMinBtnColor()
destroyHideBtnColor()
}
watch(() => props.modelValue, (visible) => {
nextTick(() => {
getThemes(({ themes, userThemes }) => {
if (visible) {
if (props.themeId) {
const theme = userThemes.find(t => t.id == props.themeId)
if (theme) {
initColors(copyTheme(theme))
return
}
}
const theme = copyTheme(themes[0])
theme.id = 'user_theme_' + Date.now()
theme.name = ''
theme.isCustom = true
initColors(theme)
} else {
destroyColors()
// 移除临时保存的背景
if (currentBgPath) removeFile(currentBgPath).catch(_ => _)
}
})
})
})
const selectBgImg = async() => {
const result = await showSelectDialog({
title: window.i18n.t('theme_edit_modal__select_bg_file'),
properties: ['openFile'],
filters: [
{
name: 'Image File',
extensions: [
'jpg', 'jpeg', 'jfif', 'pjpeg',
'pjp', 'png', 'apng', 'avif', 'gif', 'svg',
'webp', 'bmp'],
},
],
})
if (result.canceled) return
const path = result.filePaths[0]
const fileName = `${theme.id}_${Date.now()}${extname(path)}`
const tempDir = joinPath(themeInfo.dataPath, 'temp')
const bgPath = joinPath(tempDir, fileName)
if (!await checkPath(tempDir)) await createDir(tempDir)
await copyFile(path, bgPath)
currentBgPath = bgImg.value = bgPath
theme.config.extInfo['--background-image'] = 'temp/' + fileName
createPreview()
}
const removeBgImg = async() => {
if (currentBgPath) {
removeFile(currentBgPath)
currentBgPath = ''
}
bgImg.value = ''
theme.config.extInfo['--background-image'] = 'none'
createPreview()
}
const handleDark = (val) => {
theme.isDark = val
applyPrimaryColor(theme.config.themeColors['--color-primary'], theme.isDark)
}
const handlePreview = (val) => {
if (val) {
createPreview()
} else {
applyTheme(appSetting['theme.id'], appSetting['theme.lightId'], appSetting['theme.darkId'], themeInfo.dataPath)
}
}
const handleCancel = () => {
handlePreview(false)
emit('update:modelValue', false)
}
const handleSubmit = async() => {
if (!themeName.value) return
theme.name = themeName.value.substring(0, 20)
// 保存新背景
if (currentBgPath) {
const name = basename(currentBgPath)
await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))
theme.config.extInfo['--background-image'] = name
}
// 移除旧背景
if (originBgName && theme.config.extInfo['--background-image'] != originBgName) removeFile(joinPath(themeInfo.dataPath, originBgName))
if (props.themeId) {
const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)
if (index > -1) themeInfo.userThemes.splice(index, 1, theme)
} else themeInfo.userThemes.push(theme)
handlePreview(false)
await saveTheme(theme)
emit('submit')
emit('update:modelValue', false)
}
const handleRemove = async() => {
const confirm = await dialog.confirm({
message: window.i18n.t('theme_edit_modal__remove_tip'),
cancelButtonText: window.i18n.t('cancel_button_text'),
confirmButtonText: window.i18n.t('confirm_button_text'),
})
if (!confirm) return
let isRequireUpdateSetting = false
const newSetting = {}
if (appSetting['theme.id'] == props.themeId) {
newSetting['theme.id'] = 'green'
isRequireUpdateSetting = true
}
if (theme.isDark) {
if (appSetting['theme.darkId'] == props.themeId) {
newSetting['theme.darkId'] = 'black'
isRequireUpdateSetting = true
}
} else {
if (appSetting['theme.lightId'] == props.themeId) {
newSetting['theme.lightId'] = 'green'
isRequireUpdateSetting = true
}
}
if (isRequireUpdateSetting) updateSetting(newSetting)
if (originBgName) removeFile(joinPath(themeInfo.dataPath, originBgName))
await removeTheme(props.themeId)
const index = themeInfo.userThemes.findIndex(t => t.id == theme.id)
console.log(index)
if (index > -1) themeInfo.userThemes.splice(index, 1)
handlePreview(false)
emit('submit')
emit('update:modelValue', false)
}
const handleSaveNew = async() => {
if (!themeName.value) return
theme.name = themeName.value.substring(0, 20)
theme.id = 'user_theme_' + Date.now()
// 保存新背景
if (currentBgPath) {
const name = basename(currentBgPath)
await moveFile(currentBgPath, joinPath(themeInfo.dataPath, name))
theme.config.extInfo['--background-image'] = name
} else if (bgImg.value) {
const fileName = `${theme.id}_${Date.now()}${extname(bgImg.value)}`
await copyFile(bgImg.value, joinPath(themeInfo.dataPath, fileName))
theme.config.extInfo['--background-image'] = fileName
}
themeInfo.userThemes.push(theme)
handlePreview(false)
await saveTheme(theme)
emit('submit')
emit('update:modelValue', false)
}
return {
themeName,
bgImg,
isDark,
handleDark,
preview,
handlePreview,
handleCancel,
handleSubmit,
handleRemove,
handleSaveNew,
selectBgImg,
removeBgImg,
primary_color_ref,
app_bg_color_ref,
main_bg_color_ref,
aside_font_color_ref,
badge_primary_color_ref,
badge_secondary_color_ref,
badge_tertiary_color_ref,
close_btn_color_ref,
min_btn_color_ref,
hide_btn_color_ref,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
// padding: 15px;
// max-width: 400px;
min-width: 300px;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
h2 {
flex: none;
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
padding: 15px;
}
h3 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
padding-bottom: 15px;
font-size: 15px;
}
}
.content {
flex: auto;
padding: 15px 0;
font-size: 14px;
gap: 5px;
display: flex;
flex-flow: column nowrap;
}
.group {
}
.groupTitle {
padding: 20px;
display: flex;
flex-flow: row wrap;
.title {
margin-right: 5px;
}
}
.groupContent {
display: flex;
flex-flow: row wrap;
}
.item {
padding: 0 20px;
width: 60px;
display: flex;
flex-flow: column nowrap;
align-items: center;
}
.base {
.color {
width: 100%;
}
}
.color {
width: 80%;
aspect-ratio: 1 / 1;
background-color: var(--pcr-color);
border-radius: @radius-border;
cursor: pointer;
transition: @transition-fast !important;
transition-property: background-color, opacity !important;
box-shadow: 0 0 3px var(--color-primary-light-100-alpha-300);
&:hover {
opacity: .7;
}
}
.label {
.mixin-ellipsis-2;
padding-top: 10px;
text-align: center;
line-height: 1.1;
}
.bg {
width: 0;
flex: 1 1 auto;
// width: 180px;
min-width: 60px;
max-width: 200px;
}
.bgImg {
width: 100%;
height: 60px;
border: 1Px dashed var(--color-primary-light-100-alpha-300);
transition: .3s ease;
transition-property: border, color;
box-sizing: border-box;
border: 1px dashed var(--color-primary-light-100-alpha-300);
color: var(--color-primary-light-100-alpha-300);
position: relative;
border-radius: 5px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: @transition-fast !important;
transition-property: background-color, opacity !important;
overflow: hidden;
&:hover {
opacity: .7;
}
&.hasBg {
border: none;
.removeBtn {
display: block;
}
}
.img {
width: 100%;
height: 100%;
object-fit: cover;
}
.removeBtn {
position: absolute;
right: 0;
top: 0;
border: none;
cursor: pointer;
padding: 6px 8px;
background-color: rgba(0, 0, 0, 0.6);
color: rgba(255, 255, 255, 0.9);
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
display: none;
svg {
height: .7em;
}
&:hover {
background-color: rgba(0, 0, 0, 0.7);
}
&:active {
background-color: rgba(0, 0, 0, 0.8);
}
}
.icon {
// position: absolute;
// font-size: 16px;
width: auto;
height: 66%;
}
}
@control-btn-width: @height-toolbar * .26;
.controlBtn {
display: flex;
-webkit-app-region: no-drag;
align-items: center;
padding: 0 @control-btn-width;
flex-direction: row-reverse;
// height: @height-toolbar * .7;
transition: opacity @transition-normal;
opacity: .5;
&:hover {
opacity: .8;
.controlBtnIcon {
opacity: 1;
}
}
button {
display: flex;
position: relative;
background: none;
border: none;
outline: none;
padding: 1px;
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
width: @control-btn-width;
height: @control-btn-width;
border-radius: 50%;
color: var(--color-font);
+ button {
margin-right: (@control-btn-width / 2);
}
&.hide {
background-color: var(--color-btn-hide);
}
&.min, &.fullscreenExit {
background-color: var(--color-btn-min);
}
// &.max {
// background-color: var(--color-btn-max);
// }
&.close {
background-color: var(--color-btn-close);
}
}
}
.controlBtnIcon {
opacity: 0;
transition: opacity 0.2s ease-in-out;
}
.note {
padding: 8px 15px;
font-size: 13px;
line-height: 1.25;
color: var(--color-font);
// p {
// + p {
// margin-top: 5px;
// }
// }
}
.footer {
padding: 15px;
display: flex;
flex-flow: row nowrap;
align-items: center;
justify-content: space-between;
gap: 15px;
font-size: 14px;
.subContent {
display: flex;
flex-flow: row nowrap;
align-items: center;
gap: 15px;
}
.checkbox {
flex: none;
}
.input {
min-width: 0;
flex: 0 1 auto;
}
}
.btn {
// box-sizing: border-box;
// margin-left: 15px;
// margin-bottom: 15px;
// height: 36px;
// line-height: 36px;
// padding: 0 10px !important;
min-width: 70px;
// .mixin-ellipsis-1;
}
</style>
@@ -0,0 +1,32 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const app_bg_color_ref = ref(null)
let tools: PickrTools | null
const initAppBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!app_bg_color_ref.value) return
tools = pickrTools.create(app_bg_color_ref.value, color, [
'rgba(255, 255, 255, 0)',
'rgba(255, 255, 255, 0.15)',
'rgba(255, 255, 255, 0.15)',
'rgba(21.34, 18.92, 44.61, 0.81)',
], changed, reset)
}
const destroyAppBgColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setAppBgColor = (color: string) => {
tools?.setColor(color)
}
return {
app_bg_color_ref,
initAppBgColor,
destroyAppBgColor,
setAppBgColor,
}
}
@@ -0,0 +1,27 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const aside_font_color_ref = ref(null)
let tools: PickrTools | null
const initAsideFontColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!aside_font_color_ref.value) return
tools = pickrTools.create(aside_font_color_ref.value, color, null, changed, reset)
}
const destroyAsideFontColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setAsideFontColor = (color: string) => {
tools?.setColor(color)
}
return {
aside_font_color_ref,
initAsideFontColor,
destroyAsideFontColor,
setAsideFontColor,
}
}
@@ -0,0 +1,27 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const badge_primary_color_ref = ref(null)
let tools: PickrTools | null
const initBadgePrimaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!badge_primary_color_ref.value) return
tools = pickrTools.create(badge_primary_color_ref.value, color, null, changed, reset)
}
const destroyBadgePrimaryColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setBadgePrimaryColor = (color: string) => {
tools?.setColor(color)
}
return {
badge_primary_color_ref,
initBadgePrimaryColor,
destroyBadgePrimaryColor,
setBadgePrimaryColor,
}
}
@@ -0,0 +1,41 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const badge_secondary_color_ref = ref(null)
let tools: PickrTools | null
const initBadgeSecondaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!badge_secondary_color_ref.value) return
tools = pickrTools.create(badge_secondary_color_ref.value, color, [
'rgba(75, 174, 213, 1)',
'rgba(92, 191, 155, 1)',
'rgba(66, 6, 150, 7, 171, 1)',
'rgba(158, 212, 88, 1)',
'rgba(223, 187, 107, 1)',
'rgba(245, 182, 132, 1)',
'rgba(229, 163, 159, 1)',
'rgba(177, 155, 159, 1)',
'rgba(99, 118, 162, 1)',
'rgba(176, 128, 219, 1)',
'rgba(175, 148, 121, 1)',
'rgba(223, 187, 107, 1)',
'rgba(53.08, 107.67, 129.18, 1)',
], changed, reset)
}
const destroyBadgeSecondaryColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setBadgeSecondaryColor = (color: string) => {
tools?.setColor(color)
}
return {
badge_secondary_color_ref,
initBadgeSecondaryColor,
destroyBadgeSecondaryColor,
setBadgeSecondaryColor,
}
}
@@ -0,0 +1,41 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const badge_tertiary_color_ref = ref(null)
let tools: PickrTools | null
const initBadgeTertiaryColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!badge_tertiary_color_ref.value) return
tools = pickrTools.create(badge_tertiary_color_ref.value, color, [
'rgba(231, 170, 54, 1)',
'rgba(92, 191, 155, 1)',
'rgba(54, 196, 231, 1)',
'rgba(158, 212, 88, 1)',
'rgba(223, 187, 107, 1)',
'rgba(245, 182, 132, 1)',
'rgba(229, 163, 159, 1)',
'rgba(177, 155, 159, 1)',
'rgba(99, 118, 162, 1)',
'rgba(176, 128, 219, 1)',
'rgba(175, 148, 121, 1)',
'rgba(223, 187, 107, 1)',
'rgba(49.11, 161.05, 135.64, 1)',
], changed, reset)
}
const destroyBadgeTertiaryColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setBadgeTertiaryColor = (color: string) => {
tools?.setColor(color)
}
return {
badge_tertiary_color_ref,
initBadgeTertiaryColor,
destroyBadgeTertiaryColor,
setBadgeTertiaryColor,
}
}
@@ -0,0 +1,32 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const close_btn_color_ref = ref(null)
let tools: PickrTools | null
const initCloseBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!close_btn_color_ref.value) return
tools = pickrTools.create(close_btn_color_ref.value, color, [
'rgba(59, 194, 178, 1)',
'rgba(133, 196, 59, 1)',
'rgba(250, 180, 160, 1)',
'rgba(104.51, 72.55, 107.98, 1)',
], changed, reset)
}
const destroyCloseBtnColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setCloseBtnColor = (color: string) => {
tools?.setColor(color)
}
return {
close_btn_color_ref,
initCloseBtnColor,
destroyCloseBtnColor,
setCloseBtnColor,
}
}
@@ -0,0 +1,32 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const hide_btn_color_ref = ref(null)
let tools: PickrTools | null
const initHideBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!hide_btn_color_ref.value) return
tools = pickrTools.create(hide_btn_color_ref.value, color, [
'rgba(59, 194, 178, 1)',
'rgba(133, 196, 59, 1)',
'rgba(250, 180, 160, 1)',
'rgba(77.46, 103.73, 151.81, 1)',
], changed, reset)
}
const destroyHideBtnColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setHideBtnColor = (color: string) => {
tools?.setColor(color)
}
return {
hide_btn_color_ref,
initHideBtnColor,
destroyHideBtnColor,
setHideBtnColor,
}
}
@@ -0,0 +1,33 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const main_bg_color_ref = ref(null)
let tools: PickrTools | null
const initMainBgColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!main_bg_color_ref.value) return
tools = pickrTools.create(main_bg_color_ref.value, color, [
'rgba(255, 255, 255, 1)',
'rgba(19, 19, 19, 0.9)',
'rgba(255, 255, 255, 0.9)',
'rgba(255, 255, 255, 0.8)',
'rgba(25.82, 23.65, 46.6, 0.54)',
], changed, reset)
}
const destroyMainBgColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setMainBgColor = (color: string) => {
tools?.setColor(color)
}
return {
main_bg_color_ref,
initMainBgColor,
destroyMainBgColor,
setMainBgColor,
}
}
@@ -0,0 +1,39 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const primary_color_ref = ref(null)
let tools: PickrTools | null
const initMainColor = (color: string, changed: (color: string) => void) => {
if (!primary_color_ref.value) return
tools = pickrTools.create(primary_color_ref.value, color, [
'rgba(77, 175, 124, 1)',
'rgba(52, 152, 219, 1)',
'rgba(77, 131, 175, 1)',
'rgba(245, 171, 53, 1)',
'rgba(214, 69, 65, 1)',
'rgba(241, 130, 141, 1)',
'rgba(155, 89, 182, 1)',
'rgba(108, 122, 137, 1)',
'rgba(51, 110, 123, 1)',
'rgba(79, 98, 208, 1)',
'rgba(150, 150, 150, 1)',
'rgba(74, 55, 82, 1)',
'rgba(87, 144, 167, 1)',
'rgba(192, 57, 43, 1)',
'rgba(113.52, 107.21, 166.13, 1)',
], changed, () => {})
}
const destroyMainColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
return {
primary_color_ref,
initMainColor,
destroyMainColor,
}
}
@@ -0,0 +1,32 @@
import { ref } from '@common/utils/vueTools'
import { pickrTools, PickrTools } from '@renderer/utils/pickrTools'
export default () => {
const min_btn_color_ref = ref(null)
let tools: PickrTools | null
const initMinBtnColor = (color: string, changed: (color: string) => void, reset: () => void) => {
if (!min_btn_color_ref.value) return
tools = pickrTools.create(min_btn_color_ref.value, color, [
'rgba(59, 194, 178, 1)',
'rgba(133, 196, 59, 1)',
'rgba(250, 180, 160, 1)',
'rgba(116, 87, 152, 1)',
], changed, reset)
}
const destroyMinBtnColor = () => {
if (!tools) return
tools.destroy()
tools = null
}
const setMinBtnColor = (color: string) => {
tools?.setColor(color)
}
return {
min_btn_color_ref,
initMinBtnColor,
destroyMinBtnColor,
setMinBtnColor,
}
}
@@ -0,0 +1,235 @@
<template>
<material-modal :show="modelValue" bg-close="bg-close" teleport="#view" @close="$emit('update:modelValue', false)">
<main :class="$style.main">
<h2>{{ $t('theme_selector_modal__title') }}</h2>
<div class="scroll" :class="$style.content">
<div>
<h3>{{ $t('theme_selector_modal__light_title') }}</h3>
<ul :class="$style.theme">
<li
v-for="theme in themeInfo.themeLights" :key="theme.id"
:style="theme.styles" :aria-label="theme.name"
:class="[{[$style.active]: appSetting['theme.lightId'] == theme.id}]" @click="setLightId(theme.id)"
>
<span :class="$style.bg" />
<label>{{ theme.name }}</label>
</li>
</ul>
</div>
<div>
<h3>{{ $t('theme_selector_modal__dark_title') }}</h3>
<ul :class="$style.theme">
<li
v-for="theme in themeInfo.themeDarks" :key="theme.id"
:style="theme.styles" :aria-label="theme.name"
:class="[{[$style.active]: appSetting['theme.darkId'] == theme.id}]" @click="setDarkId(theme.id)"
>
<span :class="$style.bg" />
<label>{{ theme.name }}</label>
</li>
</ul>
</div>
</div>
<div :class="$style.note">
<p>{{ $t('theme_selector_modal__title_tip') }}</p>
</div>
</main>
</material-modal>
</template>
<script>
import { joinPath } from '@common/utils/nodejs'
import { markRaw, reactive, watch } from '@common/utils/vueTools'
import { appSetting, updateSetting } from '@renderer/store/setting'
import { applyTheme, getThemes } from '@renderer/store/utils'
export default {
name: 'ThemeSelectorModal',
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup(props) {
const themeInfo = reactive({
themeLights: [],
themeDarks: [],
})
let dataPath = ''
watch(() => props.modelValue, (val) => {
if (!val) return
getThemes((info) => {
// console.log(info)
const themes = [...info.themes, ...info.userThemes]
const lights = []
const darks = themes.filter(t => {
if (t.isDark) return true
lights.push(t)
return false
})
dataPath = info.dataPath
themeInfo.themeLights = lights.map(t => {
return {
id: t.id,
// @ts-expect-error
name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),
styles: {
'--color-primary-theme': t.config.themeColors['--color-theme'],
'--background-image-theme': t.isCustom
? t.config.extInfo['--background-image'] == 'none'
? 'none'
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`
: t.config.extInfo['--background-image'],
},
}
})
themeInfo.themeDarks = markRaw(darks.map(t => {
return {
id: t.id,
// @ts-expect-error
name: t.isCustom ? t.name : window.i18n.t('theme_' + t.id),
styles: {
'--color-primary-theme': t.config.themeColors['--color-theme'],
'--background-image-theme': t.isCustom
? t.config.extInfo['--background-image'] == 'none'
? 'none'
: `url(file:///${joinPath(info.dataPath, t.config.extInfo['--background-image']).replaceAll('\\', '/')})`
: t.config.extInfo['--background-image'],
},
}
}))
})
})
const setLightId = (id) => {
if (appSetting['theme.lightId'] == id) return
updateSetting({ 'theme.lightId': id })
if (appSetting['theme.id'] == 'auto') applyTheme('auto', id, appSetting['theme.darkId'], dataPath)
}
const setDarkId = (id) => {
if (appSetting['theme.darkId'] == id) return
updateSetting({ 'theme.darkId': id })
if (appSetting['theme.id'] == 'auto') applyTheme('auto', appSetting['theme.lightId'], id, dataPath)
}
return {
appSetting,
themeInfo,
setLightId,
setDarkId,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
// padding: 15px;
// max-width: 400px;
min-width: 300px;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
h2 {
flex: none;
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
padding: 15px;
}
h3 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
padding-bottom: 15px;
font-size: 15px;
}
}
.content {
flex: auto;
padding: 15px;
display: flex;
flex-flow: column nowrap;
gap: 15px;
}
.theme {
display: flex;
flex-flow: row wrap;
// padding: 0 15px;
li {
display: flex;
flex-flow: column nowrap;
align-items: center;
cursor: pointer;
// color: var(--color-primary);
margin-right: 30px;
transition: color .3s ease;
margin-bottom: 15px;
width: 56px;
&:last-child {
margin-right: 0;
}
&.active {
color: var(--color-primary-theme);
.bg {
border-color: var(--color-primary-theme);
}
}
.bg {
display: block;
width: 36px;
height: 36px;
margin-bottom: 5px;
border: 2px solid transparent;
padding: 2px;
transition: border-color .3s ease;
border-radius: 5px;
&:after {
display: block;
content: ' ';
width: 100%;
height: 100%;
border-radius: @radius-border;
background-position: center;
background-size: cover;
background-repeat: no-repeat;
background-color: var(--color-primary-theme);
background-image: var(--background-image-theme);
}
}
label {
width: 100%;
text-align: center;
height: 1.2em;
font-size: 14px;
.mixin-ellipsis-1;
}
}
}
.note {
padding: 8px 15px;
font-size: 13px;
line-height: 1.25;
color: var(--color-font);
// p {
// + p {
// margin-top: 5px;
// }
// }
}
</style>
@@ -0,0 +1,225 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close @close="handleClose" teleport="#view")
main(:class="$style.main")
h2 {{$t('user_api__title')}}
ul.scroll(v-if="apiList.length" :class="$style.content")
li(:class="[$style.listItem, {[$style.active]: appSetting['common.apiSource'] == api.id}]" v-for="(api, index) in apiList" :key="api.id")
div(:class="$style.listLeft")
h3 {{api.name}}
p {{api.description}}
div
base-checkbox(:class="$style.checkbox" :id="`user_api_${api.id}`" v-model="api.allowShowUpdateAlert" @change="handleChangeAllowUpdateAlert(api, $event)" :label="$t('user_api__allow_show_update_alert')")
base-btn(:class="$style.listBtn" outline :aria-label="$t('user_api__btn_remove')" @click.stop="handleRemove(index)")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(v-else :class="$style.content")
div(:class="$style.noitem") {{$t('user_api__noitem')}}
div(:class="$style.note")
p(:class="[$style.ruleLink]")
| {{$t('user_api__readme')}}
span.hover.underline(@click="handleOpenUrl('https://lyswhut.github.io/lx-music-doc/desktop/custom-source')" aria-label="https://lxmusic.toside.cn/desktop/custom-source") FAQ
p {{$t('user_api__note')}}
div(:class="$style.footer")
base-btn(:class="$style.footerBtn" @click="handleImport") {{$t('user_api__btn_import')}}
//- base-btn(:class="$style.footerBtn" @click="handleExport") {{$t('user_api__btn_export')}}
</template>
<script>
import { importUserApi, removeUserApi, showSelectDialog, setAllowShowUserApiUpdateAlert } from '@renderer/utils/ipc'
import { readFile } from '@common/utils/nodejs'
import { openUrl } from '@common/utils/electron'
import apiSourceInfo from '@renderer/utils/musicSdk/api-source-info'
import { userApi } from '@renderer/store'
import { appSetting, updateSetting } from '@renderer/store/setting'
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
setup() {
return {
userApi,
appSetting,
}
},
computed: {
apiList() {
return this.userApi.list
},
},
methods: {
handleImport() {
if (this.userApi.list.length > 20) {
this.$dialog({
message: this.$t('user_api__max_tip'),
confirmButtonText: this.$t('ok'),
})
return
}
showSelectDialog({
title: this.$t('user_api__import_file'),
properties: ['openFile'],
filters: [
{ name: 'LX API File', extensions: ['js'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(result => {
if (result.canceled) return
return readFile(result.filePaths[0]).then(data => {
return importUserApi(data.toString()).then(({ apiList }) => {
userApi.list = apiList
})
})
})
},
handleExport() {
},
async handleRemove(index) {
const api = this.apiList[index]
if (!api) return
if (appSetting['common.apiSource'] == api.id) {
let backApi = apiSourceInfo.find(api => !api.disabled)
if (backApi) updateSetting({ 'common.apiSource': backApi.id })
}
userApi.list = await removeUserApi([api.id])
},
handleClose() {
this.$emit('update:modelValue', false)
},
handleOpenUrl(url) {
openUrl(url)
},
handleChangeAllowUpdateAlert(api, enable) {
setAllowShowUserApiUpdateAlert(api.id, enable)
},
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
padding: 15px;
max-width: 400px;
min-width: 300px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
min-height: 0;
// max-height: 100%;
// overflow: hidden;
h2 {
font-size: 16px;
color: var(--color-font);
line-height: 1.3;
text-align: center;
}
}
.name {
color: var(--color-primary);
}
.checkbox {
margin-top: 3px;
font-size: 14px;
opacity: .86;
}
.content {
flex: auto;
min-height: 100px;
max-height: 100%;
margin-top: 15px;
}
.listItem {
display: flex;
flex-flow: row nowrap;
align-items: center;
transition: background-color 0.2s ease;
padding: 10px;
border-radius: @radius-border;
&:hover {
background-color: var(--color-primary-background-hover);
}
&.active {
background-color: var(--color-primary-background-active);
}
h3 {
font-size: 15px;
color: var(--color-font);
word-break: break-all;
}
p {
margin-top: 5px;
font-size: 14px;
color: var(--color-font-label);
word-break: break-all;
}
}
.noitem {
height: 100px;
font-size: 18px;
color: var(--color-font-label);
display: flex;
justify-content: center;
align-items: center;
}
.listLeft {
flex: auto;
min-width: 0;
display: flex;
flex-flow: column nowrap;
justify-content: center;
}
.listBtn {
flex: none;
height: 30px;
width: 30px;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
svg {
width: 60%;
}
}
.note {
margin-top: 15px;
font-size: 12px;
line-height: 1.25;
color: var(--color-font);
p {
+ p {
margin-top: 5px;
}
}
}
.footer {
margin-top: 15px;
display: flex;
flex-flow: row nowrap;
}
.footerBtn {
flex: auto;
height: 36px;
line-height: 36px;
padding: 0 10px !important;
width: 150px;
.mixin-ellipsis-1;
+ .footerBtn {
margin-left: 15px;
}
}
.ruleLink {
.mixin-ellipsis-1;
}
</style>
+303
View File
@@ -0,0 +1,303 @@
<template>
<div :class="$style.main">
<div class="scroll" :class="$style.toc">
<ul :class="$style.tocList" role="toolbar">
<li v-for="h2 in tocList" :key="h2.id" :class="$style.tocListItem" role="presentation">
<h2
:class="[$style.tocH2, {[$style.active]: avtiveComponentName == h2.id }]"
role="tab" :aria-selected="avtiveComponentName == h2.id"
:aria-label="h2.title" ignore-tip @click="toggleTab(h2.id)"
>
{{ h2.title }}
</h2>
<!-- <ul v-if="h2.children.length" :class="$style.tocList">
<li v-for="h3 in h2.children" :key="h3.id" :class="$style.tocSubListItem">
<h3 :class="[$style.tocH3, toc.activeId == h3.id ? $style.active : null]" :aria-label="h3.title">
<a :href="'#' + h3.id" @click.stop="toc.activeId = h3.id">{{ h3.title }}</a>
</h3>
</li>
</ul> -->
</li>
</ul>
</div>
<div ref="dom_content_ref" class="scroll" :class="$style.setting">
<dl>
<component :is="avtiveComponentName" />
<!-- <SettingBasic />
<SettingPlay />
<SettingPlayDetail />
<SettingDesktopLyric />
<SettingSearch />
<SettingList />
<SettingDownload />
<SettingSync />
<SettingHotKey />
<SettingNetwork />
<SettingOdc />
<SettingBackup />
<SettingOther />
<SettingUpdate />
<SettingAbout /> -->
</dl>
</div>
</div>
</template>
<script>
import { ref, computed, nextTick } from '@common/utils/vueTools'
// import { currentStting } from './setting'
import { useI18n } from '@renderer/plugins/i18n'
import { useRoute } from '@common/utils/vueRouter'
import SettingBasic from './components/SettingBasic'
import SettingPlay from './components/SettingPlay'
import SettingPlayDetail from './components/SettingPlayDetail'
import SettingDesktopLyric from './components/SettingDesktopLyric'
import SettingSearch from './components/SettingSearch'
import SettingList from './components/SettingList'
import SettingDownload from './components/SettingDownload'
import SettingSync from './components/SettingSync'
import SettingHotKey from './components/SettingHotKey'
import SettingNetwork from './components/SettingNetwork'
import SettingOdc from './components/SettingOdc'
import SettingBackup from './components/SettingBackup'
import SettingOther from './components/SettingOther'
import SettingUpdate from './components/SettingUpdate'
import SettingAbout from './components/SettingAbout'
export default {
name: 'Setting',
components: {
SettingBasic,
SettingPlay,
SettingPlayDetail,
SettingDesktopLyric,
SettingSearch,
SettingList,
SettingDownload,
SettingSync,
SettingHotKey,
SettingNetwork,
SettingOdc,
SettingBackup,
SettingOther,
SettingUpdate,
SettingAbout,
},
setup() {
const t = useI18n()
const route = useRoute()
const dom_content_ref = ref(null)
const tocList = computed(() => {
return [
{ id: 'SettingBasic', title: t('setting__basic') },
{ id: 'SettingPlay', title: t('setting__play') },
{ id: 'SettingPlayDetail', title: t('setting__play_detail') },
{ id: 'SettingDesktopLyric', title: t('setting__desktop_lyric') },
{ id: 'SettingSearch', title: t('setting__search') },
{ id: 'SettingList', title: t('setting__list') },
{ id: 'SettingDownload', title: t('setting__download') },
{ id: 'SettingSync', title: t('setting__sync') },
{ id: 'SettingHotKey', title: t('setting__hot_key') },
{ id: 'SettingNetwork', title: t('setting__network') },
{ id: 'SettingOdc', title: t('setting__odc') },
{ id: 'SettingBackup', title: t('setting__backup') },
{ id: 'SettingOther', title: t('setting__other') },
{ id: 'SettingUpdate', title: t('setting__update') },
{ id: 'SettingAbout', title: t('setting__about') },
]
})
const avtiveComponentName = ref(route.query.name && tocList.value.some(t => t.id == route.query.name)
? route.query.name
: tocList.value[0].id)
const toggleTab = id => {
avtiveComponentName.value = id
nextTick(() => {
dom_content_ref.value?.scrollTo({
top: 0,
behavior: 'smooth',
})
})
}
return {
tocList,
avtiveComponentName,
dom_content_ref,
toggleTab,
}
},
// mounted() {
// this.initTOC()
// },
// methods: {
// initTOC() {
// const list = this.$refs.dom_setting_list.children
// const toc = []
// let prevTitle
// for (const item of list) {
// if (item.tagName == 'DT') {
// prevTitle = {
// title: item.innerText.replace(/[((].+?[))]/, ''),
// id: item.getAttribute('id'),
// dom: item,
// children: [],
// }
// toc.push(prevTitle)
// continue
// }
// const h3 = item.querySelector('h3')
// if (h3) {
// prevTitle.children.push({
// title: h3.innerText.replace(/[((].+?[))]/, ''),
// id: h3.getAttribute('id'),
// dom: h3,
// })
// }
// }
// console.log(toc)
// this.toc.list = toc
// },
// handleListScroll(event) {
// // console.log(event.target.scrollTop)
// },
// },
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.main {
display: flex;
flex-flow: row nowrap;
height: 100%;
border-top: var(--color-list-header-border-bottom);
}
.toc {
flex: 0 0 16%;
overflow-y: scroll;
}
.tocH2 {
line-height: 1.5;
.mixin-ellipsis-1;
font-size: 13px;
color: var(--color-font);
padding: 8px 10px;
transition: @transition-fast;
transition-property: background-color, color;
&:not(.active) {
cursor: pointer;
&:hover {
background-color: var(--color-button-background-hover);
}
}
&.active {
color: var(--color-primary);
}
}
// .tocH3 {
// font-size: 13px;
// opacity: .8;
// }
// .tocList {
// .tocList {
// // padding-left: 15px;
// }
// }
// .tocSubListItem {
// padding-top: 10px;
// }
.setting {
padding: 0 15px 15px;
font-size: 14px;
box-sizing: border-box;
overflow-y: auto;
height: 100%;
position: relative;
width: 100%;
:global {
dt {
border-left: 5px solid var(--color-primary-alpha-700);
padding: 3px 7px;
margin: 15px 0;
+ dd h3 {
margin-top: 0;
}
}
dd {
// margin-left: 15px;
// font-size: 13px;
> div {
padding: 0 15px;
}
}
h3 {
font-size: 12px;
margin: 25px 0 15px;
}
p {
padding: 3px 0;
line-height: 1.3;
.btn {
+ .btn {
margin-left: 10px;
}
}
}
.help-btn {
padding: 0;
margin: 0 0.4em;
border: none;
background: none;
color: var(--color-button-font);
cursor: pointer;
transition: opacity 0.2s ease;
&:hover {
opacity: 0.7;
}
}
.help-icon {
margin: 0 0.4em;
}
}
}
// .btn-content {
// display: inline-block;
// transition: @transition-theme;
// transition-property: opacity, transform;
// opacity: 1;
// transform: scale(1);
// &.hide {
// opacity: 0;
// transform: scale(0);
// }
// }
// :global(dt):target, :global(h3):target {
// animation: highlight 1s ease;
// }
// @keyframes highlight {
// from { background: yellow; }
// to { background: transparent; }
// }
</style>