升级到 vue3

This commit is contained in:
lyswhut
2021-12-03 22:11:11 +08:00
parent c6635e554a
commit 8059d9cd73
260 changed files with 14443 additions and 10979 deletions
@@ -0,0 +1,75 @@
<template lang="pug">
dt#about {{$t('setting__about')}}
dd
p.small
| 本软件完全免费,代码已开源,开源地址:
span.hover.underline(:tips="$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(:tips="$t('setting__click_open')" @click="openUrl('https://www.lanzoui.com/b0bf2cfa/')") 网盘地址
| &nbsp;&nbsp;密码:
span.hover(:tips="$t('setting__click_copy')" @click="clipboardWriteText('glqw')") glqw
p.small
| 软件的常见问题可转至:
span.hover.underline(:tips="$t('setting__click_open')" @click="openUrl('https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md')") 常见问题
p.small
strong 本软件没有客服
| ,但我们整理了一些常见的使用问题,
strong 仔细 仔细 仔细
| 地阅读常见问题后,
p.small
| 仍有问题可加企鹅群&nbsp;
span.hover(:tips="$t('setting__click_open')" @click="openUrl('https://jq.qq.com/?_wv=1027&k=51ECeq2')") 830125506
| &nbsp;反馈
strong (为免满人,无事勿加,入群先看群公告)
| ,或到 GitHub 提交&nbsp;
span.hover.underline(:tips="$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(:tips="$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(:tips="$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 '@renderer/utils/vueTools'
import { isShowPact } from '@renderer/core/share'
import { openUrl, clipboardWriteText } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'About',
setup() {
const handleShowPact = () => {
isShowPact.value = true
}
return {
currentStting,
openUrl,
clipboardWriteText,
handleShowPact,
}
},
}
</script>
@@ -0,0 +1,243 @@
<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')}}
</template>
<script>
import { useCommit, useI18n, onBeforeUnmount, useGetter, toRaw } from '@renderer/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 {
getSetting,
saveSetting,
saveLxConfigFile,
readLxConfigFile,
selectDir,
openSaveDir,
} from '@renderer/utils'
import { currentStting } from '../setting'
import { getList } from '@renderer/core/share/utils'
export default {
name: 'Update',
setup() {
const { t } = useI18n()
const setting = useGetter('setting')
const settingVersion = useGetter('settingVersion')
const setSetting = useCommit('setSetting')
const setSettingVersion = useCommit('setSettingVersion')
const setList = useCommit('list', 'setList')
const handleUpdateSetting = (config) => {
currentStting.value = JSON.parse(JSON.stringify(config))
}
const refreshSetting = async(newSetting, newVersion) => {
await saveSetting(newSetting)
const { setting, version } = await getSetting()
setSetting(setting)
setSettingVersion(version)
}
const importAllData = async(path) => {
let allData
try {
allData = await readLxConfigFile(path)
} catch (error) {
return
}
if (allData.type !== 'allData') return
// 兼容0.6.2及以前版本的列表数据
if (allData.defaultList) return setList({ id: 'default', list: allData.defaultList.list, name: '试听列表' })
for (const list of allData.playList) {
setList(list)
}
const { version: settingVersion, setting } = mergeSetting(allData.setting)
setting.isAgreePact = false
await refreshSetting(setting, settingVersion)
}
const handleImportAllData = () => {
selectDir({
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
importAllData(result.filePaths[0])
})
}
const exportAllData = async(path) => {
let allData = JSON.parse(JSON.stringify({
type: 'allData',
setting: Object.assign({ version: settingVersion.value }, toRaw(setting.value)),
playList: [
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
],
}))
for await (const list of allData.playList) {
for await (const item of list.list) {
if (item.otherSource) delete item.otherSource
}
}
saveLxConfigFile(path, allData)
}
const handleExportAllData = () => {
openSaveDir({
title: t('setting__backup_all_export_desc'),
defaultPath: 'lx_datas.lxmc',
}).then(result => {
if (result.canceled) return
exportAllData(result.filePath)
})
}
const exportSetting = (path) => {
console.log(path)
const data = {
type: 'setting',
data: Object.assign({ version: settingVersion.value }, toRaw(setting.value)),
}
saveLxConfigFile(path, data)
}
const handleExportSetting = () => {
openSaveDir({
title: t('setting__backup_part_export_setting_desc'),
defaultPath: 'lx_setting.lxmc',
}).then(result => {
if (result.canceled) return
exportSetting(result.filePath)
})
}
const importSetting = async(path) => {
let settingData
try {
settingData = await readLxConfigFile(path)
} catch (error) {
return
}
if (settingData.type !== 'setting') return
const { version: settingVersion, setting } = mergeSetting(settingData.data)
setting.isAgreePact = false
refreshSetting(setting, settingVersion)
}
const handleImportSetting = () => {
selectDir({
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 = JSON.parse(JSON.stringify({
type: 'playList',
data: [
{ ...toRaw(defaultList), list: toRaw(getList(defaultList.id)) },
{ ...toRaw(loveList), list: toRaw(getList(loveList.id)) },
...userLists.map(l => ({ ...toRaw(l), list: toRaw(getList(l.id)) })),
],
}))
for await (const list of data.data) {
for await (const item of list.list) {
if (item.otherSource) delete item.otherSource
}
}
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 readLxConfigFile(path)
} catch (error) {
return
}
console.log(listData.type)
// 兼容0.6.2及以前版本的列表数据
if (listData.type === 'defautlList') return setList({ id: 'default', list: listData.data.list, name: '试听列表' })
if (listData.type !== 'playList') return
for (const list of listData.data) {
setList(list)
}
await refreshSetting(setting.value, settingVersion.value)
}
const handleImportPlayList = () => {
selectDir({
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
importPlayList(result.filePaths[0])
})
}
window.eventHub.on(eventBaseName.set_config, handleUpdateSetting)
onBeforeUnmount(() => {
window.eventHub.off(eventBaseName.set_config, handleUpdateSetting)
})
return {
currentStting,
handleExportPlayList,
handleImportPlayList,
handleExportSetting,
handleImportSetting,
handleExportAllData,
handleImportAllData,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,235 @@
<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 themes.list" :key="theme.id" :tips="$t('theme_' + theme.className)" @click="currentStting.themeId = theme.id" :class="[theme.className, {[$style.active]: themes.active == theme.id}]")
span
label {{$t('theme_' + theme.className)}}
dd
.gap-top.top
base-checkbox(id="setting_show_animate" v-model="currentStting.isShowAnimation" :label="$t('setting__basic_show_animation')")
.gap-top
base-checkbox(id="setting_animate" v-model="currentStting.randomAnimate" :label="$t('setting__basic_animation')")
.gap-top
base-checkbox(id="setting_to_tray" v-model="currentStting.tray.isShow" :label="$t('setting__basic_to_tray')")
dd(:tips="$t('setting__basic_source_title')")
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 v-model="currentStting.apiSource" :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(:tips="$t('setting__basic_window_size_title')")
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 v-model="currentStting.windowSizeId" :value="item.id" :label="$t('setting__basic_window_size_' + item.name)" :key="item.id")
dd(:tips="$t('setting__basic_lang_title')")
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 v-model="currentStting.langId" :value="item.locale" :label="item.name")
dd(:tips="$t('setting__basic_sourcename_title')")
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 v-model="currentStting.sourceNameType" :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 v-model="currentStting.controlBtnPosition" :value="item.id" :label="item.name")
teleport(to="#view")
user-api-modal(v-model="isShowUserApiModal")
</template>
<script>
import { computed, ref, useI18n, watch } from '@renderer/utils/vueTools'
import { themes as themeList, windowSizeList, apiSource, userApi } from '@renderer/core/share'
import { langList } from '@/lang'
import { currentStting } from '../setting'
import { setWindowSize } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import UserApiModal from './UserApiModal'
export default {
name: 'Basic',
components: {
UserApiModal,
},
setup() {
const { t, locale } = useI18n()
const themes = computed(() => {
return {
active: currentStting.value.themeId,
list: themeList,
}
})
watch(() => currentStting.value.apiSource, visible => {
apiSource.value = visible
})
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 == currentStting.value.apiSource ? `[${getApiStatus()}]` : ''}`,
status: api.status,
message: api.message,
disabled: false,
})),
]
})
watch(() => currentStting.value.windowSizeId, (index) => {
let info = index == null ? windowSizeList[2] : windowSizeList[index]
setWindowSize(info.width, info.height)
})
watch(() => currentStting.value.langId, (id) => {
locale.value = id
})
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') },
]
})
return {
currentStting,
themes,
apiSources,
isShowUserApiModal,
windowSizeList,
langList,
sourceNameTypes,
controlBtnPositionList,
}
},
}
</script>
<style lang="less" module>
@import '@renderer/assets/styles/layout.less';
.theme {
display: flex;
flex-flow: row wrap;
// padding: 0 15px;
margin-bottom: -20px;
li {
display: flex;
flex-flow: column nowrap;
align-items: center;
cursor: pointer;
// color: @color-theme;
margin-right: 30px;
transition: color .3s ease;
margin-bottom: 18px;
width: 56px;
&:last-child {
margin-right: 0;
}
span {
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;
}
}
label {
width: 100%;
text-align: center;
height: 1.2em;
.mixin-ellipsis-1;
}
each(@themes, {
&:global(.@{value}) {
span {
&:after {
background-color: ~'@{color-@{value}-theme}';
background-image: ~'@{color-@{value}-theme-bgimg}';
}
}
}
})
}
}
each(@themes, {
:global(#root.@{value}) {
.theme {
li {
&.active {
&:global(.@{value}) {
color: ~'@{color-@{value}-theme}';
span {
border-color: ~'@{color-@{value}-theme}';
}
}
}
}
}
}
})
</style>
@@ -0,0 +1,41 @@
<template lang="pug">
dt#desktop_lyric {{$t('setting__desktop_lyric')}}
dd
.gap-top
base-checkbox(id="setting_desktop_lyric_enable" v-model="currentStting.desktopLyric.enable" :label="$t('setting__desktop_lyric_enable')")
.gap-top
base-checkbox(id="setting_desktop_lyric_lock" v-model="currentStting.desktopLyric.isLock" :label="$t('setting__desktop_lyric_lock')")
.gap-top
base-checkbox(id="setting_desktop_lyric_alwaysOnTop" v-model="currentStting.desktopLyric.isAlwaysOnTop" :label="$t('setting__desktop_lyric_always_on_top')")
.gap-top
base-checkbox(id="setting_desktop_lyric_lockScreen" v-model="currentStting.desktopLyric.isLockScreen" :label="$t('setting__desktop_lyric_lock_screen')")
dd
h3#desktop_lyric_font {{$t('setting__desktop_lyric_font')}}
div
base-selection.gap-teft(:list="fontList" v-model="currentStting.desktopLyric.style.font" item-key="id" item-name="label")
</template>
<script>
import { ref, computed, useI18n } from '@renderer/utils/vueTools'
import { getSystemFonts } from '@renderer/utils/tools'
import { currentStting } from '../setting'
export default {
name: 'DesktopLyric',
setup() {
const { t } = useI18n()
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 {
currentStting,
fontList,
}
},
}
</script>
@@ -0,0 +1,92 @@
<template lang="pug">
dt#download {{$t('setting__download')}}
dd
base-checkbox(id="setting_download_enable" v-model="currentStting.download.enable" :label="$t('setting__download_enable')")
dd(:tips="$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(currentStting.download.savePath)" :tips="$t('setting__download_path_open_label')") {{currentStting.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')}}
div
base-checkbox(id="setting_download_isUseOtherSource" v-model="currentStting.download.isUseOtherSource" :label="$t('setting__is_enable')")
div
dd(:tips="$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
v-model="currentStting.download.fileName" 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" v-model="currentStting.download.isEmbedPic" :label="$t('setting__download_embed_pic')")
.gap-top
base-checkbox(id="setting_download_isEmbedLyric" v-model="currentStting.download.isEmbedLyric" :label="$t('setting__download_embed_lyric')")
dd(:tips="$t('setting__download_lyric_title')")
h3#download_lyric {{$t('setting__download_lyric')}}
div
base-checkbox(id="setting_download_isDownloadLrc" v-model="currentStting.download.isDownloadLrc" :label="$t('setting__is_enable')")
dd
h3#download_lyric {{$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 v-model="currentStting.download.lrcFormat" :value="item.id" :label="item.name")
</template>
<script>
import { computed, useI18n } from '@renderer/utils/vueTools'
// import { getSystemFonts } from '@renderer/utils/tools'
import { openDirInExplorer, selectDir } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'Download',
setup() {
const { t } = useI18n()
const handleChangeSavePath = () => {
selectDir({
title: t('setting__download_select_save_path'),
defaultPath: currentStting.value.download.savePath,
properties: ['openDirectory'],
}).then(result => {
if (result.canceled) return
currentStting.value.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 {
currentStting,
openDirInExplorer,
handleChangeSavePath,
musicNames,
lrcFormatList,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,314 @@
<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.key-bind(: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')")
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.key-bind(: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, useI18n, onBeforeUnmount, toRaw } from '@renderer/utils/vueTools'
import { allHotKeys, hotKeySetEnable, hotKeySetConfig, hotKeyGetStatus } from '@renderer/utils/tools'
import { isMac } from '@common/utils'
import { base as eventBaseName } from '@renderer/event/names'
import { mainWindow as eventsNameMainWindow } from '@main/events/_name'
import { currentStting } from '../setting'
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: 'HotKey',
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
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 == null) continue
configInfo[info.name] = {
key,
info,
}
}
}
hotKeyConfig.value = config
}
const handleHotKeyFocus = async(event, info, type) => {
await hotKeySetEnable(false)
window.isEditingHotKey = true
isEditHotKey = true
let config = hotKeyConfig.value[type][info.name]
newHotKey = config?.key
hotKeyTargetInput = event.target
event.target.value = t('setting__hot_key_tip_input')
}
const handleHotKeyBlur = async(event, info, type) => {
await hotKeySetEnable(true)
window.isEditingHotKey = false
isEditHotKey = false
const prevInput = hotKeyTargetInput
hotKeyTargetInput = null
if (prevInput.value == t('setting__hot_key_tip_input')) {
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)
window.appHotKeyConfig = current_hot_key.value
await hotKeySetConfig({
action: 'config',
data: toRaw(current_hot_key.value),
source: eventsNameMainWindow.name,
})
}
const handleEnableHotKey = async() => {
await hotKeySetConfig({
action: 'enable',
data: current_hot_key.value.global.enable,
source: eventsNameMainWindow.name,
})
await handleHotKeySaveConfig()
await getHotKeyStatus()
}
const getHotKeyStatus = () => {
return hotKeyGetStatus().then(status => {
// console.log(status)
hotKeyStatus.value = status
return status
})
}
current_hot_key.value = window.appHotKeyConfig
initHotKeyConfig()
getHotKeyStatus()
window.eventHub.on(eventBaseName.set_hot_key_config, handleUpdateHotKeyConfig)
window.eventHub.on(eventBaseName.key_down, handleKeyDown)
onBeforeUnmount(() => {
window.eventHub.off(eventBaseName.set_hot_key_config, handleUpdateHotKeyConfig)
window.eventHub.off(eventBaseName.key_down, handleKeyDown)
})
return {
currentStting,
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-theme;
}
.hotKeyItem {
width: 30%;
padding-right: 35px;
margin-top: 15px;
box-sizing: border-box;
}
.hotKeyItemTitle {
.mixin-ellipsis-1;
padding-bottom: 5px;
color: @color-theme_2-font-label;
font-size: 12px;
}
.hotKeyItemInput {
width: 100%;
box-sizing: border-box;
// font-family: monospace;
&:focus {
background-color: @color-theme_2-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;
}
}
each(@themes, {
:global(#root.@{value}) {
.hotKeyItemTitle {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
})
</style>
@@ -0,0 +1,30 @@
<template lang="pug">
dt#list {{$t('setting__list')}}
dd
.gap-top
base-checkbox(id="setting_list_showSource_enable" v-model="currentStting.list.isShowSource" :label="$t('setting__list_source')")
.gap-top
base-checkbox(id="setting_list_scroll_enable" v-model="currentStting.list.isSaveScrollLocation" :label="$t('setting__list_scroll')")
dd(:tips="$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 v-model="currentStting.list.addMusicLocationType" 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 v-model="currentStting.list.addMusicLocationType" value="bottom" :label="$t('setting__list_add_music_location_type_bottom')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { currentStting } from '../setting'
export default {
name: 'List',
setup() {
return {
currentStting,
}
},
}
</script>
@@ -0,0 +1,57 @@
<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" v-model="currentStting.network.proxy.enable" :label="$t('setting__is_enable')")
p
base-input.gap-left(v-model.trim="currentStting.network.proxy.host" :placeholder="$t('setting__network_proxy_host')")
base-input.gap-left(v-model.trim="currentStting.network.proxy.port" :placeholder="$t('setting__network_proxy_port')")
p
base-input.gap-left(v-model.trim="currentStting.network.proxy.username" :placeholder="$t('setting__network_proxy_username')")
base-input.gap-left(v-model.trim="currentStting.network.proxy.password" type="password" :placeholder="$t('setting__network_proxy_password')")
</template>
<script>
import { watch, onBeforeUnmount } from '@renderer/utils/vueTools'
import { proxy } from '@renderer/core/share'
import { debounce } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'Network',
setup() {
watch(() => currentStting.value.network.proxy.enable, enable => {
proxy.enable = enable
})
const setHost = debounce(host => {
proxy.host = host
}, 500)
watch(() => currentStting.value.network.proxy.host, setHost)
const setPort = debounce(port => {
proxy.port = port
}, 500)
watch(() => currentStting.value.network.proxy.port, setPort)
const setUserName = debounce(username => {
proxy.username = username
}, 500)
watch(() => currentStting.value.network.proxy.username, setUserName)
const setPassword = debounce(password => {
proxy.password = password
}, 500)
watch(() => currentStting.value.network.proxy.password, setPassword)
onBeforeUnmount(() => {
if (currentStting.value.network.proxy.enable && !currentStting.value.network.proxy.host) proxy.enable = false
})
return {
currentStting,
}
},
}
</script>
@@ -0,0 +1,22 @@
<template lang="pug">
dt#odc {{$t('setting__odc')}}
dd
.gap-top
base-checkbox(id="setting_odc_isAutoClearSearchInput" v-model="currentStting.odc.isAutoClearSearchInput" :label="$t('setting__odc_clear_search_input')")
.gap-top
base-checkbox(id="setting_odc_isAutoClearSearchList" v-model="currentStting.odc.isAutoClearSearchList" :label="$t('setting__odc_clear_search_list')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { currentStting } from '../setting'
export default {
name: 'Odc',
setup() {
return {
currentStting,
}
},
}
</script>
@@ -0,0 +1,76 @@
<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" v-model="currentStting.tray.themeId" 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')}}
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_play_list_cache {{$t('setting__other_play_list_cache')}}
div
base-btn.btn(min :disabled="isDisabledListCacheClear" @click="clearListCache") {{$t('setting__other_play_list_cache_clear_btn')}}
</template>
<script>
import { ref, computed, useI18n, useCommit } from '@renderer/utils/vueTools'
import { sizeFormate, clearCache, getCacheSize } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'Other',
setup() {
const { t } = useI18n()
const trayThemeList = computed(() => {
return [
{ id: 0, name: 'native', label: t('setting__other_tray_theme_native') },
{ id: 1, name: 'origin', label: t('setting__other_tray_theme_origin') },
{ id: 2, name: 'black', label: t('setting__other_tray_theme_black') },
]
})
const cacheSize = ref('0 B')
const isDisabledResourceCacheClear = ref(false)
const isDisabledListCacheClear = ref(false)
const refreshCacheSize = () => {
getCacheSize().then(size => {
cacheSize.value = sizeFormate(size)
})
}
const clearResourceCache = () => {
isDisabledResourceCacheClear.value = true
clearCache().then(() => {
refreshCacheSize()
isDisabledResourceCacheClear.value = false
})
}
refreshCacheSize()
const clearMyListCache = useCommit('list', 'clearCache')
const clearListCache = () => {
isDisabledListCacheClear.value = true
clearMyListCache()
isDisabledListCacheClear.value = false
}
return {
currentStting,
trayThemeList,
cacheSize,
isDisabledResourceCacheClear,
isDisabledListCacheClear,
clearResourceCache,
clearListCache,
}
},
}
</script>
@@ -0,0 +1,62 @@
<template lang="pug">
dt#play {{$t('setting__play')}}
dd
.gap-top
base-checkbox(id="setting_player_save_play_time" v-model="currentStting.player.isSavePlayTime" :label="$t('setting__play_save_play_time')")
.gap-top
base-checkbox(id="setting_player_lyric_transition" v-model="currentStting.player.isShowLyricTranslation" :label="$t('setting__play_lyric_transition')")
.gap-top
base-checkbox(id="setting_player_lyric_play_lxlrc" v-model="currentStting.player.isPlayLxlrc" :label="$t('setting__play_lyric_lxlrc')")
.gap-top
base-checkbox(id="setting_player_highQuality" v-model="currentStting.player.highQuality" :label="$t('setting__play_quality')")
.gap-top
base-checkbox(id="setting_player_showTaskProgess" v-model="currentStting.player.isShowTaskProgess" :label="$t('setting__play_task_bar')")
.gap-top
base-checkbox(id="setting_player_isMediaDeviceRemovedStopPlay" v-model="currentStting.player.isMediaDeviceRemovedStopPlay" :label="$t('setting__play_mediaDevice_remove_stop_play')")
dd(:tips="$t('setting__play_mediaDevice_title')")
h3#play_mediaDevice {{$t('setting__play_mediaDevice')}}
div
base-selection.gap-left(:list="mediaDevices" v-model="currentStting.player.mediaDeviceId" item-key="deviceId" item-name="label")
</template>
<script>
import { ref, onBeforeUnmount, watch, nextTick } from '@renderer/utils/vueTools'
import { setTaskBarProgress } from '@renderer/utils/tools'
import { currentStting } from '../setting'
export default {
name: 'Play',
setup() {
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)
})
watch(() => currentStting.value.player.isShowTaskProgess, val => {
if (val) return
nextTick(() => {
setTaskBarProgress({
status: -1,
mode: 'normal',
})
})
})
return {
currentStting,
mediaDevices,
}
},
}
</script>
@@ -0,0 +1,25 @@
<template lang="pug">
dt#search {{$t('setting__search')}}
dd
.gap-top
base-checkbox(id="setting_search_showHot_enable" v-model="currentStting.search.isShowHotSearch" :label="$t('setting__search_hot')")
.gap-top
base-checkbox(id="setting_search_showHistory_enable" v-model="currentStting.search.isShowHistorySearch" :label="$t('setting__search_history')")
.gap-top
base-checkbox(id="setting_search_focusSearchBox_enable" v-model="currentStting.search.isFocusSearchBox" :label="$t('setting__search_focus_search_box')")
</template>
<script>
// import { ref, onBeforeUnmount } from '@renderer/utils/vueTools'
import { currentStting } from '../setting'
export default {
name: 'Search',
setup() {
return {
currentStting,
}
},
}
</script>
@@ -0,0 +1,80 @@
<template lang="pug">
dt#sync {{$t('setting__sync')}}
dd
base-checkbox(id="setting_sync_enable" v-model="currentStting.sync.enable" :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(v-model.trim="currentStting.sync.port" :placeholder="$t('setting__sync_port_tip')")
</template>
<script>
import { computed, useI18n, watch } from '@renderer/utils/vueTools'
import { sync } from '@renderer/core/share'
import { refreshSyncCode, syncEnable } from '@renderer/utils/tools'
import { debounce } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'Update',
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(', ')
: ''
})
watch(() => currentStting.value.sync.enable, enable => {
syncEnable({
enable,
port: currentStting.value.sync.port,
})
sync.enable = enable
})
const setPort = debounce(port => {
syncEnable({
enable: currentStting.value.sync.enable,
port,
})
sync.port = port
}, 500)
watch(() => currentStting.value.sync.port, setPort)
return {
currentStting,
sync,
syncEnableTitle,
syncDevices,
refreshSyncCode,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,53 @@
<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 {{$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, useI18n } from '@renderer/utils/vueTools'
import { versionInfo } from '@renderer/core/share'
import { sizeFormate } from '@renderer/utils/tools'
// import { openDirInExplorer, selectDir } from '@renderer/utils'
import { currentStting } from '../setting'
export default {
name: 'Update',
setup() {
const { t } = useI18n()
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 {
currentStting,
versionInfo,
downloadProgress,
showUpdateModal,
}
},
}
</script>
<style lang="less" module>
.save-path {
font-size: 12px;
}
</style>
@@ -0,0 +1,234 @@
<template lang="pug">
material-modal(:show="modelValue" bg-close @close="handleClose")
main(:class="$style.main")
h2 {{$t('user_api__title')}}
ul.scroll(v-if="apiList.length" :class="$style.content")
li(:class="[$style.listItem, {[$style.active]: setting.apiSource == api.id}]" v-for="(api, index) in apiList" :key="api.id")
div(:class="$style.listLeft")
h3 {{api.name}}
p {{api.description}}
base-btn(:class="$style.listBtn" outline :tips="$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://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md#%E8%87%AA%E5%AE%9A%E4%B9%89%E6%BA%90%E8%84%9A%E6%9C%AC%E7%BC%96%E5%86%99%E8%AF%B4%E6%98%8E')" tips="https://github.com/lyswhut/lx-music-desktop/blob/master/FAQ.md") FAQ.md
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 { mapGetters } from 'vuex'
import { rendererInvoke, NAMES } from '@common/ipc'
import { promises as fsPromises } from 'fs'
import { selectDir, openUrl } from '@renderer/utils'
import apiSourceInfo from '@renderer/utils/music/api-source-info'
import { userApi, apiSource } from '@renderer/core/share'
export default {
props: {
modelValue: {
type: Boolean,
default: false,
},
},
setup() {
return {
userApi,
}
},
computed: {
...mapGetters(['setting']),
apiList() {
return this.userApi.list
},
},
methods: {
handleImport() {
selectDir({
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 fsPromises.readFile(result.filePaths[0]).then(data => {
return rendererInvoke(NAMES.mainWindow.import_user_api, data.toString()).then(({ apiList }) => {
userApi.list = apiList
})
})
})
},
handleExport() {
},
async handleRemove(index) {
const api = this.apiList[index]
if (!api) return
if (this.setting.apiSource == api.id) {
let backApi = apiSourceInfo.find(api => !api.disabled)
if (backApi) apiSource.value = backApi.id
}
userApi.list = await rendererInvoke(NAMES.mainWindow.remove_user_api, [api.id])
},
handleClose() {
this.$emit('update:modelValue', false)
},
handleOpenUrl(url) {
openUrl(url)
},
},
}
</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: @color-theme_2-font;
line-height: 1.3;
text-align: center;
}
}
.name {
color: @color-theme;
}
.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: @color-theme_2-hover;
}
&.active {
background-color: @color-theme_2-active;
}
h3 {
font-size: 15px;
color: @color-theme_2-font;
word-break: break-all;
}
p {
margin-top: 5px;
font-size: 14px;
color: @color-theme_2-font-label;
word-break: break-all;
}
}
.noitem {
height: 100px;
font-size: 18px;
color: @color-theme_2-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: @color-theme_2-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;
}
each(@themes, {
:global(#root.@{value}) {
.main {
h2 {
color: ~'@{color-@{value}-theme_2-font}';
}
}
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&.active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
h3 {
color: ~'@{color-@{value}-theme_2-font}';
}
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
.noitem {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
})
</style>