diff --git a/FAQ.md b/FAQ.md index 7557d5a6..23759d43 100644 --- a/FAQ.md +++ b/FAQ.md @@ -305,16 +305,34 @@ Windows 7 未开启 Aero 效果时桌面歌词会有问题,详情看上面的 - URL统一以`lxmusic://`开头 - 此技术目前只支持 Windows、Mac系统 -- URL传参以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据 - 若无特别说明,源的可用值为:`kw/kg/tx/wy/mg` - 若无特别说明,音质的可用值为:`128k/320k/flac/flac32bit` +目前支持两种传参方式: + +- 通过`data`传参,以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据,支持复杂的参数调用 +- 通过`URL`传参,适用于简单传参的调用,不需要转成JSON格式,例:`lxmusic://music/search/xxxx`,但仍然需要对数据进行URL编码,只适应于简单参数调用(v1.18.0新增) + +### `data`方式传参 + +以经过URL编码的JSON数据传参,例:`lxmusic://music/play?data=xxxx`,其中`xxxx`为经过URL编码后的JSON数据,JSON数据内容取决于下表的参数部分 + | 描述 | URL | 参数 | --- | --- | --- | 打开歌单 | `songlist/open` | `source`(源,必须)
`id`(歌单ID,可选)
`url`(歌单URL,可选)其中ID与URL必需传一个 | 播放歌单 | `songlist/play` | `source`(源,必须)
`id`(歌单ID,可选)
`url`(歌单URL,可选)其中`id`与`url`必需传一个
`index`(播放第几首歌,可选,从0开始) +| 搜索歌曲 | `music/search` | `keywords`(要搜索的内容,必须)
`source`(源,可选) | 播放歌曲 | `music/play` | `name`(歌曲名,必传)
`singer`(艺术家名,必传)
`source`(源,必传)
`songmid`(歌曲ID,必传)
`img`(歌曲图片链接,选传)
`albumId`(歌曲专辑ID,选传)
`interval`(格式化后的歌曲时长,选传,例:`03:55`)
`albumName`(歌曲专辑名称,选传)
`types`(歌曲可用音质数组,必传,
数组格式:`[{"type": "<音质>", size: "<格式化后的文件大小,选传>", hash: ""}]`,
例:`[{"type": "128k", size: "3.56M"}, {"type": "320k", size: null}]`)

以下为平台特定参数:
`hash`(歌曲hash,kg源必传)
`strMediaMid`(歌曲strMediaMid,tx源必传)
`albumMid`(歌曲albumMid,tx源专用,选传)
`copyrightId`(歌曲copyrightId,mg源必传)
`lrcUrl`(歌曲lrcUrl,mg源专用,选传) +### `URL`方式传参 + +由于URL传参只适用于简单传参场景,所以目前只支持以下功能的调用: + +| 描述 | URL | 参数 +| --- | --- | --- +| 搜索歌曲 | `music/search/{source}/{keywords}` | `source`(源,可选)
`keywords`(要搜索的内容,必须)
例:`music/search/kw/xxx`、`music/search/xxx` +| 打开歌单 | `songlist/open/{source}/{id/url}` | `source`(源,必须)
`id/url`(歌单ID或歌单URL,必须)
例:`songlist/open/kw/123456` + ## 自定义源脚本编写说明 文件请使用UTF-8编码格式编写,脚本所用编程语言为JavaScript,可以使用ES6+语法,脚本与应用的交互是使用类似事件收发的方式进行,这是一个基本的脚本例子: diff --git a/publish/changeLog.md b/publish/changeLog.md index bc4ee3f8..a99e3bd0 100644 --- a/publish/changeLog.md +++ b/publish/changeLog.md @@ -3,6 +3,8 @@ - 新增“双击列表里的歌曲时自动切换到当前列表播放”设置,此功能仅对歌单、排行榜有效,默认关闭 - 新增打开收藏的在线列表的对应平台详情页功能,可以在我的列表-列表右键菜单中使用 - 新增定时暂停播放功能,由于此功能大多数人可能不常用,所以将其放在设置-基本设置中 +- 新增Scheme URL对音乐搜索的调用支持,详情看常见问题-Scheme URL支持 +- 新增Scheme URL以url传参的方式调用,详情看常见问题-Scheme URL支持 ### 优化 diff --git a/src/renderer/core/useApp/useDeepLink.js b/src/renderer/core/useApp/useDeepLink.js deleted file mode 100644 index 1a1dea8e..00000000 --- a/src/renderer/core/useApp/useDeepLink.js +++ /dev/null @@ -1,281 +0,0 @@ -import { useCommit, useAction, onBeforeUnmount, useRouter, useI18n, markRaw } from '@renderer/utils/vueTools' -import { base as eventBaseName } from '@renderer/event/names' -import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools' -import { decodeName } from '@renderer/utils' -// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list' -import { isShowPlayerDetail, setShowPlayerDetail, playMusicInfo } from '@renderer/core/share/player' -import usePlaySonglist from './compositions/usePlaySonglist' -import { dialog } from '@renderer/plugins/Dialog' - -const useDialog = () => { - const { t } = useI18n() - const errorDialog = message => { - dialog({ - message: `${t('deep_link__handle_error_tip', { message })}`, - confirmButtonText: t('ok'), - }) - } - - return errorDialog -} - -const sources = ['kw', 'kg', 'tx', 'wy', 'mg'] -const sourceVerify = source => { - if (!sources.includes(source)) throw new Error('Source no match') -} - -const qualitys = ['128k', '320k', 'flac', 'flac32bit'] -const qualityFilter = (source, types) => { - types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => { - if (size != null && typeof size != 'string') throw new Error(type + ' size type no match') - if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match') - return hash == null ? { type, size } : { type, size, hash } - }) - if (!types.length) throw new Error('quality no match') - return types -} - -const dataVerify = (rules, data) => { - const newData = {} - for (const rule of rules) { - const val = data[rule.key] - if (rule.required && val == null) throw new Error(rule.key + ' missing') - if (val != null) { - if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match') - if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match') - if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match') - } - newData[rule.key] = val - } - return newData -} - -export default () => { - // const setList = useCommit('list', 'setList') - // const listAdd = useCommit('list', 'listAdd') - // const listMove = useCommit('list', 'listMove') - // const listAddMultiple = useCommit('list', 'listAddMultiple') - // const listMoveMultiple = useCommit('list', 'listMoveMultiple') - // const listRemove = useCommit('list', 'listRemove') - // const listRemoveMultiple = useCommit('list', 'listRemoveMultiple') - // const listClear = useCommit('list', 'listClear') - // const updateMusicInfo = useCommit('list', 'updateMusicInfo') - // const createUserList = useCommit('list', 'createUserList') - // const removeUserList = useCommit('list', 'removeUserList') - // const setUserListName = useCommit('list', 'setUserListName') - // const setMusicPosition = useCommit('list', 'setMusicPosition') - // // const setSyncListData = useCommit('list', 'setSyncListData') - // const setUserListPosition = useCommit('list', 'setUserListPosition') - const router = useRouter() - const setTempPlayList = useCommit('player', 'setTempPlayList') - const playNext = useAction('player', 'playNext') - const playSongListDetail = usePlaySonglist() - let isInited = false - - const showErrorDialog = useDialog() - - const handleOpenSonglist = params => { - if (params.id) { - router.replace({ - path: '/songList', - query: { - source: params.source, - id: params.id, - }, - }) - } else if (params.url) { - router.replace({ - path: '/songList', - query: { - source: params.source, - url: params.url, - }, - }) - } - } - - const handlePlayMusic = _musicInfo => { - const musicInfo = { - ..._musicInfo, - singer: decodeName(_musicInfo.singer), - name: decodeName(_musicInfo.name), - albumName: decodeName(_musicInfo.albumName), - otherSource: null, - _types: {}, - typeUrl: {}, - } - for (const type of musicInfo.types) { - musicInfo._types[type.type] = { size: type.size } - } - markRaw(musicInfo) - const isPlaying = !!playMusicInfo.musicInfo - setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }]) - if (isPlaying) playNext() - } - - const handleSonglist = (action, songlistInfo) => { - sourceVerify(songlistInfo.source) - switch (action) { - case 'open': - songlistInfo = dataVerify([ - { key: 'source', types: ['string'] }, - { key: 'id', types: ['string', 'number'], max: 64 }, - { key: 'url', types: ['string'], max: 500 }, - ], songlistInfo) - if (isShowPlayerDetail.value) setShowPlayerDetail(false) - handleOpenSonglist(songlistInfo) - break - case 'play': - songlistInfo = dataVerify([ - { key: 'source', types: ['string'] }, - { key: 'id', types: ['string', 'number'], max: 64 }, - { key: 'url', types: ['string'], max: 500 }, - { key: 'index', types: ['number'], max: 1000000 }, - ], songlistInfo) - playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0) - break - default: throw new Error('Unknown action: ' + action) - } - } - - const handleMusic = (action, musicInfo) => { - switch (musicInfo.source) { - case 'kw': - musicInfo = dataVerify([ - { key: 'name', types: ['string'], required: true, max: 200 }, - { key: 'singer', types: ['string'], required: true, max: 200 }, - { key: 'source', types: ['string'], required: true }, - { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, - { key: 'img', types: ['string'], max: 1024 }, - { key: 'albumId', types: ['string', 'number'], max: 64 }, - { key: 'interval', types: ['string'], max: 64 }, - { key: 'albumName', types: ['string'], max: 64 }, - { key: 'types', types: ['object'], required: true }, - ], musicInfo) - break - case 'kg': - musicInfo = dataVerify([ - { key: 'name', types: ['string'], required: true, max: 200 }, - { key: 'singer', types: ['string'], required: true, max: 200 }, - { key: 'source', types: ['string'], required: true }, - { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, - { key: 'img', types: ['string'], max: 1024 }, - { key: 'albumId', types: ['string', 'number'], max: 64 }, - { key: 'interval', types: ['string'], max: 64 }, - { key: '_interval', types: ['number'], max: 64 }, - { key: 'albumName', types: ['string'], max: 64 }, - { key: 'types', types: ['object'], required: true }, - - { key: 'hash', types: ['string'], required: true, max: 64 }, - ], musicInfo) - break - case 'tx': - musicInfo = dataVerify([ - { key: 'name', types: ['string'], required: true, max: 200 }, - { key: 'singer', types: ['string'], required: true, max: 200 }, - { key: 'source', types: ['string'], required: true }, - { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, - { key: 'img', types: ['string'], max: 1024 }, - { key: 'albumId', types: ['string', 'number'], max: 64 }, - { key: 'interval', types: ['string'], max: 64 }, - { key: 'albumName', types: ['string'], max: 64 }, - { key: 'types', types: ['object'], required: true }, - - { key: 'strMediaMid', types: ['string'], required: true, max: 64 }, - { key: 'albumMid', types: ['string'], max: 64 }, - ], musicInfo) - break - case 'wy': - musicInfo = dataVerify([ - { key: 'name', types: ['string'], required: true, max: 200 }, - { key: 'singer', types: ['string'], required: true, max: 200 }, - { key: 'source', types: ['string'], required: true }, - { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, - { key: 'img', types: ['string'], max: 1024 }, - { key: 'albumId', types: ['string', 'number'], max: 64 }, - { key: 'interval', types: ['string'], max: 64 }, - { key: 'albumName', types: ['string'], max: 64 }, - { key: 'types', types: ['object'], required: true }, - ], musicInfo) - break - case 'mg': - musicInfo = dataVerify([ - { key: 'name', types: ['string'], required: true, max: 200 }, - { key: 'singer', types: ['string'], required: true, max: 200 }, - { key: 'source', types: ['string'], required: true }, - { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, - { key: 'img', types: ['string'], max: 1024 }, - { key: 'albumId', types: ['string', 'number'], max: 64 }, - { key: 'interval', types: ['string'], max: 64 }, - { key: 'albumName', types: ['string'], max: 64 }, - { key: 'types', types: ['object'], required: true }, - - { key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 }, - { key: 'lrcUrl', types: ['string'], max: 1024 }, - ], musicInfo) - break - default: throw new Error('Unknown action: ' + action) - } - musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types) - switch (action) { - case 'play': - handlePlayMusic(musicInfo) - break - default: throw new Error('Unknown action: ' + action) - } - } - - const handleLinkAction = link => { - // console.log(link) - const [url, search] = link.split('?') - const [type, action] = url.replace('lxmusic://', '').split('/') - const params = {} - for (const param of search.split('&')) { - const [key, value] = param.split('=') - params[key] = value - } - if (params.data) params.data = JSON.parse(decodeURIComponent(params.data)) - console.log(params.data) - switch (type) { - case 'music': - handleMusic(action, params.data) - break - case 'songlist': - handleSonglist(action, params.data) - break - default: throw new Error('Unknown type: ' + type) - } - } - - const handleFocus = () => { - if (!isInited) return - - getEnvParams().then(envParams => { - if (!envParams.deeplink) return - clearEnvParamsDeeplink() - try { - handleLinkAction(envParams.deeplink) - } catch (err) { - showErrorDialog(err.message) - } - }) - } - - window.eventHub.on(eventBaseName.focus, handleFocus) - - onBeforeUnmount(() => { - window.eventHub.off(eventBaseName.focus, handleFocus) - }) - - return envParams => { - if (envParams.deeplink) { - clearEnvParamsDeeplink() - try { - handleLinkAction(envParams.deeplink) - } catch (err) { - showErrorDialog(err.message) - } - } - isInited = true - } -} diff --git a/src/renderer/core/useApp/useDeepLink/index.js b/src/renderer/core/useApp/useDeepLink/index.js new file mode 100644 index 00000000..1d3f8dc7 --- /dev/null +++ b/src/renderer/core/useApp/useDeepLink/index.js @@ -0,0 +1,74 @@ +import { onBeforeUnmount } from '@renderer/utils/vueTools' +import { base as eventBaseName } from '@renderer/event/names' +import { getEnvParams, clearEnvParamsDeeplink } from '@renderer/utils/tools' + +import { useDialog } from './utils' +import useMusicAction from './useMusicAction' +import useSonglistAction from './useSonglistAction' + +export default () => { + let isInited = false + + const showErrorDialog = useDialog() + + const handleMusicAction = useMusicAction() + const handleSonglistAction = useSonglistAction() + + + const handleLinkAction = link => { + // console.log(link) + const [url, search] = link.split('?') + const [type, action, ...paths] = url.replace('lxmusic://', '').split('/') + const params = {} + if (search) { + for (const param of search.split('&')) { + const [key, value] = param.split('=') + params[key] = value + } + if (params.data) params.data = JSON.parse(decodeURIComponent(params.data)) + } + params.paths = paths.map(p => decodeURIComponent(p)) + console.log(params) + switch (type) { + case 'music': + handleMusicAction(action, params) + break + case 'songlist': + handleSonglistAction(action, params) + break + default: throw new Error('Unknown type: ' + type) + } + } + + const handleFocus = () => { + if (!isInited) return + + getEnvParams().then(envParams => { + if (!envParams.deeplink) return + clearEnvParamsDeeplink() + try { + handleLinkAction(envParams.deeplink) + } catch (err) { + showErrorDialog(err.message) + } + }) + } + + window.eventHub.on(eventBaseName.focus, handleFocus) + + onBeforeUnmount(() => { + window.eventHub.off(eventBaseName.focus, handleFocus) + }) + + return envParams => { + if (envParams.deeplink) { + clearEnvParamsDeeplink() + try { + handleLinkAction(envParams.deeplink) + } catch (err) { + showErrorDialog(err.message) + } + } + isInited = true + } +} diff --git a/src/renderer/core/useApp/useDeepLink/useMusicAction.js b/src/renderer/core/useApp/useDeepLink/useMusicAction.js new file mode 100644 index 00000000..3c4ab01e --- /dev/null +++ b/src/renderer/core/useApp/useDeepLink/useMusicAction.js @@ -0,0 +1,167 @@ +import { useCommit, useAction, useRouter, markRaw } from '@renderer/utils/vueTools' +import { decodeName } from '@renderer/utils' +// import { allList, defaultList, loveList, userLists } from '@renderer/core/share/list' +import { playMusicInfo } from '@renderer/core/share/player' + +import { dataVerify, qualityFilter, sources } from './utils' + +const useSearchMusic = () => { + const router = useRouter() + + return ({ paths, data: params }) => { + let text + let source + if (params) { + text = dataVerify([ + { key: 'keywords', types: ['string', 'number'], max: 128, required: true }, + ], params).keywords + source = params.source + } else { + if (!paths.length) throw new Error('Keyword missing') + + if (paths.length > 1) { + text = paths[1] + source = paths[0] + } else { + text = paths[0] + } + + if (text.length > 128) text = text.substring(0, 128) + } + + const sourceList = [...sources, 'all'] + source = sourceList.includes(source) ? source : null + router.replace({ + path: '/search', + query: { + text, + source, + }, + }) + } +} + +const usePlayMusic = () => { + const setTempPlayList = useCommit('player', 'setTempPlayList') + const playNext = useAction('player', 'playNext') + + const filterInfoByPlayMusic = musicInfo => { + switch (musicInfo.source) { + case 'kw': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 64 }, + { key: 'types', types: ['object'], required: true }, + ], musicInfo) + break + case 'kg': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: '_interval', types: ['number'], max: 64 }, + { key: 'albumName', types: ['string'], max: 64 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'hash', types: ['string'], required: true, max: 64 }, + ], musicInfo) + break + case 'tx': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 64 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'strMediaMid', types: ['string'], required: true, max: 64 }, + { key: 'albumMid', types: ['string'], max: 64 }, + ], musicInfo) + break + case 'wy': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 64 }, + { key: 'types', types: ['object'], required: true }, + ], musicInfo) + break + case 'mg': + musicInfo = dataVerify([ + { key: 'name', types: ['string'], required: true, max: 200 }, + { key: 'singer', types: ['string'], required: true, max: 200 }, + { key: 'source', types: ['string'], required: true }, + { key: 'songmid', types: ['string', 'number'], max: 64, required: true }, + { key: 'img', types: ['string'], max: 1024 }, + { key: 'albumId', types: ['string', 'number'], max: 64 }, + { key: 'interval', types: ['string'], max: 64 }, + { key: 'albumName', types: ['string'], max: 64 }, + { key: 'types', types: ['object'], required: true }, + + { key: 'copyrightId', types: ['string', 'number'], required: true, max: 64 }, + { key: 'lrcUrl', types: ['string'], max: 1024 }, + ], musicInfo) + break + default: throw new Error('Unknown source: ' + musicInfo.source) + } + musicInfo.types = qualityFilter(musicInfo.source, musicInfo.types) + return musicInfo + } + + return ({ data: _musicInfo }) => { + _musicInfo = filterInfoByPlayMusic(_musicInfo) + + const musicInfo = { + ..._musicInfo, + singer: decodeName(_musicInfo.singer), + name: decodeName(_musicInfo.name), + albumName: decodeName(_musicInfo.albumName), + otherSource: null, + _types: {}, + typeUrl: {}, + } + for (const type of musicInfo.types) { + musicInfo._types[type.type] = { size: type.size } + } + markRaw(musicInfo) + const isPlaying = !!playMusicInfo.musicInfo + setTempPlayList([{ listId: '__temp__', musicInfo, isTop: true }]) + if (isPlaying) playNext() + } +} + +export default () => { + const handleSearchMusic = useSearchMusic() + const handlePlayMusic = usePlayMusic() + + return (action, info) => { + switch (action) { + case 'search': + handleSearchMusic(info) + break + case 'play': + handlePlayMusic(info) + break + default: throw new Error('Unknown action: ' + action) + } + } +} diff --git a/src/renderer/core/useApp/useDeepLink/useSonglistAction.js b/src/renderer/core/useApp/useDeepLink/useSonglistAction.js new file mode 100644 index 00000000..f5ae2b4a --- /dev/null +++ b/src/renderer/core/useApp/useDeepLink/useSonglistAction.js @@ -0,0 +1,102 @@ +import { useRouter } from '@renderer/utils/vueTools' +import { isShowPlayerDetail, setShowPlayerDetail } from '@renderer/core/share/player' +import usePlaySonglist from '../compositions/usePlaySonglist' + +import { dataVerify, sourceVerify } from './utils' + +const useOpenSonglist = () => { + const router = useRouter() + + const handleOpenSonglist = params => { + if (params.id) { + router.replace({ + path: '/songList', + query: { + source: params.source, + id: params.id, + }, + }) + } else if (params.url) { + router.replace({ + path: '/songList', + query: { + source: params.source, + url: params.url, + }, + }) + } + } + + return ({ paths, data }) => { + let songlistInfo = { + source: null, + id: null, + url: null, + } + if (!data) { + songlistInfo.source = paths[0] + songlistInfo.url = paths[1] + } + + sourceVerify(songlistInfo.source) + + songlistInfo = dataVerify([ + { key: 'source', types: ['string'] }, + { key: 'id', types: ['string', 'number'], max: 64 }, + { key: 'url', types: ['string'], max: 500 }, + ], songlistInfo) + + if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing') + if (isShowPlayerDetail.value) setShowPlayerDetail(false) + handleOpenSonglist(songlistInfo) + } +} +const usePlaySonglistDetail = () => { + const playSongListDetail = usePlaySonglist() + + return ({ paths, data }) => { + let songlistInfo = { + source: null, + id: null, + url: null, + index: null, + } + if (!data) { + songlistInfo.source = paths[0] + songlistInfo.url = paths[1] + songlistInfo.index = paths[2] + if (songlistInfo.index != null) songlistInfo.index = parseInt(songlistInfo.index) + } + + sourceVerify(songlistInfo.source) + + songlistInfo = dataVerify([ + { key: 'source', types: ['string'] }, + { key: 'id', types: ['string', 'number'], max: 64 }, + { key: 'url', types: ['string'], max: 500 }, + { key: 'index', types: ['number'], max: 1000000 }, + ], songlistInfo) + + if (!songlistInfo.id && !songlistInfo.url) throw new Error('id or url missing') + + playSongListDetail(songlistInfo.source, songlistInfo.id ?? songlistInfo.url, songlistInfo.index ?? 0) + } +} + +export default () => { + const handleOpenSonglist = useOpenSonglist() + const handlePlaySonglist = usePlaySonglistDetail() + + + return (action, info) => { + switch (action) { + case 'open': + handleOpenSonglist(info) + break + case 'play': + handlePlaySonglist(info) + break + default: throw new Error('Unknown action: ' + action) + } + } +} diff --git a/src/renderer/core/useApp/useDeepLink/utils.js b/src/renderer/core/useApp/useDeepLink/utils.js new file mode 100644 index 00000000..0606c417 --- /dev/null +++ b/src/renderer/core/useApp/useDeepLink/utils.js @@ -0,0 +1,45 @@ +import { useI18n } from '@renderer/utils/vueTools' +import { dialog } from '@renderer/plugins/Dialog' + +export const useDialog = () => { + const { t } = useI18n() + const errorDialog = message => { + dialog({ + message: `${t('deep_link__handle_error_tip', { message })}`, + confirmButtonText: t('ok'), + }) + } + + return errorDialog +} + +export const sources = ['kw', 'kg', 'tx', 'wy', 'mg'] +export const sourceVerify = source => { + if (!sources.includes(source)) throw new Error('Source no match') +} + +export const qualitys = ['128k', '320k', 'flac', 'flac32bit'] +export const qualityFilter = (source, types) => { + types = types.filter(({ type }) => qualitys.includes(type)).map(({ type, size, hash }) => { + if (size != null && typeof size != 'string') throw new Error(type + ' size type no match') + if (source == 'kg' && typeof hash != 'string') throw new Error(type + ' hash type no match') + return hash == null ? { type, size } : { type, size, hash } + }) + if (!types.length) throw new Error('quality no match') + return types +} + +export const dataVerify = (rules, data) => { + const newData = {} + for (const rule of rules) { + const val = data[rule.key] + if (rule.required && val == null) throw new Error(rule.key + ' missing') + if (val != null) { + if (rule.types && !rule.types.includes(typeof val)) throw new Error(rule.key + ' type no match') + if (rule.max && String(val).length > rule.max) throw new Error(rule.key + ' max length no match') + if (rule.min && String(val).length > rule.min) throw new Error(rule.key + ' min length no match') + } + newData[rule.key] = val + } + return newData +} diff --git a/src/renderer/store/mutations.js b/src/renderer/store/mutations.js index 5d353e9f..9b28610b 100644 --- a/src/renderer/store/mutations.js +++ b/src/renderer/store/mutations.js @@ -3,6 +3,7 @@ export default { state.setting.themeId = val }, setSearchSource(state, { searchSource, tempSearchSource }) { + console.log(searchSource, tempSearchSource) if (searchSource != null) state.setting.search.searchSource = searchSource if (tempSearchSource != null) state.setting.search.tempSearchSource = tempSearchSource }, diff --git a/src/renderer/views/Search.vue b/src/renderer/views/Search.vue index 315b075e..edfffd6d 100644 --- a/src/renderer/views/Search.vue +++ b/src/renderer/views/Search.vue @@ -2,7 +2,7 @@ div(:class="$style.search") //- transition div(:class="$style.header") - base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" v-model="searchSourceId") + base-tab(:class="$style.tab" :list="sources" align="left" item-key="id" item-name="name" @change="handleSourceChange" v-model="searchSourceId") div(:class="$style.main") div(:class="$style.list" v-show="isLoading || listInfo.list.length") div.thead(:class="$style.thead") @@ -109,12 +109,28 @@ export default { isLoading: false, } }, - beforeRouteUpdate(to, from, next) { - if (to.query.text === undefined) return - this.text = to.query.text - this.page = 1 - this.handleSearch(this.text, this.page) - next() + beforeRouteUpdate(to, from) { + if (to.query.source && (this.sourceList[to.query.source] || to.query.source == 'all')) { + if (this.setting.search.searchSource != to.query.source) { + this.setSearchSource({ + searchSource: to.query.source, + }) + } + if (this.searchSourceId != to.query.source) { + this.searchSourceId = to.query.source + } + this.$nextTick(() => { + this.handleGetHotSearch() + }) + } + if (to.query.text != null && this.text != to.query.text) { + this.text = to.query.text + } + + this.$nextTick(() => { + this.page = 1 + this.handleSearch(this.text, this.page) + }) }, created() { this.listenEvent() @@ -124,18 +140,20 @@ export default { }, mounted() { // console.log('mounted') - - // 处理搜索源不存在时页面报错的问题 - if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') { + if (this.$route.query.source && (this.sourceList[this.$route.query.source] || this.$route.query.source == 'all')) { + this.setSearchSource({ + searchSource: this.$route.query.source, + }) + } else if (!this.sourceList[this.setting.search.searchSource] && this.setting.search.searchSource != 'all') { // 处理搜索源不存在时页面报错的问题 this.setSearchSource({ searchSource: 'kw', }) } this.searchSourceId = this.setting.search.searchSource - if (this.$route.query.text === undefined) { + if (this.$route.query.text == null) { this.text = this.$store.getters['search/searchText'] this.page = this.listInfo.page - } else if (this.$route.query.text === '') { + } else if (this.$route.query.text == '') { this.clearList() } else { this.text = this.$route.query.text @@ -156,18 +174,6 @@ export default { 'listInfo.list'() { this.removeAllSelect() }, - searchSourceId(n) { - if (n === this.setting.search.searchSource) return - if (this.text !== '') this.isLoading = true - this.$nextTick(() => { - this.page = 1 - this.handleSearch(this.text, this.page) - this.handleGetHotSearch() - }) - this.setSearchSource({ - searchSource: n, - }) - }, }, computed: { ...mapGetters(['userInfo', 'setting']), @@ -417,7 +423,7 @@ export default { this.getHotSearch(this.setting.search.searchSource) }, handleNoitemSearch(text) { - this.$router.push({ + this.$router.replace({ path: 'search', query: { text, @@ -512,6 +518,15 @@ export default { break } }, + handleSourceChange(source) { + this.$router.replace({ + path: 'search', + query: { + text: this.text, + source, + }, + }) + }, }, }