diff --git a/postcss.config.js b/postcss.config.js index d44c46e1..df5bbc3b 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -15,7 +15,7 @@ module.exports = { '*-height', '*-width', 'flex', '::-webkit-scrollbar', 'top', 'left', 'bottom', 'right', - 'border-radius', + 'border-radius', 'gap', ], selectorBlackList: ['html', 'ignore-to-rem'], replace: true, diff --git a/publish/changeLog.md b/publish/changeLog.md index 3c98e5b4..80f8988d 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -1,6 +1,7 @@ ### 新增 - 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示 +- 为方便分享歌曲列表,新增单个列表导入/导出功能,可在右击“我的列表”里的列表名后弹出的菜单中使用 ### 修复 diff --git a/src/renderer/components/material/Modal.vue b/src/renderer/components/material/Modal.vue index 6b6de777..26b172d7 100644 --- a/src/renderer/components/material/Modal.vue +++ b/src/renderer/components/material/Modal.vue @@ -3,7 +3,9 @@ transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut") div(:class="$style.modal" v-show="show" @click="bgClose && close()") transition(:enter-active-class="inClass" - :leave-active-class="outClass") + :leave-active-class="outClass" + @after-leave="$emit('after-leave', $event)" + ) div(:class="$style.content" v-show="show" @click.stop) header(:class="$style.header") button(type="button" @click="close" v-if="closeBtn") @@ -141,6 +143,7 @@ export default { overflow: hidden; max-height: 80%; max-width: 76%; + min-width: 220px; position: relative; display: flex; flex-flow: column nowrap; diff --git a/src/renderer/lang/en-us/base.json b/src/renderer/lang/en-us/base.json index 09199968..b40621d8 100644 --- a/src/renderer/lang/en-us/base.json +++ b/src/renderer/lang/en-us/base.json @@ -1,5 +1,7 @@ { - "date_format_second": "{num} seconds ago", + "cancel_button_text": "Cancel", + "confirm_button_text": "OK", + "date_format_hour": "{num} hours ago", "date_format_minute": "{num} minutes ago", - "date_format_hour": "{num} hours ago" + "date_format_second": "{num} seconds ago" } diff --git a/src/renderer/lang/en-us/view/list.json b/src/renderer/lang/en-us/view/list.json index 6866910a..29c4a70d 100644 --- a/src/renderer/lang/en-us/view/list.json +++ b/src/renderer/lang/en-us/view/list.json @@ -1,28 +1,35 @@ { - "lists_new_list_btn": "Create list", - "lists_new_list_input": "New list...", - "lists_rename": "Rename", - "lists_moveup": "Move Up", - "lists_movedown": "Move Down", - "lists_sync": "Update", - "lists_remove": "Remove", + "action": "Manage", + "album": "Album", + "default_list": "Recently Played", + "list_add_to": "Add to ...", + "list_copy_name": "Copy name", + "list_download": "Download", + "list_move_to": "Move to ...", "list_play": "Play", "list_play_later": "Play later", - "list_copy_name": "Copy name", - "list_add_to": "Add to ...", - "list_move_to": "Move to ...", - "list_sort": "Adjust position", - "list_download": "Download", - "list_search": "Search", "list_remove": "Remove", + "list_search": "Search", + "list_sort": "Adjust position", "list_source_detail": "Song Page", - "default_list": "Recently Played", + "lists_export": "Export", + "lists_export_part_desc": "Choose where to save the list file", + "lists_import": "Import", + "lists_import_part_button_cancel": "No", + "lists_import_part_button_confirm": "Overwrite", + "lists_import_part_confirm": "The imported list ({importName}) has the same ID as the local list ({localName}). Do you overwrite the local list?", + "lists_import_part_desc": "Select list file", + "lists_movedown": "Move Down", + "lists_moveup": "Move Up", + "lists_new_list_btn": "Create list", + "lists_new_list_input": "New list...", + "lists_remove": "Remove", + "lists_rename": "Rename", + "lists_sync": "Update", + "loding_list": "Loading...", "love_list": "Favorites", "name": "Name", + "no_item": "Nothing's here...", "singer": "Artist", - "album": "Album", - "action": "Manage", - "time": "Length", - "loding_list": "Loading...", - "no_item": "Nothing's here..." + "time": "Length" } diff --git a/src/renderer/lang/zh-cn/base.json b/src/renderer/lang/zh-cn/base.json index 6a12f4bf..2df59ecb 100644 --- a/src/renderer/lang/zh-cn/base.json +++ b/src/renderer/lang/zh-cn/base.json @@ -1,5 +1,7 @@ { - "date_format_second": "{num}秒前", + "cancel_button_text": "我不", + "confirm_button_text": "好的", + "date_format_hour": "{num}小时前", "date_format_minute": "{num}分钟前", - "date_format_hour": "{num}小时前" + "date_format_second": "{num}秒前" } diff --git a/src/renderer/lang/zh-cn/view/list.json b/src/renderer/lang/zh-cn/view/list.json index 9bc45797..835e51b4 100644 --- a/src/renderer/lang/zh-cn/view/list.json +++ b/src/renderer/lang/zh-cn/view/list.json @@ -1,28 +1,35 @@ { - "lists_new_list_btn": "新建列表", - "lists_new_list_input": "新列表...", - "lists_rename": "重命名", - "lists_moveup": "上移", - "lists_movedown": "下移", - "lists_sync": "更新", - "lists_remove": "删除", + "action": "操作", + "album": "专辑", + "default_list": "试听列表", + "list_add_to": "添加到...", + "list_copy_name": "复制歌曲名", + "list_download": "下载", + "list_move_to": "移动到...", "list_play": "播放", "list_play_later": "稍后播放", - "list_copy_name": "复制歌曲名", - "list_source_detail": "歌曲详情页", - "list_add_to": "添加到...", - "list_move_to": "移动到...", - "list_sort": "调整位置", - "list_download": "下载", - "list_search": "搜索", "list_remove": "删除", - "default_list": "试听列表", + "list_search": "搜索", + "list_sort": "调整位置", + "list_source_detail": "歌曲详情页", + "lists_export": "导出", + "lists_export_part_desc": "选择列表文件保存位置", + "lists_import": "导入", + "lists_import_part_button_cancel": "不要啊", + "lists_import_part_button_confirm": "覆盖掉", + "lists_import_part_confirm": "导入的列表({importName})与本地列表({localName})的ID相同,是否覆盖本地列表?", + "lists_import_part_desc": "选择列表文件", + "lists_movedown": "下移", + "lists_moveup": "上移", + "lists_new_list_btn": "新建列表", + "lists_new_list_input": "新列表...", + "lists_remove": "删除", + "lists_rename": "重命名", + "lists_sync": "更新", + "loding_list": "加载中...", "love_list": "收藏", "name": "歌曲名", + "no_item": "列表竟然是空的...", "singer": "歌手", - "album": "专辑", - "action": "操作", - "time": "时长", - "loding_list": "加载中...", - "no_item": "列表竟然是空的..." + "time": "时长" } diff --git a/src/renderer/lang/zh-tw/base.json b/src/renderer/lang/zh-tw/base.json index 8c323e89..60631394 100644 --- a/src/renderer/lang/zh-tw/base.json +++ b/src/renderer/lang/zh-tw/base.json @@ -1,5 +1,7 @@ { - "date_format_second": "{num}秒前", + "cancel_button_text": "取消", + "confirm_button_text": "好的", + "date_format_hour": "{num}小時前", "date_format_minute": "{num}分鐘前", - "date_format_hour": "{num}小時前" + "date_format_second": "{num}秒前" } diff --git a/src/renderer/lang/zh-tw/view/list.json b/src/renderer/lang/zh-tw/view/list.json index 80de4fda..55cb5b36 100644 --- a/src/renderer/lang/zh-tw/view/list.json +++ b/src/renderer/lang/zh-tw/view/list.json @@ -1,28 +1,35 @@ { - "lists_new_list_btn": "新建列表", - "lists_new_list_input": "新列表...", - "lists_rename": "重命名", - "lists_moveup": "上移", - "lists_movedown": "下移", - "lists_sync": "更新", - "lists_remove": "刪除", + "action": "操作", + "album": "專輯", + "default_list": "試聽列表", + "list_add_to": "添加到...", + "list_copy_name": "複製歌曲名", + "list_download": "下載", + "list_move_to": "移動到...", "list_play": "播放", "list_play_later": "稍後播放", - "list_copy_name": "複製歌曲名", - "list_add_to": "添加到...", - "list_move_to": "移動到...", - "list_sort": "調整位置", - "list_download": "下載", - "list_search": "搜索", "list_remove": "刪除", + "list_search": "搜索", + "list_sort": "調整位置", "list_source_detail": "歌曲詳情頁", - "default_list": "試聽列表", + "lists_export": "導出", + "lists_export_part_desc": "選擇列表文件保存位置", + "lists_import": "導入", + "lists_import_part_button_cancel": "不要啊", + "lists_import_part_button_confirm": "覆蓋掉", + "lists_import_part_confirm": "導入的列表({importName})與本地列表({localName})的ID相同,是否覆蓋本地列表?", + "lists_import_part_desc": "選擇列表文件", + "lists_movedown": "下移", + "lists_moveup": "上移", + "lists_new_list_btn": "新建列表", + "lists_new_list_input": "新列表...", + "lists_remove": "刪除", + "lists_rename": "重命名", + "lists_sync": "更新", + "loding_list": "加載中...", "love_list": "收藏列表", "name": "歌曲名", + "no_item": "列表竟然是空的...", "singer": "歌手", - "album": "專輯", - "action": "操作", - "time": "時長", - "loding_list": "加載中...", - "no_item": "列表竟然是空的..." + "time": "時長" } diff --git a/src/renderer/plugins/Dialog/Dialog.vue b/src/renderer/plugins/Dialog/Dialog.vue new file mode 100644 index 00000000..561b9e7e --- /dev/null +++ b/src/renderer/plugins/Dialog/Dialog.vue @@ -0,0 +1,73 @@ + + + + + diff --git a/src/renderer/plugins/Dialog/index.js b/src/renderer/plugins/Dialog/index.js new file mode 100644 index 00000000..d470596a --- /dev/null +++ b/src/renderer/plugins/Dialog/index.js @@ -0,0 +1,50 @@ +import Dialog from './Dialog' +import i18n from '../i18n' +import store from '@renderer/store' +import Vue from 'vue' + +const defaultOptions = { + message: '', + showCancel: false, + cancelButtonText: '', + confirmButtonText: '', +} + +const dialog = { + install(Vue, options) { + const DialogConstructor = Vue.extend(Dialog) + + const dialog = function Dialog(options) { + const { message, showCancel, cancelButtonText, confirmButtonText } = + Object.assign({}, defaultOptions, typeof options == 'string' ? { message: options } : options || {}) + return new Promise((resolve, reject) => { + let instance = new DialogConstructor({ i18n, store }).$mount(document.createElement('div')) + + // 属性设置 + instance.visible = true + instance.message = message + instance.showCancel = showCancel + instance.cancelButtonText = cancelButtonText + instance.confirmButtonText = confirmButtonText + + // 挂载 + document.getElementById('container').appendChild(instance.$el) + + instance.handleCancel = () => { + instance.visible = false + resolve(false) + } + + instance.handleComfirm = () => { + instance.visible = false + resolve(true) + } + }) + } + dialog.confirm = options => dialog({ ...options, showCancel: true }) + + Vue.prototype.$dialog = dialog + }, +} + +Vue.use(dialog) diff --git a/src/renderer/plugins/index.js b/src/renderer/plugins/index.js index e94f9bdc..c62c44ae 100644 --- a/src/renderer/plugins/index.js +++ b/src/renderer/plugins/index.js @@ -1,2 +1,3 @@ // import './axios' +import './Dialog' import './Tips' diff --git a/src/renderer/store/modules/download.js b/src/renderer/store/modules/download.js index 3108b81f..2ebe539d 100644 --- a/src/renderer/store/modules/download.js +++ b/src/renderer/store/modules/download.js @@ -11,6 +11,7 @@ import { getMusicUrl as getMusicUrlFormStorage, setMusicUrl, assertApiSupport, + filterFileName, } from '../../utils' import { NAMES, rendererInvoke } from '@common/ipc' @@ -33,7 +34,6 @@ const dls = {} const tryNum = {} let isRuningActionTask = false -const filterFileName = /[\\/:*?#"<>|]/g // getters const getters = { @@ -378,9 +378,9 @@ const actions = { statusText: '待下载', url: null, // songmid: musicInfo.songmid, - fileName: `${rootState.setting.download.fileName + fileName: filterFileName(`${rootState.setting.download.fileName .replace('歌名', musicInfo.name) - .replace('歌手', musicInfo.singer)}.${ext}`.replace(filterFileName, ''), + .replace('歌手', musicInfo.singer)}.${ext}`), progress: { downloaded: 0, total: 0, diff --git a/src/renderer/store/modules/list.js b/src/renderer/store/modules/list.js index 82f712c9..8fd2c3fe 100644 --- a/src/renderer/store/modules/list.js +++ b/src/renderer/store/modules/list.js @@ -293,11 +293,11 @@ const mutations = { const targetMusicInfo = targetList.list.find(item => item.songmid == id) if (targetMusicInfo) Object.assign(targetMusicInfo, data) }, - createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, isSync }) { + createUserList(state, { name, id = `userlist_${Date.now()}`, list = [], source, sourceListId, position, isSync }) { if (!isSync) { window.eventHub.$emit(eventSyncName.send_action_list, { action: 'create_user_list', - data: { name, id, list, source, sourceListId }, + data: { name, id, list, source, sourceListId, position }, }) } @@ -311,7 +311,11 @@ const mutations = { source, sourceListId, } - state.userList.push(newList) + if (position == null) { + state.userList.push(newList) + } else { + state.userList.splice(position + 1, 0, newList) + } allListUpdate(newList) } this.commit('list/listAddMultiple', { id, list, isSync: true }) diff --git a/src/renderer/utils/index.js b/src/renderer/utils/index.js index fd507f7a..374603b4 100644 --- a/src/renderer/utils/index.js +++ b/src/renderer/utils/index.js @@ -4,6 +4,7 @@ import { shell, clipboard } from 'electron' import crypto from 'crypto' import { rendererSend, rendererInvoke, NAMES } from '../../common/ipc' import iconv from 'iconv-lite' +import { gzip, gunzip } from 'zlib' /** * 获取两个数之间的随机整数,大于等于min,小于max @@ -433,3 +434,39 @@ export const setMusicUrl = (musicInfo, type, url) => rendererSend(NAMES.mainWind url, }) export const clearMusicUrl = () => rendererSend(NAMES.mainWindow.clear_music_url) + +export const gzipData = str => { + return new Promise((resolve, reject) => { + gzip(str, (err, result) => { + if (err) return reject(err) + resolve(result) + }) + }) +} +export const gunzipData = buf => { + return new Promise((resolve, reject) => { + gunzip(buf, (err, result) => { + if (err) return reject(err) + resolve(result.toString()) + }) + }) +} + +export const saveLxConfigFile = async(path, data) => { + if (!path.endsWith('.lxmc')) path += '.lxmc' + fs.writeFile(path, await gzipData(JSON.stringify(data)), 'binary', err => { + console.log(err) + }) +} + +export const readLxConfigFile = async path => { + let isJSON = path.endsWith('.json') + let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary') + if (!data || isJSON) return data + data = await gunzipData(Buffer.from(data, 'binary')) + return data.toString('utf8') +} + + +const fileNameRxp = /[\\/:*?#"<>|]/g +export const filterFileName = name => name.replace(fileNameRxp, '') diff --git a/src/renderer/views/List.vue b/src/renderer/views/List.vue index e9022860..bd5df148 100644 --- a/src/renderer/views/List.vue +++ b/src/renderer/views/List.vue @@ -7,9 +7,13 @@ svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='70%' viewBox='0 0 24 24' space='preserve') use(xlink:href='#icon-list-add') ul.scroll(:class="$style.listsContent" ref="dom_lists_list") - li(:class="[$style.listsItem, defaultList.id == listId ? $style.active : null]" :tips="defaultList.name" @click="handleListToggle(defaultList.id)") + li(:class="[$style.listsItem, defaultList.id == listId ? $style.active : null]" :tips="defaultList.name" + @contextmenu="handleListsItemRigthClick($event, -2)" + @click="handleListToggle(defaultList.id)") span(:class="$style.listsLabel") {{defaultList.name}} - li(:class="[$style.listsItem, loveList.id == listId ? $style.active : null]" :tips="loveList.name" @click="handleListToggle(loveList.id)") + li(:class="[$style.listsItem, loveList.id == listId ? $style.active : null]" :tips="loveList.name" + @contextmenu="handleListsItemRigthClick($event, -1)" + @click="handleListToggle(loveList.id)") span(:class="$style.listsLabel") {{loveList.name}} li.user-list( :class="[$style.listsItem, item.id == listId ? $style.active : null, listsData.rightClickItemIndex == index ? $style.clicked : null, fetchingListStatus[item.id] ? $style.fetching : null]" @@ -74,7 +78,7 @@ diff --git a/src/renderer/views/Setting.vue b/src/renderer/views/Setting.vue index da70527a..9d5cc8a7 100644 --- a/src/renderer/views/Setting.vue +++ b/src/renderer/views/Setting.vue @@ -329,16 +329,16 @@ import { setWindowSize, getSetting, saveSetting, + saveLxConfigFile, + readLxConfigFile, } from '../utils' import { rendererSend, rendererInvoke, rendererOn, NAMES, rendererOff } from '@common/ipc' import { mergeSetting, isMac } from '../../common/utils' import apiSourceInfo from '../utils/music/api-source-info' -import fs from 'fs' import languageList from '@renderer/lang/languages.json' import { base as eventBaseName } from '../event/names' import * as hotKeys from '../../common/hotKey' import { mainWindow as eventsNameMainWindow, winLyric as eventsNameWinLyric } from '../../main/events/_name' -import { gzip, gunzip } from 'zlib' import music from '../utils/music' let hotKeyTargetInput @@ -815,7 +815,7 @@ export default { async importSetting(path) { let settingData try { - settingData = JSON.parse(await this.handleReadFile(path)) + settingData = JSON.parse(await readLxConfigFile(path)) } catch (error) { return } @@ -830,12 +830,12 @@ export default { type: 'setting', data: Object.assign({ version: this.settingVersion }, this.setting), } - this.handleSaveFile(path, JSON.stringify(data)) + saveLxConfigFile(path, JSON.stringify(data)) }, async importPlayList(path) { let listData try { - listData = JSON.parse(await this.handleReadFile(path)) + listData = JSON.parse(await readLxConfigFile(path)) } catch (error) { return } @@ -867,12 +867,12 @@ export default { if (item.otherSource) delete item.otherSource } } - this.handleSaveFile(path, JSON.stringify(data)) + saveLxConfigFile(path, JSON.stringify(data)) }, async importAllData(path) { let allData try { - allData = JSON.parse(await this.handleReadFile(path)) + allData = JSON.parse(await readLxConfigFile(path)) } catch (error) { return } @@ -906,7 +906,7 @@ export default { if (item.otherSource) delete item.otherSource } } - this.handleSaveFile(path, JSON.stringify(allData)) + saveLxConfigFile(path, JSON.stringify(allData)) }, handleImportAllData() { selectDir({ @@ -1198,35 +1198,6 @@ export default { handleTrayShowChange(isShow) { this.current_setting.tray.isToTray = isShow }, - async handleSaveFile(path, data) { - if (!path.endsWith('.lxmc')) path += '.lxmc' - fs.writeFile(path, await this.gzip(data), 'binary', err => { - console.log(err) - }) - }, - async handleReadFile(path) { - let isJSON = path.endsWith('.json') - let data = await fs.promises.readFile(path, isJSON ? 'utf8' : 'binary') - if (!data || isJSON) return data - data = await this.gunzip(Buffer.from(data, 'binary')) - return data.toString('utf8') - }, - gzip(str) { - return new Promise((resolve, reject) => { - gzip(str, (err, result) => { - if (err) return reject(err) - resolve(result) - }) - }) - }, - gunzip(buf) { - return new Promise((resolve, reject) => { - gunzip(buf, (err, result) => { - if (err) return reject(err) - resolve(result.toString()) - }) - }) - }, getApiStatus() { let status if (window.globalObj.userApi.status) status = this.$t('view.setting.basic_source_status_success')