diff --git a/README.md b/README.md index 0aca4da..48401b1 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # SimMusic 2024 -专注本地音乐播放。 +高颜值模块化音频播放器。 -官网 & 软件下载:https://simsv.com/products#/simmusic \ No newline at end of file +# Downloads +https://simsv.com/products#/simmusic diff --git a/src/frontend/assets/components/PublicConfig.js b/src/frontend/assets/components/PublicConfig.js index 6e6b119..3720c88 100644 --- a/src/frontend/assets/components/PublicConfig.js +++ b/src/frontend/assets/components/PublicConfig.js @@ -7,10 +7,23 @@ const defaultConfig = { loop: 0, lrcShow: true, musicFormats: ".mp3 .wav .flac", + listDomCache: true, backgroundBlur: true, lyricBlur: true, lyricSize: 1.5, + lyricTranslation: .8, lyricSpace: .5, + lyricMultiLang: true, + leftBarWidth: 200, + autoDesktopLyrics: false, + desktopLyricsProtection: true, + desktopLyricsAutoHide: true, + desktopLyricsColor: "#1E9FFF", + desktopLyricsStroke: "#1672B8", + desktopLyricsSize: 30, + desktopLyricsWidth: 700, + desktopLyricsTop: screen.height - 300, + desktopLyricsLeft: screen.width / 2, extensions: ["assets/extensions/local.json"], extensionCache: {}, } diff --git a/src/frontend/assets/components/SimAP.css b/src/frontend/assets/components/SimAP.css index 23a258b..10a0b79 100644 --- a/src/frontend/assets/components/SimAP.css +++ b/src/frontend/assets/components/SimAP.css @@ -1,6 +1,7 @@ -#background{filter:blur(100px);z-index:-1;top:-200px;left:-200px;right:-200px;bottom:-200px;position:absolute;pointer-events:none;opacity:.15;transition:background .3s;} +#background{filter:blur(100px);z-index:-1;top:-200px;left:-200px;right:-200px;bottom:-200px;position:absolute;pointer-events:none;opacity:.2;transition:background .3s;} #background>div{width:max(40vw,40vh);height:max(40vw,40vh);animation-duration:20s;animation-iteration-count:infinite;animation-timing-function:cubic-bezier(0.1, 0, 0.9, 1);position:absolute;border-radius:100%;opacity:.8;transition:background .3s;} +#background>section{position:absolute;inset:0;background:linear-gradient(135deg, rgba(255,255,255,.1), rgba(255,255,255,.5));} .disableBackgroundBlur #background{filter:none;} .disableBackgroundBlur #background>div{display:none;} body:not(.playing) #background>div{animation-play-state:paused;} @@ -39,8 +40,8 @@ body:not(.playing) #background>div{animation-play-state:paused;} .playing .controls .buttons>.play>i:last-child,.playing .bottom .center>.play>i:last-child{opacity:1;transform:none;} /* 音量控制 */ .volume .controls .buttons>div{width:0;opacity:0!important;} -.volume .controls .buttons>.volBtn{width:200px;color:rgba(0,0,0,.7);background:rgba(0,0,0,.05)!important;transform:none!important;opacity:1!important;mask:unset;border-radius:100px;} -.volume .controls .buttons>.volBtn>i{right:140px;} +.volume .controls .buttons>.volBtn{width:180px;color:rgba(0,0,0,.7);background:rgba(0,0,0,.05)!important;transform:none!important;opacity:1!important;mask:unset;border-radius:100px;} +.volume .controls .buttons>.volBtn>i{right:120px;} .volume .controls .buttons>.volBtn>i:hover{color:var(--SimAPTheme);} .controls .buttons>.volBtn>div{width:calc(100% - 85px);position:absolute;margin:auto 0;top:0;bottom:0;right:30px;opacity:0;pointer-events:none;transition:opacity .3s;} .volume .controls .buttons>.volBtn>div{opacity:1;pointer-events:all;} @@ -61,10 +62,14 @@ body:not(.hideLyrics) .lyricsBtn{color:var(--SimAPTheme);opacity:.7;} .list::before,.list::after{content:"";display:block;height:50%;} .hideList .list{transform:scale(.6);opacity:0;pointer-events:none;} body:not(.hideList) .listBtn{color:var(--SimAPTheme);opacity:.7;} -.list>div{width:100%;padding:10px;border-radius:10px;display:flex;align-items:center;transition:background .2s;} +.list>div{width:100%;padding:0 10px;height:80px;border-radius:10px;display:flex;align-items:center;transition:background .2s;} .list>div:hover{background:rgba(0,0,0,.025);} .list>div.active,.list>div:active{background:rgba(0,0,0,.05);} +.list>div.removed{transition:all .3s,opacity .15s;height:0;background:rgba(0,0,0,.025);opacity:0;transform:scaleX(.9) scaleY(.5);} .list>div>img{min-width:60px;height:60px;border-radius:5px;margin-right:10px;background:white;} -.list>div>div{width:calc(100% - 70px);} +.list>div>div{width:calc(100% - 100px);} .list>div>div>b{display:block;width:100%;font-size:1.1em;} -.list>div>div>span{display:block;width:100%;opacity:.8;font-size:.9em;} \ No newline at end of file +.list>div>div>span{display:block;width:100%;opacity:.8;font-size:.9em;} +.list>div i{opacity:.3;width:30px;height:30px;display:flex;align-items:center;justify-content:center;transition:opacity .2s;} +.list>div i:hover{opacity:.8;} +.list>div.active i{opacity:0;pointer-events:none;} diff --git a/src/frontend/assets/components/SimAP.js b/src/frontend/assets/components/SimAP.js index fabeb42..f0c5ab1 100644 --- a/src/frontend/assets/components/SimAP.js +++ b/src/frontend/assets/components/SimAP.js @@ -63,7 +63,7 @@ const switchMusic = (playConfig) => { document.querySelector(".musicInfo>div").innerText = document.querySelector(".musicInfoBottom>div").innerText = playConfig.artist; document.getElementById("audio").src = playConfig.audio; document.getElementById("audio").currentTime = 0; - if (playConfig.play) setTimeout(() => {document.body.classList.add("playing");}); + if (playConfig.play) setTimeout(() => {document.body.classList.add("playing");SimAPControls.loadAudioState();}); SimAPControls.loadLoop(); document.title = playConfig.title + " - SimMusic"; // 初始化背景 @@ -92,8 +92,7 @@ const switchMusic = (playConfig) => { SimAPProgress.setValue(audio.currentTime); SimAPProgressBottom.setValue(audio.currentTime); current.innerText = SimAPTools.formatTime(audio.currentTime); document.body.classList[!audio.paused ? "add" : "remove"]("playing"); - navigator.mediaSession.playbackState = audio.paused ? "paused" : "playing"; - ipcRenderer.invoke(audio.paused ? "musicPause" : "musicPlay"); + SimAPControls.loadAudioState(); }; audio.onended = () => { if (config.getItem("loop") == 1) { audio.duration = 0; audio.play(); } @@ -102,7 +101,6 @@ const switchMusic = (playConfig) => { audio.onerror = () => { document.body.classList.add("withCurrentMusic"); shell.beep(); - setTimeout(() => {SimAPControls.next();}, 5000); }; // 系统级控件 navigator.mediaSession.metadata = new MediaMetadata({ title: playConfig.title, artist: playConfig.artist, artwork: [{ src: playConfig.album }], }); @@ -112,18 +110,27 @@ const switchMusic = (playConfig) => { navigator.mediaSession.setActionHandler("nexttrack", SimAPControls.next); // 初始化歌词 const slrc = new SimLRC(playConfig.lyrics); - slrc.render(document.querySelector(".lyrics>div"), audio, {align: "left", lineSpace: config.getItem("lyricSpace"), activeColor: "var(--SimAPTheme)", normalColor: "rgba(0,0,0,.4)", callback: txt => { - ipcRenderer.invoke("lrcUpdate", audio.currentTime, txt); - }}); + slrc.render(document.querySelector(".lyrics>div"), audio, { + align: "left", + lineSpace: config.getItem("lyricSpace"), + activeColor: "var(--SimAPTheme)", + normalColor: "rgba(0,0,0,.4)", + multiLangSupport: config.getItem("lyricMultiLang"), + callback: txt => { ipcRenderer.invoke("lrcUpdate", txt); } + }); SimAPControls.loadConfig(); }; const SimAPControls = { + loadAudioState() { + const playing = document.body.classList.contains("playing"); + navigator.mediaSession.playbackState = playing ? "playing" : "paused"; + ipcRenderer.invoke(playing ? "musicPlay" : "musicPause"); + }, togglePlay() { document.body.classList[audio.paused ? "add" : "remove"]("playing"); audio[audio.paused ? "play" : "pause"](); - navigator.mediaSession.playbackState = audio.paused ? "paused" : "playing"; - ipcRenderer.invoke(audio.paused ? "musicPause" : "musicPlay"); + SimAPControls.loadAudioState(); }, prev() {this.switchIndex(-1);}, next() {this.switchIndex(1);}, @@ -195,6 +202,7 @@ const SimAPControls = { loadConfig() { document.querySelector(".SimLRC").style.setProperty("--lineSpace", config.getItem("lyricSpace") + "em"); document.querySelector(".lyrics").style.setProperty("--lrcSize", config.getItem("lyricSize") + "em"); + document.querySelector(".lyrics").style.setProperty("--lrcTranslation", config.getItem("lyricTranslation") + "em"); document.body.classList[config.getItem("backgroundBlur") ? "remove" : "add"]("disableBackgroundBlur"); document.body.classList[config.getItem("lyricBlur") ? "remove" : "add"]("disableLyricsBlur"); } @@ -203,6 +211,7 @@ const SimAPControls = { const SimAPUI = { show() { if (this.playingAnimation) return; + if (document.body.classList.contains("playerShown")) return; if (!config.getItem("playList").length || !document.getElementById("album").src) return; document.getElementById("playPage").hidden = false; this.playingAnimation = true; @@ -213,16 +222,24 @@ const SimAPUI = { document.querySelector(".list div.active").scrollIntoView({block: "center"}); document.querySelector(".lyrics div.active").scrollIntoView({block: "center"}); this.playingAnimation = false; + this.toggleDesktopLyrics(); + addEventListener("visibilitychange", this.toggleDesktopLyrics); }, 50); }, hide() { if (this.playingAnimation) return; + if (!document.body.classList.contains("playerShown")) return; document.body.classList.remove("playerShown"); this.playingAnimation = true; setTimeout(() => { + this.toggleDesktopLyrics(); + removeEventListener("visibilitychange", this.toggleDesktopLyrics); document.getElementById("playPage").hidden = true; this.playingAnimation = false; }, 300); + }, + toggleDesktopLyrics() { + if (config.getItem("desktopLyricsAutoHide") && WindowStatus.lyricsWin) ipcRenderer.invoke("toggleLyrics"); } } @@ -246,6 +263,9 @@ document.documentElement.addEventListener("keydown", event => { case "ArrowLeft": audio.currentTime = Math.max(0, audio.currentTime - 5); break; + case "Escape": + SimAPUI.hide(); + break; } }); @@ -261,9 +281,17 @@ const loadVolumeUi = () => { } loadVolumeUi(); config.listenChange("volume", loadVolumeUi); -SimAPVolume.onchange = SimAPVolumeBottom.onchange = value => { config.setItem("volume", value); } +SimAPVolume.ondrag = SimAPVolumeBottom.ondrag = value => { config.setItem("volume", value); } document.body.onpointerdown = () => {document.body.classList.remove("volume");}; document.querySelector(".volBtn").onpointerdown = e => {e.stopPropagation();}; +const handleWheel = e => { + e.preventDefault(); + const value = config.getItem("volume"); + config.setItem("volume", e.deltaY > 0 ? Math.max(0, config.getItem("volume") - .05) : Math.min(1, config.getItem("volume") + .05)); +}; +document.addEventListener("wheel", e => { if (document.body.classList.contains("volume")) handleWheel(e); }, {passive: false}); +document.querySelector(".volBtn").onwheel = () => { document.body.classList.add("volume"); }; +document.querySelector(".volBtnBottom").onwheel = e => { if (!document.body.classList.contains("volume")) handleWheel(e); }; const SimAPProgress = new SimProgress(document.getElementById("progressBar")); @@ -274,3 +302,4 @@ config.listenChange("backgroundBlur", SimAPControls.loadConfig); config.listenChange("lyricBlur", SimAPControls.loadConfig); config.listenChange("lyricSize", SimAPControls.loadConfig); config.listenChange("lyricSpace", SimAPControls.loadConfig); +config.listenChange("lyricTranslation", SimAPControls.loadConfig); diff --git a/src/frontend/assets/components/SimLRC.css b/src/frontend/assets/components/SimLRC.css index aa3afaf..5a75544 100644 --- a/src/frontend/assets/components/SimLRC.css +++ b/src/frontend/assets/components/SimLRC.css @@ -6,3 +6,4 @@ .SimLRC>div.active{color:var(--activeColor);transform:scale(1);margin:var(--lineSpace) .2em;} .SimLRC>div:hover{color:var(--hoverColor);} .SimLRC>div>span,.SimLRC>div>small{display:block;} +.SimLRC>div>small{font-size:var(--lrcTranslation);} \ No newline at end of file diff --git a/src/frontend/assets/components/SimLRC.js b/src/frontend/assets/components/SimLRC.js index 120b362..cb2b29e 100644 --- a/src/frontend/assets/components/SimLRC.js +++ b/src/frontend/assets/components/SimLRC.js @@ -84,7 +84,7 @@ class SimLRC { let div = lrcEles[index]; if (div.dataset.stamp <= currentTime && (!div.nextElementSibling || div.nextElementSibling.dataset.stamp > currentTime)) { // 执行回调 - if (!div.classList.contains("active") && options.callback) options.callback(div.innerText); + if (!div.classList.contains("active") && options.callback) options.callback(div.querySelector("span") ? div.querySelector("span").innerText : div.innerText); if (!div.classList.contains("active") || forceScroll) { // 取消用户滚动模式 if (forceScroll) { @@ -130,6 +130,7 @@ class SimLRC { setTimeout(() => {container.querySelector("div.active").scrollIntoView({block: "center", behavior: "smooth"});}); // 处理用户滚动 const handleUserScroll = () => { + if (document.body.classList.contains("volume")) return; clearTimeout(this.scrollTimeoutId); this.scrollTimeoutId = setTimeout(() => { container.classList.remove("scrolling"); diff --git a/src/frontend/assets/components/SimProgress.js b/src/frontend/assets/components/SimProgress.js index b60c89d..aaae9b2 100644 --- a/src/frontend/assets/components/SimProgress.js +++ b/src/frontend/assets/components/SimProgress.js @@ -13,6 +13,7 @@ class SimProgress { const progress = Math.min(Math.max(clickX / element.clientWidth, 0), 1); element.style.setProperty("--SimProgressWidth", progress * 100 + "%"); this.value = this.min + (this.max - this.min) * progress; + if (this.ondrag) this.ondrag(this.value); } // 鼠标事件 element.addEventListener("mousedown", () => { diff --git a/src/frontend/assets/components/dialog.html b/src/frontend/assets/components/dialog.html index 3e81789..d58287f 100644 --- a/src/frontend/assets/components/dialog.html +++ b/src/frontend/assets/components/dialog.html @@ -53,10 +53,11 @@

提示

window.close(); } document.documentElement.onkeydown = e => { - if (e.ctrlKey && e.key != "a") e.preventDefault(); + if ((e.ctrlKey && ["i", "I", "r", "R"].includes(e.key)) || e.key == "Tab") e.preventDefault(); + if (document.activeElement.tagName.toLowerCase() != "input") e.preventDefault(); if (e.key == "Enter") submitDialog(); if (e.key == "Escape") cancelDialog(); - } + }; \ No newline at end of file diff --git a/src/frontend/assets/components/require.js b/src/frontend/assets/components/require.js index bba6e2c..43e128a 100644 --- a/src/frontend/assets/components/require.js +++ b/src/frontend/assets/components/require.js @@ -1,4 +1,5 @@ const {ipcRenderer, shell} = require("electron"); const fs = require("fs"); const path = require("path"); -const musicMetadata = require('music-metadata'); +const musicMetadata = require("music-metadata"); +const nodeId3 = require("node-id3"); \ No newline at end of file diff --git a/src/frontend/assets/desktop.css b/src/frontend/assets/desktop.css deleted file mode 100644 index cdb0e5c..0000000 --- a/src/frontend/assets/desktop.css +++ /dev/null @@ -1 +0,0 @@ -html{background:linear-gradient(90deg,rgba(0,0,0,.5));} \ No newline at end of file diff --git a/src/frontend/assets/extensions/local.js b/src/frontend/assets/extensions/local.js index 1278637..4adf86c 100644 --- a/src/frontend/assets/extensions/local.js +++ b/src/frontend/assets/extensions/local.js @@ -36,10 +36,24 @@ const FileExtensionTools = { }); return list; } catch { return []; } - } + }, + formatTime(ms) { + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + const milliseconds = ms % 1000; + const formattedMinutes = minutes.toString().padStart(2, '0'); + const formattedSeconds = seconds.toString().padStart(2, '0'); + const formattedMilliseconds = milliseconds.toString(); + return `${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; + }, + fileMenuItem: [ + {type: ["single"], content: { label: "在资源管理器显示", click() {shell.showItemInFolder(getCurrentSelected()[0])} }} + ] } + /**************** 左侧导航 ****************/ // 如果你懒,这个字段可以不写,这样插件就没有左侧导航功能(你可以参考下面的写搜索功能) ExtensionConfig.file.musicList = { @@ -54,7 +68,7 @@ ExtensionConfig.file.musicList = { // 内置config读取可用getItem const lists = config.getItem("folderLists"); // 由于数据格式由开发者自行定义,重复导入 & 其他错误需要开发者自行处理 - if (dir.split("\\").length == 2) return alert("您不能导入磁盘根目录。"); + if (dir.split("\\").length == 2 && !dir.split("\\")[1]) return alert("您不能导入磁盘根目录。"); if (lists.includes(dir)) return alert("此目录已被添加到目录列表中。"); lists.push(dir); // 内置config写入可用setItem @@ -82,6 +96,10 @@ ExtensionConfig.file.musicList = { { label: "查看歌曲", click() {element.click();} }, { label: "在资源管理器中显示", click() {shell.openPath(name);} }, { type: "separator" }, + { label: "添加到歌单", submenu: MusicList.getMenuItems(listName => { + MusicList.importToMusicList(listName, FileExtensionTools.scanMusic(name)); + MusicList.switchList(listName, true); + }) }, { label: "从列表中移除", click() { confirm(`目录「${folderName}」将从 SimMusic 目录列表中移除,但不会从文件系统中删除。是否继续?`, () => { const lists = config.getItem("folderLists"); @@ -109,7 +127,7 @@ ExtensionConfig.file.musicList = { document.getElementById("musicListDir").innerText = name; // 统一调用renderMusicList即可,第二个参数需要传入一个用于识别“当前歌单”的唯一的参数,推荐使用插件名+歌单id以防重复 // 如果你的scanMusic必须是异步的,可以先renderMusicList([], id)以切换界面,再renderMusicList(list, id),id一样就可以 - renderMusicList(FileExtensionTools.scanMusic(name), "folder-" + name); + renderMusicList(FileExtensionTools.scanMusic(name), "folder-" + name, false, false, "当前目录为空", FileExtensionTools.fileMenuItem); // 这个用于把当前歌单标蓝,放在renderMusicList函数后运行,推荐借鉴我的写法在renderList函数里自己设一个dataset,然后遍历dataset document.querySelectorAll(".left .leftBar div").forEach(ele => { if (ele.dataset.folderName != name) ele.classList.remove("active"); @@ -126,7 +144,7 @@ ExtensionConfig.file.musicList = { ExtensionConfig.file.readMetadata = async (file) => { file = file.replace("file:", ""); try { - const metadata = await musicMetadata.parseFile(file) + const metadata = await musicMetadata.parseFile(file); let nativeLyrics; for (const tagType in metadata.native) { if (metadata.native[tagType].forEach) metadata.native[tagType].forEach(tag => { @@ -143,7 +161,7 @@ ExtensionConfig.file.readMetadata = async (file) => { album: metadata.common.album ? metadata.common.album : file.split("\\")[file.split("\\").length - 2], time: metadata.format.duration, cover: metadataCover ? metadataCover : "", - lyrics: metadata.common.lyrics || nativeLyrics, + lyrics: nativeLyrics ? nativeLyrics : "", }; } catch { return {}; @@ -165,7 +183,16 @@ ExtensionConfig.file.player = { const lastDotIndex = file.lastIndexOf("."); lrcPath = file.substring(0, lastDotIndex) + ".lrc"; try {return fs.readFileSync(lrcPath, "utf8");} - catch {return "";} + catch { + let id3Lyrics = ""; + const id3LyricsArray = await nodeId3.Promise.read(file); + if (id3LyricsArray && id3LyricsArray.synchronisedLyrics && id3LyricsArray.synchronisedLyrics[0]) { + id3LyricsArray.synchronisedLyrics[0].synchronisedText.forEach(obj => { + id3Lyrics += `[${FileExtensionTools.formatTime(obj.timeStamp)}]${obj.text}\n`; + }); + } + return id3Lyrics; + } }, }; @@ -180,8 +207,10 @@ ExtensionConfig.file.search = async keyword => { fileArray.forEach(file => { if (SimMusicTools.getTitleFromPath(file).includes(keyword)) resultArray.push(file); else if (lastMusicIndex[file]) { - if (Object.values(lastMusicIndex[file]).join(" ").includes(keyword)) resultArray.push(file); + const songInfo = lastMusicIndex[file]; + const songInfoString = songInfo.title + songInfo.album + songInfo.artist; + if (songInfoString.includes(keyword)) resultArray.push(file); } }); - return resultArray; + return {files: resultArray, menu: FileExtensionTools.fileMenuItem}; } \ No newline at end of file diff --git a/src/frontend/assets/extensions/local.json b/src/frontend/assets/extensions/local.json index 22747b2..e48f20e 100644 --- a/src/frontend/assets/extensions/local.json +++ b/src/frontend/assets/extensions/local.json @@ -3,5 +3,5 @@ "url": "assets/extensions/local.js", "isDev": true, "scheme": "file", - "version": "1.0.0" + "version": "1.0.3" } \ No newline at end of file diff --git a/src/frontend/assets/main.css b/src/frontend/assets/main.css index 0332bc5..f4e6cfd 100644 --- a/src/frontend/assets/main.css +++ b/src/frontend/assets/main.css @@ -44,9 +44,9 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non -/* 主窗体 - 侧栏界面 */ -.left{position:fixed;top:0;bottom:0;width:200px;padding:35px 10px 0 10px;text-align:center;} -.left img{width:150px;} +/* 主窗体 - 左侧界面 */ +.left{position:fixed;top:0;bottom:0;width:var(--leftBarWidth);padding:35px 10px 0 10px;text-align:center;} +.left img{width:150px;max-width:calc(100% - 25px);} .left .leftBar{margin-top:5px;text-align:left;overflow-y:scroll;padding-bottom:100px;height:calc(100% - 45px);} .left .leftBar section{margin-top:2px;} .left .leftBar .title{opacity:.5;padding:0 8px 0 10px;font-size:.9em;display:flex;align-items:center;margin-top:15px;margin-bottom:4px;} @@ -54,20 +54,23 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .left .leftBar .title i{transition:background .2s;padding:2px;border-radius:5px;} .left .leftBar .title i:hover{background:rgba(0,0,0,.1);} .left .leftBar .title i:active{background:rgba(0,0,0,.2);} -.left .leftBar div{text-align:left;padding:5px 10px;border-radius:5px;transition:background .2s;} +.left .leftBar div{text-align:left;padding:5px 10px;border-radius:5px;transition:color .15s, background .2s;position:relative;} .left .leftBar div:hover{background:rgba(0,0,0,.05);} .left .leftBar div:active{background:rgba(0,0,0,.1);} -.left .leftBar div.active{background:linear-gradient(90deg,#1E9FFF,#1E9FFFBB);color:white;} +.left .leftBar div.active{color:white;background:transparent;} +.left .leftBar div::before{content:"";background:linear-gradient(90deg,#1E9FFF,#1E9FFFBB);opacity:0;transition:opacity .2s;position:absolute;inset:0;z-index:-1;} +.left .leftBar div.active::before{opacity:1;} /* 主窗体 - 右侧界面 */ -.right{position:fixed;top:0;right:0;bottom:0;width:calc(100% - 200px);border-left:1px solid rgba(0,0,0,.05);background:rgba(252,252,252,.8);} +.right{position:fixed;top:0;right:0;bottom:0;width:calc(100% - var(--leftBarWidth));border-left:1px solid rgba(0,0,0,.05);background:rgba(252,252,252,.8);} .right #rightPlaceholder{position:absolute;inset:0;bottom:80px;display:flex;align-items:center;justify-content:center;} .right #rightPlaceholder img{width:150px;} +.right #leftBarResizer{position:absolute;z-index:10;left:-2.5px;width:5px;height:100%;top:0;cursor:col-resize;} /* 歌单 */ .right .musicListTitle{padding:30px;height:180px;width:100%;display:flex;align-items:center;position:absolute;top:0;left:0;background:rgba(252,252,252,.9);backdrop-filter:blur(20px);border-bottom:1px solid rgba(0,0,0,.05);z-index:5;} -.right .musicListTitle img{width:120px;} +.right .musicListTitle img{width:120px;height:120px;background:white;border-radius:5px;object-fit:cover;} .right .musicListTitle section{margin-left:30px;width:calc(100% - 150px);} .right .musicListTitle section b{font-size:1.3em;display:block;} .right .musicListTitle section .details{opacity:.8;font-size:.9em;margin:5px 0 10px 0;} @@ -94,6 +97,7 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .right .searchTitle .inputGroup select{height:37px;border-radius:5px 0 0 5px;margin-right:2px;width:fit-content;padding:0 10px;} .right .searchTitle .inputGroup input{height:37px;margin-right:10px;max-width:500px;width:100%;border-radius:0 5px 5px 0;padding:0 10px;} .right .searchTitle .inputGroup button{height:37px;font-size:1.05em;} +.right #searchLoader{text-align:center;opacity:.8;margin-top:50px;} /* 扩展 */ .right #extensionPage #extensionContainer{padding-top:90px!important;} /* 设置 */ @@ -105,7 +109,7 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .right #settingsPage #settingsContainer .block, .right #extensionPage #extensionContainer>div{background:white;width:100%;border-radius:5px;margin-bottom:5px;padding:10px 15px;display:flex;align-items:center;} .right #settingsPage #settingsContainer .block section, .right #extensionPage #extensionContainer>div section{width:100%;margin-right:10px;} .right #settingsPage #settingsContainer .block section div, .right #extensionPage #extensionContainer>div section div{font-size:1em;} -.right #settingsPage #settingsContainer .block section span, .right #extensionPage #extensionContainer>div section span{display:block;font-size:.9em;opacity:.8;} +.right #settingsPage #settingsContainer .block section span, .right #extensionPage #extensionContainer>div section span{display:block;font-size:.9em;opacity:.8;word-break:break-all;line-height:1.1em;margin-top:3px;} .right #settingsPage #settingsContainer .block button, .right #extensionPage #extensionContainer button{white-space:nowrap;} .right #settingsPage #settingsContainer .block .toggle{display:inline-block;height:20px;min-width:35px;max-width:35px;padding:2px;background:rgba(0,0,0,.05);border-radius:10px;vertical-align:middle;transition:background .2s;} .right #settingsPage #settingsContainer .block .toggle::before{transition:margin-left .2s,transform .2s;transform:scale(.7);width:16px;height:16px;background:white;border-radius:50%;content:"";display:block;} @@ -114,6 +118,9 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .right #settingsPage #settingsContainer .block .range{min-width:150px;max-width:150px;} .right #settingsPage #settingsContainer .block .range>div{background:rgba(0,0,0,.05);} .right #settingsPage #settingsContainer .block input{min-width:150px;max-width:150px;} +.right #settingsPage #settingsContainer .block .colorInput{background:rgba(0,0,0,.03);position:relative;border-radius:5px;} +.right #settingsPage #settingsContainer .block .colorInput>input{opacity:0;} +.right #settingsPage #settingsContainer .block .colorInput>span{position:absolute;inset:0;height:fit-content;margin:auto;text-align:center;text-transform:uppercase;} /* 关于 */ .right #aboutPage{padding:30px;overflow-y:scroll;} .right #aboutPage .top{display:flex;align-items:center;} @@ -137,7 +144,15 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non -/* 主窗体 - 播放界面 */ +/* 主窗体 - 文件拖入 */ +#dropTipContainer{position:fixed;top:0;left:0;height:100%;width:100%;z-index:6;background:rgba(0,0,0,.1);border:0;border-radius:0;pointer-events:none;opacity:0;transition:opacity .2s;} +#dropTipContainer #dropTip{position:fixed;padding:5px 10px;border-radius:7px;background:white;border:1px solid #CDCDCD;box-shadow:0 4px 6px rgba(0,0,0,.04);pointer-events:none;font-size:.9em;height:fit-content;width:160px;} +#dropTipContainer #dropTip>i{color:#1E9FFF;} +.dragOver #dropTipContainer{opacity:1;} + + + +/* 主窗体 - 底部控件 */ .bottom{position:fixed;bottom:-90px;height:80px;width:100%;background:rgba(252,252,252,.9);backdrop-filter:blur(20px);transition:bottom .2s;} .withCurrentMusic:not(.playerShown) .bottom{bottom:0;} .bottom #bottomProgressBar{width:calc(100% - 10px);position:absolute;left:5px;top:0;z-index:2;} @@ -158,5 +173,8 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .bottom .volBtnBottom{width:150px;position:absolute;top:5px;bottom:0;right:30px;display:flex;align-items:center;} .bottom .volBtnBottom>div{opacity:.7;margin-right:5px;min-width:40px;height:40px;} + + +/* 主窗体 - 播放内页 */ #playPage{position:fixed;top:120vh;left:0;width:100%;height:100%;background:white;transition:top .3s;overflow:hidden;} .playerShown #playPage{top:0;} \ No newline at end of file diff --git a/src/frontend/assets/main.js b/src/frontend/assets/main.js index 485d67e..429adf8 100644 --- a/src/frontend/assets/main.js +++ b/src/frontend/assets/main.js @@ -1,13 +1,12 @@ -SimMusicVersion = "0.0.2-alpha"; +SimMusicVersion = "0.0.3-alpha"; // 窗口处理 const WindowStatus = { maximized: false, lyricsWin: false, - desktopWin: false, }; const WindowOps = { close () { @@ -21,21 +20,12 @@ const WindowOps = { ipcRenderer.invoke("winOps", [document.documentElement.dataset.windowId, "minimize"]); }, toggleLyrics () { - return alert("很抱歉,此功能尚未开发。"); ipcRenderer.invoke("toggleLyrics") .then(lyricsShow => { WindowStatus.lyricsWin = lyricsShow; document.getElementById("lyricsBtn").classList[lyricsShow ? "add" : "remove"]("active"); }); }, - toggleDesktop () { - return alert("很抱歉,此功能尚未开发。"); - ipcRenderer.invoke("toggleDesktop") - .then(desktopShow => { - WindowStatus.desktopWin = desktopShow; - document.getElementById("desktopBtn").classList[desktopShow ? "add" : "remove"]("active"); - }); - }, }; document.body.onresize = () => { ipcRenderer.invoke("winOps", [document.documentElement.dataset.windowId, "isMaximized"]) @@ -46,7 +36,7 @@ document.body.onresize = () => { }; document.body.onresize(); document.documentElement.onkeydown = e => { - if (e.ctrlKey || e.key == "Tab") e.preventDefault(); + if ((e.ctrlKey && ["i", "I", "r", "R"].includes(e.key)) || e.key == "Tab") e.preventDefault(); if (document.activeElement.tagName.toLowerCase() != "input") e.preventDefault(); }; document.documentElement.ondragstart = e => {e.preventDefault();}; @@ -104,6 +94,10 @@ const SimMusicTools = { txn.objectStore('s').put({ 'k': "MusicIndex", 'v': value }); txn.commit(); }, + getCoverUrl(arrayOrUrl) { + if (typeof(arrayOrUrl) == "object") return URL.createObjectURL(new Blob([arrayOrUrl])); + return arrayOrUrl; + } }; SimMusicTools.initMusicIndex(); @@ -129,20 +123,12 @@ const ExtensionRuntime = { }); }) .catch(() => { - this.renderExtension(json, {}, index); + this.renderExtension(json, {}, index, true); }) }); }, - renderExtension(jsonUrl, data, index) { - this.count++; - if (this.count == config.getItem("extensions").length) { - SimMusicTools.readMusicIndex(index => { - lastMusicIndex = index; - if (config.getItem("currentMusic")) { - PlayerController.switchMusicWithList(config.getItem("currentMusic"), config.getItem("playList"), false, true); - } - }); - } + renderExtension(jsonUrl, data, index, isErr) { + if (isErr) { this.count++; this.checkMusicInit(); } const extensionContainer = document.getElementById("extensionContainer"); extensionContainer.innerHTML += `
@@ -178,14 +164,17 @@ const ExtensionRuntime = { } } else Function(code)(); + this.count++; + this.checkMusicInit(); }, install() { prompt("请输入扩展索引文件 URL ...", url => { + if (!url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("file:")) return alert("索引文件 URL 无效。"); confirm("请务必确保扩展来源可信,由攻击者开发的扩展将可能会恶意操作您的文件、控制您的设备。按「确认」以继续。", () => { const extensions = config.getItem("extensions"); extensions.push(url); config.setItem("extensions", extensions); - alert("扩展已添加,按「确认」重载主界面生效。", () => {location.reload();}); + alert("扩展已添加,按「确认」重载此应用生效。", () => {ipcRenderer.invoke("restart");}); }); }); }, @@ -195,13 +184,46 @@ const ExtensionRuntime = { const extensions = config.getItem("extensions"); extensions.splice(index, 1); config.setItem("extensions", extensions); - alert("扩展已卸载,按「确认」重载主界面生效。", () => {location.reload();}); + alert("扩展已卸载,按「确认」重载此应用生效。", () => {ipcRenderer.invoke("restart");}); }); + }, + checkMusicInit() { + if (this.count == config.getItem("extensions").length) { + SimMusicTools.readMusicIndex(index => { + lastMusicIndex = index; + if (config.getItem("currentMusic") && lastMusicIndex[config.getItem("currentMusic")]) { + PlayerController.switchMusicWithList(config.getItem("currentMusic"), config.getItem("playList"), false, true); + } else { + config.setItem("currentMusic", ""); + } + }); + } } }; ExtensionRuntime.init(); + + +// 左侧栏大小调整 +document.getElementById("leftBarResizer").addEventListener("mousedown", e => { + document.addEventListener("mousemove", resize); + document.addEventListener("mouseup", stopResize); + document.documentElement.style.cursor = "col-resize"; +}); +function resize(e) { + let x = e.pageX; + let distance = Math.max(150, Math.min(400, x)); + config.setItem("leftBarWidth", distance); +} +function stopResize() { + document.removeEventListener("mousemove", resize); + document.removeEventListener("mouseup", stopResize); + document.documentElement.style.cursor = ""; +} +config.listenChange("leftBarWidth", width => document.body.style.setProperty("--leftBarWidth", width + "px")); +document.body.style.setProperty("--leftBarWidth", config.getItem("leftBarWidth") + "px"); + // 综合歌单 const MusicList = { add(callback) { @@ -255,13 +277,28 @@ const MusicList = { document.getElementById("musicLists").appendChild(element); } }, - switchList(name) { + switchList(name, force) { document.querySelector(".musicListTitle").hidden = false; document.querySelector(".searchTitle").hidden = true; document.getElementById("musicListName").innerText = name; document.getElementById("folderDir").hidden = true; const lists = config.getItem("musicLists"); - renderMusicList(lists[name], "musiclist-" + name); + if (force) currentMusicList = null; + renderMusicList(lists[name], "musiclist-" + name, false, false, "拖入文件以导入歌单", [ + {type: ["single", "multiple"], content: { label: "从歌单中移除", click() { + const files = getCurrentSelected(); + const confirmDelete = () => { + files.forEach(file => { + const lists = config.getItem("musicLists"); + lists[name].splice(lists[name].indexOf(file), 1); + config.setItem("musicLists", lists); + }); + document.querySelectorAll("#musicListContent>tr.selected").forEach(ele => ele.remove()); + } + if (files.length > 4) confirm(`确实要从歌单「${name}」删除这 ${files.length} 首曲目吗?`, confirmDelete); + else confirmDelete(); + } }} + ]); document.querySelectorAll(".left .leftBar div").forEach(ele => { if (ele.dataset.listName != name) ele.classList.remove("active"); else ele.classList.add("active"); @@ -287,13 +324,48 @@ const MusicList = { if (!lists[name].includes(file)) lists[name].push(file); }); config.setItem("musicLists", lists); - this.switchList(name); + document.getElementById("musicListContainer").click(); } }; MusicList.renderList(); +// 歌曲拖放 +document.documentElement.ondragover = e => { + e.preventDefault(); + if (!currentMusicList || !currentMusicList.startsWith("musiclist-")) return; + if (e.dataTransfer.types.includes("Files")) { + document.body.classList.add("dragOver"); + document.getElementById("dropTip").style.left = e.clientX + 10 > document.documentElement.clientWidth - 160 ? document.documentElement.clientWidth - 165 : e.clientX + 10 + "px"; + document.getElementById("dropTip").style.top = e.clientY + 30 + "px"; + } +}; +document.documentElement.ondrop = e => { + e.preventDefault(); + if (!currentMusicList || !currentMusicList.startsWith("musiclist-")) return; + document.body.classList.remove("dragOver"); + if (e.dataTransfer.types.includes("Files")) { + let files = []; + const supportedExtensions = config.getItem("musicFormats").split(" "); + for (let i = 0; i < e.dataTransfer.files.length; i++){ + const file = e.dataTransfer.files[i]; + const fullPath = file.path; + const ext = path.extname(fullPath).toLowerCase(); + if (supportedExtensions.includes(ext)) files.push("file:" + fullPath); + } + const name = currentMusicList.substring(10); + MusicList.importToMusicList(name, files); + currentMusicList = []; + MusicList.switchList(name, true); + } +}; +document.documentElement.ondragleave = e => { + document.body.classList.remove("dragOver"); +}; + + + // 音乐搜索 Search = { switchSearch() { @@ -302,11 +374,10 @@ Search = { document.querySelector(".musicListTitle").hidden = true; document.querySelector(".searchTitle").hidden = false; document.getElementById("musicListContent").innerHTML = ""; - document.getElementById("musicListErrorOverlay").hidden = false; - document.getElementById("musicListErrorOverlayText").innerText = "还未发起搜索"; document.querySelectorAll(".left .leftBar div").forEach(ele => ele.classList.remove("active")); document.getElementById("searchBtn").classList.add("active"); - currentViewingDir = currentViewingLength = null; + showErrorOverlay("还未发起搜索"); + currentMusicList = currentViewingLength = null; const searchSource = document.getElementById("searchSource"); if (!searchSource.innerHTML) { for (const name in ExtensionConfig) { @@ -330,8 +401,11 @@ Search = { document.getElementById("searchSubmitBtn").disabled = true; setTimeout(() => { ExtensionConfig[ext].search(keyword) - .then(data => { - renderMusicList(data, `search-${ext}-${keyword}`, false, false, "暂无搜索结果"); + .then((data) => { + renderMusicList(data.files ?? [], `search-${ext}-${keyword}`, false, false, "暂无搜索结果", data.menu ?? []); + }) + .catch(err => { + showErrorOverlay(err); }); }, 200); } @@ -341,10 +415,11 @@ Search = { // 右侧列表界面 // 为提高性能,先用缓存的信息渲染,然后再获取没获取过的元数据 -let currentViewingDir,currentViewingLength,lastMusicIndex; +let currentMusicList,currentViewingLength,lastMusicIndex; +let musicListDomCache = {}; function switchRightPage(id) { if (id != "musicListContainer") { - currentViewingDir = null; + currentMusicList = null; document.querySelectorAll(".left .leftBar div").forEach(ele => { if (ele.dataset.pageId != id) ele.classList.remove("active"); else ele.classList.add("active"); @@ -357,29 +432,40 @@ const coverObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting && !entry.target.dataset.coverShown) { entry.target.dataset.coverShown = 1; - const coverArray = lastMusicIndex[entry.target.dataset.file] ? lastMusicIndex[entry.target.dataset.file].cover : null; - if (!coverArray) return; - const blob = new Blob([coverArray]); + const coverData = lastMusicIndex[entry.target.dataset.file] ? lastMusicIndex[entry.target.dataset.file].cover : null; + if (!coverData) return; const img = entry.target.querySelector("img"); - img.src = URL.createObjectURL(blob); - img.onload = () => { reloadMusicListCover(() => {URL.revokeObjectURL(img.src);}); } + img.src = SimMusicTools.getCoverUrl(coverData); + img.onload = () => { + reloadMusicListCover(); + if (entry.target.tagName == "TR" && config.getItem("listDomCache")) { + musicListDomCache[entry.target.dataset.file] = entry.target.cloneNode(true); + } + } } }); }); -function renderMusicList(files, dir, isFinalRender, dontRenderBeforeLoaded, errorText = "当前歌单为空\n拖入本地文件以导入") { +function showErrorOverlay(err) { + document.getElementById("musicListContent").innerHTML = ""; + document.getElementById("musicListErrorOverlay").hidden = false; + document.getElementById("musicListErrorOverlayText").innerText = err; + document.getElementById("searchSubmitBtn").disabled = false; +} +function renderMusicList(files, dir, isFinalRender, dontRenderBeforeLoaded, errorText = "当前歌单为空", menuItems = []) { const musicListContent = document.getElementById("musicListContent"); if (isFinalRender) { document.getElementById("searchSubmitBtn").disabled = false; - if (dir != currentViewingDir) return; /*防止刷新索引完成后用户已经跑了*/ + if (dir != currentMusicList) return; /*防止刷新索引完成后用户已经跑了*/ } else { if (dontRenderBeforeLoaded) return; - if (dir == currentViewingDir && files.length == currentViewingLength) return; - currentViewingDir = dir; + if (dir == currentMusicList && files.length == currentViewingLength) return document.getElementById("searchSubmitBtn").disabled = false;; + currentMusicList = dir; currentViewingLength = files.length; document.getElementById("musicListCover").src = "assets/placeholder.svg"; switchRightPage("musicListContainer"); musicListContent.innerHTML = ""; document.getElementById("musicListErrorOverlay").hidden = true; + document.getElementById("searchSubmitBtn").disabled = false; } document.querySelector(".tableContainer").scrollTo(0, 0); document.getElementById("musicListNum").innerText = files.length; @@ -387,7 +473,7 @@ function renderMusicList(files, dir, isFinalRender, dontRenderBeforeLoaded, erro SimMusicTools.readMusicIndex(musicIndex => { const renderObject = []; lastMusicIndex = musicIndex; - if (!isFinalRender) updateMusicIndex(files, dir, errorText); + if (!isFinalRender) updateMusicIndex(files, dir, errorText, menuItems); files.forEach(name => { if (musicIndex[name]) renderObject.push([name, musicIndex[name]]); else renderObject.push([name, {}]); @@ -395,12 +481,25 @@ function renderMusicList(files, dir, isFinalRender, dontRenderBeforeLoaded, erro musicListContent.innerHTML = ""; let totalTime = 0; renderObject.forEach(music => { - const tr = document.createElement("tr"); - tr.dataset.file = music[0]; - if (isFinalRender) coverObserver.observe(tr); + let tr; + // 如果有缓存就用缓存 + if (!musicListDomCache[music[0]]) { + tr = document.createElement("tr"); + tr.dataset.file = music[0]; + if (isFinalRender) coverObserver.observe(tr); + tr.innerHTML = ` + + ${SimMusicTools.escapeHtml(music[1].title ?? SimMusicTools.getTitleFromPath(music[0]))} + ${SimMusicTools.escapeHtml(music[1].artist ?? "正在读取")} + ${SimMusicTools.escapeHtml(music[1].album ?? SimMusicTools.getDefaultAlbum(music[0]))} + ${SimMusicTools.formatTime(music[1].time)}`; + } else { + tr = musicListDomCache[music[0]].cloneNode(true); + } + // 绑定点击事件 tr.oncontextmenu = e => { if (!tr.classList.contains("selected")) tr.click(); - handleMusicContextmenu(e); + handleMusicContextmenu(e, menuItems); }; tr.onclick = e => { e.stopPropagation(); @@ -425,31 +524,24 @@ function renderMusicList(files, dir, isFinalRender, dontRenderBeforeLoaded, erro tr.classList.remove("selected"); PlayerController.switchMusicWithList(music[0], getCurrentMusicList()); }; - tr.innerHTML = ` - - ${SimMusicTools.escapeHtml(music[1].title ?? SimMusicTools.getTitleFromPath(music[0]))} - ${SimMusicTools.escapeHtml(music[1].artist ?? "正在读取")} - ${SimMusicTools.escapeHtml(music[1].album ?? SimMusicTools.getDefaultAlbum(music[0]))} - ${SimMusicTools.formatTime(music[1].time)}`; - musicListContent.appendChild(tr); + // 统计时间,加入musicListContent if (music[1].time) totalTime += music[1].time; + musicListContent.appendChild(tr); }); if (isFinalRender) document.getElementById("musicListTime").innerText = SimMusicTools.formatTime(totalTime, true); - if (!musicListContent.innerHTML) { - document.getElementById("musicListErrorOverlay").hidden = false; - document.getElementById("musicListErrorOverlayText").innerText = errorText; - } + if (!musicListContent.innerHTML) showErrorOverlay(errorText); PlayerController.loadMusicListActive(); document.getElementById("musicListContainer").onclick = () => { document.querySelectorAll("#musicListContent>tr").forEach(tr => tr.classList.remove("selected")); }; + reloadMusicListCover(); }); } -function updateMusicIndex(allFiles, dir, errorText) { +function updateMusicIndex(allFiles, dir, errorText, menuItems) { const existedFiles = Object.keys(lastMusicIndex); const files = allFiles.filter(file => !existedFiles.includes(file)); let finished = -1; const record = () => { finished ++; - if (!files.length) renderMusicList(allFiles, dir, true, false, errorText); + if (!files.length) renderMusicList(allFiles, dir, true, false, errorText, menuItems); else if (finished == files.length) SimMusicTools.writeMusicIndex(lastMusicIndex, () => {renderMusicList(allFiles, dir, true);}); } files.forEach(file => { @@ -474,16 +566,14 @@ function updateMusicIndex(allFiles, dir, errorText) { }); record(); } -function reloadMusicListCover(callBack) { +function reloadMusicListCover() { let musicListCover = document.getElementById("musicListCover"); const img = document.querySelector("#musicListContent>tr:first-child>td:first-child>img"); if (!img) return; let currentCover = img.src; if (musicListCover.src != currentCover) { musicListCover.src = currentCover; - return musicListCover.onload = callBack(); } - callBack(); } function getCurrentMusicList() { return Array.from(document.querySelectorAll("#musicListContent>tr")).map(tr => tr.dataset.file); @@ -491,7 +581,7 @@ function getCurrentMusicList() { function getCurrentSelected() { return Array.from(document.querySelectorAll("#musicListContent>tr.selected")).map(tr => tr.dataset.file); } -function handleMusicContextmenu(event) { +function handleMusicContextmenu(event, extraMenu = []) { const list = getCurrentMusicList(); const files = getCurrentSelected(); if (!files.length) return; @@ -508,7 +598,13 @@ function handleMusicContextmenu(event) { { type: "separator" }, { label: "添加到歌单", submenu: MusicList.getMenuItems(name => {MusicList.importToMusicList(name, files);}) }, ]; - new ContextMenu((files.length == 1 ? singleFileOptions : multiFileOptions).concat(commonOptions)).popup([event.clientX, event.clientY]); + const basicOptions = (files.length == 1 ? singleFileOptions : multiFileOptions).concat(commonOptions); + let extraOptions = []; + extraMenu.forEach(menu => { + if (files.length == 1 && menu.type.includes("single")) extraOptions.push(menu.content); + else if (files.length != 1 && menu.type.includes("multiple")) extraOptions.push(menu.content); + }); + new ContextMenu(basicOptions.concat(extraOptions)).popup([event.clientX, event.clientY]); } @@ -517,6 +613,7 @@ function handleMusicContextmenu(event) { const PlayerController = { // 替换列表并播放 switchMusicWithList(file, list, showAP, isInit) { + if (!list.length) return; if (config.getItem("loop") == 2) { list = list.sort(() => Math.random() - 0.5); if (file) { @@ -546,7 +643,7 @@ const PlayerController = { } const metadata = lastMusicIndex[file]; switchMusic({ - album: metadata.cover ? URL.createObjectURL(new Blob([metadata.cover])) : "assets/placeholder.svg", + album: metadata.cover ? SimMusicTools.getCoverUrl(metadata.cover) : "assets/placeholder.svg", title: metadata.title, artist: metadata.artist, audio: await ExtensionConfig[fileScheme].player.getPlayUrl(file), @@ -607,9 +704,17 @@ const PlayerController = { div.innerHTML = `
${SimMusicTools.escapeHtml(lastMusicIndex[file].title)}${SimMusicTools.escapeHtml(lastMusicIndex[file].artist)}
+ `; div.dataset.file = file; div.onclick = () => {this.switchMusic(file);} + div.querySelector("i").onclick = () => { + const list = config.getItem("playList"); + list.splice(list.indexOf(file), 1); + config.setItem("playList", list); + div.classList.add("removed"); + setTimeout(() => {div.remove();}, 400); + } coverObserver.observe(div); return div; }, @@ -630,19 +735,32 @@ if (!config.getItem("lrcShow")) document.body.classList.add("hideLyrics"); + + // 设置页面 const SettingsPage = { data: [ {type: "title", text: "音频扫描"}, {type: "input", text: "音频格式", description: "扫描本地音乐时的音频文件扩展名,以空格分隔。", configItem: "musicFormats"}, - {type: "button", text: "清除音频索引", description: "若您更改了音频元数据,可在此删除索引数据以重新从文件读取。", button: "清除", onclick: () => { SimMusicTools.writeMusicIndex({}, () => { alert("索引数据已清除。"); }); }}, + {type: "boolean", text: "歌单元素缓存", description: "开启后可提升歌单中曲目专辑封面在首次加载后的渲染速度,但会占用更多内存空间。", configItem: "listDomCache" }, + {type: "button", text: "清除音频索引", description: "若您更改了音频元数据,可在此删除索引数据以重新从文件读取。", button: "清除", onclick: () => { SimMusicTools.writeMusicIndex({}, () => { alert("索引数据已清除,按「确认」重载此应用生效。", () => { ipcRenderer.invoke("restart"); }); }); }}, {type: "title", text: "播放界面"}, - {type: "boolean", text: "背景流光", description: "关闭后可牺牲部分视觉效果以显著减少播放页硬件占用。(视效性能问题调整优化中,第一版先饶了我x", configItem: "backgroundBlur"}, + {type: "boolean", text: "背景流光", description: "关闭后可牺牲部分视觉效果以显著减少播放页硬件占用。", configItem: "backgroundBlur"}, {type: "boolean", text: "歌词层级虚化", description: "若无需虚化效果或需要提升性能可关闭此功能。", configItem: "lyricBlur"}, {type: "range", text: "歌词字号", configItem: "lyricSize", min: 1, max: 3}, {type: "range", text: "歌词间距", configItem: "lyricSpace", min: .2, max: 1}, + {type: "boolean", text: "歌词多语言支持", description: "开启后,时间戳一致的不同歌词将作为多语言翻译同时渲染。此配置项切换曲目生效。", configItem: "lyricMultiLang"}, + {type: "range", text: "歌词翻译字号", description: "开启多语言支持后,可使用此选项调整多语言翻译的字号。", configItem: "lyricTranslation", min: .5, max: 1}, {type: "title", text: "音频输出"}, {type: "button", text: "均衡器", description: "不用点,还没写,别骂了", button: "配置", onclick: () => { alert("都让你别点了(恼"); }}, + {type: "title", text: "桌面歌词"}, + {type: "boolean", text: "启动时打开", configItem: "autoDesktopLyrics"}, + {type: "boolean", text: "在截图中隐藏", description: "其他应用截图或录屏时隐藏桌面歌词的内容,与多数截图或录屏软件相兼容,支持 Windows 10 2004 以上版本及 Windows 11。此功能不会影响您查看歌词,对采集卡等外置硬件无效。", configItem: "desktopLyricsProtection"}, + {type: "boolean", text: "在播放页自动关闭", description: "当 SimMusic 主窗口打开播放页时自动关闭桌面歌词。", configItem: "desktopLyricsAutoHide"}, + {type: "color", text: "字体颜色", inputType: "color", configItem: "desktopLyricsColor"}, + {type: "color", text: "边框颜色", inputType: "color", configItem: "desktopLyricsStroke"}, + {type: "range", text: "字体大小", configItem: "desktopLyricsSize", min: 20, max: 60}, + {type: "range", text: "歌词区域宽度", configItem: "desktopLyricsWidth", min: 500, max: screen.width}, ], init() { const settingsContainer = document.getElementById("settingsContainer"); @@ -678,7 +796,7 @@ const SettingsPage = {
`; const range = new SimProgress(div.querySelector(".range")); - range.onchange = value => { config.setItem(data.configItem, value); }; + range.ondrag = value => { config.setItem(data.configItem, value); }; break; case "input": div.classList.add("block"); @@ -687,12 +805,30 @@ const SettingsPage = {
${SimMusicTools.escapeHtml(data.text)}
${data.description ? `${SimMusicTools.escapeHtml(data.description)}` : ""} - `; + `; const input = div.querySelector("input"); input.value = config.getItem(data.configItem); input.autocomplete = input.spellcheck = false; input.onchange = () => { config.setItem(data.configItem, input.value); }; break; + case "color": + div.classList.add("block"); + div.innerHTML = ` +
+
${SimMusicTools.escapeHtml(data.text)}
+ ${data.description ? `${SimMusicTools.escapeHtml(data.description)}` : ""} +
+
`; + const colorInput = div.querySelector("input"); + colorInput.value = config.getItem(data.configItem); + div.querySelector(".colorInput>span").innerText = config.getItem(data.configItem); + div.querySelector(".colorInput>span").style.color = config.getItem(data.configItem); + colorInput.onchange = () => { + div.querySelector(".colorInput>span").innerText = colorInput.value; + div.querySelector(".colorInput>span").style.color = colorInput.value; + config.setItem(data.configItem, colorInput.value); + }; + break; case "button": div.classList.add("block"); div.innerHTML = ` @@ -710,6 +846,20 @@ const SettingsPage = { } + +// 桌面歌词 +function updateDesktopLyricsConfig() { + ipcRenderer.invoke("updateDesktopLyricsConfig", config.getItem("desktopLyricsProtection")); +} +config.listenChange("desktopLyricsColor", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsStroke", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsSize", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsProtection", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsWidth", updateDesktopLyricsConfig); +updateDesktopLyricsConfig(); +if (config.getItem("autoDesktopLyrics")) WindowOps.toggleLyrics(); + + // 关于页面 function initAboutPage() { document.querySelectorAll("#aboutPage a").forEach(a => { diff --git a/src/frontend/desktop.html b/src/frontend/desktop.html deleted file mode 100644 index 4e6e591..0000000 --- a/src/frontend/desktop.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - SimMusicTaskbarWindow - - - - - -
- -

你的桌面被我狠狠撅入了

- - \ No newline at end of file diff --git a/src/frontend/lrc.html b/src/frontend/lrc.html index e69de29..887b2d0 100644 --- a/src/frontend/lrc.html +++ b/src/frontend/lrc.html @@ -0,0 +1,96 @@ + + + + SimMusicDesktopLyrics + + + + + + +
+ +
+
+ + + + + \ No newline at end of file diff --git a/src/frontend/main.html b/src/frontend/main.html index 123d52e..08cdf77 100644 --- a/src/frontend/main.html +++ b/src/frontend/main.html @@ -1,7 +1,7 @@ - SimMusic 开发预览版 + SimMusic @@ -12,12 +12,10 @@ - - +
- @@ -42,6 +40,7 @@
+
@@ -66,7 +65,7 @@
- +
@@ -137,6 +136,8 @@
+ +
松手加入当前歌单
@@ -190,6 +191,7 @@
+
diff --git a/src/main.js b/src/main.js index 0f03a6c..70f96f8 100644 --- a/src/main.js +++ b/src/main.js @@ -10,6 +10,12 @@ app.commandLine.appendSwitch("enable-features", "WindowsScrollingPersonality"); // 创建窗口 const SimMusicWindows = {}; +let tray; +function showMainWin() { + SimMusicWindows.mainWin.show(); + if (SimMusicWindows.mainWin.isMinimized()) {SimMusicWindows.mainWin.restore();} + SimMusicWindows.mainWin.focus(); +} const createWindow = () => { // 主窗体 SimMusicWindows.mainWin = new BrowserWindow({ @@ -24,86 +30,61 @@ const createWindow = () => { }); SimMusicWindows.mainWin.loadURL(path.join(__dirname, "frontend/main.html")); SimMusicWindows.mainWin.once("ready-to-show", () => { SimMusicWindows.mainWin.show(); }); + SimMusicWindows.mainWin.on("close", e => { + e.preventDefault(); + SimMusicWindows.mainWin.hide(); + }); // 歌词窗体 SimMusicWindows.lrcWin = new BrowserWindow({ width: 0, height: 0, frame: false, - resizable: true, - show: false, - showInTaskbar: false, - webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } - }); - SimMusicWindows.lrcWin.loadURL(path.join(__dirname, "frontend/lrc.html")); - // 桌面窗体 - SimMusicWindows.desktopWin = new BrowserWindow({ resizable: false, show: false, transparent: true, - frame: false, + focusable: false, webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } }); - SimMusicWindows.desktopWin.loadURL(path.join(__dirname, "frontend/desktop.html")); + SimMusicWindows.lrcWin.loadURL(path.join(__dirname, "frontend/lrc.html")); + SimMusicWindows.lrcWin.maximize(); } app.whenReady().then(() => { + tray = new Tray(nativeImage.createFromPath(path.join(__dirname, "frontend/assets/icon-blue.png"))); + tray.on("click", () => { showMainWin(); }); createWindow(); if (!app.requestSingleInstanceLock()) { - app.quit(); + app.exit(); return; } - app.on('second-instance', () => { - SimMusicWindows.mainWin.show(); - if (SimMusicWindows.mainWin.isMinimized()) {SimMusicWindows.mainWin.restore();} - SimMusicWindows.mainWin.focus(); + app.on("second-instance", () => { + showMainWin(); }); - const tray = new Tray(nativeImage.createFromPath(path.join(__dirname, "frontend/assets/icon-blue.png"))); - const menu = Menu.buildFromTemplate([ - { label: "显示", type: "normal", click: () => { - SimMusicWindows.mainWin.show(); - if (SimMusicWindows.mainWin.isMinimized()) {SimMusicWindows.mainWin.restore();} - }}, - { label: "退出", type: "normal", click: app.quit}, - ]); - tray.setContextMenu(menu); }); // 处理窗口事件 -let desktopShowing = false; let lyricsShowing = false; ipcMain.handle("winOps", (_event, args) => { return SimMusicWindows[args[0]][args[1]](); }); -ipcMain.handle("toggleDesktop", () => { - if (desktopShowing) { - SimMusicWindows.desktopWin.hide(); - desktopShowing = false; - } else { - SimMusicWindows.desktopWin.show(); - SimMusicWindows.desktopWin.maximize(); - SimMusicWindows.desktopWin.setIgnoreMouseEvents(true); - exec(".\\nativeApi\\embedWindow.exe"); - setTimeout(() => {exec(".\\nativeApi\\embedWindow.exe");}, 100); - SimMusicWindows.desktopWin.setSkipTaskbar(true); - desktopShowing = true; - } - return desktopShowing; -}); ipcMain.handle("toggleLyrics", () => { if (lyricsShowing) { - SimMusicWindows.lrcWin.hide(); + SimMusicWindows.lrcWin.webContents.send("lrcHidden", true); + setTimeout(() => {SimMusicWindows.lrcWin.hide();}, 100); lyricsShowing = false; } else { SimMusicWindows.lrcWin.show(); - SimMusicWindows.lrcWin.setContentProtection(true); + SimMusicWindows.lrcWin.setIgnoreMouseEvents("true", {forward: true}); SimMusicWindows.lrcWin.setSkipTaskbar(true); SimMusicWindows.lrcWin.setAlwaysOnTop(true); lyricsShowing = true; + setTimeout(() => {SimMusicWindows.lrcWin.webContents.send("lrcHidden", false);}, 300); } return lyricsShowing; }); -ipcMain.handle("quitApp", () => { - app.quit(); +ipcMain.handle("restart", () => { + app.exit(); + app.relaunch(); }); @@ -147,17 +128,41 @@ const createTaskbarButtons = (isPlay) => { click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.next()", true);} } ]); + const menu = Menu.buildFromTemplate([ + { label: "SimMusic", type: "normal", enabled: false}, + { type: "separator" }, + { label: "显示主窗口", type: "normal", click() { showMainWin(); }}, + { label: isPlay ? "暂停" : "播放", type: "normal", click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.togglePlay()", true);}}, + { type: "separator" }, + { label: "退出应用", type: "normal", click: app.exit}, + ]); + tray.setContextMenu(menu); } -ipcMain.handle("musicPlay", () => { createTaskbarButtons(true); }); -ipcMain.handle("musicPause", () => { createTaskbarButtons(false); }); +ipcMain.handle("musicPlay", () => { + if (lyricsShowing) SimMusicWindows.lrcWin.webContents.send("lrcHidden", false); + createTaskbarButtons(true); +}); +ipcMain.handle("musicPause", () => { + SimMusicWindows.lrcWin.webContents.send("lrcHidden", true); + createTaskbarButtons(false); +}); + -// 歌词更新 -ipcMain.handle("lrcUpdate", (_event, time, lrc) => { +// 桌面歌词 +ipcMain.handle("lrcUpdate", (_event, lrc) => { SimMusicWindows.lrcWin.webContents.send("lrcUpdate", lrc); - SimMusicWindows.desktopWin.webContents.send("lrcUpdate", time); }); - +ipcMain.handle("focusDesktopLyrics", () => { + SimMusicWindows.lrcWin.setIgnoreMouseEvents(false); +}); +ipcMain.handle("unfocusDesktopLyrics", () => { + SimMusicWindows.lrcWin.setIgnoreMouseEvents(true, {forward: true}); +}); +ipcMain.handle("updateDesktopLyricsConfig", (_event, isProtected) => { + SimMusicWindows.lrcWin.webContents.send("lrcWinReload"); + SimMusicWindows.lrcWin.setContentProtection(isProtected); +}); // 主窗口调用 @@ -171,4 +176,5 @@ ipcMain.handle("pickFolder", () => { }); ipcMain.handle("openDevtools", () => { SimMusicWindows.mainWin.webContents.openDevTools(); + SimMusicWindows.lrcWin.webContents.openDevTools(); }); \ No newline at end of file diff --git a/src/package.json b/src/package.json index c5c92d7..1e4326f 100644 --- a/src/package.json +++ b/src/package.json @@ -5,6 +5,7 @@ "main": "main.js", "author": "Simsv Studio", "dependencies": { - "music-metadata": "^10.0.1" + "music-metadata": "^7.13.5", + "node-id3": "^0.2.6" } }