Merge branch 'dev'

This commit is contained in:
lyswhut 2021-10-02 13:43:46 +08:00
commit b726ee42f7
48 changed files with 1561 additions and 693 deletions

View File

@ -6,6 +6,24 @@ Project versioning adheres to [Semantic Versioning](http://semver.org/).
Commit convention is based on [Conventional Commits](http://conventionalcommits.org).
Change log format is based on [Keep a Changelog](http://keepachangelog.com/).
## [1.14.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.13.0...v1.14.0) - 2021-10-02
### 新增
- 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在右击“我的列表”里的列表名后弹出的菜单中使用
- 新增删除列表前的确认弹窗,防止误删列表
- 新增歌词文本选择复制功能,可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式,选择完成后可鼠标右击或者使用系统快捷键复制
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在右击“我的列表”里的列表名后弹出的菜单中使用
### 修复
- 修复mg排行榜无法加载的问题
- 修复点击播放详情页的进度条跳进度时会出现偏移的问题
- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题
- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题
- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题
## [1.13.0](https://github.com/lyswhut/lx-music-desktop/compare/v1.12.2...v1.13.0) - 2021-09-05
如果你喜欢并经常使用洛雪音乐并想要第一时间尝鲜洛雪的新功能可以加入测试企鹅群768786588

2
FAQ.md
View File

@ -58,7 +58,7 @@
## 无法打开外部歌单
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
不支持源打开歌单,请**确认**你需要打开的歌单平台是否与软件标签所写的**歌单源**对应(不一样的话请通过右上角切换歌单源);<br>
对于分享出来的歌单若打开失败可尝试先在浏览器中打开后再从浏览器地址栏复制URL地址到软件打开<br>
或者如果你知道歌单 id 也可以直接输入歌单 id 打开。<br>

726
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{
"name": "lx-music-desktop",
"version": "1.13.0",
"version": "1.14.0",
"description": "一个免费的音乐查找助手",
"main": "./dist/electron/main.js",
"productName": "lx-music-desktop",
@ -172,30 +172,30 @@
"@babel/plugin-transform-modules-umd": "^7.14.5",
"@babel/plugin-transform-runtime": "^7.15.0",
"@babel/polyfill": "^7.12.1",
"@babel/preset-env": "^7.15.4",
"@babel/preset-env": "^7.15.6",
"babel-eslint": "^10.1.0",
"babel-loader": "^8.2.2",
"babel-preset-minify": "^0.5.1",
"browserslist": "^4.16.8",
"cfonts": "^2.9.3",
"browserslist": "^4.17.2",
"cfonts": "^2.10.0",
"chalk": "^4.1.2",
"changelog-parser": "^2.8.0",
"copy-webpack-plugin": "^9.0.1",
"core-js": "^3.17.2",
"core-js": "^3.18.1",
"cross-env": "^7.0.3",
"css-loader": "^6.2.0",
"css-loader": "^6.3.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"del": "^6.0.0",
"electron": "^13.3.0",
"electron": "^13.4.0",
"electron-builder": "^22.11.7",
"electron-debug": "^3.2.0",
"electron-devtools-installer": "^3.2.0",
"electron-to-chromium": "^1.3.830",
"electron-to-chromium": "^1.3.856",
"eslint": "^7.32.0",
"eslint-config-standard": "^16.0.3",
"eslint-formatter-friendly": "^7.0.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-html": "^6.1.2",
"eslint-plugin-html": "^6.2.0",
"eslint-plugin-import": "^2.24.2",
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^5.1.0",
@ -207,9 +207,9 @@
"less-loader": "^10.0.1",
"less-plugin-clean-css": "^1.5.1",
"markdown-it": "^12.2.0",
"mini-css-extract-plugin": "^2.2.2",
"mini-css-extract-plugin": "^2.3.0",
"node-loader": "^2.0.0",
"postcss": "^8.3.6",
"postcss": "^8.3.8",
"postcss-loader": "^6.1.1",
"postcss-pxtorem": "^6.0.0",
"pug": "^3.0.2",
@ -218,36 +218,36 @@
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"spinnies": "^0.5.1",
"terser-webpack-plugin": "^5.2.3",
"terser-webpack-plugin": "^5.2.4",
"url-loader": "^4.1.1",
"vue-loader": "^15.9.8",
"vue-template-compiler": "^2.6.14",
"webpack": "^5.52.0",
"webpack": "^5.56.0",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^3.11.2",
"webpack-hot-middleware": "^2.25.0",
"webpack-hot-middleware": "^2.25.1",
"webpack-merge": "^5.8.0"
},
"dependencies": {
"bufferutil": "^4.0.3",
"bufferutil": "^4.0.4",
"crypto-js": "^4.1.1",
"electron-log": "^4.4.1",
"electron-store": "^8.0.0",
"electron-store": "^8.0.1",
"electron-updater": "^4.3.9",
"font-list": "git+https://github.com/lyswhut/node-font-list.git#c6caf4060e471afe143a4aca30d554644522966d",
"http-terminator": "^3.0.0",
"http-terminator": "^3.0.3",
"iconv-lite": "^0.6.3",
"image-size": "^1.0.0",
"koa": "^2.13.1",
"koa": "^2.13.3",
"long": "^4.0.0",
"lrc-file-parser": "^1.1.2",
"lrc-file-parser": "^1.2.1",
"needle": "^3.0.0",
"node-id3": "^0.2.3",
"request": "^2.88.2",
"socket.io": "^4.2.0",
"utf-8-validate": "^5.0.5",
"utf-8-validate": "^5.0.6",
"vue": "^2.6.14",
"vue-i18n": "^8.25.0",
"vue-i18n": "^8.26.5",
"vue-router": "^3.5.2",
"vuex": "^3.6.2",
"vuex-router-sync": "^5.0.0"

View File

@ -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,

View File

@ -1,18 +1,15 @@
如果你喜欢并经常使用洛雪音乐并想要第一时间尝鲜洛雪的新功能可以加入测试企鹅群768786588
注意:测试版的功可能会不稳定,打算潜水的勿加。
### 新增
- 歌曲搜索框新增清理按钮,点击此按钮可以清理搜索框并返回初始搜索界面
- 新增“下载的歌词文件编码格式”设置,默认下载的歌词编码仍是`UTF-8`,对于某些在设备(如车机)上出现歌词中文乱码的用户可以尝试选择以`GBK`编码格式保存歌词文件
- 新增设置-桌面歌词-歌词字体设置此设置可用于设置桌面歌词的字体已知的问题Windows 7 下可能会出现字体列表为空的情况,这是当前系统的 Powershell 版本小于5.1导致的,请自行**尝试**看常见解决)
### 优化
- 支持网易源“我喜欢”歌单以注入token的方式打开。由于网易源的“我喜欢”歌单需要登录才能打开若你看不懂后半句就去阅读 常见问题-无法打开外部歌单),现若想要打开此类歌单,需要在歌单链接后面拼上 `###` 再加上有效的token拼接格式`[id|url]###token`例子最后面的xxxxxx替换成你的token`https://music.163.com/#/playlist?id=123456&userid=123456###xxxxxx`
- 软件内快捷键的最小化触发时,如果已启用托盘,则隐藏程序,否则最小化程序
- 新增歌词简体中文转繁体中文,当软件语言被设置为繁体中文后,播放歌曲的歌词也将自动转成繁体中文显示
- 新增单个列表导入/导出功能,可以方便分享歌曲列表,可在右击“我的列表”里的列表名后弹出的菜单中使用
- 新增删除列表前的确认弹窗,防止误删列表
- 新增歌词文本选择复制功能,可在详情页进度条上方的歌词文本选择按钮进入歌词文本选择模式,选择完成后可鼠标右击或者使用系统快捷键复制
- 新增重复歌曲列表,可以方便移除我的列表中的重复歌曲,此列表会列出目标列表里歌曲名相同的歌曲,可在右击“我的列表”里的列表名后弹出的菜单中使用
### 修复
- 修复某些情况下同步功能会导致切歌混乱的问题
- 修复从电脑浏览器复制的企鹅歌单链接无法打开的问题
- 修复mg排行榜无法加载的问题
- 修复点击播放详情页的进度条跳进度时会出现偏移的问题
- 修复在有提示信息的地方长按鼠标按键时提示信息会闪烁的问题
- 修复下载歌曲时的歌词下载不尝试获取缓存歌词的问题
- 修复GNOME等桌面下每次打开应用时需重新设置歌词窗口置顶的问题

File diff suppressed because one or more lines are too long

View File

@ -33,6 +33,8 @@ const names = {
restart_window: 'restart_window',
lang_s2t: 'lang_s2t',
handle_kw_decode_lyric: 'handle_kw_decode_lyric',
get_lyric_info: 'get_lyric_info',
set_lyric_info: 'set_lyric_info',
@ -74,6 +76,7 @@ const names = {
sync_generate_code: 'sync_generate_code',
sync_action_list: 'sync_action_list',
sync_list: 'sync_list',
},
winLyric: {
close: 'close',

View File

@ -1,7 +1,7 @@
const path = require('path')
const { BrowserWindow } = require('electron')
const { winLyric: WIN_LYRIC_EVENT_NAME } = require('../../events/_name')
const { debounce } = require('../../../common/utils')
const { debounce, isLinux } = require('../../../common/utils')
const { getLyricWindowBounds } = require('./utils')
require('./event')
@ -66,6 +66,10 @@ const winEvent = lyricWindow => {
if (global.appSetting.desktopLyric.isLock) {
global.modules.lyricWindow.setIgnoreMouseEvents(true, { forward: false })
}
// linux下每次重开时貌似要重新设置置顶
if (isLinux && global.appSetting.desktopLyric.isAlwaysOnTop) {
global.modules.lyricWindow.setAlwaysOnTop(global.appSetting.desktopLyric.isAlwaysOnTop, 'screen-saver')
}
})
}

View File

@ -25,3 +25,5 @@ require('./kw_decodeLyric')
require('./userApi')
require('./sync')
require('./s2t')

View File

@ -0,0 +1,9 @@
const { mainHandle, NAMES: { mainWindow: ipcMainWindowNames } } = require('../../common/ipc')
const { tranditionalize } = require('../utils/simplify-chinese-main')
mainHandle(ipcMainWindowNames.lang_s2t, async(event, textBase64) => {
if (!global.modules.mainWindow) throw new Error('mainWindow is undefined')
const text = tranditionalize(Buffer.from(textBase64, 'base64').toString())
return Buffer.from(text).toString('base64')
})

View File

@ -0,0 +1,18 @@
todo.md
node_modules/
npm-debug.log
yarn-debug.log
yarn-error.log
package-lock.json
tsconfig.tsbuildinfo
report.*.json
.eslintcache
.DS_Store
.idea
.vscode
.yarn
*.suo
*.ntvs*
*.njsproj
*.sln

View File

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2021 Shigma
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -0,0 +1,10 @@
# simplify-chinese
Convert chinese characters between simplified form and tranditional form / 汉字简繁体转换工具。
```js
const { simplify, tranditionalize } = require('simplify-chinese')
console.log(simplify('窩窩頭')) // 窝窝头
console.log(tranditionalize('窝窝头')) // 窩窩頭
```

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,2 @@
export function simplify(source: string): string
export function tranditionalize(source: string): string

View File

@ -0,0 +1,30 @@
const { simplified, traditional } = require('./chinese')
const stMap = new Map()
const tsMap = new Map()
simplified.split('').forEach((char, index) => {
stMap.set(char, traditional[index])
tsMap.set(traditional[index], char)
})
function simplify(source) {
let result = []
for (const char of source) {
result.push(tsMap.get(char) || char)
}
return result.join('')
}
function tranditionalize(source) {
let result = []
for (const char of source) {
result.push(stMap.get(char) || char)
}
return result.join('')
}
module.exports = {
simplify,
tranditionalize,
}

View File

@ -0,0 +1,10 @@
{
"name": "simplify-chinese",
"description": "Convert chinese characters between simplified form and tranditional form 汉字简繁体转换工具",
"version": "1.1.0",
"main": "index.js",
"typings": "index.d.ts",
"repository": "https://github.com/koishijs/simplify-chinese.git",
"author": "Shigma <1700011071@pku.edu.cn>",
"license": "MIT"
}

View File

@ -217,5 +217,10 @@ svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/19
g#icon-comment(fill='currentColor')
// 0 0 24 24
path(d='M16 11H8V9H16V11M22 4V16C22 17.11 21.11 18 20 18H13.9L10.2 21.71C10 21.9 9.75 22 9.5 22H9C8.45 22 8 21.55 8 21V18H4C2.9 18 2 17.11 2 16V4C2 2.89 2.9 2 4 2H20C21.11 2 22 2.9 22 4M20 4H4V16H10V19.08L13.08 16H20V4')
g#icon-text(fill='currentColor')
// 0 0 24 24
path(fill='currentColor', d='M21,6V8H3V6H21M3,18H12V16H3V18M3,13H21V11H3V13Z')
</template>

View File

@ -87,7 +87,7 @@ div(:class="$style.player")
<script>
import Lyric from '@renderer/utils/lyric-font-player'
import { rendererSend, rendererOn, NAMES } from '../../../common/ipc'
import { rendererSend, rendererOn, NAMES, rendererInvoke } from '../../../common/ipc'
import { formatPlayTime2, getRandom, checkPath, setTitle, clipboardWriteText, debounce, throttle, assertApiSupport } from '../../utils'
import { mapGetters, mapActions, mapMutations } from 'vuex'
import { requestMsg } from '../../utils/message'
@ -655,10 +655,27 @@ export default {
setLrc(targetSong) {
this.getLrc(targetSong).then(({ lyric, tlyric, lxlyric }) => {
if (targetSong.songmid !== this.musicInfo.songmid) return
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
}).catch(() => {
return (
global.i18n.locale == 'zh-tw'
? Promise.all([
lyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
tlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(tlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
lxlyric
? rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lxlyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString())
: Promise.resolve(''),
])
: Promise.resolve([lyric, tlyric, lxlyric])
).then(([lyric, tlyric, lxlyric]) => {
this.musicInfo.lrc = lyric
this.musicInfo.tlrc = tlyric
this.musicInfo.lxlrc = lxlyric
})
}).catch((err) => {
console.log(err)
this.status = this.statusText = this.$t('core.player.lyric_error')
}).finally(() => {
this.handleUpdateWinLyricInfo('lyric', { lrc: this.musicInfo.lrc, tlrc: this.musicInfo.tlrc, lxlrc: this.musicInfo.lxlrc })
@ -849,7 +866,7 @@ export default {
this.playNext()
break
case 'progress':
this.handleSetProgress(data)
this.setProgress(data * this.maxPlayTime)
break
case 'volume':
break

View File

@ -27,17 +27,28 @@
p(v-if="musicInfo.album") {{$t('core.player.album')}}{{musicInfo.album}}
div(:class="$style.right")
div(:class="[$style.lyric, lyricEvent.isMsDown ? $style.draging : null]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="[$style.lyric, { [$style.draging]: lyricEvent.isMsDown }]" @wheel="handleWheel" @mousedown="handleLyricMouseDown" ref="dom_lyric")
div(:class="$style.lyricSpace")
div(:class="[$style.lyricText]" ref="dom_lyric_text")
//- p(v-for="(info, index) in lyricLines" :key="index" :class="lyric.line == index ? $style.lrcActive : null") {{info.text}}
div(:class="$style.lyricSpace")
transition(enter-active-class="animated fadeIn" leave-active-class="animated fadeOut")
div(:class="[$style.lyricSelectContent, 'select', 'scroll']" v-if="isShowLrcSelectContent" @contextmenu="handleCopySelectText")
//- div(:class="$style.lyricSpace")
div(v-for="(info, index) in lyricLines" :key="index" :class="[$style.lyricSelectline, { [$style.lrcActive]: lyric.line == index }]")
span {{info.text}}
br(v-if="info.translation")
span(:class="$style.lyricSelectlineTransition") {{info.translation}}
//- div(:class="$style.lyricSpace")
material-music-comment(:class="$style.comment" :titleFormat="this.setting.download.fileName" :musicInfo="musicInfo" v-model="isShowComment")
div(:class="$style.footer")
div(:class="$style.footerLeft")
div(:class="$style.footerLeftControlBtns")
div(:class="[$style.footerLeftControlBtn, { [$style.active]: isShowLrcSelectContent }]" @click="isShowLrcSelectContent = !isShowLrcSelectContent" :tips="$t('core.player.lyric_select')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-text')
div(:class="[$style.footerLeftControlBtn, isShowComment ? $style.active : null]" @click="isShowComment = !isShowComment" :tips="$t('core.player.comment_show')")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' width='95%' viewBox='0 0 24 24' space='preserve')
use(xlink:href='#icon-comment')
@ -87,7 +98,7 @@
<script>
import { mapGetters, mapMutations } from 'vuex'
import { base as eventBaseName } from '../../event/names'
import { scrollTo } from '../../utils'
import { clipboardWriteText, scrollTo } from '../../utils'
let cancelScrollFn = null
@ -230,23 +241,22 @@ export default {
lyricLines: [],
isSetedLines: false,
isShowComment: false,
isShowLrcSelectContent: false,
}
},
mounted() {
this.$nextTick(() => {
this.setProgressWidth()
})
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
this.listenEvent()
// console.log('object', this.$refs.dom_lyric_text)
this.setLyric(this.lyricLines)
},
beforeDestroy() {
this.unlistenEvent()
this.clearLyricScrollTimeout()
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
computed: {
...mapGetters(['setting']),
@ -256,6 +266,16 @@ export default {
...mapMutations('player', [
'visiblePlayerDetail',
]),
listenEvent() {
document.addEventListener('mousemove', this.handleMouseMsMove)
document.addEventListener('mouseup', this.handleMouseMsUp)
window.addEventListener('resize', this.handleResize)
},
unlistenEvent() {
document.removeEventListener('mousemove', this.handleMouseMsMove)
document.removeEventListener('mouseup', this.handleMouseMsUp)
window.removeEventListener('resize', this.handleResize)
},
setLyric(lines) {
const dom_lines = document.createDocumentFragment()
for (const line of lines) {
@ -282,7 +302,7 @@ export default {
setProgress(event) {
this.$emit('action', {
type: 'progress',
data: event,
data: event.offsetX / this.pregessWidth,
})
},
setProgressWidth() {
@ -359,6 +379,12 @@ export default {
close() {
window.eventHub.$emit(eventBaseName.close)
},
handleCopySelectText() {
let str = window.getSelection().toString()
str = str.trim()
if (!str.length) return
clipboardWriteText(str)
},
},
}
</script>
@ -635,14 +661,41 @@ export default {
// transition: @transition-theme !important;
// transition-property: color, font-size;
// }
// .lrc-active {
// color: @color-theme;
// font-size: 1.2em;
// }
}
.lyricSelectContent {
position: absolute;
left: 0;
top: 0;
// text-align: center;
height: 100%;
width: 100%;
font-size: 16px;
background-color: @color-theme_2-background_1;
z-index: 10;
color: @color-player-detail-lyric;
.lyricSelectline {
padding: 8px 0;
overflow-wrap: break-word;
transition: @transition-theme !important;
transition-property: color, font-size;
line-height: 1.3;
}
.lyricSelectlineTransition {
font-size: 14px;
}
.lrc-active {
color: @color-theme;
}
}
.lyric-space {
height: 70%;
}
.lrc-active {
color: @color-theme;
font-size: 1.2em;
}
.comment {
flex: 0 0 0;
@ -853,6 +906,13 @@ each(@themes, {
// .lrc-active {
// color: ~'@{color-@{value}-theme}';
// }
.lyricSelectContent {
background-color: ~'@{color-@{value}-theme_2-background_1}';
color: ~'@{color-@{value}-player-detail-lyric}';
.lrc-active {
color: ~'@{color-@{value}-theme}';
}
}
.footerLeftControlBtns {
color: ~'@{color-@{value}-theme_2-font}';
}

View File

@ -0,0 +1,257 @@
<template lang="pug">
Modal(:show="visible" @close="$emit('update:visible', false)" bg-close)
div(:class="$style.header")
h2 {{listInfo.name}}
main.scroll(:class="$style.main")
ul(ref="dom_list" v-if="duplicateList.length" :class="$style.list")
li(v-for="(item, index) in duplicateList" :key="item.songmid" :class="$style.listItem")
div(:class="$style.num") {{item.index + 1}}
div(:class="$style.text")
h3(:class="$style.text") {{item.musicInfo.name}} - {{item.musicInfo.singer}}
h3(v-if="item.musicInfo.albumName" :class="[$style.text, $style.albumName]") {{item.musicInfo.albumName}}
div(:class="$style.label") {{item.musicInfo.source}}
div(:class="$style.label") {{item.musicInfo.interval}}
div(:class="$style.btns")
button(type="button" @click="handlePlay(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 287.386 287.386' space='preserve' v-once)
use(xlink:href='#icon-testPlay')
button(type="button" @click="handleRemove(index)" :class="$style.btn")
svg(version='1.1' xmlns='http://www.w3.org/2000/svg' xlink='http://www.w3.org/1999/xlink' height='50%' viewBox='0 0 212.982 212.982' space='preserve' v-once)
use(xlink:href='#icon-delete')
div(:class="$style.noItem" v-else)
p(v-text="$t('view.list.no_item')")
</template>
<script>
import { mapMutations } from 'vuex'
import Modal from './Modal'
import Btn from './Btn'
export default {
props: {
visible: {
type: Boolean,
default: false,
},
listInfo: {
type: Object,
required: true,
},
},
model: {
prop: 'visible',
event: 'visible',
},
components: {
Modal,
Btn,
},
watch: {
visible(n) {
if (n) this.handleFilterList()
},
},
data() {
return {
duplicateList: [],
}
},
mounted() {
if (this.listInfo.list) this.handleFilterList()
},
methods: {
...mapMutations('list', [
'listRemove',
]),
...mapMutations('player', {
setPlayList: 'setList',
}),
handleFilterList() {
const listMap = new Map()
const duplicateList = []
this.listInfo.list.forEach((musicInfo, index) => {
const musicInfoName = musicInfo.name.toLowerCase().trim()
if (listMap.has(musicInfoName)) {
const targetMusicInfo = listMap.get(musicInfoName)
duplicateList.push({
index: this.listInfo.list.indexOf(targetMusicInfo),
musicInfo: targetMusicInfo,
}, {
index,
musicInfo,
})
} else {
listMap.set(musicInfoName, musicInfo)
}
})
this.duplicateList = duplicateList
},
handleRemove(index) {
const { musicInfo: targetMusicInfo } = this.duplicateList.splice(index, 1)[0]
// let duplicates = []
// for (let index = 0; index < this.duplicateList.length; index++) {
// const { musicInfo } = this.duplicateList[index]
// if (musicInfo.name == targetMusicInfo.name) {
// duplicates.push(index)
// if (duplicates.length > 1) break
// }
// }
// console.log(duplicates)
// if (duplicates.length < 2) this.duplicateList.splice(duplicates[0], 1)
this.listRemove({ listId: this.listInfo.id, id: targetMusicInfo.songmid })
this.handleFilterList()
},
handlePlay(index) {
const { index: musicInfoIndex } = this.duplicateList[index]
this.setPlayList({ list: this.listInfo, index: musicInfoIndex })
},
},
}
</script>
<style lang="less" module>
@import '../../assets/styles/layout.less';
.header {
flex: none;
padding: 15px;
text-align: center;
}
.main {
min-height: 200px;
width: 460px;
}
.list {
// background-color: @color-search-form-background;
font-size: 13px;
transition-property: height;
position: relative;
.listItem {
position: relative;
padding: 8px 5px;
transition: background-color .2s ease;
line-height: 1.3;
// overflow: hidden;
display: flex;
flex-flow: row nowrap;
align-items: center;
&:hover {
background-color: @color-theme_2-hover;
}
// border-radius: 4px;
// &:last-child {
// border-bottom-left-radius: 4px;
// border-bottom-right-radius: 4px;
// }
}
}
.num {
flex: none;
font-size: 12px;
width: 30px;
text-align: center;
color: @color-theme_2-font-label;
}
.text {
flex: auto;
padding-left: 5px;
.mixin-ellipsis-1;
}
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.label {
flex: none;
font-size: 12px;
opacity: 0.5;
padding: 0 5px;
display: flex;
align-items: center;
// transform: rotate(45deg);
// background-color:
}
.btns {
flex: none;
font-size: 12px;
padding: 0 5px;
display: flex;
align-items: center;
}
.btn {
background-color: transparent;
border: none;
border-radius: @form-radius;
margin-right: 5px;
cursor: pointer;
padding: 4px 7px;
color: @color-btn;
outline: none;
transition: background-color 0.2s ease;
line-height: 0;
&:last-child {
margin-right: 0;
}
svg {
height: 16px;
}
&:hover {
background-color: @color-theme_2-hover;
}
&:active {
background-color: @color-theme_2-active;
}
}
.no-item {
position: relative;
height: 200px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
p {
font-size: 16px;
color: @color-theme_2-font-label;
}
}
each(@themes, {
:global(#container.@{value}) {
.listItem {
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
}
.num {
color: ~'@{color-@{value}-theme_2-font-label}';
}
.btn {
color: ~'@{color-@{value}-btn}';
&:hover {
background-color: ~'@{color-@{value}-theme_2-hover}';
}
&:active {
background-color: ~'@{color-@{value}-theme_2-active}';
}
}
.no-item {
p {
color: ~'@{color-@{value}-theme_2-font-label}';
}
}
}
})
</style>

View File

@ -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;

View File

@ -374,6 +374,7 @@ export default {
.albumName {
font-size: 12px;
opacity: 0.6;
.mixin-ellipsis-1;
}
.source {
flex: none;

View File

@ -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"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": " (Click to copy)",
"volume": "Volume: ",
"pause": "Pause",
"play": "Play",
"prev": "Prev",
"next": "Next",
"playing": "Now playing...",
"stop": "Paused",
"end": "Stopped",
"refresh_url": "Music URL expired, refreshing...",
"error": "Error loading music. Switch to next song after 5 seconds",
"loading": "Music loading...",
"buffering": "Buffering...",
"geting_url": "Getting music link...",
"lyric_error": "Failed to get lyrics",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"name": "Name: ",
"singer": "Artist: ",
"album": "Album: ",
"add_music_to": "Add the current song to...",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_single_loop": "Single Loop",
"play_toggle_mode_off": "Disable",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"comment_show": "Song comments",
"comment_hot_loading": "Hot comments are loading",
"comment_new_loading": "Latest comments are loading",
"album": "Album: ",
"buffering": "Buffering...",
"comment_hot_load_error": "Hot comments failed to load, click to try to reload",
"comment_new_load_error": "The latest comment failed to load, click to try to reload",
"comment_refresh": "Refresh comments",
"comment_no_content": "No comments yet",
"comment_hot_loading": "Hot comments are loading",
"comment_hot_title": "Hot Comment",
"comment_new_load_error": "The latest comment failed to load, click to try to reload",
"comment_new_loading": "Latest comments are loading",
"comment_new_title": "Latest comment",
"comment_title": "{name} comment"
"comment_no_content": "No comments yet",
"comment_refresh": "Refresh comments",
"comment_show": "Song comments",
"comment_title": "{name} comment",
"copy_title": " (Click to copy)",
"desktop_lyric_lock": "Right click to lock lyrics",
"desktop_lyric_off": "Close Desktop Lyrics",
"desktop_lyric_on": "Open Desktop Lyrics",
"desktop_lyric_unlock": "Right click to unlock lyrics",
"end": "Stopped",
"error": "Error loading music. Switch to next song after 5 seconds",
"geting_url": "Getting music link...",
"hide_detail": "Hide detail page (Right-click in the view to quickly hide the details page)",
"loading": "Music loading...",
"lyric_error": "Failed to get lyrics",
"lyric_select": "Lyric text selection",
"name": "Name: ",
"next": "Next",
"pause": "Pause",
"pic_tip": "Right click to locate the currently playing song in \"My List\"",
"play": "Play",
"play_toggle_mode_list": "Play in order",
"play_toggle_mode_list_loop": "List Loop",
"play_toggle_mode_off": "Disable",
"play_toggle_mode_random": "List Random",
"play_toggle_mode_single_loop": "Single Loop",
"playing": "Now playing...",
"prev": "Prev",
"refresh_url": "Music URL expired, refreshing...",
"singer": "Artist: ",
"stop": "Paused",
"volume": "Volume: "
}

View File

@ -1,28 +1,38 @@
{
"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_duplicate": "Duplicate song",
"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_remove_tip": "Do you really want to remove {name}?",
"lists_remove_tip_button": "Yes, that's right",
"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"
}

View File

@ -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}秒前"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": "(点击复制)",
"volume": "当前音量:",
"pause": "暂停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暂停播放",
"end": "播放完毕",
"refresh_url": "URL过期正在刷新URL...",
"error": "音频加载出错5 秒后切换下一首",
"loading": "音乐加载中...",
"buffering": "缓冲中...",
"geting_url": "歌曲链接获取中...",
"lyric_error": "歌词获取失败",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"name": "歌曲名:",
"singer": "艺术家:",
"album": "专辑名:",
"add_music_to": "添加当前歌曲到...",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_single_loop": "单曲循环",
"play_toggle_mode_off": "禁用",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"comment_show": "歌曲评论",
"comment_hot_loading": "热门评论加载中",
"comment_new_loading": "最新评论加载中",
"album": "专辑名:",
"buffering": "缓冲中...",
"comment_hot_load_error": "热门评论加载失败,点击尝试重新加载",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_refresh": "刷新评论",
"comment_no_content": "暂无评论",
"comment_hot_loading": "热门评论加载中",
"comment_hot_title": "热门评论",
"comment_new_load_error": "最新评论加载失败,点击尝试重新加载",
"comment_new_loading": "最新评论加载中",
"comment_new_title": "最新评论",
"comment_title": "{name} 的评论"
"comment_no_content": "暂无评论",
"comment_refresh": "刷新评论",
"comment_show": "歌曲评论",
"comment_title": "{name} 的评论",
"copy_title": "(点击复制)",
"desktop_lyric_lock": "右击锁定歌词",
"desktop_lyric_off": "关闭桌面歌词",
"desktop_lyric_on": "开启桌面歌词",
"desktop_lyric_unlock": "右击解锁歌词",
"end": "播放完毕",
"error": "音频加载出错5 秒后切换下一首",
"geting_url": "歌曲链接获取中...",
"hide_detail": "隐藏详情页(界面内右键双击可快速隐藏详情页)",
"loading": "音乐加载中...",
"lyric_error": "歌词获取失败",
"lyric_select": "歌词文本选择",
"name": "歌曲名:",
"next": "下一首",
"pause": "暂停",
"pic_tip": "右击在“我的列表”定位当前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "顺序播放",
"play_toggle_mode_list_loop": "列表循环",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表随机",
"play_toggle_mode_single_loop": "单曲循环",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL过期正在刷新URL...",
"singer": "艺术家:",
"stop": "暂停播放",
"volume": "当前音量:"
}

View File

@ -1,28 +1,38 @@
{
"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_duplicate": "重复歌曲",
"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_remove_tip": "你真的想要移除 {name} 吗?",
"lists_remove_tip_button": "是的 没错",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加载中...",
"love_list": "收藏",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "专辑",
"action": "操作",
"time": "时长",
"loding_list": "加载中...",
"no_item": "列表竟然是空的..."
"time": "时长"
}

View File

@ -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}秒前"
}

View File

@ -1,43 +1,43 @@
{
"copy_title": "(點擊複製)",
"volume": "當前音量:",
"pause": "暫停",
"play": "播放",
"prev": "上一首",
"next": "下一首",
"playing": "播放中...",
"stop": "暫停播放",
"end": "播放完畢",
"refresh_url": "URL過期正在刷新URL...",
"error": "音頻加載出錯5 秒後切換下一首",
"loading": "音樂加載中...",
"buffering": "緩衝中...",
"geting_url": "歌曲鏈接獲取中...",
"lyric_error": "歌詞獲取失敗",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"name": "歌曲名:",
"singer": "藝術家:",
"album": "專輯名:",
"add_music_to": "添加當前歌曲到...",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_single_loop": "單曲循環",
"play_toggle_mode_off": "禁用",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"comment_show": "歌曲評論",
"comment_hot_loading": "熱門評論加載中",
"comment_new_loading": "最新評論加載中",
"album": "專輯名:",
"buffering": "緩衝中...",
"comment_hot_load_error": "熱門評論加載失敗,點擊嘗試重新加載",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_refresh": "刷新評論",
"comment_no_content": "暫無評論",
"comment_hot_loading": "熱門評論加載中",
"comment_hot_title": "熱門評論",
"comment_new_load_error": "最新評論加載失敗,點擊嘗試重新加載",
"comment_new_loading": "最新評論加載中",
"comment_new_title": "最新評論",
"comment_title": "{name} 的評論"
"comment_no_content": "暫無評論",
"comment_refresh": "刷新評論",
"comment_show": "歌曲評論",
"comment_title": "{name} 的評論",
"copy_title": "(點擊複製)",
"desktop_lyric_lock": "右擊鎖定歌詞",
"desktop_lyric_off": "關閉桌面歌詞",
"desktop_lyric_on": "開啟桌面歌詞",
"desktop_lyric_unlock": "右擊解鎖歌詞",
"end": "播放完畢",
"error": "音頻加載出錯5 秒後切換下一首",
"geting_url": "歌曲鏈接獲取中...",
"hide_detail": "隱藏詳情頁(界面內右鍵雙擊可快速隱藏詳情頁)",
"loading": "音樂加載中...",
"lyric_error": "歌詞獲取失敗",
"lyric_select": "歌詞文本選擇",
"name": "歌曲名:",
"next": "下一首",
"pause": "暫停",
"pic_tip": "右擊在“我的列表”定位當前播放的歌曲",
"play": "播放",
"play_toggle_mode_list": "順序播放",
"play_toggle_mode_list_loop": "列表循環",
"play_toggle_mode_off": "禁用",
"play_toggle_mode_random": "列表隨機",
"play_toggle_mode_single_loop": "單曲循環",
"playing": "播放中...",
"prev": "上一首",
"refresh_url": "URL過期正在刷新URL...",
"singer": "藝術家:",
"stop": "暫停播放",
"volume": "當前音量:"
}

View File

@ -1,28 +1,38 @@
{
"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_duplicate": "重複歌曲",
"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_remove_tip": "你真的想要移除 {name} 嗎?",
"lists_remove_tip_button": "是的 沒錯",
"lists_rename": "重命名",
"lists_sync": "更新",
"loding_list": "加載中...",
"love_list": "收藏列表",
"name": "歌曲名",
"no_item": "列表竟然是空的...",
"singer": "歌手",
"album": "專輯",
"action": "操作",
"time": "時長",
"loding_list": "加載中...",
"no_item": "列表竟然是空的..."
"time": "時長"
}

View File

@ -0,0 +1,74 @@
<template>
<Modal :show="visible" @close="handleCancel" @after-leave="afterLeave" :closeBtn="false">
<main :class="$style.main">{{message}}</main>
<footer :class="$style.footer">
<Btn :class="$style.btn" v-if="showCancel" @click="handleCancel">{{cancelBtnText}}</Btn>
<Btn :class="$style.btn" @click="handleComfirm">{{confirmBtnText}}</Btn>
</footer>
</Modal>
</template>
<script>
import Modal from '@renderer/components/material/Modal'
import Btn from '@renderer/components/material/Btn'
export default {
components: {
Modal,
Btn,
},
data() {
return {
visible: false,
message: '',
showCancel: false,
cancelButtonText: '',
confirmButtonText: '',
}
},
computed: {
cancelBtnText() {
return this.cancelButtonText || this.$t('base.cancel_button_text')
},
confirmBtnText() {
return this.confirmButtonText || this.$t('base.confirm_button_text')
},
},
beforeDestroy() {
const el = this.$el
el.parentNode.removeChild(el)
},
methods: {
afterLeave(el, done) {
this.$destroy()
},
handleCancel() {
},
handleComfirm() {
},
},
}
</script>
<style lang="less" module>
.main {
flex: auto;
min-height: 40px;
padding: 15px;
font-size: 14px;
max-width: 320px;
min-width: 220px;
line-height: 1.5;
}
.footer {
flex: none;
padding: 0 15px 15px;
display: flex;
flex-flow: row nowrap;
justify-content: flex-end;
gap: 15px;
}
</style>

View File

@ -0,0 +1,54 @@
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(
typeof options == 'string'
? { message: options, showCancel: true }
: { ...options, showCancel: true },
)
Vue.prototype.$dialog = dialog
},
}
Vue.use(dialog)

View File

@ -3,6 +3,8 @@ import { debounce } from '../../utils'
let instance
let prevTips
let prevX = 0
let prevY = 0
const getTips = el =>
el
@ -53,6 +55,9 @@ const updateTips = event => {
}
document.body.addEventListener('mousemove', event => {
if (event.x == prevX && event.y == prevY) return
prevX = event.x
prevY = event.y
hideTips()
showTips(event)
})

View File

@ -1,2 +1,3 @@
// import './axios'
import './Dialog'
import './Tips'

View File

@ -11,7 +11,9 @@ import {
getMusicUrl as getMusicUrlFormStorage,
setMusicUrl,
assertApiSupport,
filterFileName,
} from '../../utils'
import { NAMES, rendererInvoke } from '@common/ipc'
window.downloadList = []
// state
@ -32,7 +34,6 @@ const dls = {}
const tryNum = {}
let isRuningActionTask = false
const filterFileName = /[\\/:*?#"<>|]/g
// getters
const getters = {
@ -224,7 +225,8 @@ const getPic = function(musicInfo, retryedSource = [], originMusic) {
})
})
}
const getLyric = function(musicInfo, retryedSource = [], originMusic) {
const handleGetLyric = function(musicInfo, retryedSource = [], originMusic) {
if (!originMusic) originMusic = musicInfo
let reqPromise
try {
@ -248,6 +250,33 @@ const getLyric = function(musicInfo, retryedSource = [], originMusic) {
})
}
const getLyric = function(musicInfo, isUseOtherSource) {
return getLyricFromStorage(musicInfo).then(lrcInfo => {
return (
lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? handleGetLyric.call(this, musicInfo)
: music[musicInfo.source].getLyric(musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
).then(lrcs => {
if (!lrcs) return lrcs
if (global.i18n.locale != 'zh-tw') return lrcs
return rendererInvoke(NAMES.mainWindow.lang_s2t, Buffer.from(lrcs.lyric).toString('base64')).then(b64 => Buffer.from(b64, 'base64').toString()).then(lyric => {
lrcs.lyric = lyric
return lrcs
})
})
})
}
// 修复 1.1.x版本 酷狗源歌词格式
const fixKgLyric = lrc => /\[00:\d\d:\d\d.\d+\]/.test(lrc) ? lrc.replace(/(?:\[00:(\d\d:\d\d.\d+\]))/gm, '[$1') : lrc
@ -273,21 +302,7 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
})
: Promise.resolve(),
isEmbedLyric
? getLyricFromStorage(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: (
isUseOtherSource
? getLyric.call(this, downloadInfo.musicInfo)
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise
).then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
}).catch(err => {
console.log(err)
return null
})
})
? getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource)
: Promise.resolve(),
]
Promise.all(tasks).then(([imgUrl, lyrics = {}]) => {
@ -307,17 +322,9 @@ const saveMeta = function(downloadInfo, filePath, isUseOtherSource, isEmbedPic,
* @param {*} downloadInfo
* @param {*} filePath
*/
const downloadLyric = (downloadInfo, filePath, lrcFormat) => {
const promise = getLyric(downloadInfo.musicInfo).then(lrcInfo => {
return lrcInfo.lyric
? Promise.resolve({ lyric: lrcInfo.lyric, tlyric: lrcInfo.tlyric || '' })
: music[downloadInfo.musicInfo.source].getLyric(downloadInfo.musicInfo).promise.then(({ lyric, tlyric, lxlyric }) => {
setLyric(downloadInfo.musicInfo, { lyric, tlyric, lxlyric })
return { lyric, tlyric, lxlyric }
})
})
promise.then(lrcs => {
if (lrcs.lyric) {
const downloadLyric = function(downloadInfo, isUseOtherSource, filePath, lrcFormat) {
getLyric.call(this, downloadInfo.musicInfo, isUseOtherSource).then(lrcs => {
if (lrcs?.lyric) {
lrcs.lyric = fixKgLyric(lrcs.lyric)
saveLrc(filePath.replace(/(mp3|flac|ape|wav)$/, 'lrc'), lrcs.lyric, lrcFormat)
}
@ -371,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,
@ -436,7 +443,7 @@ const actions = {
dispatch('startTask')
saveMeta.call(_this, downloadInfo, downloadInfo.filePath, rootState.setting.download.isUseOtherSource, rootState.setting.download.isEmbedPic, rootState.setting.download.isEmbedLyric)
if (rootState.setting.download.isDownloadLrc) downloadLyric(downloadInfo, downloadInfo.filePath, rootState.setting.download.lrcFormat)
if (rootState.setting.download.isDownloadLrc) downloadLyric.call(_this, downloadInfo, rootState.setting.download.isUseOtherSource, downloadInfo.filePath, rootState.setting.download.lrcFormat)
console.log('on complate')
},
onError(err) {

View File

@ -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 })

View File

@ -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, '')

View File

@ -120,6 +120,7 @@ module.exports = class Lyric {
return {
text: line.text,
time: line.time,
translation: line.translation,
dom_line: fontPlayer.lineContent,
}
})

View File

@ -48,7 +48,7 @@ module.exports = class LinePlayer {
_initLines() {
this.lines = []
this.translationLines = []
const lines = this.lyric.split('\n')
const lines = this.lyric.split(/\r\n|\r|\n/)
const linesMap = {}
// const translationLines = this.translationLyric.split('\n')
for (let i = 0; i < lines.length; i++) {

View File

@ -1,5 +1,5 @@
import { apis } from '../api-source'
import leaderboard from './leaderboard2'
import leaderboard from './leaderboard'
import songList from './songList'
import musicSearch from './musicSearch'
import pic from './pic'

View File

@ -2,7 +2,9 @@ import { httpFetch } from '../../request'
import { sizeFormate } from '../../index'
const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
// const boardList = [{ id: 'mg__27553319', name: '咪咕尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '咪咕尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '咪咕尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '咪咕港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '咪咕内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '咪咕欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '咪咕日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '咪咕彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: '咪咕KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '咪咕网络榜', bangid: '15140034' }, { id: 'mg__23217754', name: 'MV榜', bangid: '23217754' }, { id: 'mg__23218151', name: '新专辑榜', bangid: '23218151' }, { id: 'mg__21958042', name: 'iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: 'billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272904', name: '中国TOP排行榜', bangid: '22272904' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
const boardList = [{ id: 'mg__27553319', name: '尖叫新歌榜', bangid: '27553319' }, { id: 'mg__27186466', name: '尖叫热歌榜', bangid: '27186466' }, { id: 'mg__27553408', name: '尖叫原创榜', bangid: '27553408' }, { id: 'mg__23189800', name: '港台榜', bangid: '23189800' }, { id: 'mg__23189399', name: '内地榜', bangid: '23189399' }, { id: 'mg__19190036', name: '欧美榜', bangid: '19190036' }, { id: 'mg__23189813', name: '日韩榜', bangid: '23189813' }, { id: 'mg__23190126', name: '彩铃榜', bangid: '23190126' }, { id: 'mg__15140045', name: 'KTV榜', bangid: '15140045' }, { id: 'mg__15140034', name: '网络榜', bangid: '15140034' }, { id: 'mg__21958042', name: '美国iTunes榜', bangid: '21958042' }, { id: 'mg__21975570', name: '美国billboard榜', bangid: '21975570' }, { id: 'mg__22272815', name: '台湾Hito中文榜', bangid: '22272815' }, { id: 'mg__22272943', name: '韩国Melon榜', bangid: '22272943' }, { id: 'mg__22273437', name: '英国UK榜', bangid: '22273437' }]
export default {
limit: 200,
@ -59,7 +61,7 @@ export default {
},
],
getUrl(id, page) {
return `https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-detail/release?columnId=${id}`
return `https://app.c.nf.migu.cn/MIGUM2.0/v1.0/content/querycontentbyId.do?columnId=${id}&needAll=0`
// return `http://m.music.migu.cn/migu/remoting/cms_list_tag?nid=${id}&pageSize=${this.limit}&pageNo=${page - 1}`
},
successCode: '000000',
@ -67,16 +69,12 @@ export default {
requestObj: null,
getBoardsData() {
if (this.requestBoardsObj) this._requestBoardsObj.cancelHttp()
this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {
this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM3.0/v1.0/template/rank-list/release', {
// this.requestBoardsObj = httpFetch('https://app.c.nf.migu.cn/MIGUM2.0/v2.0/content/indexrank.do?templateVersion=8', {
headers: {
sign: 'c3b7ae985e2206e97f1b2de8f88691e2',
timestamp: 1578225871982,
appId: 'yyapp2',
mode: 'android',
ua: 'Android_migu',
version: '6.9.4',
osVersion: 'android 7.0',
'User-Agent': 'okhttp/3.9.1',
Referer: 'https://app.c.nf.migu.cn/',
'User-Agent': 'Mozilla/5.0 (Linux; Android 5.1.1; Nexus 6 Build/LYZ28E) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/59.0.3071.115 Mobile Safari/537.36',
channel: '0146921',
},
})
return this.requestBoardsObj.promise
@ -98,13 +96,13 @@ export default {
// console.log(rawData)
let ids = new Set()
const list = []
rawData.forEach(item => {
rawData.forEach(({ objectInfo: item }) => {
if (ids.has(item.copyrightId)) return
ids.add(item.copyrightId)
const types = []
const _types = {}
item.rateFormats && item.rateFormats.forEach(type => {
item.newRateFormats && item.newRateFormats.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':
@ -131,6 +129,8 @@ export default {
}
})
const intervalTest = /(\d\d:\d\d)$/.test(item.length)
list.push({
singer: this.getSinger(item.artists),
name: item.songName,
@ -140,7 +140,7 @@ export default {
songId: item.songId,
copyrightId: item.copyrightId,
source: 'mg',
interval: null,
interval: intervalTest ? RegExp.$1 : null,
img: item.albumImgs && item.albumImgs.length ? item.albumImgs[0].img : null,
lrc: null,
lrcUrl: item.lrcUrl,
@ -197,9 +197,9 @@ export default {
getList(bangid, page, retryNum = 0) {
if (++retryNum > 3) return Promise.reject(new Error('try max num'))
return this.getData(this.getUrl(bangid, page)).then(({ statusCode, body }) => {
console.log(body)
// console.log(body)
if (statusCode !== 200 || body.code !== this.successCode) return this.getList(bangid, page, retryNum)
const list = this.filterData(body.data.columnInfo.dataList)
const list = this.filterData(body.columnInfo.contents)
return {
total: list.length,
list,

View File

@ -45,7 +45,7 @@ export default {
ids.add(item.id)
const types = []
const _types = {}
item.rateFormats && item.rateFormats.forEach(type => {
item.newRateFormats && item.newRateFormats.forEach(type => {
let size
switch (type.formatType) {
case 'PQ':

File diff suppressed because one or more lines are too long

View File

@ -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]"
@ -70,11 +74,12 @@
material-menu(:menus="listItemMenu" :location="listMenu.menuLocation" item-name="name" :isShow="listMenu.isShowItemMenu" @menu-click="handleListItemMenuClick")
material-search-list(:list="list" @action="handleMusicSearchAction" :visible="isVisibleMusicSearch")
material-list-sort-modal(:show="isShowListSortModal" :music-info="musicInfo" :selected-num="selectdListDetailData.length" @close="isShowListSortModal = false" @confirm="handleSortMusicInfo")
material-duplicate-music-modal(:visible.sync="isShowDuplicateMusicModal" :list-info="selectedListInfo")
</template>
<script>
import { mapMutations, mapGetters, mapActions } from 'vuex'
import { throttle, scrollTo, clipboardWriteText, assertApiSupport, openUrl } from '../utils'
import { throttle, scrollTo, clipboardWriteText, assertApiSupport, openUrl, openSaveDir, saveLxConfigFile, selectDir, readLxConfigFile, filterFileName } from '../utils'
import musicSdk from '../utils/music'
export default {
name: 'List',
@ -93,6 +98,7 @@ export default {
isShowListAdd: false,
isShowListAddMultiple: false,
isShowListSortModal: false,
isShowDuplicateMusicModal: false,
delayTimeout: null,
isToggleList: true,
focusTarget: 'listDetail',
@ -105,6 +111,9 @@ export default {
isShowItemMenu: false,
itemMenuControl: {
rename: true,
duplicate: true,
import: true,
export: true,
sync: false,
moveup: true,
movedown: true,
@ -141,6 +150,7 @@ export default {
isMoveMultiple: false,
isVisibleMusicSearch: false,
fetchingListStatus: {},
selectedListInfo: {},
}
},
computed: {
@ -195,6 +205,21 @@ export default {
action: 'sync',
disabled: !this.listsData.itemMenuControl.sync,
},
{
name: this.$t('view.list.lists_duplicate'),
action: 'duplicate',
disabled: !this.listsData.itemMenuControl.duplicate,
},
{
name: this.$t('view.list.lists_import'),
action: 'import',
disabled: !this.listsData.itemMenuControl.export,
},
{
name: this.$t('view.list.lists_export'),
action: 'export',
disabled: !this.listsData.itemMenuControl.export,
},
{
name: this.$t('view.list.lists_moveup'),
action: 'moveup',
@ -722,10 +747,25 @@ export default {
}).catch(_ => _)
},
handleListsItemRigthClick(event, index) {
const source = this.userList[index].source
this.listsData.itemMenuControl.sync = !!source && !!musicSdk[source].songList
this.listsData.itemMenuControl.moveup = index > 0
this.listsData.itemMenuControl.movedown = index < this.userList.length - 1
let source
switch (index) {
case -1:
case -2:
this.listsData.itemMenuControl.rename = false
this.listsData.itemMenuControl.remove = false
this.listsData.itemMenuControl.sync = false
this.listsData.itemMenuControl.moveup = false
this.listsData.itemMenuControl.movedown = false
break
default:
this.listsData.itemMenuControl.rename = true
this.listsData.itemMenuControl.remove = true
source = this.userList[index].source
this.listsData.itemMenuControl.sync = !!source && !!musicSdk[source]?.songList
this.listsData.itemMenuControl.moveup = index > 0
this.listsData.itemMenuControl.movedown = index < this.userList.length - 1
break
}
this.listsData.rightClickItemIndex = index
this.listsData.menuLocation.x = event.currentTarget.offsetLeft + event.offsetX
this.listsData.menuLocation.y = event.currentTarget.offsetTop + event.offsetY - this.$refs.dom_lists_list.scrollTop
@ -771,6 +811,16 @@ export default {
dom.querySelector('input').focus()
})
break
case 'duplicate':
this.selectedListInfo = this.getTargetListInfo(index)
this.isShowDuplicateMusicModal = true
break
case 'import':
this.handleImportList(index)
break
case 'export':
this.handleExportList(index)
break
case 'sync':
this.handleSyncSourceList(index)
break
@ -781,7 +831,13 @@ export default {
this.movedownUserList({ id: this.userList[index].id })
break
case 'remove':
this.removeUserList({ id: this.userList[index].id })
this.$dialog.confirm({
message: this.$t('view.list.lists_remove_tip', { name: this.userList[index].name }),
confirmButtonText: this.$t('view.list.lists_remove_tip_button'),
}).then(isRemove => {
if (!isRemove) return
this.removeUserList({ id: this.userList[index].id })
})
break
}
},
@ -946,6 +1002,89 @@ export default {
},
})
},
getTargetListInfo(index) {
let list
switch (index) {
case -2:
list = this.defaultList
break
case -1:
list = this.loveList
break
default:
list = this.userList[index]
break
}
return list
},
handleExportList(index) {
const list = this.getTargetListInfo(index)
if (!list) return
openSaveDir({
title: this.$t('view.list.lists_export_part_desc'),
defaultPath: `lx_list_part_${filterFileName(list.name)}.lxmc`,
}).then(async result => {
if (result.canceled) return
const data = JSON.parse(JSON.stringify({
type: 'playListPart',
data: list,
}))
for await (const item of data.data.list) {
if (item.otherSource) delete item.otherSource
if (item.lrc) delete item.lrc
}
saveLxConfigFile(result.filePath, data)
})
},
handleImportList(index) {
const list = this.getTargetListInfo(index)
selectDir({
title: this.$t('view.list.lists_import_part_desc'),
properties: ['openFile'],
filters: [
{ name: 'Play List Part', extensions: ['json', 'lxmc'] },
{ name: 'All Files', extensions: ['*'] },
],
}).then(async result => {
if (result.canceled) return
let listData
try {
listData = JSON.parse(await readLxConfigFile(result.filePaths[0]))
} catch (error) {
return
}
if (listData.type !== 'playListPart') return
const targetList = this.lists.find(l => l.id == listData.data.id)
if (targetList) {
const confirm = await this.$dialog.confirm({
message: this.$t('view.list.lists_import_part_confirm', { importName: listData.data.name, localName: targetList.name }),
cancelButtonText: this.$t('view.list.lists_import_part_button_cancel'),
confirmButtonText: this.$t('view.list.lists_import_part_button_confirm'),
})
if (confirm) {
listData.data.name = list.name
this.setList({
name: listData.data.name,
id: listData.data.id,
list: listData.data.list,
source: listData.data.source,
sourceListId: listData.data.sourceListId,
})
return
}
listData.data.id += `__${Date.now()}`
}
this.createUserList({
name: listData.data.name,
id: listData.data.id,
list: listData.data.list,
source: listData.data.source,
sourceListId: listData.data.sourceListId,
position: Math.max(index, -1),
})
})
},
},
}
</script>

View File

@ -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')