From 260e730741d9e756da1015ab68d49ce3e7f0247a Mon Sep 17 00:00:00 2001 From: Link Date: Thu, 31 Dec 2020 13:41:18 +0800 Subject: [PATCH] feat: local music supports synchronization of different folders and songs --- packages/api/module/song_url.js | 12 +- src/components/dialog/index.tsx | 20 ++- src/electron/event/action-types.ts | 4 + src/electron/event/ipc-main/index.ts | 15 +- src/electron/event/ipc-renderer/index.ts | 4 +- src/electron/preload/init.ts | 35 ++++- src/electron/utils/index.ts | 17 +-- src/global.d.ts | 12 +- src/layout/container.less | 2 +- src/pages/download/children/song.tsx | 2 +- .../footer/components/lyrice-embed/index.less | 2 +- src/pages/music/children/song.tsx | 16 ++- src/pages/music/interface.ts | 11 +- src/pages/music/sage.ts | 13 +- src/pages/music/state.ts | 3 +- src/pages/music/view/index.less | 19 +++ src/pages/music/view/index.tsx | 133 +++++++++++++++++- src/theme/cover.less | 4 + src/utils/index.ts | 12 ++ tsconfig.json | 2 +- 20 files changed, 294 insertions(+), 44 deletions(-) diff --git a/packages/api/module/song_url.js b/packages/api/module/song_url.js index c5b3c453..eef9a5cf 100644 --- a/packages/api/module/song_url.js +++ b/packages/api/module/song_url.js @@ -6,11 +6,21 @@ const crypto = require('crypto') const { cookieToJson } = require('../util/index') const find = (id) => { - return match(id) + return match(id, [ + 'qq', + 'xiami', + 'baidu', + 'kugou', + 'kuwo', + 'migu', + 'joox', + 'youtube', + ]) .then((url) => { return url.url }) .catch((e) => { + console.warn(e) return '' }) } diff --git a/src/components/dialog/index.tsx b/src/components/dialog/index.tsx index d8b97279..5f3b01d8 100644 --- a/src/components/dialog/index.tsx +++ b/src/components/dialog/index.tsx @@ -1,9 +1,23 @@ -import { defineComponent } from 'vue' +import { defineComponent, createApp, VNodeTypes } from 'vue' import { Modal } from 'ant-design-vue' +import { create } from '@/utils/index' + +interface Config { + centered: boolean +} export const Dialog = defineComponent({ name: 'Dialog', - setup() { - return () => + setup(props, { slots }) { + return () => {slots.default && slots.default()} } }) + +export const instance = function(content: VNodeTypes, config: Config) { + const app = createApp({ + setup() { + return () => {content} + } + }) + create(app) +} diff --git a/src/electron/event/action-types.ts b/src/electron/event/action-types.ts index 170c1cbc..5de56de3 100644 --- a/src/electron/event/action-types.ts +++ b/src/electron/event/action-types.ts @@ -34,3 +34,7 @@ export const enum UpdateType { export const enum ReadLocalFile { READ_MP3_FROM_PATH = 'READ_MP3_FROM_PATH' } + +export const enum Dialog { + SHOW_DIALOG = 'SHOW_DIALOG' +} diff --git a/src/electron/event/ipc-main/index.ts b/src/electron/event/ipc-main/index.ts index ce500f17..b0a9eeeb 100644 --- a/src/electron/event/ipc-main/index.ts +++ b/src/electron/event/ipc-main/index.ts @@ -1,12 +1,13 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { ipcMain, IpcMainEvent, BrowserWindow, screen } from 'electron' +import { ipcMain, IpcMainEvent, BrowserWindow, screen, dialog } from 'electron' import { Action, MiddlewareView, LyriceAction, UpdateType, DownloadIpcType, - ReadLocalFile + ReadLocalFile, + Dialog } from '../action-types' import store from '@/electron/store/index' import { readFileSync } from 'fs' @@ -102,4 +103,14 @@ export const onIpcMainEvent = (win: BrowserWindow) => { const buffer = readFileSync(arg) event.returnValue = buffer }) + ipcMain.on(Dialog.SHOW_DIALOG, (event, arg) => { + dialog + .showOpenDialog(win, { + title: '添加文件夹', + properties: ['openDirectory', 'multiSelections'] + }) + .then(v => { + event.returnValue = v + }) + }) } diff --git a/src/electron/event/ipc-renderer/index.ts b/src/electron/event/ipc-renderer/index.ts index 9a4ea489..da5b7344 100644 --- a/src/electron/event/ipc-renderer/index.ts +++ b/src/electron/event/ipc-renderer/index.ts @@ -12,7 +12,8 @@ import { LyriceAction, UpdateType, DownloadIpcType, - ReadLocalFile + ReadLocalFile, + Dialog } from '../action-types' export const getWindow = () => remote.BrowserWindow.getFocusedWindow() @@ -25,6 +26,7 @@ type ActionType = | LyriceAction | UpdateType | ReadLocalFile + | Dialog export function sendAsyncIpcRendererEvent( action: ActionType, diff --git a/src/electron/preload/init.ts b/src/electron/preload/init.ts index acac6654..a918af54 100644 --- a/src/electron/preload/init.ts +++ b/src/electron/preload/init.ts @@ -1,4 +1,9 @@ -// runtime web +/** + * runtime: web + * The electron package cannot be imported directly, it can only be imported dynamically. + * Because webpack will import the corresponding package when analyzing dependencies, + * an electron-related error will be prompted in the browser host. + */ import { Platform } from '@/config/build' import { DownloadMutations, LocalMusicMutations } from '@/interface' @@ -12,16 +17,17 @@ const initStorage = async () => { const v = await import('@/electron/utils/index') const downloadState = store.state.Download const localMusicState = store.state.LocalMusic - const os = v.getUserOS() - const userMusicPath = v.join(os.homedir + '/Music') + const os = v.getUserOS() + const userDownloadPath = v.join(os.homedir + '/Downloads') if (!downloadState.downloadPath) { store.commit( DownloadNameSpaced + '/' + DownloadMutations.SET_DOWNLOAD_PATH, - userMusicPath + userDownloadPath ) } + const userMusicPath = v.join(os.homedir + '/Music') if (!localMusicState.normalPath) { store.commit( LocalMusicNameSpaced + '/' + LocalMusicMutations.SET_NORMAL_PATH, @@ -29,7 +35,26 @@ const initStorage = async () => { ) } - const songs = await v.readPathMusic(localMusicState.normalPath) + const paths = [ + { + path: userMusicPath, + name: '我的音乐', + check: true + }, + { + path: userDownloadPath, + name: '下载', + check: true + } + ] + if (!localMusicState.localPath.length) { + store.commit( + LocalMusicNameSpaced + '/' + LocalMusicMutations.SET_LOCAL_PATH, + paths + ) + } + + const songs = await v.readPathMusic(paths.map(item => item.path)) store.commit( LocalMusicNameSpaced + '/' + LocalMusicMutations.SET_LOCAL_MUSIC, diff --git a/src/electron/utils/index.ts b/src/electron/utils/index.ts index 01598db8..64808965 100644 --- a/src/electron/utils/index.ts +++ b/src/electron/utils/index.ts @@ -112,15 +112,16 @@ export const getMp3Tags = async ( } } -export const readPathMusic = async (abPath: string) => { - const files = readdirSync(abPath).filter(mp3 => /\.mp3$/.test(mp3)) - +export const readPathMusic = async (abPath: string[]) => { const fls: LocalSongsDetail[] = [] - for (let i = 0; i < files.length; i++) { - const file = files[i] - const path = join(abPath, file) - const mp3 = await getMp3Tags(path, file) - fls.push(mp3) + for (let i = 0; i < abPath.length; i++) { + const files = readdirSync(abPath[i]).filter(mp3 => /\.mp3$/.test(mp3)) + for (let j = 0; j < files.length; j++) { + const file = files[j] + const path = join(abPath[i], file) + const mp3 = await getMp3Tags(path, file) + fls.push(mp3) + } } return fls diff --git a/src/global.d.ts b/src/global.d.ts index 16372b65..7a09d8fa 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -8,17 +8,9 @@ declare interface MediaMetadataTypeParams { type: string }[] } + declare interface MediaMetadataType { new (option: MediaMetadataTypeParams): MediaMetadataTypeParams } + declare const MediaMetadata: MediaMetadataType -declare module '@/mp3/jsmediatags' { - export * from '@types/jsmediatags/index' -} -declare module 'mp3-duration' { - const Fn = ( - path: string, - callback: (err: unknown, duration: number) => void - ) => unknown - export default Fn -} diff --git a/src/layout/container.less b/src/layout/container.less index 639c591c..ee8ddb5f 100644 --- a/src/layout/container.less +++ b/src/layout/container.less @@ -8,7 +8,7 @@ height: 100%; box-shadow: -1px 2px 6px 2px #dcdcdc; transition: width 0.2s, height 0.2s, transform 0.2s; - transform: matrix(1, 0, 0, 1, 0, 0); + // transform: matrix(1, 0, 0, 1, 0, 0); will-change: transform; backface-visibility: hidden; &-sm { diff --git a/src/pages/download/children/song.tsx b/src/pages/download/children/song.tsx index 19a3eded..b5e89e1f 100644 --- a/src/pages/download/children/song.tsx +++ b/src/pages/download/children/song.tsx @@ -35,7 +35,7 @@ export const DownloadSong = defineComponent({ {VUE_APP_PLATFORM === Platform.ELECTRON && (
- 存储目录:{state.downloadPath}{' '} + 存储目录:{state.downloadPath} 打开目录 diff --git a/src/pages/footer/components/lyrice-embed/index.less b/src/pages/footer/components/lyrice-embed/index.less index f023c37f..88fba31e 100644 --- a/src/pages/footer/components/lyrice-embed/index.less +++ b/src/pages/footer/components/lyrice-embed/index.less @@ -22,7 +22,7 @@ &-left { min-width: 270px; height: 100%; - margin-top: 100px; + margin-top: 50px; margin-right: 80px; div&-pic { display: flex; diff --git a/src/pages/music/children/song.tsx b/src/pages/music/children/song.tsx index 9af9fb47..da8929d8 100644 --- a/src/pages/music/children/song.tsx +++ b/src/pages/music/children/song.tsx @@ -3,7 +3,7 @@ import { PlayAll } from '@/components-business/button' import { Button } from 'ant-design-vue' import { Table } from '@/components-business/table' import { useLocalMusicModule } from '@/modules' -import { SongsDetail, FooterMutations } from '@/interface' +import { SongsDetail, FooterMutations, LocalMusicMutations } from '@/interface' import { useFooterModule } from '@/modules/index' import { importIpc } from '@/electron/event/ipc-browser' import { ReadLocalFile } from '@/electron/event/action-types' @@ -11,7 +11,7 @@ import { ReadLocalFile } from '@/electron/event/action-types' export const LocalMusicSong = defineComponent({ name: 'LocalMusicSong', setup() { - const { useState } = useLocalMusicModule() + const { useState, useMutations } = useLocalMusicModule() const footerModule = useFooterModule() const state = useState() @@ -31,6 +31,14 @@ export const LocalMusicSong = defineComponent({ }) footerModule.useMutations(FooterMutations.PLAY_MUSIC) } + const handleSyncMusic = async () => { + const v = await import('@/electron/utils/index') + const songs = await v.readPathMusic( + state.localPath.map(item => item.path) + ) + + useMutations(LocalMusicMutations.SET_LOCAL_MUSIC, songs) + } onUnmounted(() => { // Release the URL object @@ -41,7 +49,9 @@ export const LocalMusicSong = defineComponent({
- +
= {} @@ -10,5 +15,11 @@ export const mutations: MutationTree = { }, [LocalMusicMutations.SET_LOCAL_MUSIC](state, songs: SongsDetail[]) { state.localMusic = songs + }, + [LocalMusicMutations.SET_LOCAL_PATH](state, paths: LocalMusicPath[]) { + state.localPath = paths + }, + [LocalMusicMutations.SET_LOCAL_INCREMENT_PATH](state, path: LocalMusicPath) { + state.localPath.push(path) } } diff --git a/src/pages/music/state.ts b/src/pages/music/state.ts index e45b5635..d90256ae 100644 --- a/src/pages/music/state.ts +++ b/src/pages/music/state.ts @@ -2,5 +2,6 @@ import { LocalMusicState } from '@/interface' export const state: LocalMusicState = { normalPath: '', - localMusic: [] + localMusic: [], + localPath: [] } diff --git a/src/pages/music/view/index.less b/src/pages/music/view/index.less index ce195efe..1cdfb8f5 100644 --- a/src/pages/music/view/index.less +++ b/src/pages/music/view/index.less @@ -1,4 +1,23 @@ .local-music { + &-directory { + &-description { + margin-bottom: 30px; + font-size: 12px; + } + &-group { + margin-bottom: 30px; + .van-checkbox { + margin-bottom: 10px; + } + } + &-footer { + display: flex; + align-items: center; + justify-content: space-between; + width: 180px; + margin: 0 auto; + } + } &-head { display: flex; align-items: center; diff --git a/src/pages/music/view/index.tsx b/src/pages/music/view/index.tsx index 09be5ede..f61bc026 100644 --- a/src/pages/music/view/index.tsx +++ b/src/pages/music/view/index.tsx @@ -1,4 +1,4 @@ -import { defineComponent } from 'vue' +import { defineComponent, nextTick, ref, watch } from 'vue' import { SecondaryBar, renderNavList @@ -6,19 +6,144 @@ import { import { navRouter } from '@/router/index' import { RouterView } from 'vue-router' import { MusicLayout } from '@/layout/music/music' +import { Modal } from 'ant-design-vue' +import { CheckboxGroup, Checkbox, Button } from 'vant' +import { useDrag } from '@/hooks/index' +import { importIpc } from '@/electron/event/ipc-browser' +import { Dialog } from '@/electron/event/action-types' +import { useLocalMusicModule } from '@/modules' +import { LocalMusicMutations } from '../interface' import './index.less' export const LocalMusic = defineComponent({ name: 'LocalMusic', - render() { + setup() { + const { useState, useMutations } = useLocalMusicModule() + const state = useState() const nav = renderNavList(navRouter, LocalMusic.name) - return ( + const modalContanier = ref() + const visibleDirectory = ref(false) + const checkPath = ref( + state.localPath.filter(item => item.check).map(item => item.path) + ) + + watch(visibleDirectory, visible => { + if (visible) { + nextTick(() => { + if (modalContanier.value) { + const el = modalContanier.value as HTMLElement + const contanier = el?.parentElement?.parentElement + const head = contanier?.querySelector( + '.ant-modal-header' + ) + if (contanier && head) { + const { start, stop } = useDrag(contanier, head, { + moveCB(x, y) { + requestAnimationFrame(() => { + if (el.parentElement && el.parentElement.parentElement) { + el.parentElement.parentElement.style.transform = `matrix(1, 0, 0, 1, ${x}, ${y}) translateZ(0)` + } + }) + } + }) + start() + } + } + }) + } + }) + + const handleAddDirectory = async () => { + const ipc = await importIpc() + const dir = ipc.sendSyncIpcRendererEvent( + Dialog.SHOW_DIALOG + ) as Electron.OpenDialogReturnValue + if (!dir.canceled) { + const path = dir.filePaths[0] + const pathSingle = { + name: path, + path: path, + check: true + } + checkPath.value.push(path) + useMutations(LocalMusicMutations.SET_LOCAL_INCREMENT_PATH, pathSingle) + } + } + + const handleCloseModal = () => { + visibleDirectory.value = false + } + + const handleConfirm = async () => { + const v = await import('@/electron/utils/index') + const songs = await v.readPathMusic(checkPath.value) + + useMutations(LocalMusicMutations.SET_LOCAL_MUSIC, songs) + visibleDirectory.value = false + } + + return () => ( ( <>
本地音乐
- 选择目录 + + (visibleDirectory.value = !visibleDirectory.value) + } + > + 选择目录 + + +
+
+ 将自动扫描您勾选的目录,文件增删实时同步。 +
+ + {state.localPath.map(item => ( + + {item.name} + + ))} + + +
+
), head: () => , diff --git a/src/theme/cover.less b/src/theme/cover.less index 55dfa96f..caf3b7ae 100644 --- a/src/theme/cover.less +++ b/src/theme/cover.less @@ -49,3 +49,7 @@ button.easy-button-text { } } } + +.ant-modal-header { + user-select: none; +} diff --git a/src/utils/index.ts b/src/utils/index.ts index 3293a945..95700b5d 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { App } from 'vue' import dayjs, { OpUnitType } from 'dayjs' import UTC from 'dayjs/plugin/utc' import customParseFormat from 'dayjs/plugin/customParseFormat' @@ -134,6 +135,17 @@ export const toArrayBuffer = (buf: Buffer) => { return ab } +export const create = (appFunction: App) => { + const div = document.createElement('div') + document.body.appendChild(div) + appFunction.mixin({ + unmounted() { + appFunction.unmount(div) + } + }) + appFunction.mount(div) +} + export function on( container: Window, type: T, diff --git a/tsconfig.json b/tsconfig.json index 38101967..f6863bf7 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ "allowSyntheticDefaultImports": true, "sourceMap": true, "baseUrl": ".", - "types": ["webpack-env", "mocha", "chai"], + "types": ["webpack-env", "mocha", "chai", "electron"], "paths": { "@/*": ["src/*"] },