diff --git a/src/frontend/assets/extensions/local.js b/src/frontend/assets/components/LocalFolderExtension.js similarity index 97% rename from src/frontend/assets/extensions/local.js rename to src/frontend/assets/components/LocalFolderExtension.js index ba24edd..5592cae 100644 --- a/src/frontend/assets/extensions/local.js +++ b/src/frontend/assets/components/LocalFolderExtension.js @@ -7,8 +7,6 @@ /**************** 基础配置 ****************/ -// ExtensionRuntime在加载时会自动添加json中的scheme字段到ExtensionConfig下,所以无需担心ExtensionConfig.xxx是否存在 -ExtensionConfig.file.uiName = "本地"; // 当没有config.setItem时,调用config.getItem会返回defaultConfig中的值 defaultConfig["folderLists"] = []; @@ -209,5 +207,9 @@ ExtensionConfig.file.search = async keyword => { if (songInfoString.includes(keyword)) resultArray.push(file); } }); - return {files: resultArray, menu: FileExtensionTools.fileMenuItem, hasMore: false}; + return { + files: resultArray, + menu: FileExtensionTools.fileMenuItem,//.concat(DownloadController.getMenuItems()), + hasMore: false + }; } \ No newline at end of file diff --git a/src/frontend/assets/components/PublicConfig.js b/src/frontend/assets/components/PublicConfig.js index 3d4e564..b2087cf 100644 --- a/src/frontend/assets/components/PublicConfig.js +++ b/src/frontend/assets/components/PublicConfig.js @@ -19,14 +19,16 @@ const defaultConfig = { desktopLyricsProtection: true, desktopLyricsAutoHide: true, desktopLyricsColor: "#1E9FFF", + desktopLyricsStrokeEnabled: true, desktopLyricsStroke: "#1672B8", desktopLyricsSize: 30, desktopLyricsWidth: 700, desktopLyricsTop: screen.height - 300, desktopLyricsLeft: screen.width / 2, - extensions: ["assets/extensions/local.json"], - extensionCache: {}, + ext: {}, musicListSort: [1, 1], + parallelDownload: 3, + downloadFileName: "[title] - [artist]", } const configListeners = {}; diff --git a/src/frontend/assets/components/SimAP.css b/src/frontend/assets/components/SimAP.css index 95c6b55..fce2602 100644 --- a/src/frontend/assets/components/SimAP.css +++ b/src/frontend/assets/components/SimAP.css @@ -27,7 +27,7 @@ body:not(.playing) #background>div{animation-play-state:paused;} .controls .progressControl{display:flex;align-items:center;margin:10px 0;} .controls .progressControl span{width:55px;font-size:.85em;opacity:.8;} .controls .progressControl span:last-child{text-align:right;} -.musicLoading #progressBar>div,.musicLoading #bottomProgressBar>div{animation:progressLoading 1s linear infinite;background-image:linear-gradient(-45deg,rgba(0,0,0,.1) 25%,transparent 0,transparent 50%,rgba(0,0,0,.1) 0,rgba(0,0,0,.1) 75%,transparent 0,transparent)!important;background-repeat:repeat-x;background-size:25px 20px;} +.musicLoading #progressBar>div,.musicLoading #bottomProgressBar>div{animation:progressLoading 1s linear infinite;background-image:linear-gradient(-45deg,rgba(0,0,0,.05) 25%,transparent 0,transparent 50%,rgba(0,0,0,.05) 0,rgba(0,0,0,.05) 75%,transparent 0,transparent)!important;background-repeat:repeat-x;background-size:25px 20px;} @keyframes progressLoading{to{background-position:25px 0;}} /* 下方按钮 */ .controls .buttons,.bottom .center{align-items:center;margin-top:5px;display:flex;align-items:center;justify-content:center;} @@ -56,7 +56,7 @@ body:not(.playing) #background>div{animation-play-state:paused;} /* 3D特效 */ body:not(.hideLyrics.hideList) .playerContainer{transform:translateX(-25px);} .threeEffect .controls{transform:perspective(900px) rotateY(10deg);} -.threeEffect .lyrics,.list{transform:perspective(900px) rotateY(-12.5deg);} +.threeEffect .lyrics,.threeEffect .list{transform:perspective(900px) rotateY(-12.5deg);} /* 歌词区域 */ .lyrics{position:absolute;left:410px;top:0;width:calc(100% - 410px);font-size:var(--lrcSize);height:100%;transform-origin:left center;transition:all .3s;mask:linear-gradient(180deg,hsla(0,0%,100%,0),hsla(0,0%,100%,.6) 15%,#fff 25%,#fff 75%,hsla(0,0%,100%,.6) 85%,hsla(0,0%,100%,0));} diff --git a/src/frontend/assets/components/SimAP.js b/src/frontend/assets/components/SimAP.js index 284c8a4..a1c54aa 100644 --- a/src/frontend/assets/components/SimAP.js +++ b/src/frontend/assets/components/SimAP.js @@ -146,7 +146,11 @@ const SimAPControls = { audio[audio.paused ? "play" : "pause"](); SimAPControls.loadAudioState(); }, - prev() {this.switchIndex(-1);}, + prev() { + const audio = document.getElementById("audio"); + if (!config.getItem("fastPlayback") || audio.currentTime / audio.duration < .9) this.switchIndex(-1); + else audio.currentTime = 0; + }, next() {this.switchIndex(1);}, switchIndex(offset) { const list = config.getItem("playList"); @@ -202,16 +206,14 @@ const SimAPControls = { document.querySelector(".volBtnBottom i").innerHTML = icon; }, toggleList(isShow = document.body.classList.contains("hideList")) { - document.body.classList[isShow ? "add" : "remove"]("hideList"); document.body.classList[isShow ? "remove" : "add"]("hideList"); if (isShow) document.body.classList.add("hideLyrics"); PlayerController.loadMusicListActive(); }, - toggleLyrics() { - document.body.classList[document.body.classList.contains("hideLyrics") ? "add" : "remove"]("hideLyrics"); - document.body.classList[document.body.classList.contains("hideLyrics") ? "remove" : "add"]("hideLyrics"); - if (!document.body.classList.contains("hideLyrics")) document.body.classList.add("hideList"); - config.setItem("lrcShow", !document.body.classList.contains("hideLyrics")); + toggleLyrics(isShow = document.body.classList.contains("hideLyrics")) { + document.body.classList[isShow ? "remove" : "add"]("hideLyrics"); + if (isShow) document.body.classList.add("hideList"); + config.setItem("lrcShow", isShow); }, loadConfig() { document.querySelector(".SimLRC").style.setProperty("--lineSpace", config.getItem("lyricSpace") + "em"); diff --git a/src/frontend/assets/components/SimLRC.js b/src/frontend/assets/components/SimLRC.js index 1e6c2f1..ae1283a 100644 --- a/src/frontend/assets/components/SimLRC.js +++ b/src/frontend/assets/components/SimLRC.js @@ -78,7 +78,7 @@ class SimLRC { container.style.setProperty("--lineSpace", options.lineSpace + "em"); // 监听事件 const refreshLrcProgress = forceScroll => { - const currentTime = audio.currentTime * 1000; + const currentTime = Math.round(audio.currentTime * 1000); let lrcEles = Array.from(container.getElementsByTagName("div")); for (let index in lrcEles) { let div = lrcEles[index]; diff --git a/src/frontend/assets/components/require.js b/src/frontend/assets/components/require.js index 43e128a..53f7f8e 100644 --- a/src/frontend/assets/components/require.js +++ b/src/frontend/assets/components/require.js @@ -2,4 +2,5 @@ const {ipcRenderer, shell} = require("electron"); const fs = require("fs"); const path = require("path"); const musicMetadata = require("music-metadata"); -const nodeId3 = require("node-id3"); \ No newline at end of file +const nodeId3 = require("node-id3"); +const fflate = require("fflate"); \ No newline at end of file diff --git a/src/frontend/assets/extensions/local.json b/src/frontend/assets/extensions/local.json deleted file mode 100644 index aa88f35..0000000 --- a/src/frontend/assets/extensions/local.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "本地歌曲", - "url": "assets/extensions/local.js", - "isDev": true, - "scheme": "file", - "version": "1.0.4" -} \ No newline at end of file diff --git a/src/frontend/assets/main.css b/src/frontend/assets/main.css index 8165a02..0f41ede 100644 --- a/src/frontend/assets/main.css +++ b/src/frontend/assets/main.css @@ -25,7 +25,11 @@ input,select{background:rgba(0,0,0,.03);padding:8px 10px;border-radius:5px;borde -.left .leftBar div,.right .musicListTitle section b,.right .musicListTitle section .details,.bottom .info .musicInfoBottom,.controls .musicInfo,.list>div>div>*{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} +.left .leftBar div,.right .musicListTitle section b, +.right .musicListTitle section .details, +.bottom .info .musicInfoBottom, +.controls .musicInfo,.list>div>div>*, +.right #downloadContainer>div>.info>.music>b{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} /* 主窗体 - 加载动画 */ @@ -39,12 +43,13 @@ header{position:fixed;top:0;height:35px;display:flex;justify-content:right;width header i{font-family:"windowsicon";height:100%;width:40px;display:flex;align-items:center;justify-content:center;transition:background .2s,opacity .2s,color .2s;-webkit-app-region:no-drag;} header i#lyricsBtn{font-size:.95em;padding-bottom:1px;font-family:"微软雅黑"!important;} header i#miniBtn{transform:scaleY(-1);font-size:.95em;} +header i#devBtn{font-size:.95em;} header i:hover{background:rgba(0,0,0,.05);} header i:active{background:rgba(0,0,0,.1);opacity:.8;} header i#closeBtn:hover{background:#E81123;color:white;} header i#closeBtn:active{background:#E81123;color:rgba(252,252,252,.8);} header i.active{color:#1E9FFF;} -.playerShown #miniBtn,.playerShown #desktopBtn,.playerShown #lyricsBtn{opacity:0;pointer-events:none;} +.playerShown #miniBtn,.playerShown #lyricsBtn,.playerShown #devBtn{opacity:0;pointer-events:none;} header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:none;} .playerShown header #hidePlayerBtn{opacity:1;pointer-events:all;} @@ -113,13 +118,33 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non .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 #searchBottomIndicator{text-align:center;opacity:.8;margin-top:50px;margin:30px 0 10px 0;font-size:.9em;} +/* 下载 */ +.right #downloadContainer{padding-top:90px!important;} +.right #downloadContainer>div{background:white;width:100%;border-radius:5px;margin-bottom:5px;padding:10px 15px;position:relative;height:65px;overflow:hidden;transition:background .2s;} +.right #downloadContainer>div[data-status="success"]{background:#EFF6EF;} +.right #downloadContainer>div[data-status="error"]{background:#FCEFEF;} +.right #downloadContainer>div>.info{display:flex;align-items:center;white-space:nowrap;position:absolute;inset:0;z-index:5;padding:0 15px;} +.right #downloadContainer>div>.info>.music{display:flex;flex-direction:column;width:calc(100% - 150px);} +.right #downloadContainer>div>.info>.music>span{display:block;opacity:.8;font-size:.9em;margin-top:3px;} +.right #downloadContainer>div[data-status="pending"]>.info>.music>span>i::after{content:"\F337";} +.right #downloadContainer>div[data-status="download"]>.info>.music>span>i::after{content:"\EC5A";} +.right #downloadContainer>div[data-status="success"]>.info>.music>span>i::after{content:"\EB7B";} +.right #downloadContainer>div[data-status="error"]>.info>.music>span>i::after{content:"\F4C8";} +.right #downloadContainer>div>.info>.buttons{display:flex;justify-content:flex-end;width:150px;} +.right #downloadContainer>div>.info>.buttons>i{width:30px;height:30px;display:flex;align-items:center;justify-content:center;font-size:1.1em;border-radius:50%;transition:background .2s;} +.right #downloadContainer>div>.info>.buttons>i:hover{background:rgba(0,0,0,.05);} +.right #downloadContainer>div>.info>.buttons>i:active{background:rgba(0,0,0,.1);} +.right #downloadContainer>div:not([data-status="error"])>.info>.buttons>i.errorOnly{display:none;} +.right #downloadContainer>div:not([data-status="success"])>.info>.buttons>i.successOnly{display:none;} +.right #downloadContainer>div>.progressBar{transition:width .2s,opacity .2s;position:absolute;inset:0;right:unset;background:#DAEFFF;width:var(--progressWidth);} +.right #downloadContainer>div:not([data-status="download"])>.progressBar{opacity:0;} /* 扩展 */ -.right #extensionPage #extensionContainer{padding-top:90px!important;} +.right #extensionContainer{padding-top:90px!important;} /* 设置 */ .right .page .header{padding:30px 30px 10px 30px;font-size:1.5em;border-bottom:1px solid rgba(0,0,0,.05);position:absolute;background:rgba(252,252,252,.9);backdrop-filter:blur(20px);width:100%;z-index:2;display:flex;align-items:center;} .right .page .header i{margin-right:5px;} -.right .page .header button{margin-left:15px;} -.right #settingsPage #settingsContainer, .right #extensionPage #extensionContainer{position:absolute;z-index:1;padding:70px 27.5px 100px 27.5px;width:100%;height:100%;overflow-y:scroll;} +.right .page .header small{font-size:.6em;margin-left:15px;opacity:.8;} +.right #settingsPage #settingsContainer, .right #extensionContainer, .right #downloadContainer{position:absolute;z-index:1;padding:70px 27.5px 100px 27.5px;width:100%;height:100%;overflow-y:scroll;} .right #settingsPage #settingsContainer .title{font-weight:bold;margin:20px 2.5px 5px 2.5px;} .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;} @@ -192,4 +217,19 @@ header #hidePlayerBtn{opacity:0;position:absolute;right:120px;pointer-events:non /* 主窗体 - 播放内页 */ #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 +.playerShown #playPage{top:0;} + + +/* 主窗体 - 迷你模式 */ +header{-webkit-app-region:no-drag;} +.miniMode .bottom{z-index:300;height:60px;background:white;} +.miniMode .bottom .progressBefore,.miniMode .bottom .progressAfter{display:none;} +.miniMode .bottom #bottomProgressBar{bottom:0;top:unset;width:100%;left:0;} +.miniMode .bottom #bottomProgressBar::after{display:none;} +.miniMode .bottom .info{right:0;zoom:.72;top:0px;width:230px;bottom:10px;-webkit-app-region:drag;} +.miniMode .bottom .info .musicInfoBottom{font-size:1.1em;max-width:160px;} +.miniMode .bottom .info .musicInfoBottom div{margin-top:-2px;} +.miniMode .bottom .center{right:15px;zoom:.7;bottom:10px;width:220px;left:unset;} +.miniMode .bottom .center .bottomListBtn{display:none;} +body:not(.miniMode) .bottom .center .miniModeBtn{display:none;} +.miniMode .bottom .volume{display:none;} \ No newline at end of file diff --git a/src/frontend/assets/main.js b/src/frontend/assets/main.js index 9a9a0dc..260ded1 100644 --- a/src/frontend/assets/main.js +++ b/src/frontend/assets/main.js @@ -1,6 +1,4 @@ - - -SimMusicVersion = "0.0.4-beta"; +SimMusicVersion = "0.1.0"; // 窗口处理 @@ -30,6 +28,12 @@ const WindowOps = { document.getElementById("lyricsBtn").classList[lyricsShow ? "add" : "remove"]("active"); }); }, + toggleMini () { + ipcRenderer.invoke("toggleMini") + .then(isMini => { + document.body.classList[isMini ? "add" : "remove"]("miniMode"); + }); + }, }; document.body.onresize = () => { ipcRenderer.invoke("winOps", [document.documentElement.dataset.windowId, "isMaximized"]) @@ -44,7 +48,6 @@ document.documentElement.onkeydown = e => { if (document.activeElement.tagName.toLowerCase() == "input") return; e.preventDefault(); if (document.activeElement.tagName.toLowerCase() == "input") return; - const audio = document.getElementById("audio"); switch (e.key.toLowerCase()) { case "w": config.setItem("desktopLyricsTop", Math.max(config.getItem("desktopLyricsTop") - 2, 0)); break; case "a": config.setItem("desktopLyricsLeft", Math.max(config.getItem("desktopLyricsLeft") - 2, 0)); break; @@ -110,6 +113,9 @@ const SimMusicTools = { getCoverUrl(arrayOrUrl) { if (typeof(arrayOrUrl) == "object") return URL.createObjectURL(new Blob([arrayOrUrl])); return arrayOrUrl; + }, + getWindowsLegalName(original) { + return original.replaceAll("\\", "_").replaceAll("/", "_").replaceAll(":", ":").replaceAll("*", "×").replaceAll("?", "?").replaceAll('"', "'").replaceAll("<", "《").replaceAll(">", "》").replaceAll("|", "丨").replaceAll("\n", ""); } }; SimMusicTools.initMusicIndex(); @@ -118,102 +124,116 @@ SimMusicTools.initMusicIndex(); // 扩展运行环境 const ExtensionConfig = {}; const ExtensionRuntime = { - count: 0, - init() { - config.getItem("extensions").forEach((json, index) => { - fetch(json) - .then(res => res.json()) - .then(data => { - this.renderExtension(json, data, index); - const cacheData = config.getItem("extensionCache"); - if (cacheData[json] && cacheData[json].version == data.version && !data.isDev) this.runExtension(cacheData[json].code, data); - else fetch((data.url + "?v=" +data.version)) - .then(res => res.text()) - .then(code => { - cacheData[json] = { version: data.version, code: code }; - config.setItem("extensionCache", cacheData); - this.runExtension(code, data); - }); - }) - .catch(() => { - this.renderExtension(json, {}, index, true); - }) - }); - }, - renderExtension(jsonUrl, data, index, isErr) { - if (isErr) { this.count++; this.checkMusicInit(); } - const extensionContainer = document.getElementById("extensionContainer"); - extensionContainer.innerHTML += ` -
+ async init() { + const extData = await this.getExtData(); + for (const packageId in extData) { + let extError; + try { + ExtensionConfig[packageId] = {}; + Function(extData[packageId].code)(); + if (ExtensionConfig[packageId].musicList) { + const span = document.createElement("span"); + span.innerHTML = ` +
${extData[packageId].uiName}
+
`; + if (ExtensionConfig[packageId].musicList.add) { + span.querySelector("i").onclick = () => { + ExtensionConfig[packageId].musicList.add(() => { + span.querySelector(".lists").innerHTML = ""; + ExtensionConfig[packageId].musicList.renderList(span.querySelector(".lists")); + }); + } + } else { + span.querySelector("i").remove(); + } + ExtensionConfig[packageId].musicList.renderList(span.querySelector(".lists")); + document.getElementById("extBars").appendChild(span); + } + } catch (err) { + extError = err; + } + const div = document.createElement("div"); + div.innerHTML = `
- ${data.name ?? "扩展加载失败"} - 加载来源:${SimMusicTools.escapeHtml(jsonUrl) == "assets/extensions/local.json" ? "SimMusic 内置组件" : SimMusicTools.escapeHtml(jsonUrl)} +
${SimMusicTools.escapeHtml(extData[packageId].extName)}
+ + 扩展包名: ${SimMusicTools.escapeHtml(packageId)}
+ 扩展版本: ${SimMusicTools.escapeHtml(extData[packageId].version)}
+ ${extError ? ` ${extError}` : ""} +
- -
`; - }, - runExtension(code, metadata) { - if (metadata.scheme) { - const scheme = metadata.scheme; - ExtensionConfig[scheme] = {}; - Function(code)(); - if (ExtensionConfig[scheme].musicList) { - const span = document.createElement("span"); - span.innerHTML = ` -
${ExtensionConfig[scheme].uiName}
-
`; - if (ExtensionConfig[scheme].musicList.add) { - span.querySelector("i").onclick = () => { - ExtensionConfig[scheme].musicList.add(() => { - span.querySelector(".lists").innerHTML = ""; - ExtensionConfig[scheme].musicList.renderList(span.querySelector(".lists")); - }); - } - } else { - span.querySelector("i").remove(); - } - ExtensionConfig[scheme].musicList.renderList(span.querySelector(".lists")); - document.getElementById("extBars").appendChild(span); - } + `; + div.querySelector("button").onclick = () => { + this.uninstall(packageId); + }; + document.getElementById("extensionContainer").appendChild(div); } - 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("扩展已添加,按「确认」重载此应用生效。", () => {ipcRenderer.invoke("restart");}); - }); + + 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", ""); + } + setTimeout(() => { + document.body.classList.remove("appLoading"); + }, 500); }); }, - uninstall(index) { - if (!index) return alert("内置扩展无法进行卸载。"); - confirm("确实要卸载此扩展吗?由此扩展提供的音乐将在移除后无法播放,请慎重选择。", () => { - const extensions = config.getItem("extensions"); - extensions.splice(index, 1); - config.setItem("extensions", extensions); - alert("扩展已卸载,按「确认」重载此应用生效。", () => {ipcRenderer.invoke("restart");}); + async install(file) { + confirm("请确保扩展包来源可信,由攻击者提供的扩展可能会对您的系统执行恶意操作。", async () => { + try { + if (file.size > 256 * 1024) return alert("扩展包文件过大,请确保扩展包内仅包含必要的代码文件。"); + const arrBuffer = await file.arrayBuffer(); + const unzipped = fflate.unzipSync(new Uint8Array(arrBuffer)); + const manifestContent = fflate.strFromU8(unzipped["manifest.json"]); + const manifest = JSON.parse(manifestContent); + if (!manifest.packageId || !manifest.version || !manifest.extName) return alert("扩展清单字段数据不完整。"); + const packageId = manifest.packageId; + let code = ""; + for (const i in manifest.entries) { + const filename = manifest.entries[i]; + const jsCode = fflate.strFromU8(unzipped[filename]); + code += jsCode + "\n"; + } + console.log(code) + const extData = config.getItem("ext"); + extData[packageId] = { + version: manifest.version, + extName: manifest.extName, + uiName: manifest.uiName ?? manifest.extName, + code: code, + }; + config.setItem("ext", extData); + alert("扩展已成功安装,按「确定」重载此应用生效。", () => { + ipcRenderer.invoke("restart"); + }); + } catch (err) { + console.warn(err); + alert("扩展包已损坏、无法读取或存在未知错误。"); + } }); }, - 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", ""); - } + uninstall(packageId) { + confirm("确实要卸载此扩展吗?卸载后由此扩展提供的曲目将无法正常播放,请慎重。", () => { + const extData = config.getItem("ext"); + delete extData[packageId]; + config.setItem("ext", extData); + alert("扩展已成功卸载,按「确定」重载此应用生效。", () => { + ipcRenderer.invoke("restart"); }); - setTimeout(() => { - document.body.classList.remove("appLoading"); - }, 1000); + }); + }, + async getExtData() { + const extData = config.getItem("ext"); + extData["file"] = { + code: await (await fetch("assets/components/LocalFolderExtension.js")).text(), + uiName: "本地", + extName: "本地文件播放支持", + version: "1.0.0", } + return extData; } }; ExtensionRuntime.init(); @@ -342,34 +362,47 @@ MusicList.renderList(); -// 歌曲拖放 +// 歌曲&扩展包拖放 document.documentElement.ondragover = e => { e.preventDefault(); - if (document.getElementById("musicListContainer").hidden || !document.querySelector("#musicListContainer>.show") || !document.querySelector("#musicListContainer>.show").dataset.musicListId.startsWith("musiclist-")) return; + if ((document.getElementById("extensionPage").hidden) && + (document.getElementById("musicListContainer").hidden || + !document.querySelector("#musicListContainer>.show") || + !document.querySelector("#musicListContainer>.show").dataset.musicListId.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.getElementById("dropTipText").textContent = document.getElementById("musicListContainer").hidden ? "松手安装扩展" : "松手加入当前歌单"; } }; document.documentElement.ondrop = e => { e.preventDefault(); - if (!document.querySelector("#musicListContainer>.show") || document.getElementById("musicListContainer").hidden) return; - const currentMusicList = document.querySelector("#musicListContainer>.show").dataset.musicListId; - if (!currentMusicList.startsWith("musiclist-")) return; + if ((document.getElementById("extensionPage").hidden) && + (document.getElementById("musicListContainer").hidden || + !document.querySelector("#musicListContainer>.show") || + !document.querySelector("#musicListContainer>.show").dataset.musicListId.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); + if (!document.getElementById("musicListContainer").hidden) { + const currentMusicList = document.querySelector("#musicListContainer>.show").dataset.musicListId; + 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); + MusicList.switchList(name, true); + } else { + const file = e.dataTransfer.files[0]; + ExtensionRuntime.install(file); } - const name = currentMusicList.substring(10); - MusicList.importToMusicList(name, files); - MusicList.switchList(name, true); } }; document.documentElement.ondragleave = () => { @@ -380,16 +413,25 @@ document.documentElement.ondragleave = () => { // 音乐搜索 Search = { - switchSearch() { + async switchSearch() { if (document.getElementById("searchBtn").classList.contains("active")) return; document.getElementById("searchSubmitBtn").disabled = false; document.querySelectorAll(".left .leftBar div").forEach(ele => ele.classList.remove("active")); document.getElementById("searchBtn").classList.add("active"); const searchSource = document.getElementById("searchSource"); if (!searchSource.innerHTML) { + const extData = await ExtensionRuntime.getExtData(); for (const name in ExtensionConfig) { if (ExtensionConfig[name].search) { - searchSource.innerHTML += ``; + searchSource.innerHTML += ``; + } + } + document.getElementById("searchInput").onkeydown = e => { + if (e.key === "Tab") { + var currentOption = searchSource.options[searchSource.selectedIndex]; + var nextOption = currentOption.nextElementSibling; + if (nextOption) searchSource.selectedIndex = searchSource.selectedIndex + 1; + else searchSource.selectedIndex = 0; } } } @@ -411,6 +453,7 @@ Search = { if (!keyword) return alert("请输入搜索关键字。"); if (keyword == "OPENDEVTOOLS") { ipcRenderer.invoke("openDevtools"); + config.setItem("devMode", 1); return document.getElementById("searchInput").value = ""; } const btn = document.getElementById("searchSubmitBtn"); @@ -439,13 +482,14 @@ Search = { if (this.hasMore) { const searchBottomIndicator = document.getElementById("searchBottomIndicator"); if (searchBottomIndicator.dataset.status == "loading") return; + document.getElementById("searchBottomIndicator").style = ""; searchBottomIndicator.dataset.status = "loading"; searchBottomIndicator.textContent = "正在加载更多..."; const currentKeyword = this.currentSearchKeyword; setTimeout(() => { ExtensionConfig[this.currentSearchExt].search(currentKeyword, this.searchPage + 1) .then((data) => { - if (this.currentSearchKeyword != currentKeyword) return searchBottomIndicator.textContent = "暂无更多搜索结果"; + if (this.currentSearchKeyword != currentKeyword) return; searchBottomIndicator.dataset.status = ""; renderMusicList(getCurrentMusicList().concat(data.files ?? []), "search", false, true, "暂无搜索结果", data.menu ?? [], {}, true, this.loadIndicatorStatus); this.searchPage++; @@ -529,7 +573,7 @@ function renderMusicList(files, uniqueId, isFinalRender, dontRenderBeforeLoaded, // 处理首次渲染与二次渲染 if (!isFinalRender) { // 切换页面 - switchRightPage("musicListContainer"); + if (uniqueId != "search") switchRightPage("musicListContainer"); unselectAll(); document.getElementById("searchSubmitBtn").disabled = false; if (uniqueId != "search") { @@ -544,15 +588,17 @@ function renderMusicList(files, uniqueId, isFinalRender, dontRenderBeforeLoaded, // 清空容器,搜索除外 musicListContent.innerHTML = ""; } - // 处理一些其他元素 - containerElement.querySelector(".musicListErrorOverlay").hidden = true; } + // 处理一些其他元素 + if (isFinalRender || !dontRenderBeforeLoaded) containerElement.querySelector(".musicListErrorOverlay").hidden = true; // 读取索引并渲染列表 SimMusicTools.readMusicIndex(musicIndex => { const renderObject = []; lastMusicIndex = musicIndex; if (!isFinalRender) { - updateMusicIndex(files, uniqueId, errorText, menuItems, finishCallback); + updateMusicIndex(files, () => { + renderMusicList(files, uniqueId, true, dontRenderBeforeLoaded, errorText, menuItems, musicListInfo, force, finishCallback); + }); if (dontRenderBeforeLoaded) return; } else { if (finishCallback) finishCallback(); @@ -576,33 +622,35 @@ function renderMusicList(files, uniqueId, isFinalRender, dontRenderBeforeLoaded, ${SimMusicTools.escapeHtml(music[1].album ?? SimMusicTools.getDefaultAlbum(music[0]))} ${SimMusicTools.formatTime(music[1].time)}`; // 绑定点击事件 - tr.oncontextmenu = e => { - if (!tr.classList.contains("selected")) tr.click(); - handleMusicContextmenu(e, menuItems); - }; - tr.onclick = e => { - e.stopPropagation(); - const allTrs = Array.from(musicListContent.querySelectorAll("tr")); - const lastSelectedElement = musicListContent.querySelector("tr.selected"); - if (e.ctrlKey) { - if (tr.classList.contains("selected")) tr.classList.remove("selected"); - else tr.classList.add("selected"); - } else if (e.shiftKey && lastSelectedElement) { - const indexLastSelected = allTrs.indexOf(lastSelectedElement); - const indexCurrentSelected = allTrs.indexOf(tr); - var start = Math.min(indexLastSelected, indexCurrentSelected); - var end = Math.max(indexLastSelected, indexCurrentSelected); - const selectedTrs = allTrs.slice(start, end + 1); - selectedTrs.forEach(tr => tr.classList.add("selected")); - } else { - allTrs.forEach(tr => tr.classList.remove("selected")); - tr.classList.add("selected"); + if (isFinalRender) { + tr.oncontextmenu = e => { + if (!tr.classList.contains("selected")) tr.click(); + handleMusicContextmenu(e, menuItems); + }; + tr.onclick = e => { + e.stopPropagation(); + const allTrs = Array.from(musicListContent.querySelectorAll("tr")); + const lastSelectedElement = musicListContent.querySelector("tr.selected"); + if (e.ctrlKey) { + if (tr.classList.contains("selected")) tr.classList.remove("selected"); + else tr.classList.add("selected"); + } else if (e.shiftKey && lastSelectedElement) { + const indexLastSelected = allTrs.indexOf(lastSelectedElement); + const indexCurrentSelected = allTrs.indexOf(tr); + var start = Math.min(indexLastSelected, indexCurrentSelected); + var end = Math.max(indexLastSelected, indexCurrentSelected); + const selectedTrs = allTrs.slice(start, end + 1); + selectedTrs.forEach(tr => tr.classList.add("selected")); + } else { + allTrs.forEach(tr => tr.classList.remove("selected")); + tr.classList.add("selected"); + } } + tr.ondblclick = () => { + tr.classList.remove("selected"); + PlayerController.switchMusicWithList(music[0], getCurrentMusicList()); + }; } - tr.ondblclick = () => { - tr.classList.remove("selected"); - PlayerController.switchMusicWithList(music[0], getCurrentMusicList()); - }; // 统计音乐时间 if (music[1].time) totalTime += music[1].time; // 加入列表 @@ -653,14 +701,14 @@ function loadMusicListSort() { } reloadMusicListCover(); } -function updateMusicIndex(allFiles, uniqueId, errorText, menuItems, finishCallback) { +function updateMusicIndex(allFiles, callback) { const existedFiles = Object.keys(lastMusicIndex); const files = allFiles.filter(file => !existedFiles.includes(file)); let finished = -1; const record = () => { finished ++; - if (!files.length) renderMusicList(allFiles, uniqueId, true, false, errorText, menuItems, undefined, undefined, finishCallback); - else if (finished == files.length) SimMusicTools.writeMusicIndex(lastMusicIndex, () => {renderMusicList(allFiles, uniqueId, true);}); + if (!files.length) callback(); + else if (finished == files.length) SimMusicTools.writeMusicIndex(lastMusicIndex, () => {callback();}); } files.forEach(file => { const updateMusicIndex = (data) => { @@ -765,7 +813,9 @@ const PlayerController = { const fileScheme = file.split(":")[0]; if (!ExtensionConfig[fileScheme] || !ExtensionConfig[fileScheme].player || !ExtensionConfig[fileScheme].player.getPlayUrl || !ExtensionConfig[fileScheme].player.getLyrics) { shell.beep(); - return alert("播放此曲目所需的扩展程序已损坏或被删除。"); + return confirm("播放此曲目所需的扩展程序已损坏或被删除,是否将其从播放列表中移除?", () => { + PlayerController.deleteFromList(config.getItem("currentMusic")); + }); } // 这里是为了防止音频timeupdate事件撅飞加载动画 setTimeout(async () => { @@ -860,6 +910,11 @@ const PlayerController = { config.setItem("currentMusic", ""); SimAPUI.hide(); document.body.classList.remove("withCurrentMusic"); + document.getElementById("audio").remove(); + const audio = document.createElement("audio"); + audio.id = "audio"; + audio.volume = config.getItem("volume"); + document.body.appendChild(audio); } else { SimAPControls.next(); } @@ -887,8 +942,182 @@ const loadPlayColor = () => { document.body.classList[config.getItem("playBtnCol config.listenChange("playBtnColor", loadPlayColor); loadPlayColor(); const load3dEffect = () => { document.body.classList[config.getItem("3dEffect") ? "add" : "remove"]("threeEffect"); } config.listenChange("3dEffect", load3dEffect); load3dEffect(); +if (config.getItem("devMode", 1)) document.getElementById("devBtn").hidden = false; + +// 歌曲下载 +const DownloadController = { + getMenuItems() { + const array = []; + const lists = config.getItem("folderLists"); + lists.forEach(name => array.push({ + label: name, + click: () => {DownloadController.addTask(name);} + })); + if (lists.length) array.push(({ type: "separator" })); + array.push(({ + label: Object.keys(lists).length ? "选择其他目录" : "选择目录", + click: () => { ipcRenderer.invoke("pickFolder").then(dir => { DownloadController.addTask(dir); }); } + })); + return {type: ["single", "multiple"], content: { label: "下载到本地", submenu: array },}; + }, + addTask(destination) { + const files = getCurrentSelected(); + updateMusicIndex(files, () => { + const downloadContainer = document.getElementById("downloadContainer"); + files.forEach(file => { + if (lastMusicIndex[file]) { + const div = document.createElement("div"); + div.dataset.file = file; + div.dataset.destination = destination; + div.dataset.status = "pending"; + div.innerHTML = ` +
+
+
+ ${lastMusicIndex[file].title} - ${lastMusicIndex[file].artist} + 正在等待下载 +
+
+ + + + + +
+
`; + div.querySelector(".buttons i:nth-child(1)").onclick = () => { // 重试 + div.dataset.status = "pending"; + div.querySelector(".progressText").textContent = "正在等待下载"; + div.style.setProperty("--progressWidth", "0%"); + DownloadController.loadDownloadStatus(); + } + div.querySelector(".buttons i:nth-child(2)").onclick = () => { // 播放 + const smFile = "file:" + div.dataset.fileName; + updateMusicIndex([smFile], () => { + PlayerController.switchMusicWithList(smFile, [smFile]); + }); + } + div.querySelector(".buttons i:nth-child(3)").onclick = () => { // 资源管理器 + shell.showItemInFolder(div.dataset.fileName); + } + div.querySelector(".buttons i:nth-child(4)").onclick = () => { // 删除文件 + confirm("确实要删除此任务与本地已下载的文件吗?", () => { + fs.unlinkSync(div.dataset.fileName); + PlayerController.deleteFromList("file:" + div.dataset.fileName); + div.remove(); + }) + } + div.querySelector(".buttons i:nth-child(5)").onclick = () => { // 删除任务 + div.remove(); + } + downloadContainer.appendChild(div); + } + }); + DownloadController.loadDownloadStatus(); + }); + document.querySelector('.left div[data-page-id="downloadPage"]').hidden = false; + switchRightPage("downloadPage"); + }, + loadDownloadStatus() { + const currentDownloadingCount = document.querySelectorAll("#downloadContainer>div[data-status='download']").length; + if (currentDownloadingCount < config.getItem("parallelDownload") && document.querySelector("#downloadContainer>div[data-status='pending']")) { + this.downloadFile(); + if (currentDownloadingCount + 1 < config.getItem("parallelDownload")) this.loadDownloadStatus(); + } + }, + async downloadFile() { + const element = document.querySelector("#downloadContainer>div[data-status='pending']"); + if (!element) return; + const updateDownloadStatus = (status, text, progress = 0) => { + element.dataset.status = status; + element.querySelector(".progressText").textContent = text; + element.style.setProperty("--progressWidth", progress + "%"); + DownloadController.loadDownloadStatus(); + } + // 获取歌曲信息 + updateDownloadStatus("download", "正在获取下载地址"); + const file = element.dataset.file; + const destination = element.dataset.destination; + const fileScheme = file.split(":")[0]; + const downUrl = await ExtensionConfig[fileScheme].player.getPlayUrl(file); + // 发起网络请求 + updateDownloadStatus("download", "正在连接下载地址"); + const xhr = new XMLHttpRequest(); + xhr.open("GET", downUrl, true); + xhr.responseType = "arraybuffer"; + xhr.onprogress = event => { + if (event.lengthComputable) { + const percentComplete = Math.round((event.loaded / event.total) * 100); + updateDownloadStatus("download", `正在下载 ${percentComplete}%`, percentComplete); + } else { + updateDownloadStatus("download", "正在下载曲目"); + } + }; + xhr.onload = () => { + updateDownloadStatus("download", "正在保存曲目", 100); + const buffer = Buffer.from(xhr.response); + const fileName = SimMusicTools.getWindowsLegalName(config.getItem("downloadFileName").replaceAll("[title]", lastMusicIndex[file].title).replaceAll("[artist]", lastMusicIndex[file].artist)); + const tempPath = path.join(destination, `${fileName}.simtemp`); + const ws = fs.createWriteStream(tempPath); + ws.on("finish", async () => { + updateDownloadStatus("download", "正在写入元数据", 100); + let coverArrBuffer; + const coverData = lastMusicIndex[file].cover; + if (typeof(coverData) == "object") coverArrBuffer = coverData; + else { + const coverRes = await fetch(coverData) + coverArrBuffer = await coverRes.arrayBuffer(); + } + const metadataResult = nodeId3.write({ + title: lastMusicIndex[file].title, + artist: lastMusicIndex[file].artist, + album: lastMusicIndex[file].album, + unsynchronisedLyrics: { + language: "XXX", + text: lastMusicIndex[file].lyrics ? lastMusicIndex[file].lyrics : await ExtensionConfig[fileScheme].player.getLyrics(file) + }, + image: { + type: {id: 3, name: "front cover"}, + imageBuffer: Buffer.from(coverArrBuffer) + } + }, tempPath); + if (metadataResult) { + const renameResult = this.renameDownloadFile(destination, fileName) + if (renameResult) { + updateDownloadStatus("success", "曲目下载成功", 100); + element.dataset.fileName = renameResult; + } + else updateDownloadStatus("error", "曲目写入失败", 0); + } else { + updateDownloadStatus("error", "曲目元数据写入失败", 0); + } + }); + ws.on("error", () => { updateDownloadStatus("error", "曲目保存失败", 0); }); + ws.write(buffer); + ws.end(); + }; + xhr.onerror = () => { + updateDownloadStatus("error", "曲目下载失败", 0); + }; + xhr.send(); + }, + renameDownloadFile(dir, filename) { + try { + const originalFile = path.join(dir, `${filename}.simtemp`); + const baseTargetFile = path.join(dir, `${filename}.mp3`); + let targetFile = baseTargetFile; + let count = 1; + // 序号递增 + while (fs.existsSync(targetFile)) { targetFile = path.join(dir, `${filename} (${count++}).mp3`); } + fs.renameSync(originalFile, targetFile); + return targetFile; + } catch (err) { + return false; + } + } +} @@ -899,7 +1128,7 @@ const SettingsPage = { {type: "boolean", text: "不驻留后台进程", description: "关闭主界面时停止播放并完全退出应用。", configItem: "disableBackground"}, {type: "title", text: "音频扫描"}, {type: "input", text: "音频格式", description: "扫描本地音乐时的音频文件扩展名,以空格分隔。", configItem: "musicFormats"}, - {type: "button", text: "清除音频索引", description: "若您更改了音频元数据,可在此删除索引数据以重新从文件读取。", button: "清除", onclick: () => { SimMusicTools.writeMusicIndex({}, () => { alert("索引数据已清除,按「确认」重载此应用生效。", () => { ipcRenderer.invoke("restart"); }); }); }}, + {type: "button", text: "清除音频索引", description: "若您更改了音频元数据,可在此删除索引数据以重新从文件读取。", button: "清除", onclick: () => { SimMusicTools.writeMusicIndex({}, () => { alert("索引数据已清除,按「确定」重载此应用生效。", () => { ipcRenderer.invoke("restart"); }); }); }}, {type: "title", text: "播放界面"}, {type: "boolean", text: "背景流光", description: "关闭后可牺牲部分视觉效果以显著减少播放页硬件占用。", configItem: "backgroundBlur"}, {type: "boolean", text: "播放页 3D 特效", description: "在播放页的歌曲信息、播放列表与歌词界面使用 3D 视觉效果。", configItem: "3dEffect"}, @@ -909,16 +1138,22 @@ const SettingsPage = { {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: "快速重播", description: "在曲目即将结束时按「快退」按钮以回到当前曲目的开头。", configItem: "fastPlayback"}, + {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: "color", text: "字体颜色", configItem: "desktopLyricsColor"}, + {type: "boolean", text: "启用边框", configItem: "desktopLyricsStrokeEnabled"}, + {type: "color", text: "边框颜色", description: "在启用边框时生效。", configItem: "desktopLyricsStroke"}, {type: "range", text: "字体大小", configItem: "desktopLyricsSize", min: 20, max: 60}, {type: "range", text: "歌词区域宽度", configItem: "desktopLyricsWidth", min: 500, max: screen.width}, + {type: "boolean", text: "始终居中", description: "无视用户左右拖拽操作,保持桌面歌词在屏幕中央。", configItem: "desktopLyricsCentered"}, + {type: "title", text: "曲目下载"}, + {type: "input", text: "并行下载数", inputType: "number", description: "下载在线歌曲时并行下载的任务数量。", configItem: "parallelDownload"}, + {type: "input", text: "命名格式", description: "可使用 [title] 表示歌曲名,[artist] 表示艺术家。SimMusic 会自动处理 Windows 无法写入的文件名。", configItem: "downloadFileName"}, ], init() { const settingsContainer = document.getElementById("settingsContainer"); @@ -976,7 +1211,7 @@ const SettingsPage = {
${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").textContent = config.getItem(data.configItem); @@ -1010,12 +1245,14 @@ function updateDesktopLyricsConfig() { ipcRenderer.invoke("updateDesktopLyricsConfig", config.getItem("desktopLyricsProtection")); } config.listenChange("desktopLyricsColor", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsStrokeEnabled", updateDesktopLyricsConfig); config.listenChange("desktopLyricsStroke", updateDesktopLyricsConfig); config.listenChange("desktopLyricsSize", updateDesktopLyricsConfig); config.listenChange("desktopLyricsProtection", updateDesktopLyricsConfig); config.listenChange("desktopLyricsWidth", updateDesktopLyricsConfig); config.listenChange("desktopLyricsTop", updateDesktopLyricsConfig); config.listenChange("desktopLyricsLeft", updateDesktopLyricsConfig); +config.listenChange("desktopLyricsCentered", updateDesktopLyricsConfig); updateDesktopLyricsConfig(); if (config.getItem("autoDesktopLyrics")) WindowOps.toggleLyrics(); @@ -1032,4 +1269,5 @@ function initAboutPage() { shell.openExternal(link.dataset.href); } }); + document.getElementById("copyrightYear").textContent = new Date().getFullYear(); } \ No newline at end of file diff --git a/src/frontend/lrc.html b/src/frontend/lrc.html index c31404d..26523ff 100644 --- a/src/frontend/lrc.html +++ b/src/frontend/lrc.html @@ -14,7 +14,8 @@ #lyrics{position:fixed;width:var(--fontWidth);} #move{position:absolute;inset:0 0 auto 0;width:fit-content;height:fit-content;border-radius:50%;color:white;opacity:0;text-shadow:0 0 2px black;padding:0 10px 10px 10px;z-index:114;margin:auto;transition:opacity .2s;} .showBtn #move,.focused #move,.dragging #move{opacity:.7;} - #text{font-size:var(--fontSize);color:var(--fontColor);-webkit-text-stroke:0.01px var(--fontStroke);position:absolute;inset:20px 0 auto 0;padding:0 5px;width:100%;font-family:"font";text-align:center;border-radius:10px;font-weight:bold;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;transition:font-size .2s,background .2s;} + #text{font-size:var(--fontSize);color:var(--fontColor);position:absolute;inset:20px 0 auto 0;padding:0 5px;width:100%;font-family:"font";text-align:center;border-radius:10px;font-weight:bold;width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;transition:font-size .2s,background .2s;} + .showStroke #text{-webkit-text-stroke:0.01px var(--fontStroke);} .dragging #text{background:rgba(0,0,0,.1);} @@ -82,9 +83,11 @@ const width = config.getItem("desktopLyricsWidth"); document.body.style.setProperty("--fontSize", config.getItem("desktopLyricsSize") + "px"); document.body.style.setProperty("--fontColor", config.getItem("desktopLyricsColor")); + document.body.classList[config.getItem("desktopLyricsStrokeEnabled") ? "add" : "remove"]("showStroke"); document.body.style.setProperty("--fontStroke", config.getItem("desktopLyricsStroke")); document.body.style.setProperty("--fontWidth", width + "px"); - lyrics.style.left = config.getItem("desktopLyricsLeft") - width / 2 + "px"; + if (!config.getItem("desktopLyricsCentered")) lyrics.style.left = config.getItem("desktopLyricsLeft") - width / 2 + "px"; + else lyrics.style.left = screen.width / 2 - width / 2 + "px"; lyrics.style.top = config.getItem("desktopLyricsTop") - 10 + "px"; } ipcRenderer.on("lrcWinReload", reloadConfig); diff --git a/src/frontend/main.html b/src/frontend/main.html index 211dc23..6b8015d 100644 --- a/src/frontend/main.html +++ b/src/frontend/main.html @@ -15,6 +15,7 @@
+ @@ -39,6 +40,7 @@
其他
搜索
+
扩展
设置
关于
@@ -84,7 +86,7 @@
- +
@@ -98,11 +100,18 @@ + + @@ -119,7 +128,7 @@ SimMusic 2024
高颜值插件化音频播放器 · 应用版本
- © 2020-2024 Simsv Software, Licenced under GPL-3.0
+ © 2020- Simsv Software, Licenced under GPL-3.0
@@ -128,11 +137,13 @@
巨人的肩膀本软件使用、参考或二次开发了以下项目的实现,在此表示感谢。
electron/electron + Borewit/music-metadata + Zazama/node-id3 zhujin917/3sqrt7-context-menu + 101arrowz/fflate lokesh/color-thief Remix-Design/RemixIcon microsoft/vscode-codicons - Moriafly/SaltPlayerSource
相关推荐一些其他优秀的项目。
@@ -154,7 +165,7 @@
-
松手加入当前歌单
+
@@ -167,7 +178,8 @@
-
+
+
diff --git a/src/main.js b/src/main.js index 9f59b10..f0c005f 100644 --- a/src/main.js +++ b/src/main.js @@ -1,7 +1,7 @@ // © 2020 - 2024 Simsv Studio -const {app, BrowserWindow, ipcMain, dialog, nativeImage, Tray, Menu} = require("electron"); +const {app, BrowserWindow, ipcMain, dialog, nativeImage, Tray, Menu, screen} = require("electron"); const {exec} = require("child_process"); const path = require("path"); @@ -48,11 +48,12 @@ const createWindow = () => { webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } }); SimMusicWindows.lrcWin.loadURL(path.join(__dirname, "frontend/lrc.html")); - SimMusicWindows.lrcWin.setFullScreen(true); + SimMusicWindows.lrcWin.maximize(); } app.whenReady().then(() => { tray = new Tray(nativeImage.createFromPath(path.join(__dirname, "frontend/assets/icon-blue.png"))); tray.on("click", () => { showMainWin(); }); + tray.setToolTip("SimMusic"); createWindow(); if (!app.requestSingleInstanceLock()) { app.exit(); @@ -65,30 +66,9 @@ app.whenReady().then(() => { // 处理窗口事件 -let lyricsShowing = false; -let lyricsInterval; // 就这么写了有时候还是不生效 估计是elec的bug 暂时修不好=( ipcMain.handle("winOps", (_event, args) => { return SimMusicWindows[args[0]][args[1]](); }); -ipcMain.handle("toggleLyrics", (_event, isShow) => { - if (isShow || isShow === false) {lyricsShowing = !isShow;} - if (lyricsShowing) { - SimMusicWindows.lrcWin.webContents.send("setHidden", "text", true); - setTimeout(() => {SimMusicWindows.lrcWin.hide();}, 100); - lyricsShowing = false; - clearInterval(lyricsInterval); - } else { - SimMusicWindows.lrcWin.show(); - SimMusicWindows.lrcWin.setIgnoreMouseEvents("true", {forward: true}); - SimMusicWindows.lrcWin.setSkipTaskbar(true); - SimMusicWindows.lrcWin.setAlwaysOnTop(false); - SimMusicWindows.lrcWin.setAlwaysOnTop(true); - lyricsShowing = true; - setTimeout(() => {SimMusicWindows.lrcWin.webContents.send("setHidden", "text", false);}, 400); - lyricsInterval = setInterval(() => {if (SimMusicWindows.lrcWi) SimMusicWindows.lrcWin.setAlwaysOnTop(true);}, 100); - } - return lyricsShowing; -}); ipcMain.handle("restart", () => { app.exit(); app.relaunch(); @@ -160,6 +140,24 @@ ipcMain.handle("musicPause", () => { // 桌面歌词 +let lyricsShowing = false; +ipcMain.handle("toggleLyrics", (_event, isShow) => { + if (isShow || isShow === false) {lyricsShowing = !isShow;} + if (lyricsShowing) { + SimMusicWindows.lrcWin.webContents.send("setHidden", "text", true); + setTimeout(() => {SimMusicWindows.lrcWin.hide();}, 100); + lyricsShowing = false; + } else { + SimMusicWindows.lrcWin.show(); + SimMusicWindows.lrcWin.setIgnoreMouseEvents("true", {forward: true}); + SimMusicWindows.lrcWin.setSkipTaskbar(true); + SimMusicWindows.lrcWin.setAlwaysOnTop(false); + SimMusicWindows.lrcWin.setAlwaysOnTop(true); + lyricsShowing = true; + setTimeout(() => {SimMusicWindows.lrcWin.webContents.send("setHidden", "text", false);}, 400); + } + return lyricsShowing; +}); ipcMain.handle("lrcUpdate", (_event, lrc) => { SimMusicWindows.lrcWin.webContents.send("lrcUpdate", lrc); }); @@ -175,16 +173,73 @@ ipcMain.handle("updateDesktopLyricsConfig", (_event, isProtected) => { }); + +// 迷你模式 +let isMiniMode = false; +ipcMain.handle("toggleMini", () => { + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + SimMusicWindows.mainWin.hide(); + if (isMiniMode) { + setTimeout(() => { + SimMusicWindows.mainWin.setMinimumSize(1000, 700); + SimMusicWindows.mainWin.setSize(1000, 700); + SimMusicWindows.mainWin.setPosition(width / 2 - 500, height / 2 - 350); + SimMusicWindows.mainWin.setResizable(true); + SimMusicWindows.mainWin.setAlwaysOnTop(false); + SimMusicWindows.mainWin.setSkipTaskbar(false); + SimMusicWindows.mainWin.show(); + }, 500); + return isMiniMode = false; + } else { + setTimeout(() => { + SimMusicWindows.mainWin.unmaximize(); + SimMusicWindows.mainWin.setMinimumSize(340, 60); + SimMusicWindows.mainWin.setSize(340, 60); + SimMusicWindows.mainWin.setResizable(false); + SimMusicWindows.mainWin.setAlwaysOnTop(true); + SimMusicWindows.mainWin.setSkipTaskbar(true); + SimMusicWindows.mainWin.setPosition(width - 360, height - 90); + SimMusicWindows.mainWin.show(); + }, 500); + return isMiniMode = true; + } +}); + + + // 主窗口调用 ipcMain.handle("pickFolder", () => { return dialog.showOpenDialogSync(SimMusicWindows.mainWin, { - title: "导入目录", + title: "选择目录 - SimMusic", defaultPath: "C:\\", - buttonLabel: "导入", + buttonLabel: "使用此目录", properties: ["openDirectory"], }); }); ipcMain.handle("openDevtools", () => { SimMusicWindows.mainWin.webContents.openDevTools(); - SimMusicWindows.lrcWin.webContents.openDevTools(); + // 傻逼谷歌搞个宋体当默认代码字体 怎么想的 给你眼珠子扣下来踩两脚 + SimMusicWindows.mainWin.webContents.once("devtools-opened", () => { + const css = ` + :root { + --sys-color-base: var(--ref-palette-neutral100); + --source-code-font-family: consolas; + --source-code-font-size: 12px; + --monospace-font-family: consolas; + --monospace-font-size: 12px; + --default-font-family: system-ui, sans-serif; + --default-font-size: 12px; + } + .-theme-with-dark-background { + --sys-color-base: var(--ref-palette-secondary25); + } + body { + --default-font-family: system-ui,sans-serif; + }`; + SimMusicWindows.mainWin.webContents.devToolsWebContents.executeJavaScript(` + const overriddenStyle = document.createElement('style'); + overriddenStyle.innerHTML = '${css.replaceAll('\n', ' ')}'; + document.body.append(overriddenStyle); + document.body.classList.remove('platform-windows');`); + }); }); \ No newline at end of file diff --git a/src/package.json b/src/package.json index 1e4326f..0f55890 100644 --- a/src/package.json +++ b/src/package.json @@ -1,11 +1,12 @@ { - "name": "sim-music", - "version": "1.0.0", - "description": "SimMusic", - "main": "main.js", - "author": "Simsv Studio", - "dependencies": { - "music-metadata": "^7.13.5", - "node-id3": "^0.2.6" - } + "name": "sim-music", + "version": "1.0.0", + "description": "SimMusic", + "main": "main.js", + "author": "Simsv Studio", + "dependencies": { + "fflate": "^0.8.2", + "music-metadata": "^7.13.5", + "node-id3": "^0.2.6" + } }