├── .gitattributes ├── LICENSE ├── README.md └── src ├── frontend ├── assets │ ├── components │ │ ├── ContextMenu.css │ │ ├── ContextMenu.js │ │ ├── LocalFolderExtension.js │ │ ├── PublicConfig.js │ │ ├── SimAP.css │ │ ├── SimAP.js │ │ ├── SimLRC.css │ │ ├── SimLRC.js │ │ ├── SimProgress.css │ │ ├── SimProgress.js │ │ ├── dialog.css │ │ ├── dialog.html │ │ ├── dialog.js │ │ ├── marked.min.js │ │ ├── modal-eq.html │ │ ├── modal-update.html │ │ ├── modal.css │ │ ├── require.js │ │ ├── shutdown.html │ │ ├── webview.css │ │ └── webview.html │ ├── font-bold.woff2 │ ├── font.woff2 │ ├── icon-blue.png │ ├── icon-error.svg │ ├── icon-grey.svg │ ├── icon.woff2 │ ├── logo.svg │ ├── main.css │ ├── main.js │ ├── misc │ │ ├── recommend-musictag.png │ │ ├── recommend-salt.png │ │ ├── recommend-vnimusic.png │ │ ├── taskbar-next.png │ │ ├── taskbar-pause.png │ │ ├── taskbar-play.png │ │ ├── taskbar-prev.png │ │ └── text.png │ ├── placeholder.svg │ └── windowsicon.ttf ├── lrc.html └── main.html ├── main.js └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SimMusic 2024 2 | 高颜值插件化音频播放器。 3 | 4 | ## ✏️ 相关说明 5 | 1. 因维护者学业原因,开学后项目将缓慢更新,Issue 处理时间亦会有所延长,烦请理解,仍欢迎各位随时提出问题反馈或功能建议。 6 | 2. 此 Repo 为 Windows 端仓库,Linux 用户请右转 [SimMusic2024-Linux](https://github.com/Simsv-Software/SimMusic2024-Linux) 使用官方的 Linux 适配版( Special thanks to [@XIAYM-gh](https://github.com/XIAYM-gh) )。 7 | 8 | ## 🔗 帮助文档 9 | - 📄 SimMusic 用户指南 10 | - 🧩 SimMusic 扩展索引 11 | - 🧑‍💻 SimMusic 开发者参考 12 | 13 | ## ✨ 特色介绍 14 | ![Features](https://github.com/user-attachments/assets/2285413f-51d9-406f-a473-65eab79fa794) 15 | ![Features](https://github.com/user-attachments/assets/57a55928-ced3-482d-bb02-6a5fd5eb3698) 16 | ![Features](https://github.com/user-attachments/assets/b5ea101e-07f9-464c-aff3-c677ecdf1a69) 17 | ![Features](https://github.com/user-attachments/assets/5066b893-9884-4ba8-9a38-20abe568d61d) 18 | 19 | -------------------------------------------------------------------------------- /src/frontend/assets/components/ContextMenu.css: -------------------------------------------------------------------------------- 1 | 2 | .context-menu { 3 | position: absolute; 4 | background-color: rgb(255 255 255 / .9); 5 | backdrop-filter: blur(15px); 6 | top: 22px; 7 | width: fit-content; 8 | z-index: 9; 9 | box-shadow: rgb(0 0 0 / .06) 0 10px 15px, rgb(0 0 0 / .06) 0 0 15px; 10 | border: solid 1px rgb(0 0 0 / .05); 11 | border-radius: 5px; 12 | padding: 5px 0; 13 | transition: opacity .15s; 14 | opacity: 0; 15 | } 16 | .context-menu .item { 17 | position: relative; 18 | padding: 4px 24px 5px 12px; 19 | font-size: 14px; 20 | background-color: transparent; 21 | color: #000; 22 | white-space: nowrap; 23 | } 24 | .context-menu .item::before { 25 | font-family: 'icon'; 26 | width: 20px; 27 | display: inline-block; 28 | content: var(--icon); 29 | color: rgba(0,0,0,.8); 30 | } 31 | .context-menu .item:hover, .context-menu .item-focused { 32 | background-color: rgba(0,0,0,.05); 33 | } 34 | .context-menu .item:not(.sub):active { 35 | opacity: .8; 36 | background-color: rgba(0,0,0,.05); 37 | } 38 | .context-menu .separator { 39 | margin: 5px 0; 40 | border: none; 41 | border-top: solid 1px rgb(0 0 0 / .05); 42 | } 43 | .context-menu .disabled { 44 | color: black !important; 45 | background-color: transparent !important; 46 | opacity: .5 !important; 47 | } 48 | 49 | .context-menu .sub { 50 | padding-right: 40px; 51 | } 52 | .context-menu .sub::after { 53 | content: "\eab6"; 54 | font-family: 'windowsicon'; 55 | font-size: 16px; 56 | position: absolute; 57 | right: 6px; 58 | top: 50%; 59 | transform: translateY(-50%); 60 | opacity:.8; 61 | } 62 | 63 | #context-menu-mask { 64 | position: fixed; 65 | left: 0; 66 | top: 0; 67 | height: 100%; 68 | width: 100%; 69 | z-index: 1919810; 70 | } -------------------------------------------------------------------------------- /src/frontend/assets/components/ContextMenu.js: -------------------------------------------------------------------------------- 1 | class ContextMenu { 2 | dom; 3 | isSubmenu; 4 | submenuShowTimer = null; 5 | submenuRemoveTimer = null; 6 | 7 | constructor(items, { isSubmenu, parentItem } = {}) { 8 | this.isSubmenu = isSubmenu; 9 | this.dom = document.createElement("div"); 10 | this.dom.classList.add("context-menu"); 11 | this.dom.addEventListener("mousedown", (evt) => { 12 | evt.stopPropagation(); 13 | }); 14 | this.dom.addEventListener("mouseenter", () => { 15 | if (isSubmenu) { 16 | parentItem.classList.add("item-focused"); 17 | } 18 | else { 19 | for (let i = 0; i < this.dom.getElementsByClassName("item-focused").length; i += 1) { 20 | this.dom.getElementsByClassName("item-focused")[i].classList.remove("item-focused"); 21 | i -= 1; 22 | } 23 | } 24 | }); 25 | 26 | for (let item of items) { 27 | if (!item) { 28 | continue; 29 | } 30 | let d; 31 | if (item.label) { 32 | d = document.createElement("div"); 33 | d.classList.add("item"); 34 | d.textContent = item.label; 35 | d.style.setProperty("--icon", item.icon ? `'\\${item.icon}'` : ""); 36 | if (item.submenu) { 37 | if (item.submenu.length == 0) { 38 | item.submenu = [{ 39 | label: "(空)", 40 | disabled: true 41 | }]; 42 | } 43 | d.classList.add("sub"); 44 | d.addEventListener("mouseenter", () => { 45 | let submenu = new ContextMenu(item.submenu, { 46 | isSubmenu: true, 47 | parentItem: d 48 | }); 49 | this.submenuShowTimer && clearTimeout(this.submenuShowTimer); 50 | this.submenuShowTimer = setTimeout(() => { 51 | if (this.dom.nextElementSibling) { 52 | this.dom.nextElementSibling.remove(); 53 | } 54 | submenu.popup([ 55 | this.dom.offsetLeft + d.offsetLeft + d.clientWidth - 2, 56 | this.dom.offsetTop + d.offsetTop - 7, 57 | ], [d.clientWidth - 4, -d.clientHeight - 9]); 58 | }, 250); 59 | }); 60 | d.addEventListener("mouseleave", () => { 61 | this.submenuShowTimer && clearTimeout(this.submenuShowTimer); 62 | if (!this.dom.nextElementSibling) { 63 | return; 64 | } 65 | this.submenuRemoveTimer && clearTimeout(this.submenuRemoveTimer); 66 | this.submenuRemoveTimer = setTimeout(() => { 67 | if (this.dom.nextElementSibling) { 68 | this.dom.nextElementSibling.remove(); 69 | } 70 | }, 200); 71 | this.dom.nextElementSibling.addEventListener("mouseenter", () => { 72 | this.submenuRemoveTimer && clearTimeout(this.submenuRemoveTimer); 73 | }); 74 | }); 75 | } 76 | if (item.click) { 77 | d.addEventListener("click", () => { 78 | this.dom.parentElement.remove(); 79 | setTimeout(() => { 80 | item.click(); 81 | }, 100); 82 | }); 83 | } 84 | if (item.disabled) { 85 | d.classList.add("disabled"); 86 | } 87 | } 88 | else if (item.type == "separator") { 89 | d = document.createElement("hr"); 90 | d.classList.add("separator"); 91 | } 92 | this.dom.appendChild(d); 93 | } 94 | }; 95 | 96 | popup([x, y], [offsetX, offsetY] = [0, 0]) { 97 | let maskDom = document.getElementById("context-menu-mask"); 98 | if (!maskDom) { 99 | if (this.isSubmenu) { 100 | return; 101 | } 102 | maskDom = document.createElement("div"); 103 | maskDom.id = "context-menu-mask"; 104 | maskDom.addEventListener("mousedown", function (evt) { 105 | if (evt.button == 0) { 106 | this.remove(); 107 | } 108 | }); 109 | maskDom.addEventListener("mousedown", function () { 110 | this.remove(); 111 | }); 112 | document.body.appendChild(maskDom); 113 | } 114 | maskDom.appendChild(this.dom); 115 | 116 | this.dom.style.left = `${(x + this.dom.clientWidth < window.innerWidth) ? x : x - this.dom.clientWidth - offsetX}px`; 117 | this.dom.style.top = `${(y + this.dom.clientHeight < window.innerHeight) ? y : y - this.dom.clientHeight - offsetY}px`; 118 | 119 | setTimeout(() => { 120 | this.dom.style.opacity = "1"; 121 | }, 100); 122 | }; 123 | }; 124 | 125 | function closeContextMenu() { 126 | let maskDom = document.getElementById("context-menu-mask"); 127 | if (maskDom) { 128 | maskDom.remove(); 129 | } 130 | }; 131 | window.addEventListener("resize", closeContextMenu); 132 | window.addEventListener("blur", closeContextMenu); -------------------------------------------------------------------------------- /src/frontend/assets/components/LocalFolderExtension.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * SimMusic 内置本地音乐加载器 4 | * 亦可用作扩展开发示例以添加其他音乐源 5 | * 若无特殊说明,基本所有的file变量格式都是“scheme: + ”,自己开发时候请不要忘了添加scheme:前缀 6 | */ 7 | 8 | 9 | /**************** 基础配置 ****************/ 10 | // 当没有config.setItem时,调用config.getItem会返回defaultConfig中的值 11 | defaultConfig["folderLists"] = []; 12 | 13 | 14 | /**************** 工具函数 ****************/ 15 | // 这些函数是插件自己需要的函数,个人推荐const一个object然后都用它存放,防止和主程序内置函数名冲突 16 | const FileExtensionTools = { 17 | scanMusic(directory) { 18 | try { 19 | const supportedExtensions = config.getItem("musicFormats").split(" "); 20 | let list = []; 21 | fs.readdirSync(directory).forEach(file => { 22 | const fullPath = path.join(directory, file); 23 | if (fs.statSync(fullPath).isDirectory()) { 24 | list = list.concat(this.scanMusic(fullPath)); 25 | } else { 26 | const ext = path.extname(fullPath).toLowerCase(); 27 | if (supportedExtensions.includes(ext)) { 28 | // 请务必以插件定义的“scheme:”开头,不然扫描元数据和获取播放链接的时候不知道问哪个插件调方法 29 | // 后面的随意,例如在线歌曲可以使用“xxmusic:114514”,用歌曲id来,你喜欢就好 30 | // 一首歌的内部id应该是唯一的(用于播放和元数据索引),不然歌曲索引的时候会重复请求数据,消耗无意义的资源 31 | list.push("file:" + fullPath); 32 | } 33 | } 34 | }); 35 | return list; 36 | } catch { return []; } 37 | }, 38 | formatTime(ms) { 39 | const totalSeconds = Math.floor(ms / 1000); 40 | const minutes = Math.floor(totalSeconds / 60); 41 | const seconds = totalSeconds % 60; 42 | const milliseconds = ms % 1000; 43 | const formattedMinutes = minutes.toString().padStart(2, '0'); 44 | const formattedSeconds = seconds.toString().padStart(2, '0'); 45 | const formattedMilliseconds = milliseconds.toString(); 46 | return `${formattedMinutes}:${formattedSeconds}.${formattedMilliseconds}`; 47 | }, 48 | fileMenuItem: [ 49 | {type: ["single"], content: { label: "在资源管理器显示", icon: "ED8A", click() {shell.showItemInFolder(getCurrentSelected()[0])} }} 50 | ] 51 | } 52 | 53 | 54 | 55 | /**************** 左侧导航 ****************/ 56 | // 如果你懒,这个字段可以不写,这样插件就没有左侧导航功能(你可以参考下面的写搜索功能) 57 | ExtensionConfig.file.musicList = { 58 | // 这个函数用于处理用户点击歌单“加号”的事件 59 | // 如果没有(例如你的插件是自动同步一个用户的所有歌单),可以不写,这样加号图标就不会显示 60 | add(callback) { 61 | // 这里自己实现添加逻辑,简单输入可直接调内置的 prompt(placeholder:str, callback:function) 方法 62 | ipcRenderer.invoke("pickFolder") 63 | .then(dir => { 64 | if (!dir || !dir[0]) return; 65 | dir = dir[0].trim().replaceAll("/", "\\"); 66 | // 内置config读取可用getItem 67 | const lists = config.getItem("folderLists"); 68 | // 由于数据格式由开发者自行定义,重复导入 & 其他错误需要开发者自行处理 69 | if (dir.split("\\").length == 2 && !dir.split("\\")[1]) return alert("您不能导入磁盘根目录。"); 70 | if (lists.includes(dir)) return alert("此目录已被添加到目录列表中。"); 71 | lists.push(dir); 72 | // 内置config写入可用setItem 73 | config.setItem("folderLists", lists); 74 | // 导入成功后需开发者自行调用callback以更新左侧显示内容(必须),switchList以打开刚才导入的歌单(可选) 75 | callback(); 76 | ExtensionConfig.file.musicList.switchList(dir); 77 | }); 78 | }, 79 | // 这个函数用于渲染左侧的歌单列表 80 | renderList(container) { 81 | const lists = config.getItem("folderLists"); 82 | lists.forEach(name => { 83 | const splitted = name.split("\\"); 84 | const folderName = splitted[splitted.length - 1]; 85 | // 创建一个div即可,可以不需要有类名 86 | const element = document.createElement("div"); 87 | element.textContent = folderName; 88 | element.dataset.folderName = name; 89 | // 处理点击,一般直接switchList即可 90 | element.onclick = () => {this.switchList(name);}; 91 | // 创建右键菜单,具体使用方法参考 zhujin917/3sqrt7-context-menu/README.md 92 | element.oncontextmenu = event => { 93 | new ContextMenu([ 94 | { label: "查看歌曲", icon: "ECB5", click() {element.click();} }, 95 | { label: "在资源管理器中显示", icon: "ED8A", click() {shell.openPath(name);} }, 96 | { type: "separator" }, 97 | { label: "添加到歌单", icon: "EE0D", submenu: MusicList.getMenuItems(listName => { 98 | MusicList.importToMusicList(listName, FileExtensionTools.scanMusic(name)); 99 | MusicList.switchList(listName, true); 100 | }) }, 101 | { label: "从列表中移除", icon: "ED74", click() { 102 | confirm(`目录「${folderName}」将从 SimMusic 目录列表中移除,但不会从文件系统中删除。是否继续?`, () => { 103 | const lists = config.getItem("folderLists"); 104 | lists.splice(lists.indexOf(name), 1); 105 | config.setItem("folderLists", lists); 106 | if (element.classList.contains("active")) switchRightPage("rightPlaceholder"); 107 | element.remove(); 108 | }); 109 | } }, 110 | ]).popup([event.clientX, event.clientY]); 111 | }; 112 | // 把div附加到左侧界面,container会由ExtensionRuntime自动传入,无需担心是否存在 113 | container.appendChild(element); 114 | }); 115 | }, 116 | // 这个函数用于切换歌单 117 | switchList(name) { 118 | const splitted = name.split("\\"); 119 | // 统一调用renderMusicList即可,第二个参数需要传入一个用于识别“当前歌单”的唯一的参数,推荐使用插件名+歌单id以防重复 120 | // 如果你的scanMusic必须是异步的,可以先renderMusicList([], id)以切换界面,再renderMusicList(list, id),id一样就可以 121 | // rML第三个参数请固定false,第4个参数指定是否进行预先渲染,如果为true则在二次渲染之前不会显示歌单(适用于在线歌曲必须要获取metadata的情况) 122 | renderMusicList(FileExtensionTools.scanMusic(name), { 123 | uniqueId: "folder-" + name, 124 | errorText: "当前目录为空", 125 | menuItems: FileExtensionTools.fileMenuItem, 126 | musicListInfo: { 127 | name: splitted[splitted.length - 1], 128 | dirName: name, 129 | } 130 | }); 131 | // 这个用于把当前歌单标蓝,放在renderMusicList函数后运行,推荐借鉴我的写法在renderList函数里自己设一个dataset,然后遍历dataset 132 | document.querySelectorAll(".left .leftBar div").forEach(ele => { 133 | if (ele.dataset.folderName != name) ele.classList.remove("active"); 134 | else ele.classList.add("active"); 135 | }); 136 | }, 137 | }; 138 | 139 | 140 | /**************** 获取数据 ****************/ 141 | // 这个函数用于读取音乐元数据,不管你是本地还是在线,无所谓你咋获取,最后都调callback(data)就行。 142 | // 如果是在线的用fetch就更好做,直接修改我musicmetadata的promise就得 143 | //【注意:读取失败可以返回null,各字段值可以没有】 144 | ExtensionConfig.file.readMetadata = async (file) => { 145 | file = file.replace("file:", ""); 146 | try { 147 | const metadata = await musicMetadata.parseFile(file); 148 | let nativeLyrics; 149 | for (const tagType in metadata.native) { 150 | if (metadata.native[tagType].forEach) metadata.native[tagType].forEach(tag => { 151 | if (tag.value && tag.value.match && tag.value.match(/\[\d+\:\d+\.\d+\]/g)) { 152 | nativeLyrics = tag.value; 153 | } 154 | else if (tag.value && tag.value.text && tag.value.text.match && tag.value.text.match(/\[\d+\:\d+\.\d+\]/g)) { 155 | nativeLyrics = tag.value.text; 156 | } 157 | }); 158 | } 159 | const metadataArtist = metadata.common.artists ? metadata.common.artists.join(", ") : null || metadata.common.artist; 160 | const metadataCover = metadata.common.picture ? metadata.common.picture[0] ? metadata.common.picture[0].data : null : null; 161 | return { 162 | title: metadata.common.title, 163 | artist: metadataArtist, 164 | album: metadata.common.album ? metadata.common.album : file.split("\\")[file.split("\\").length - 2], 165 | time: metadata.format.duration, 166 | cover: metadataCover ? metadataCover : "", 167 | lyrics: nativeLyrics ? nativeLyrics : "", 168 | }; 169 | } catch { 170 | return {}; 171 | } 172 | }; 173 | 174 | 175 | /**************** 歌曲播放 ****************/ 176 | ExtensionConfig.file.player = { 177 | // 这个函数用于获取播放地址,返回值可以是本地文件地址 / http(s)地址 / blob地址 / base64 dataurl,不成功可以用空参数调callback 178 | //【注意:读取失败return可以用空串】 179 | async getPlayUrl(file) { 180 | return file.replace("file:", ""); 181 | }, 182 | // 这个函数用于(在本地索引没有歌词的情况下获取歌词),例如在线播放时把歌词全部写到索引不太现实,就会调用这个方法直接读取 183 | //【注意:读取失败return可以用空串】 184 | async getLyrics(file) { 185 | file = file.replace("file:", ""); 186 | const lastDotIndex = file.lastIndexOf("."); 187 | lrcPath = file.substring(0, lastDotIndex) + ".lrc"; 188 | if (!fs.existsSync(lrcPath)) return ""; 189 | try {return fs.readFileSync(lrcPath, "utf8");} 190 | catch { 191 | let id3Lyrics = ""; 192 | const id3LyricsArray = await nodeId3.Promise.read(file); 193 | if (id3LyricsArray && id3LyricsArray.synchronisedLyrics && id3LyricsArray.synchronisedLyrics[0]) { 194 | id3LyricsArray.synchronisedLyrics[0].synchronisedText.forEach(obj => { 195 | id3Lyrics += `[${FileExtensionTools.formatTime(obj.timeStamp)}]${obj.text}\n`; 196 | }); 197 | } 198 | return id3Lyrics; 199 | } 200 | }, 201 | // 这个函数用于在播放器菜单中插入内容 202 | getPlayerMenu(file) { 203 | return [{ 204 | label: "在资源管理器显示", 205 | icon: "ED8A", 206 | click() {shell.showItemInFolder(file);} 207 | }]; 208 | } 209 | }; 210 | 211 | 212 | /**************** 歌曲搜索 ****************/ 213 | ExtensionConfig.file.search = async (keyword, _page) => { 214 | let allFiles = {}; 215 | config.getItem("folderLists").forEach(folder => { 216 | FileExtensionTools.scanMusic(folder).forEach(file => { 217 | const musicInfo = lastMusicIndex[file]; 218 | const musicInfoString = (SimMusicTools.getTitleFromPath(file) + (musicInfo ? (musicInfo.title + musicInfo.album + musicInfo.artist) : "")).toLowerCase(); 219 | allFiles[file] = musicInfoString; 220 | }); 221 | }); 222 | const fileArray = Object.keys(allFiles); 223 | // 切割搜索词 224 | const splitted = SimMusicTools.naturalSplit(keyword, true); 225 | // 先把啥都匹配不到的丢垃圾桶里 226 | const filteredFiles = fileArray.filter(file => splitted.some(splitItem => allFiles[file].includes(splitItem))); 227 | // 然后按照匹配到的数量进行排序 228 | const resultArray = filteredFiles.sort((a, b) => { 229 | const countA = splitted.filter(keyword => allFiles[a].includes(keyword)).length; 230 | const countB = splitted.filter(keyword => allFiles[b].includes(keyword)).length; 231 | return countB - countA; 232 | }); 233 | return { 234 | files: resultArray, 235 | menu: FileExtensionTools.fileMenuItem,//.concat(DownloadController.getMenuItems()), 236 | hasMore: false 237 | }; 238 | } -------------------------------------------------------------------------------- /src/frontend/assets/components/PublicConfig.js: -------------------------------------------------------------------------------- 1 | 2 | const defaultEq = [ { F: 70, G: 0, Q: 1 }, { F: 180, G: 0, Q: 1 }, { F: 320, G: 0, Q: 1 }, { F: 600, G: 0, Q: 1 }, { F: 1000, G: 0, Q: 1 }, { F: 3000, G: 0, Q: 1 }, { F: 6000, G: 0, Q: 1 }, { F: 12000, G: 0, Q: 1 }, { F: 14000, G: 0, Q: 1 }, { F: 16000, G: 0, Q: 1 } ]; 3 | 4 | const defaultConfig = { 5 | musicLists: {}, 6 | playList: [], 7 | currentMusic: null, 8 | volume: .8, 9 | loop: 0, 10 | lrcShow: true, 11 | updatePrefix: "", 12 | albumScale: true, 13 | musicFormats: ".mp3 .wav .flac", 14 | showLocator: true, 15 | themeImageType: "cover", 16 | backgroundBlur: true, 17 | audioFade: true, 18 | eqProfile: "basic", 19 | eqConfBasic: defaultEq, 20 | eqConfPro: defaultEq, 21 | sleepModePlayEnd: true, 22 | sleepModeOperation: "none", 23 | lyricBlur: true, 24 | lyricAlign: "left", 25 | lyricSize: 1.5, 26 | lyricTranslation: .8, 27 | lyricSpace: .5, 28 | lyricMultiLang: true, 29 | leftBarWidth: 200, 30 | autoDesktopLyrics: false, 31 | desktopLyricsProtection: true, 32 | desktopLyricsAutoHide: true, 33 | desktopLyricsColor: "#1E9FFF", 34 | desktopLyricsStrokeEnabled: true, 35 | desktopLyricsStroke: "#1672B8", 36 | desktopLyricsSize: 30, 37 | desktopLyricsWidth: 700, 38 | desktopLyricsTop: screen.height - 300, 39 | desktopLyricsLeft: screen.width / 2, 40 | ext: {}, 41 | extPerms: {}, 42 | musicListSort: [1, 1], 43 | parallelDownload: 3, 44 | downloadFileName: "[title] - [artist]", 45 | downloadMetadataTitle: true, 46 | downloadMetadataArtist: true, 47 | downloadMetadataCover: true, 48 | downloadMetadataLyrics: 1, 49 | } 50 | 51 | const configListeners = {}; 52 | 53 | const config = { 54 | getItem(key) { 55 | const data = localStorage.SimMusicConfig; 56 | if (!data) { 57 | localStorage.SimMusicConfig = "{}"; 58 | return this.getItem(key); 59 | } 60 | try { 61 | const config = JSON.parse(data); 62 | if (config[key] || config[key] === false || config[key] === 0) return config[key]; 63 | return defaultConfig[key]; 64 | } catch { 65 | alert("配置文件损坏,程序将无法正常运行。"); 66 | } 67 | }, 68 | setItem(key, value) { 69 | const data = localStorage.SimMusicConfig; 70 | if (!data) { 71 | localStorage.SimMusicConfig = "{}"; 72 | return this.setItem(key, value); 73 | } 74 | try { 75 | const config = JSON.parse(data); 76 | config[key] = value; 77 | const newConfig = JSON.stringify(config); 78 | localStorage.SimMusicConfig = newConfig; 79 | } catch { 80 | alert("配置文件损坏,程序将无法正常运行。"); 81 | } 82 | if (configListeners[key]) configListeners[key](value); 83 | }, 84 | listenChange(key, callback) { 85 | configListeners[key] = callback; 86 | } 87 | } -------------------------------------------------------------------------------- /src/frontend/assets/components/SimAP.css: -------------------------------------------------------------------------------- 1 | 2 | .playerContainer{position:absolute;inset:0;margin:20px max(calc(50vw - 500px), 90px);transition:transform .2s;} 3 | #playPage .SimProgress{--SimProgressTheme:var(--SimAPTheme)!important;} 4 | 5 | /* 音频控件 */ 6 | .controls{display:flex;flex-direction:column;width:350px;position:absolute;inset:0;height:fit-content;margin:auto 0;transition:margin .3s,transform .3s;z-index:5;} 7 | .hideLyrics.hideList .controls{margin:auto calc(50% - 175px);transform:scale(1.05);} 8 | .controls #album{width:calc(100% + 10px);aspect-ratio:1;object-fit:cover;border-radius:10px;box-shadow:0 20px 25px -5px rgba(0,0,0,.1),0 8px 10px -6px rgba(0,0,0,.1);background:white;margin:-5px;transition:transform .4s;transition-timing-function:cubic-bezier(0.3, 0.7, 0, 1.8);} 9 | body.albumScale:not(.playing) .controls #album{transform:scale(.95);} 10 | /* 专辑信息 */ 11 | .controls .infoBar{display:flex;align-items:center;margin:30px 0 10px 0;} 12 | .controls .infoBar i{display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,.025);color:rgba(0,0,0,.6);font-size:1.1em;border-radius:15px;width:30px;height:30px;transition:all .2s,font-size 0s;} 13 | .controls .infoBar i:hover{background:rgba(0,0,0,.05);color:rgba(0,0,0,.8);} 14 | .controls .infoBar i:active{background:rgba(0,0,0,.05);color:rgba(0,0,0,.8);transform:scale(.95);} 15 | .sleepMode .controls .infoBar i{width:70px;font-size:.9em;font-family:"font";} 16 | .controls .musicInfo{width:calc(100% - 30px)!important;} 17 | .controls .musicInfo b,.controls .musicInfo div,.musicInfoBottom b,.musicInfoBottom>div{display:block;font-size:1.3em;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} 18 | .controls .musicInfo div,.musicInfoBottom>div{font-size:.9em;opacity:.8;} 19 | /* 进度条 */ 20 | .controls .progressControl{display:flex;align-items:center;margin:10px 0;} 21 | .controls .progressControl span{width:55px;font-size:.85em;opacity:.8;} 22 | .controls .progressControl span:last-child{text-align:right;} 23 | .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;} 24 | @keyframes progressLoading{to{background-position:25px 0;}} 25 | /* 下方按钮 */ 26 | .controls .buttons,.bottom .center{align-items:center;margin-top:5px;display:flex;align-items:center;justify-content:center;} 27 | .controls .buttons>div,.bottom .center>div,.bottom .volBtnBottom>div{width:50px;height:50px;position:relative;font-size:1.5em;transition:all .3s;opacity:.3;border-radius:100px;overflow:hidden;} 28 | .controls .buttons>div:hover,.bottom .center>div:hover,.bottom .volBtnBottom>div:hover{background:rgba(0,0,0,.05);opacity:.7;} 29 | .controls .buttons>div:active,.bottom .center>div:active,.bottom .volBtnBottom>div:active{background:rgba(0,0,0,.1);opacity:.7;transform:scale(.95);} 30 | .controls .buttons>div>i,.bottom .center>div>i,.bottom .volBtnBottom>div>i{display:block;width:fit-content;height:fit-content;position:absolute;inset:0;margin:auto;transition:all .3s;} 31 | .controls .buttons>div.larger>i,.bottom .center>div.larger>i{font-size:1.15em;padding-bottom:1px;} 32 | .controls .buttons>.play,.bottom .center>.play{width:60px;height:60px;font-size:2.1em;opacity:1!important;font-weight:bold;} 33 | .controls .buttons>.play>i:last-child,.bottom .center>.play>i:last-child{opacity:0;transform:scale(.4);} 34 | .playing .controls .buttons>.play>i:first-child,.playing .bottom .center>.play>i:first-child{opacity:0;transform:scale(.4);} 35 | .playing .controls .buttons>.play>i:last-child,.playing .bottom .center>.play>i:last-child{opacity:1;transform:none;} 36 | .controls .buttons>.play i:first-child,.bottom .center>.play i:first-child{padding-left:3.5px;} 37 | /* 音量控制 */ 38 | .volume .controls .buttons>div{width:0;opacity:0!important;} 39 | .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;} 40 | .volume .controls .buttons>.volBtn>i{right:120px;} 41 | .volume .controls .buttons>.volBtn>i:hover{color:var(--SimAPTheme);} 42 | .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;} 43 | .volume .controls .buttons>.volBtn>div{opacity:1;pointer-events:all;} 44 | .loopList .loopBtn i::after{content:"\F072";} 45 | .loopSingle .loopBtn i::after{content:"\F075";} 46 | .loopRandom .loopBtn i::after{content:"\F124";} 47 | /* 3D特效 */ 48 | body:not(.hideLyrics.hideList) .playerContainer{transform:translateX(-25px);} 49 | .threeEffect .controls{transform:perspective(900px) rotateY(10deg);} 50 | .threeEffect .lyrics,.threeEffect .list{transform:perspective(900px) rotateY(-12.5deg);} 51 | 52 | /* 歌词区域 */ 53 | .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));} 54 | .hideLyrics .lyrics{transform:scale(.6);opacity:0;pointer-events:none;} 55 | .lyrics>div{height:100%;} 56 | .lyrics>div>div.active{font-weight:bold;} 57 | body:not(.hideLyrics) .lyricsBtn{color:var(--SimAPTheme);opacity:.7;} 58 | .disableLyricsBlur .lyrics div div{filter:none!important;} 59 | 60 | /* 播放列表 */ 61 | .list{position:absolute;left:410px;top:0;width:calc(100% - 410px);height:100%;transform-origin:left center;transition:all .3s;overflow-y:scroll;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));} 62 | .list::before,.list::after{content:"";display:block;height:50%;} 63 | .hideList .list{transform:scale(.6);opacity:0;pointer-events:none;} 64 | body:not(.hideList) .listBtn{color:var(--SimAPTheme);opacity:.7;} 65 | .list>div{width:100%;padding:0 10px;height:80px;border-radius:10px;display:flex;align-items:center;transition:background .2s;} 66 | .list>div:hover{background:rgba(0,0,0,.025);} 67 | .list>div.active,.list>div:active{background:rgba(0,0,0,.05);} 68 | .list>div.removed{transition:all .3s,opacity .15s;height:0;background:rgba(0,0,0,.025);opacity:0;transform:scaleX(.9) scaleY(.5);} 69 | .list>div>img{min-width:60px;height:60px;border-radius:5px;margin-right:10px;background:white;} 70 | .list>div>div{width:calc(100% - 100px);} 71 | .list>div>div>b{display:block;width:100%;font-size:1.1em;} 72 | .list>div>div>span{display:block;width:100%;opacity:.8;font-size:.9em;} 73 | .list>div i{opacity:.3;width:30px;height:30px;display:flex;align-items:center;justify-content:center;transition:opacity .2s;} 74 | .list>div i:hover,.list>div i:active{opacity:.8;} 75 | .list>div.active i{opacity:0;pointer-events:none;} 76 | 77 | 78 | 79 | /* 流光背景 */ 80 | #background{z-index:-1;inset:0;position:absolute;pointer-events:none;opacity:.2;transition:background .3s;} 81 | #background>div{position:absolute;inset:0;background:linear-gradient(135deg, rgba(255,255,255,.1), rgba(255,255,255,.5));} 82 | .disableBackgroundBlur #background>canvas{display:none;} 83 | 84 | -------------------------------------------------------------------------------- /src/frontend/assets/components/SimAP.js: -------------------------------------------------------------------------------- 1 | const SimAPTools = { 2 | getPalette(sourceImage) { 3 | // 读取图片数据 4 | const canvas = document.createElement("canvas"); 5 | canvas.width = sourceImage.width; 6 | canvas.height = sourceImage.height; 7 | const ctx = canvas.getContext("2d"); 8 | ctx.drawImage(sourceImage, 0, 0, canvas.width, canvas.height); 9 | const pixels = ctx.getImageData(0, 0, canvas.width, canvas.height).data; 10 | // 读取图片颜色 11 | const pixelArray = []; 12 | const pixelCount = canvas.width * canvas.height; 13 | for (let i = 0, offset, r, g, b; i < pixelCount; i = i + Math.round(pixelCount / 500)) { 14 | offset = i * 4; 15 | r = pixels[offset + 0]; 16 | g = pixels[offset + 1]; 17 | b = pixels[offset + 2]; 18 | pixelArray.push([r, g, b]); 19 | } 20 | return pixelArray; 21 | }, 22 | getTopColors(sourceImage) { 23 | const colors = this.getPalette(sourceImage); 24 | let colorCounts = new Map(); 25 | colors.forEach(color => { 26 | let found = false; 27 | for (let [mergedColor, count] of colorCounts) { 28 | const colorDistance = Math.sqrt( 29 | Math.pow(color[0] - mergedColor[0], 2) + 30 | Math.pow(color[1] - mergedColor[1], 2) + 31 | Math.pow(color[2] - mergedColor[2], 2) 32 | ); 33 | if (colorDistance < 80) { 34 | const newColor = [ 35 | Math.floor((mergedColor[0] * count + color[0]) / (count + 1)), 36 | Math.floor((mergedColor[1] * count + color[1]) / (count + 1)), 37 | Math.floor((mergedColor[2] * count + color[2]) / (count + 1)) 38 | ]; 39 | colorCounts.delete(mergedColor); 40 | colorCounts.set(newColor, count + 1); 41 | found = true; 42 | break; 43 | } 44 | } 45 | if (!found) { 46 | colorCounts.set(color, 1); 47 | } 48 | }); 49 | let sortedColors = Array.from(colorCounts.entries()).sort((a, b) => b[1] - a[1]); 50 | return sortedColors.slice(0, 4).map(entry => entry[0]); 51 | }, 52 | formatTime(time) { 53 | let minutes = Math.floor(time / 60); 54 | let seconds = Math.floor(time % 60); 55 | return `${minutes}:${seconds < 10 ? "0" : ""}${seconds}`; 56 | }, 57 | } 58 | 59 | 60 | // 初始化SimAP 61 | const switchMusic = (playConfig) => { 62 | // 初始化界面 63 | const audio = document.getElementById("audio"); 64 | document.getElementById("album").src = document.getElementById("albumBottom").src = playConfig.album; 65 | document.querySelector(".musicInfo>b").textContent = document.querySelector(".musicInfoBottom>b").textContent = playConfig.title; 66 | document.querySelector(".musicInfo>div").textContent = document.querySelector("#bottomArtist").textContent = playConfig.artist; 67 | const lastPlaybackRate = audio.playbackRate; 68 | audio.src = playConfig.audio; 69 | audio.currentTime = 0; 70 | document.body.classList.add("withCurrentMusic"); 71 | document.body.classList.add("musicLoading"); 72 | if (playConfig.play) setTimeout(() => { 73 | document.body.classList.add("playing"); 74 | SimAPControls.loadAudioState(); 75 | }); 76 | SimAPControls.loadLoop(); 77 | document.title = playConfig.title + " - SimMusic"; 78 | loadThemeImage(); 79 | // 初始化背景 80 | document.getElementById("album").onload = SimAPUI.loadColors; 81 | // 初始化音频控件 82 | const current = document.getElementById("progressCurrent"); 83 | const duration = document.getElementById("progressDuration"); 84 | audio.onloadedmetadata = () => { 85 | document.body.classList.remove("musicLoading"); 86 | applyEq(); 87 | SimAPProgress.max = SimAPProgressBottom.max = audio.duration; 88 | SimAPProgress.setValue(0); SimAPProgressBottom.setValue(0); 89 | duration.textContent = SimAPTools.formatTime(audio.duration); 90 | SimAPProgress.onchange = SimAPProgressBottom.onchange = value => { 91 | audio.currentTime = value; 92 | setMiniModeStatus(`${SimAPTools.formatTime(value)} / ${duration.textContent}`); 93 | } 94 | SimAPProgress.ondrag = SimAPProgressBottom.ondrag = value => { 95 | current.textContent = SimAPTools.formatTime(value); 96 | setMiniModeStatus(`${SimAPTools.formatTime(value)} / ${duration.textContent}`); 97 | } 98 | loadVolumeUi(); 99 | audio.playbackRate = lastPlaybackRate ?? 1; 100 | if (playConfig.play) audio.play(); else audio.pause(); 101 | }; 102 | audio.ontimeupdate = () => { 103 | document.body.classList.remove("musicLoading"); 104 | SimAPProgress.setValue(audio.currentTime); SimAPProgressBottom.setValue(audio.currentTime); 105 | if (!SimAPProgress.progressElement.classList.contains("dragging")) current.textContent = SimAPTools.formatTime(audio.currentTime); 106 | if (SimAPControls.audioFadeInterval) return; 107 | document.body.classList[!audio.paused ? "add" : "remove"]("playing"); 108 | SimAPControls.loadAudioState(); 109 | }; 110 | audio.onwaiting = () => { 111 | document.body.classList.add("musicLoading"); 112 | }; 113 | audio.onended = () => { 114 | if (SleepMode.checkMusicSwitch()) return; 115 | if (config.getItem("loop") == 1) { PlayerController.switchMusic(config.getItem("currentMusic"), false, true); } 116 | else SimAPControls.next(); 117 | }; 118 | audio.onerror = () => { 119 | shell.beep(); 120 | document.body.classList.remove("playing"); 121 | document.body.classList.remove("musicLoading"); 122 | confirm("当前曲目播放失败,是否从播放列表中移除?", () => { 123 | PlayerController.deleteFromList(config.getItem("currentMusic")); 124 | }); 125 | }; 126 | // 系统级控件 127 | navigator.mediaSession.metadata = new MediaMetadata({ title: playConfig.title, artist: playConfig.artist, artwork: [{ src: playConfig.album }], }); 128 | navigator.mediaSession.setActionHandler("play", () => {SimAPControls.togglePlay(true);}); 129 | navigator.mediaSession.setActionHandler("pause", () => {SimAPControls.togglePlay(true);}); 130 | navigator.mediaSession.setActionHandler("previoustrack", () => {SimAPControls.prev(true);}); 131 | navigator.mediaSession.setActionHandler("nexttrack", () => {SimAPControls.next(true);}); 132 | // 初始化歌词 133 | const slrc = new SimLRC(playConfig.lyrics); 134 | slrc.render(document.querySelector(".lyrics>div"), audio, { 135 | align: "left", 136 | lineSpace: config.getItem("lyricSpace"), 137 | activeColor: "var(--SimAPTheme)", 138 | normalColor: "rgba(0,0,0,.4)", 139 | multiLangSupport: config.getItem("lyricMultiLang"), 140 | align: config.getItem("lyricAlign"), 141 | callback: txt => { ipcRenderer.invoke("lrcUpdate", txt); } 142 | }); 143 | SimAPControls.loadConfig(); 144 | }; 145 | 146 | 147 | 148 | // 动态混色控制器 149 | const PlayerBackground = { 150 | init() { 151 | canvas = document.getElementById("backgroundAnimation"); 152 | this.ctx = canvas.getContext("2d"); 153 | canvas.width = window.innerWidth; 154 | canvas.height = window.innerHeight; 155 | window.addEventListener("resize", () => { 156 | canvas.width = window.innerWidth; 157 | canvas.height = window.innerHeight; 158 | }); 159 | this.blobs = []; 160 | this.animate(true); 161 | }, 162 | animate(isInit) { 163 | requestAnimationFrame(() => {PlayerBackground.animate();}); 164 | if (!config.getItem("backgroundBlur")) return; 165 | if (!document.body.classList.contains("playing") && !isInit) return; 166 | const ctx = PlayerBackground.ctx; 167 | ctx.clearRect(0, 0, window.innerWidth, window.innerHeight); 168 | for (const blob of PlayerBackground.blobs) { 169 | // 位移 170 | blob.x += blob.dx; 171 | blob.y += blob.dy; 172 | if (blob.x - blob.radius < 0 || blob.x + blob.radius > window.innerWidth) blob.dx *= -1; 173 | if (blob.y - blob.radius < 0 || blob.y + blob.radius > window.innerHeight) blob.dy *= -1; 174 | } 175 | PlayerBackground.drawBlobs(); 176 | }, 177 | update(mainColor, subColors) { 178 | document.getElementById("background").style.background = mainColor; 179 | this.mainColor = mainColor; 180 | this.blobs = []; 181 | for (let i = 0; i < 3; i++) { 182 | this.blobs.push({ 183 | x: Math.random() * canvas.width, 184 | y: Math.random() * canvas.height, 185 | radius: Math.random() * screen.width / 3 + screen.width / 5, 186 | color: subColors[i], 187 | dx: ((Math.random() < 0.5) ? 1 : -1) * (Math.random() * 0.5 + 0.5), 188 | dy: ((Math.random() < 0.5) ? 1 : -1) * (Math.random() * 0.5 + 0.5), 189 | }); 190 | } 191 | PlayerBackground.drawBlobs(); 192 | }, 193 | drawBlobs() { 194 | const ctx = PlayerBackground.ctx; 195 | for (const blob of PlayerBackground.blobs) { 196 | ctx.beginPath(); 197 | const gradient = ctx.createRadialGradient(blob.x, blob.y, 0, blob.x, blob.y, blob.radius); 198 | gradient.addColorStop(0, blob.color); 199 | gradient.addColorStop(1, "transparent"); 200 | ctx.fillStyle = gradient; 201 | ctx.arc(blob.x, blob.y, blob.radius, 0, Math.PI * 2); 202 | ctx.fill(); 203 | } 204 | } 205 | } 206 | PlayerBackground.init(); 207 | 208 | 209 | 210 | // 播放控件 211 | const SimAPProgress = new SimProgress(document.getElementById("progressBar")); 212 | const SimAPProgressBottom = new SimProgress(document.getElementById("bottomProgressBar")); 213 | const SimAPControls = { 214 | loadAudioState() { 215 | const playing = document.body.classList.contains("playing"); 216 | navigator.mediaSession.playbackState = playing ? "playing" : "paused"; 217 | ipcRenderer.invoke(playing ? "musicPlay" : "musicPause"); 218 | }, 219 | togglePlay(isManual) { 220 | if (isManual) SleepMode.checkManualOperation(); 221 | if (document.body.classList.contains("musicLoading")) return; 222 | const audio = document.getElementById("audio"); 223 | if (!audio || !audio.src) return; 224 | const isPlay = audio.paused; 225 | document.body.classList[isPlay ? "add" : "remove"]("playing"); 226 | SimAPControls.loadAudioState(); 227 | clearInterval(SimAPControls.audioFadeInterval); 228 | // 音频淡入淡出处理 229 | if (config.getItem("audioFade") && config.getItem("volume")) { 230 | const configVolume = config.getItem("volume"); 231 | const volumeOffset = configVolume / 10; 232 | if (isPlay) audio.play(); 233 | SimAPControls.audioFadeInterval = setInterval(() => { 234 | if (isPlay) { 235 | const newVolume = audio.volume + volumeOffset; 236 | if (newVolume > configVolume) { 237 | clearInterval(SimAPControls.audioFadeInterval); 238 | SimAPControls.audioFadeInterval = null; 239 | } else audio.volume = newVolume; 240 | } else { 241 | const newVolume = audio.volume - volumeOffset; 242 | if (newVolume < 0) { 243 | clearInterval(SimAPControls.audioFadeInterval); 244 | SimAPControls.audioFadeInterval = null; 245 | audio.pause(); 246 | } 247 | else audio.volume = newVolume; 248 | } 249 | }, 50); 250 | } else { 251 | audio[isPlay ? "play" : "pause"](); 252 | } 253 | }, 254 | prev(isManual) { 255 | if (isManual) SleepMode.checkManualOperation(); 256 | const audio = document.getElementById("audio"); 257 | if (!config.getItem("fastPlayback") || audio.currentTime / audio.duration < .9) SimAPControls.switchIndex(-1); 258 | else audio.currentTime = 0; 259 | }, 260 | next(isManual) { 261 | if (isManual) SleepMode.checkManualOperation(); 262 | SimAPControls.switchIndex(1); 263 | }, 264 | switchIndex(offset) { 265 | if (SleepMode.checkMusicSwitch()) return; 266 | const list = config.getItem("playList"); 267 | const currentPlayingIndex = list.indexOf(config.getItem("currentMusic")); 268 | let newIndex = currentPlayingIndex + offset; 269 | if (config.getItem("loop") == 2 && (newIndex < 0 || newIndex > list.length - 1)) { 270 | this.shufflePlaylist(); 271 | newIndex = 0; 272 | } 273 | if (newIndex < 0) newIndex = list.length - 1; 274 | if (newIndex > list.length - 1) newIndex = 0; 275 | PlayerController.switchMusic(config.getItem("playList")[newIndex], false, true); 276 | }, 277 | toggleLoop() { 278 | switch (config.getItem("loop")) { 279 | case 0: config.setItem("loop", 1); break; 280 | case 1: 281 | config.setItem("loop", 2); 282 | this.shufflePlaylist(true); 283 | break; 284 | case 2: config.setItem("loop", 0); break; 285 | } 286 | this.loadLoop(); 287 | }, 288 | shufflePlaylist(keepCurrent) { 289 | let shuffledList = config.getItem("playList").sort(() => Math.random() - 0.5); 290 | if (keepCurrent) { 291 | const currentPlayingIndex = shuffledList.indexOf(config.getItem("currentMusic")); 292 | const currentFirst = shuffledList[0]; 293 | shuffledList[0] = shuffledList[currentPlayingIndex]; 294 | shuffledList[currentPlayingIndex] = currentFirst; 295 | } 296 | PlayerController.replacePlayList(shuffledList); 297 | }, 298 | loadLoop() { 299 | ["loopList", "loopSingle", "loopRandom"].forEach(className => {document.body.classList.remove(className);}) 300 | document.body.classList.add(["loopList", "loopSingle", "loopRandom"][config.getItem("loop")]); 301 | }, 302 | toggleVolume() { 303 | const volIcon = document.querySelector(".volBtn i"); 304 | if (document.body.classList.contains("volume") && event.target == volIcon) { 305 | this.toggleMuted(); 306 | } else if (audio.muted) { 307 | this.toggleMuted(false); 308 | } else { 309 | document.body.classList.add("volume"); 310 | } 311 | }, 312 | toggleMuted(isMute = !audio.muted) { 313 | audio.muted = isMute; 314 | const icon = audio.muted ? "" : ""; 315 | document.querySelector(".volBtn i").innerHTML = icon; 316 | document.querySelector(".volBtnBottom i").innerHTML = icon; 317 | }, 318 | toggleList(isShow = document.body.classList.contains("hideList")) { 319 | document.body.classList[isShow ? "remove" : "add"]("hideList"); 320 | if (isShow) document.body.classList.add("hideLyrics"); 321 | PlayerController.loadMusicListActive(); 322 | }, 323 | toggleLyrics(isShow = document.body.classList.contains("hideLyrics")) { 324 | document.body.classList[isShow ? "remove" : "add"]("hideLyrics"); 325 | if (isShow) document.body.classList.add("hideList"); 326 | config.setItem("lrcShow", isShow); 327 | }, 328 | loadConfig() { 329 | document.querySelector(".SimLRC").style.setProperty("--lineSpace", config.getItem("lyricSpace") + "em"); 330 | document.querySelector(".lyrics").style.setProperty("--lrcSize", config.getItem("lyricSize") + "em"); 331 | document.querySelector(".lyrics").style.setProperty("--lrcTranslation", config.getItem("lyricTranslation") + "em"); 332 | document.body.classList[config.getItem("backgroundBlur") ? "remove" : "add"]("disableBackgroundBlur"); 333 | document.body.classList[config.getItem("lyricBlur") ? "remove" : "add"]("disableLyricsBlur"); 334 | } 335 | }; 336 | 337 | 338 | 339 | // 播放器显隐处理 340 | const SimAPUI = { 341 | show() { 342 | if (this.playingAnimation) return; 343 | if (document.body.classList.contains("playerShown") || document.body.classList.contains("miniMode")) return; 344 | if (!config.getItem("playList").length || !document.getElementById("album").src) return; 345 | document.getElementById("playPage").hidden = false; 346 | this.playingAnimation = true; 347 | setTimeout(() => { 348 | document.body.classList.add("playerShown"); 349 | if (config.getItem("darkPlayer")) ipcRenderer.invoke("overlayWhite"); 350 | const listActive = document.querySelector(".list div.active"); 351 | if (!listActive) document.querySelector(".list div").click(); 352 | document.querySelector(".list div.active").scrollIntoView({block: "center"}); 353 | document.querySelector(".lyrics div.active").scrollIntoView({block: "center"}); 354 | this.playingAnimation = false; 355 | this.toggleDesktopLyrics(null, false); 356 | addEventListener("visibilitychange", this.toggleDesktopLyrics); 357 | }, 50); 358 | }, 359 | hide() { 360 | if (this.playingAnimation) return; 361 | if (!document.body.classList.contains("playerShown")) return; 362 | ipcRenderer.invoke("overlayBlack"); 363 | SimAPUI.toggleFullScreen(true); 364 | document.body.classList.remove("playerShown"); 365 | this.playingAnimation = true; 366 | setTimeout(() => { 367 | this.toggleDesktopLyrics(null, true); 368 | removeEventListener("visibilitychange", this.toggleDesktopLyrics); 369 | document.getElementById("playPage").hidden = true; 370 | this.playingAnimation = false; 371 | }, 300); 372 | }, 373 | toggleDesktopLyrics(_event, showWindow = document.visibilityState == "hidden" ? true : false) { 374 | if (config.getItem("desktopLyricsAutoHide") && WindowStatus.lyricsWin) ipcRenderer.invoke("toggleLyrics", showWindow); 375 | }, 376 | loadColors() { 377 | const themeColors = SimAPTools.getTopColors(document.getElementById("album")); 378 | PlayerBackground.update(`rgb(${themeColors[0].join(",")})`, [ 379 | `rgb(${themeColors[1] ? themeColors[1].join(",") : "255,255,255"})`, 380 | `rgb(${themeColors[2] ? themeColors[2].join(",") : "255,255,255"})`, 381 | `rgb(${themeColors[3] ? themeColors[3].join(",") : "255,255,255"})` ]); 382 | const themeColorNum = Math.min( 255 / (themeColors[0][0] + themeColors[0][1] + themeColors[0][2] + 1), 1); 383 | document.body.style.setProperty("--SimAPTheme", `rgb(${themeColors[0].map(num => num * themeColorNum).join(",")})`); 384 | document.body.classList[config.getItem("darkPlayer") ? "add" : "remove"]("darkPlayer"); 385 | }, 386 | toggleFullScreen(isQuit) { 387 | if (!document.fullscreenElement && document.body.classList.contains("playerShown") && !isQuit) { 388 | document.body.requestFullscreen(); 389 | document.body.classList.add("fullscreen"); 390 | document.onfullscreenchange = () => { 391 | if (!document.fullscreenElement) document.body.classList.remove("fullscreen"); 392 | window.dispatchEvent(new Event("resize")); 393 | }; 394 | } else { 395 | document.exitFullscreen().catch(() => {}); 396 | document.body.classList.remove("fullscreen"); 397 | } 398 | window.dispatchEvent(new Event("resize")); 399 | } 400 | } 401 | ipcRenderer.invoke("musicPause"); 402 | config.listenChange("darkPlayer", SimAPUI.loadColors); 403 | 404 | 405 | // 处理键盘操作 406 | let keydownLock; 407 | document.documentElement.addEventListener("keydown", e => { 408 | if (keydownLock) return; 409 | keydownLock = true; 410 | setTimeout(() => { keydownLock = false; }, 150) 411 | if (document.activeElement.tagName.toLowerCase() == "input") return; 412 | const audio = document.getElementById("audio"); 413 | const duration = document.getElementById("progressDuration").textContent; 414 | switch (e.key) { 415 | case " ": 416 | SimAPControls.togglePlay(true); 417 | break; 418 | case "ArrowUp": 419 | const upVol = Math.min(1, config.getItem("volume") + .05); 420 | config.setItem("volume", upVol); 421 | setMiniModeStatus(`音量:${Math.round(upVol * 100)}%`); 422 | break; 423 | case "ArrowDown": 424 | const downVol = Math.max(0, config.getItem("volume") - .05); 425 | config.setItem("volume", downVol); 426 | setMiniModeStatus(`音量:${Math.round(downVol * 100)}%`); 427 | break; 428 | case "ArrowRight": 429 | const value1 = Math.min(audio.duration, audio.currentTime + 5); 430 | audio.currentTime = value1; 431 | setMiniModeStatus(`${SimAPTools.formatTime(value1)} / ${duration}`); 432 | break; 433 | case "ArrowLeft": 434 | const value2 = Math.max(0, audio.currentTime - 5); 435 | audio.currentTime = value2; 436 | setMiniModeStatus(`${SimAPTools.formatTime(value2)} / ${duration}`); 437 | break; 438 | case "Escape": 439 | SimAPUI.hide(); 440 | document.body.classList.remove("volume"); 441 | break; 442 | case "F11": 443 | SimAPUI.toggleFullScreen(); 444 | break; 445 | } 446 | }); 447 | 448 | 449 | // 音量相关操作 450 | const SimAPVolume = new SimProgress(document.getElementById("volBar")); 451 | const SimAPVolumeBottom = new SimProgress(document.getElementById("volBarBottom")); 452 | const loadVolumeUi = () => { 453 | const value = config.getItem("volume"); 454 | document.getElementById("audio").volume = value; 455 | SimAPVolume.setValue(value); 456 | SimAPVolumeBottom.setValue(value); 457 | SimAPControls.toggleMuted(false); 458 | } 459 | loadVolumeUi(); 460 | config.listenChange("volume", loadVolumeUi); 461 | SimAPVolume.ondrag = SimAPVolumeBottom.ondrag = value => { config.setItem("volume", value); } 462 | document.body.onpointerdown = () => {document.body.classList.remove("volume");}; 463 | addEventListener("blur", () => {document.body.classList.remove("volume");}); 464 | document.querySelector(".volBtn").onpointerdown = e => {e.stopPropagation();}; 465 | const handleWheel = e => { 466 | e.preventDefault(); 467 | let value = config.getItem("volume"); 468 | value = e.deltaY > 0 ? Math.max(0, value - .05) : Math.min(1, value + .05); 469 | config.setItem("volume", value); 470 | setMiniModeStatus(`音量:${Math.round(value * 100)}%`); 471 | }; 472 | document.addEventListener("wheel", e => { if (document.body.classList.contains("volume")) handleWheel(e); }, {passive: false}); 473 | document.querySelector(".volBtn").onwheel = () => { document.body.classList.add("volume"); }; 474 | document.querySelector(".volBtnBottom").onwheel = e => { handleWheel(e); }; 475 | document.querySelector(".bottom").onwheel = e => { if (document.body.classList.contains("miniMode")) handleWheel(e); }; 476 | 477 | 478 | 479 | // 响应配置更新 480 | config.listenChange("backgroundBlur", SimAPControls.loadConfig); 481 | config.listenChange("lyricBlur", SimAPControls.loadConfig); 482 | config.listenChange("lyricSize", SimAPControls.loadConfig); 483 | config.listenChange("lyricSpace", SimAPControls.loadConfig); 484 | config.listenChange("lyricTranslation", () => {SimAPControls.loadConfig(); updateDesktopLyricsConfig();}); 485 | -------------------------------------------------------------------------------- /src/frontend/assets/components/SimLRC.css: -------------------------------------------------------------------------------- 1 | 2 | .SimLRC{position:relative;overflow-x:hidden;overflow-y:scroll;text-align:var(--align);} 3 | .SimLRC::before,.SimLRC::after{content:"";display:block;height:50%;} 4 | .SimLRC>div{color:var(--normalColor);margin:calc(var(--lineSpace) * var(--inactiveZoom)) .2em;font-size:1.5em;transform:scale(var(--inactiveZoom));transform-origin:center var(--align);transition:all .3s,filter 0s;} 5 | .SimLRC.scrolling>div{filter:none!important;} 6 | .SimLRC>div.active{color:var(--activeColor);transform:scale(1);margin:var(--lineSpace) .2em;transition:all .3s;} 7 | .SimLRC>div:hover{color:var(--hoverColor);} 8 | .SimLRC>div>span,.SimLRC>div>small{display:block;} 9 | .SimLRC>div>small{font-size:var(--lrcTranslation);} -------------------------------------------------------------------------------- /src/frontend/assets/components/SimLRC.js: -------------------------------------------------------------------------------- 1 | 2 | class SimLRC { 3 | constructor(lrc) { 4 | // 解析歌词 5 | const lrcSpilitted = lrc.split("\n"); 6 | this.lrcParsed = {}; 7 | for (let lineNum in lrcSpilitted) { 8 | const line = lrcSpilitted[lineNum]; 9 | const regex = /\[\d+\:\d+\.\d+\]/g; 10 | const tags = (line.match(regex) || []).map(match => match.slice(1, -1)); 11 | const text = line.replace(regex, "").trim(); 12 | if (!tags || !text) continue; 13 | tags.forEach(tag => { 14 | const [minutes, seconds] = tag.split(':').map(Number); 15 | const msTime = Math.round(minutes * 60000 + seconds * 1000); 16 | if (msTime || msTime === 0) { 17 | if (!this.lrcParsed[msTime]) this.lrcParsed[msTime] = []; 18 | this.lrcParsed[msTime].push(text); 19 | } 20 | }); 21 | if (!this.lrcParsed[0]) { 22 | const firstTs = Object.keys(this.lrcParsed)[0]; 23 | this.lrcParsed[0] = this.lrcParsed[firstTs]; 24 | delete this.lrcParsed[firstTs]; 25 | } 26 | } 27 | if (!Object.keys(this.lrcParsed).length) this.lrcParsed = {0: ["暂无歌词"]}; 28 | } 29 | 30 | render(container, audio, options = {}) { 31 | if (!container || !audio) return; 32 | // 初始化配置项 33 | const defaultOptions = { 34 | blurStep: 1, 35 | blurMin: 2, 36 | blurMax: 5, 37 | normalColor: "#00000088", 38 | activeColor: "#000000", 39 | clickUpdate: true, 40 | multiLangSupport: true, 41 | align: "center", 42 | inactiveZoom: .8, 43 | lineSpace: .8, 44 | scrollTimeout: 3000, 45 | }; 46 | options = Object.assign(defaultOptions, options); 47 | // 渲染歌词HTML 48 | container.innerHTML = ""; 49 | for (let timestamp in this.lrcParsed) { 50 | const currentLrc = this.lrcParsed[timestamp]; 51 | if (options.multiLangSupport) { 52 | // 启用多语言支持,则同时间戳不同歌词在同一个div渲染 53 | const lrcDiv = document.createElement("div"); 54 | lrcDiv.dataset.stamp = timestamp; 55 | currentLrc.forEach((text, index) => { 56 | const textElement = document.createElement(index ? "small" : "span"); 57 | textElement.textContent = text; 58 | lrcDiv.appendChild(textElement); 59 | }); 60 | container.appendChild(lrcDiv); 61 | } else { 62 | // 禁用多语言支持,则同时间戳不同歌词分开渲染 63 | currentLrc.forEach(text => { 64 | const lrcDiv = document.createElement("div"); 65 | lrcDiv.dataset.stamp = timestamp; 66 | lrcDiv.textContent = text; 67 | container.appendChild(lrcDiv); 68 | }); 69 | } 70 | } 71 | // 设置样式 72 | container.classList.add("SimLRC"); 73 | container.style.setProperty("--align", options.align); 74 | container.style.setProperty("--normalColor", options.normalColor); 75 | container.style.setProperty("--activeColor", options.activeColor); 76 | container.style.setProperty("--hoverColor", options.clickUpdate ? options.activeColor : options.normalColor); 77 | container.style.setProperty("--inactiveZoom", options.inactiveZoom); 78 | container.style.setProperty("--lineSpace", options.lineSpace + "em"); 79 | // 监听事件 80 | const refreshLrcProgress = forceScroll => { 81 | const currentTime = Math.round(audio.currentTime * 1000); 82 | let lrcEles = Array.from(container.getElementsByTagName("div")); 83 | for (let index in lrcEles) { 84 | let div = lrcEles[index]; 85 | if (div.dataset.stamp <= currentTime && (!div.nextElementSibling || div.nextElementSibling.dataset.stamp > currentTime)) { 86 | // 执行回调 87 | if (!div.classList.contains("active") && options.callback) options.callback(div.innerHTML); 88 | if (!div.classList.contains("active") || forceScroll) { 89 | // 取消用户滚动模式 90 | if (forceScroll) { 91 | container.classList.remove("scrolling"); 92 | clearTimeout(this.scrollTimeoutId); 93 | } 94 | // 设置为当前歌词并滚动 95 | div.classList.add("active"); 96 | if (!container.classList.contains("scrolling")) div.scrollIntoView({ behavior: "smooth", block: "center" }); 97 | // 渲染歌词模糊效果 98 | if (options.blurStep && options.blurMax) { 99 | div.style.filter = "none"; 100 | const prevSiblings = []; 101 | let prev = div.previousElementSibling; 102 | while (prev) { 103 | prevSiblings.push(prev); 104 | prev = prev.previousElementSibling; 105 | } 106 | let next = div.nextElementSibling; 107 | const nextSiblings = []; 108 | while (next) { 109 | nextSiblings.push(next); 110 | next = next.nextElementSibling; 111 | } 112 | for (let index = 0; index <= Math.max(prevSiblings.length, nextSiblings.length); index++) { 113 | const blurPixel = Math.min(options.blurMin + options.blurStep * index, options.blurMax); 114 | if (prevSiblings[index]) prevSiblings[index].style.filter = `blur(${blurPixel}px)`; 115 | if (nextSiblings[index]) nextSiblings[index].style.filter = `blur(${blurPixel}px)`; 116 | } 117 | } 118 | } 119 | } else div.classList.remove("active"); 120 | } 121 | }; 122 | audio.addEventListener("timeupdate", () => { refreshLrcProgress(); }); 123 | window.addEventListener("resize", () => { refreshLrcProgress(true); }); 124 | if (options.clickUpdate) { 125 | Array.from(container.getElementsByTagName("div")).forEach(div => { 126 | div.onclick = () => { audio.currentTime = div.dataset.stamp / 1000; refreshLrcProgress(true); }; 127 | }); 128 | } 129 | refreshLrcProgress(true); 130 | setTimeout(() => {container.querySelector("div.active").scrollIntoView({block: "center", behavior: "smooth"});}, 50); 131 | // 处理用户滚动 132 | const handleUserScroll = () => { 133 | if (document.body.classList.contains("volume")) return; 134 | clearTimeout(this.scrollTimeoutId); 135 | this.scrollTimeoutId = setTimeout(() => { 136 | container.classList.remove("scrolling"); 137 | refreshLrcProgress(true); 138 | }, options.scrollTimeout); 139 | container.classList.add("scrolling"); 140 | } 141 | container.onwheel = handleUserScroll; 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/frontend/assets/components/SimProgress.css: -------------------------------------------------------------------------------- 1 | 2 | .SimProgress{--SimProgressBackground:rgba(0,0,0,.1);--SimProgressTheme:#1E9FFF;} 3 | .SimProgress{display:block;position:relative;height:7px;width:100%;--SimProgressWidth:0%;} 4 | .SimProgress.vertical{height:100%;width:7px;} 5 | .SimProgress:not(.readOnly)::after{background:var(--SimProgressTheme);border-radius:50%;height:11px;width:11px;position:absolute;top:0;bottom:0;margin:auto;left:calc(var(--SimProgressWidth) - 5px);content:"";transition:left .2s;box-shadow:0 0 5px 0 rgba(0,0,0,.2);} 6 | .SimProgress.SimProgress.vertical::after{position:absolute;bottom:calc(var(--SimProgressWidth) - 5px);left:0;margin-left:-2px;top:unset;transition:bottom .2s;} 7 | .SimProgress.dragging::after{transition:none!important;} 8 | .SimProgress>div{position:absolute;height:100%;width:100%;background:var(--SimProgressBackground);border-radius:3.5px;overflow:hidden;} 9 | .SimProgress>div>div{position:absolute;background:var(--SimProgressTheme);height:100%;left:0;bottom:0;transition:width .2s;width:var(--SimProgressWidth);} 10 | .SimProgress.vertical>div>div{height:var(--SimProgressWidth);left:0;right:0;bottom:0;transition:height .2s;width:100%;} 11 | .SimProgress.dragging>div>div{transition:none;} 12 | -------------------------------------------------------------------------------- /src/frontend/assets/components/SimProgress.js: -------------------------------------------------------------------------------- 1 | 2 | class SimProgress { 3 | constructor(element, options = {}) { 4 | // 初始化 5 | this.progressElement = element; 6 | element.innerHTML = "
"; 7 | // 事件监听 8 | if (options.readOnly) element.classList.add("readOnly"); 9 | if (options.vertical) element.classList.add("vertical"); 10 | if (!element.classList.contains("SimProgress") && !options.readOnly) { 11 | // 拖动处理 12 | const handleDrag = e => { 13 | e.preventDefault(); 14 | if (!element.classList.contains("dragging")) return; 15 | let progress; 16 | if (options.vertical) { 17 | const clickY = (e.pageY || e.changedTouches[0].pageY || e.touches[0].pageY) - element.getBoundingClientRect().top; 18 | progress = 1 - Math.min(Math.max(clickY / element.clientHeight, 0), 1); 19 | } else { 20 | const clickX = (e.pageX || e.changedTouches[0].pageX || e.touches[0].pageX) - element.getBoundingClientRect().left; 21 | progress = Math.min(Math.max(clickX / element.clientWidth, 0), 1); 22 | } 23 | element.style.setProperty("--SimProgressWidth", progress * 100 + "%"); 24 | this.value = this.min + (this.max - this.min) * progress; 25 | if (this.ondrag) this.ondrag(this.value); 26 | } 27 | // 鼠标事件 28 | element.addEventListener("mousedown", () => { 29 | document.addEventListener("mousemove", handleDrag); 30 | element.classList.add("dragging"); 31 | }, {passive: true}); 32 | element.addEventListener("mouseup", handleDrag); 33 | document.addEventListener("mouseup", () => { 34 | document.removeEventListener("mousemove", handleDrag); 35 | if (this.onchange && element.classList.contains("dragging")) this.onchange(this.value); 36 | element.classList.remove("dragging"); 37 | }); 38 | // 触摸事件 39 | element.addEventListener("touchstart", () => { 40 | document.addEventListener("touchmove", handleDrag, {passive: false}); 41 | element.classList.add("dragging"); 42 | }, {passive: true}); 43 | element.addEventListener("touchend", handleDrag); 44 | document.addEventListener("touchend", () => { 45 | document.removeEventListener("touchmove", handleDrag, {passive: false}); 46 | if (this.onchange && element.classList.contains("dragging")) this.onchange(this.value); 47 | element.classList.remove("dragging"); 48 | }); 49 | } 50 | // 读取信息 51 | element.classList.add("SimProgress"); 52 | this.min = Number(element.getAttribute("min")) ?? 0; 53 | this.max = Number(element.getAttribute("max")) ?? this.min + 100; 54 | this.setValue(Number(element.getAttribute("value")) ?? this.min); 55 | } 56 | setValue(value = this.value) { 57 | if (value < this.min) value = this.min; 58 | if (value > this.max) value = this.max; 59 | if (this.progressElement.classList.contains("dragging")) return; 60 | this.value = value; 61 | this.progressElement.style.setProperty("--SimProgressWidth", (this.value - this.min) / (this.max - this.min) * 100 + "%"); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/frontend/assets/components/dialog.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:"icon";src:url("../icon.woff2");} 2 | @font-face{font-family:"windowsicon";src:url("../windowsicon.ttf");} 3 | @font-face{font-family:"font";src:url("../font.ttf");} 4 | html{font-size:16px;user-select:none;font-family:"font","微软雅黑";background:#F9F9FB;} 5 | body{margin:0;} 6 | i{font-family:"icon";font-style:normal;} 7 | img{pointer-events:none;} 8 | *{box-sizing:border-box;outline:none;} 9 | *[hidden]{display:none!important;} 10 | ::-webkit-scrollbar{display:none;} 11 | button{background:#1E9FFF;color:white;border:0;border-radius:5px;padding:0 20px;font-size:16px;transition:filter .2s;font-family:inherit;height:30px;margin-left:5px;} 12 | button.sub{background:#DAEFFF;color:#1E9FFF;} 13 | button.square{width:30px;padding:0;} 14 | button:hover{filter:brightness(.95);} 15 | button:active{filter:brightness(.9);} 16 | button:disabled{filter:grayscale(1)!important;} 17 | header{position:fixed;top:0;height:35px;display:flex;justify-content:right;width:100%;z-index:100;-webkit-app-region:drag;} 18 | 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;} 19 | header i:hover{background:rgba(0,0,0,.05);} 20 | header i:active{background:rgba(0,0,0,.1);opacity:.8;} 21 | header i#closeBtn:hover{background:#E81123;color:white;} 22 | header i#closeBtn:active{background:#E81123;color:rgba(255,255,255,.8);} 23 | form{position:fixed;inset:30px 40px;} 24 | form h1{margin-top:0;margin-bottom:10px;font-size:1.8em;} 25 | form .buttons{position:absolute;bottom:0;right:0;} 26 | input{background:white;padding:8px 10px;border-radius:5px;border:0;font-size:1rem;font-family:inherit;width:100%;margin:5px 0;} -------------------------------------------------------------------------------- /src/frontend/assets/components/dialog.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 |
15 | 16 |
17 |

提示

18 |
19 | 20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 | 28 | 62 | 63 | -------------------------------------------------------------------------------- /src/frontend/assets/components/dialog.js: -------------------------------------------------------------------------------- 1 | const DialogData = {}; 2 | let DialogShowing = false; 3 | 4 | function alert(txt, callback) { 5 | if (DialogShowing) return; 6 | DialogShowing = true; 7 | const dialogId = new Date().getTime() + Math.random(); 8 | DialogData[dialogId] = callback; 9 | ipcRenderer.invoke("dialog", "alert", txt, document.documentElement.dataset.windowId, dialogId); 10 | } 11 | function prompt(txt, callback) { 12 | if (DialogShowing) return; 13 | DialogShowing = true; 14 | const dialogId = new Date().getTime() + Math.random(); 15 | DialogData[dialogId] = callback; 16 | ipcRenderer.invoke("dialog", "prompt", txt, document.documentElement.dataset.windowId, dialogId); 17 | } 18 | function confirm(txt, callback) { 19 | if (DialogShowing) return; 20 | DialogShowing = true; 21 | const dialogId = new Date().getTime() + Math.random(); 22 | DialogData[dialogId] = callback; 23 | ipcRenderer.invoke("dialog", "confirm", txt, document.documentElement.dataset.windowId, dialogId); 24 | } 25 | function webview(url, options = {}, callback) { 26 | if (DialogShowing) return; 27 | DialogShowing = true; 28 | const dialogId = "wv" + new Date().getTime() + Math.random(); 29 | DialogData[dialogId] = callback; 30 | ipcRenderer.invoke("webview", url, document.documentElement.dataset.windowId, dialogId, options.width, options.height, !!callback); 31 | } 32 | function modalWindow(url, height) { 33 | ipcRenderer.invoke("modal", url, height, document.documentElement.dataset.windowId); 34 | } 35 | 36 | ipcRenderer.on("dialogSubmit", (_event, dialogId, txt) => { 37 | DialogShowing = false; 38 | if (DialogData[dialogId]) DialogData[dialogId](dialogId.startsWith("wv") ? JSON.parse(txt) : txt); 39 | }); 40 | ipcRenderer.on("dialogCancel", () => { 41 | DialogShowing = false; 42 | }); 43 | 44 | -------------------------------------------------------------------------------- /src/frontend/assets/components/marked.min.js: -------------------------------------------------------------------------------- 1 | /** 2 | * marked v14.1.0 - a markdown parser 3 | * Copyright (c) 2011-2024, Christopher Jeffrey. (MIT Licensed) 4 | * https://github.com/markedjs/marked 5 | */ 6 | !function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).marked={})}(this,(function(e){"use strict";function t(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}function n(t){e.defaults=t}e.defaults={async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null};const s=/[&<>"']/,r=new RegExp(s.source,"g"),i=/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,l=new RegExp(i.source,"g"),o={"&":"&","<":"<",">":">",'"':""","'":"'"},a=e=>o[e];function c(e,t){if(t){if(s.test(e))return e.replace(r,a)}else if(i.test(e))return e.replace(l,a);return e}const h=/(^|[^\[])\^/g;function p(e,t){let n="string"==typeof e?e:e.source;t=t||"";const s={replace:(e,t)=>{let r="string"==typeof t?t:t.source;return r=r.replace(h,"$1"),n=n.replace(e,r),s},getRegex:()=>new RegExp(n,t)};return s}function u(e){try{e=encodeURI(e).replace(/%25/g,"%")}catch{return null}return e}const k={exec:()=>null};function g(e,t){const n=e.replace(/\|/g,((e,t,n)=>{let s=!1,r=t;for(;--r>=0&&"\\"===n[r];)s=!s;return s?"|":" |"})).split(/ \|/);let s=0;if(n[0].trim()||n.shift(),n.length>0&&!n[n.length-1].trim()&&n.pop(),t)if(n.length>t)n.splice(t);else for(;n.length0)return{type:"space",raw:t[0]}}code(e){const t=this.rules.block.code.exec(e);if(t){const e=t[0].replace(/^ {1,4}/gm,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?e:f(e,"\n")}}}fences(e){const t=this.rules.block.fences.exec(e);if(t){const e=t[0],n=function(e,t){const n=e.match(/^(\s+)(?:```)/);if(null===n)return t;const s=n[1];return t.split("\n").map((e=>{const t=e.match(/^\s+/);if(null===t)return e;const[n]=t;return n.length>=s.length?e.slice(s.length):e})).join("\n")}(e,t[3]||"");return{type:"code",raw:e,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:n}}}heading(e){const t=this.rules.block.heading.exec(e);if(t){let e=t[2].trim();if(/#$/.test(e)){const t=f(e,"#");this.options.pedantic?e=t.trim():t&&!/ $/.test(t)||(e=t.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:e,tokens:this.lexer.inline(e)}}}hr(e){const t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:f(t[0],"\n")}}blockquote(e){const t=this.rules.block.blockquote.exec(e);if(t){let e=f(t[0],"\n").split("\n"),n="",s="";const r=[];for(;e.length>0;){let t=!1;const i=[];let l;for(l=0;l/.test(e[l]))i.push(e[l]),t=!0;else{if(t)break;i.push(e[l])}e=e.slice(l);const o=i.join("\n"),a=o.replace(/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,"\n $1").replace(/^ {0,3}>[ \t]?/gm,"");n=n?`${n}\n${o}`:o,s=s?`${s}\n${a}`:a;const c=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(a,r,!0),this.lexer.state.top=c,0===e.length)break;const h=r[r.length-1];if("code"===h?.type)break;if("blockquote"===h?.type){const t=h,i=t.raw+"\n"+e.join("\n"),l=this.blockquote(i);r[r.length-1]=l,n=n.substring(0,n.length-t.raw.length)+l.raw,s=s.substring(0,s.length-t.text.length)+l.text;break}if("list"!==h?.type);else{const t=h,i=t.raw+"\n"+e.join("\n"),l=this.list(i);r[r.length-1]=l,n=n.substring(0,n.length-h.raw.length)+l.raw,s=s.substring(0,s.length-t.raw.length)+l.raw,e=i.substring(r[r.length-1].raw.length).split("\n")}}return{type:"blockquote",raw:n,tokens:r,text:s}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim();const s=n.length>1,r={type:"list",raw:"",ordered:s,start:s?+n.slice(0,-1):"",loose:!1,items:[]};n=s?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=s?n:"[*+-]");const i=new RegExp(`^( {0,3}${n})((?:[\t ][^\\n]*)?(?:\\n|$))`);let l=!1;for(;e;){let n=!1,s="",o="";if(!(t=i.exec(e)))break;if(this.rules.block.hr.test(e))break;s=t[0],e=e.substring(s.length);let a=t[2].split("\n",1)[0].replace(/^\t+/,(e=>" ".repeat(3*e.length))),c=e.split("\n",1)[0],h=!a.trim(),p=0;if(this.options.pedantic?(p=2,o=a.trimStart()):h?p=t[1].length+1:(p=t[2].search(/[^ ]/),p=p>4?1:p,o=a.slice(p),p+=t[1].length),h&&/^ *$/.test(c)&&(s+=c+"\n",e=e.substring(c.length+1),n=!0),!n){const t=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ \t][^\\n]*)?(?:\\n|$))`),n=new RegExp(`^ {0,${Math.min(3,p-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),r=new RegExp(`^ {0,${Math.min(3,p-1)}}(?:\`\`\`|~~~)`),i=new RegExp(`^ {0,${Math.min(3,p-1)}}#`);for(;e;){const l=e.split("\n",1)[0];if(c=l,this.options.pedantic&&(c=c.replace(/^ {1,4}(?=( {4})*[^ ])/g," ")),r.test(c))break;if(i.test(c))break;if(t.test(c))break;if(n.test(e))break;if(c.search(/[^ ]/)>=p||!c.trim())o+="\n"+c.slice(p);else{if(h)break;if(a.search(/[^ ]/)>=4)break;if(r.test(a))break;if(i.test(a))break;if(n.test(a))break;o+="\n"+c}h||c.trim()||(h=!0),s+=l+"\n",e=e.substring(l.length+1),a=c.slice(p)}}r.loose||(l?r.loose=!0:/\n *\n *$/.test(s)&&(l=!0));let u,k=null;this.options.gfm&&(k=/^\[[ xX]\] /.exec(o),k&&(u="[ ] "!==k[0],o=o.replace(/^\[[ xX]\] +/,""))),r.items.push({type:"list_item",raw:s,task:!!k,checked:u,loose:!1,text:o,tokens:[]}),r.raw+=s}r.items[r.items.length-1].raw=r.items[r.items.length-1].raw.trimEnd(),r.items[r.items.length-1].text=r.items[r.items.length-1].text.trimEnd(),r.raw=r.raw.trimEnd();for(let e=0;e"space"===e.type)),n=t.length>0&&t.some((e=>/\n.*\n/.test(e.raw)));r.loose=n}if(r.loose)for(let e=0;e$/,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",s=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:e,raw:t[0],href:n,title:s}}}table(e){const t=this.rules.block.table.exec(e);if(!t)return;if(!/[:|]/.test(t[2]))return;const n=g(t[1]),s=t[2].replace(/^\||\| *$/g,"").split("|"),r=t[3]&&t[3].trim()?t[3].replace(/\n[ \t]*$/,"").split("\n"):[],i={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===s.length){for(const e of s)/^ *-+: *$/.test(e)?i.align.push("right"):/^ *:-+: *$/.test(e)?i.align.push("center"):/^ *:-+ *$/.test(e)?i.align.push("left"):i.align.push(null);for(let e=0;e({text:e,tokens:this.lexer.inline(e),header:!1,align:i.align[t]}))));return i}}lheading(e){const t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:"="===t[2].charAt(0)?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){const t=this.rules.block.paragraph.exec(e);if(t){const e="\n"===t[1].charAt(t[1].length-1)?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:e,tokens:this.lexer.inline(e)}}}text(e){const t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){const t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:c(t[1])}}tag(e){const t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&/^/i.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&/^<(pre|code|kbd|script)(\s|>)/i.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&/^<\/(pre|code|kbd|script)(\s|>)/i.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){const t=this.rules.inline.link.exec(e);if(t){const e=t[2].trim();if(!this.options.pedantic&&/^$/.test(e))return;const t=f(e.slice(0,-1),"\\");if((e.length-t.length)%2==0)return}else{const e=function(e,t){if(-1===e.indexOf(t[1]))return-1;let n=0;for(let s=0;s-1){const n=(0===t[0].indexOf("!")?5:4)+t[1].length+e;t[2]=t[2].substring(0,e),t[0]=t[0].substring(0,n).trim(),t[3]=""}}let n=t[2],s="";if(this.options.pedantic){const e=/^([^'"]*[^\s])\s+(['"])(.*)\2/.exec(n);e&&(n=e[1],s=e[3])}else s=t[3]?t[3].slice(1,-1):"";return n=n.trim(),/^$/.test(e)?n.slice(1):n.slice(1,-1)),d(t,{href:n?n.replace(this.rules.inline.anyPunctuation,"$1"):n,title:s?s.replace(this.rules.inline.anyPunctuation,"$1"):s},t[0],this.lexer)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){const e=t[(n[2]||n[1]).replace(/\s+/g," ").toLowerCase()];if(!e){const e=n[0].charAt(0);return{type:"text",raw:e,text:e}}return d(n,e,n[0],this.lexer)}}emStrong(e,t,n=""){let s=this.rules.inline.emStrongLDelim.exec(e);if(!s)return;if(s[3]&&n.match(/[\p{L}\p{N}]/u))return;if(!(s[1]||s[2]||"")||!n||this.rules.inline.punctuation.exec(n)){const n=[...s[0]].length-1;let r,i,l=n,o=0;const a="*"===s[0][0]?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(a.lastIndex=0,t=t.slice(-1*e.length+n);null!=(s=a.exec(t));){if(r=s[1]||s[2]||s[3]||s[4]||s[5]||s[6],!r)continue;if(i=[...r].length,s[3]||s[4]){l+=i;continue}if((s[5]||s[6])&&n%3&&!((n+i)%3)){o+=i;continue}if(l-=i,l>0)continue;i=Math.min(i,i+l+o);const t=[...s[0]][0].length,a=e.slice(0,n+s.index+t+i);if(Math.min(n,i)%2){const e=a.slice(1,-1);return{type:"em",raw:a,text:e,tokens:this.lexer.inlineTokens(e)}}const c=a.slice(2,-2);return{type:"strong",raw:a,text:c,tokens:this.lexer.inlineTokens(c)}}}}codespan(e){const t=this.rules.inline.code.exec(e);if(t){let e=t[2].replace(/\n/g," ");const n=/[^ ]/.test(e),s=/^ /.test(e)&&/ $/.test(e);return n&&s&&(e=e.substring(1,e.length-1)),e=c(e,!0),{type:"codespan",raw:t[0],text:e}}}br(e){const t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){const t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){const t=this.rules.inline.autolink.exec(e);if(t){let e,n;return"@"===t[2]?(e=c(t[1]),n="mailto:"+e):(e=c(t[1]),n=e),{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let e,n;if("@"===t[2])e=c(t[0]),n="mailto:"+e;else{let s;do{s=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??""}while(s!==t[0]);e=c(t[0]),n="www."===t[1]?"http://"+t[0]:t[0]}return{type:"link",raw:t[0],text:e,href:n,tokens:[{type:"text",raw:e,text:e}]}}}inlineText(e){const t=this.rules.inline.text.exec(e);if(t){let e;return e=this.lexer.state.inRawBlock?t[0]:c(t[0]),{type:"text",raw:t[0],text:e}}}}const b=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,w=/(?:[*+-]|\d{1,9}[.)])/,m=p(/^(?!bull |blockCode|fences|blockquote|heading|html)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html))+?)\n {0,3}(=+|-+) *(?:\n+|$)/).replace(/bull/g,w).replace(/blockCode/g,/ {4}/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).getRegex(),y=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,$=/(?!\s*\])(?:\\.|[^\[\]\\])+/,z=p(/^ {0,3}\[(label)\]: *(?:\n *)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n *)?| *\n *)(title))? *(?:\n+|$)/).replace("label",$).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),T=p(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,w).getRegex(),R="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",_=/|$))/,A=p("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|\\n*|$)|\\n*|$)|)[\\s\\S]*?(?:(?:\\n *)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$)|(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n *)+\\n|$))","i").replace("comment",_).replace("tag",R).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),S=p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),I={blockquote:p(/^( {0,3}> ?(paragraph|[^\n]*)(?:\n|$))+/).replace("paragraph",S).getRegex(),code:/^( {4}[^\n]+(?:\n(?: *(?:\n|$))*)?)+/,def:z,fences:/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,heading:/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,hr:b,html:A,lheading:m,list:T,newline:/^(?: *(?:\n|$))+/,paragraph:S,table:k,text:/^[^\n]+/},E=p("^ *([^\\n ].*)\\n {0,3}((?:\\| *)?:?-+:? *(?:\\| *:?-+:? *)*(?:\\| *)?)(?:\\n((?:(?! *\\n|hr|heading|blockquote|code|fences|list|html).*(?:\\n|$))*)\\n*|$)").replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("blockquote"," {0,3}>").replace("code"," {4}[^\\n]").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex(),q={...I,table:E,paragraph:p(y).replace("hr",b).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("table",E).replace("blockquote"," {0,3}>").replace("fences"," {0,3}(?:`{3,}(?=[^`\\n]*\\n)|~{3,})[^\\n]*\\n").replace("list"," {0,3}(?:[*+-]|1[.)]) ").replace("html",")|<(?:script|pre|style|textarea|!--)").replace("tag",R).getRegex()},Z={...I,html:p("^ *(?:comment *(?:\\n|\\s*$)|<(tag)[\\s\\S]+? *(?:\\n{2,}|\\s*$)|\\s]*)*?/?> *(?:\\n{2,}|\\s*$))").replace("comment",_).replace(/tag/g,"(?!(?:a|em|strong|small|s|cite|q|dfn|abbr|data|time|code|var|samp|kbd|sub|sup|i|b|u|mark|ruby|rt|rp|bdi|bdo|span|br|wbr|ins|del|img)\\b)\\w+(?!:|[^\\w\\s@]*@)\\b").getRegex(),def:/^ *\[([^\]]+)\]: *]+)>?(?: +(["(][^\n]+[")]))? *(?:\n+|$)/,heading:/^(#{1,6})(.*)(?:\n+|$)/,fences:k,lheading:/^(.+?)\n {0,3}(=+|-+) *(?:\n+|$)/,paragraph:p(y).replace("hr",b).replace("heading"," *#{1,6} *[^\n]").replace("lheading",m).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},P=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,L=/^( {2,}|\\)\n(?!\s*$)/,v="\\p{P}\\p{S}",Q=p(/^((?![*_])[\spunctuation])/,"u").replace(/punctuation/g,v).getRegex(),B=p(/^(?:\*+(?:((?!\*)[punct])|[^\s*]))|^_+(?:((?!_)[punct])|([^\s_]))/,"u").replace(/punct/g,v).getRegex(),M=p("^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)[punct](\\*+)(?=[\\s]|$)|[^punct\\s](\\*+)(?!\\*)(?=[punct\\s]|$)|(?!\\*)[punct\\s](\\*+)(?=[^punct\\s])|[\\s](\\*+)(?!\\*)(?=[punct])|(?!\\*)[punct](\\*+)(?!\\*)(?=[punct])|[^punct\\s](\\*+)(?=[^punct\\s])","gu").replace(/punct/g,v).getRegex(),O=p("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)[punct](_+)(?=[\\s]|$)|[^punct\\s](_+)(?!_)(?=[punct\\s]|$)|(?!_)[punct\\s](_+)(?=[^punct\\s])|[\\s](_+)(?!_)(?=[punct])|(?!_)[punct](_+)(?!_)(?=[punct])","gu").replace(/punct/g,v).getRegex(),j=p(/\\([punct])/,"gu").replace(/punct/g,v).getRegex(),D=p(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),C=p(_).replace("(?:--\x3e|$)","--\x3e").getRegex(),H=p("^comment|^|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^|^").replace("comment",C).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),U=/(?:\[(?:\\.|[^\[\]\\])*\]|\\.|`[^`]*`|[^\[\]\\`])*?/,X=p(/^!?\[(label)\]\(\s*(href)(?:\s+(title))?\s*\)/).replace("label",U).replace("href",/<(?:\\.|[^\n<>\\])+>|[^\s\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),F=p(/^!?\[(label)\]\[(ref)\]/).replace("label",U).replace("ref",$).getRegex(),N=p(/^!?\[(ref)\](?:\[\])?/).replace("ref",$).getRegex(),G={_backpedal:k,anyPunctuation:j,autolink:D,blockSkip:/\[[^[\]]*?\]\([^\(\)]*?\)|`[^`]*?`|<[^<>]*?>/g,br:L,code:/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,del:k,emStrongLDelim:B,emStrongRDelimAst:M,emStrongRDelimUnd:O,escape:P,link:X,nolink:N,punctuation:Q,reflink:F,reflinkSearch:p("reflink|nolink(?!\\()","g").replace("reflink",F).replace("nolink",N).getRegex(),tag:H,text:/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\t+" ".repeat(n.length)));e;)if(!(this.options.extensions&&this.options.extensions.block&&this.options.extensions.block.some((n=>!!(s=n.call({lexer:this},e,t))&&(e=e.substring(s.raw.length),t.push(s),!0)))))if(s=this.tokenizer.space(e))e=e.substring(s.raw.length),1===s.raw.length&&t.length>0?t[t.length-1].raw+="\n":t.push(s);else if(s=this.tokenizer.code(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?t.push(s):(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.fences(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.heading(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.hr(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.blockquote(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.list(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.html(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.def(e))e=e.substring(s.raw.length),r=t[t.length-1],!r||"paragraph"!==r.type&&"text"!==r.type?this.tokens.links[s.tag]||(this.tokens.links[s.tag]={href:s.href,title:s.title}):(r.raw+="\n"+s.raw,r.text+="\n"+s.raw,this.inlineQueue[this.inlineQueue.length-1].src=r.text);else if(s=this.tokenizer.table(e))e=e.substring(s.raw.length),t.push(s);else if(s=this.tokenizer.lheading(e))e=e.substring(s.raw.length),t.push(s);else{if(i=e,this.options.extensions&&this.options.extensions.startBlock){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startBlock.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(i=e.substring(0,t+1))}if(this.state.top&&(s=this.tokenizer.paragraph(i)))r=t[t.length-1],n&&"paragraph"===r?.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s),n=i.length!==e.length,e=e.substring(s.raw.length);else if(s=this.tokenizer.text(e))e=e.substring(s.raw.length),r=t[t.length-1],r&&"text"===r.type?(r.raw+="\n"+s.raw,r.text+="\n"+s.text,this.inlineQueue.pop(),this.inlineQueue[this.inlineQueue.length-1].src=r.text):t.push(s);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n,s,r,i,l,o,a=e;if(this.tokens.links){const e=Object.keys(this.tokens.links);if(e.length>0)for(;null!=(i=this.tokenizer.rules.inline.reflinkSearch.exec(a));)e.includes(i[0].slice(i[0].lastIndexOf("[")+1,-1))&&(a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;null!=(i=this.tokenizer.rules.inline.blockSkip.exec(a));)a=a.slice(0,i.index)+"["+"a".repeat(i[0].length-2)+"]"+a.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);for(;null!=(i=this.tokenizer.rules.inline.anyPunctuation.exec(a));)a=a.slice(0,i.index)+"++"+a.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;e;)if(l||(o=""),l=!1,!(this.options.extensions&&this.options.extensions.inline&&this.options.extensions.inline.some((s=>!!(n=s.call({lexer:this},e,t))&&(e=e.substring(n.raw.length),t.push(n),!0)))))if(n=this.tokenizer.escape(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.tag(e))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.link(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.reflink(e,this.tokens.links))e=e.substring(n.raw.length),s=t[t.length-1],s&&"text"===n.type&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(n=this.tokenizer.emStrong(e,a,o))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.codespan(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.br(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.del(e))e=e.substring(n.raw.length),t.push(n);else if(n=this.tokenizer.autolink(e))e=e.substring(n.raw.length),t.push(n);else if(this.state.inLink||!(n=this.tokenizer.url(e))){if(r=e,this.options.extensions&&this.options.extensions.startInline){let t=1/0;const n=e.slice(1);let s;this.options.extensions.startInline.forEach((e=>{s=e.call({lexer:this},n),"number"==typeof s&&s>=0&&(t=Math.min(t,s))})),t<1/0&&t>=0&&(r=e.substring(0,t+1))}if(n=this.tokenizer.inlineText(r))e=e.substring(n.raw.length),"_"!==n.raw.slice(-1)&&(o=n.raw.slice(-1)),l=!0,s=t[t.length-1],s&&"text"===s.type?(s.raw+=n.raw,s.text+=n.text):t.push(n);else if(e){const t="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(t);break}throw new Error(t)}}else e=e.substring(n.raw.length),t.push(n);return t}}class te{options;parser;constructor(t){this.options=t||e.defaults}space(e){return""}code({text:e,lang:t,escaped:n}){const s=(t||"").match(/^\S*/)?.[0],r=e.replace(/\n$/,"")+"\n";return s?'
'+(n?r:c(r,!0))+"
\n":"
"+(n?r:c(r,!0))+"
\n"}blockquote({tokens:e}){return`
\n${this.parser.parse(e)}
\n`}html({text:e}){return e}heading({tokens:e,depth:t}){return`${this.parser.parseInline(e)}\n`}hr(e){return"
\n"}list(e){const t=e.ordered,n=e.start;let s="";for(let t=0;t\n"+s+"\n"}listitem(e){let t="";if(e.task){const n=this.checkbox({checked:!!e.checked});e.loose?e.tokens.length>0&&"paragraph"===e.tokens[0].type?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&"text"===e.tokens[0].tokens[0].type&&(e.tokens[0].tokens[0].text=n+" "+e.tokens[0].tokens[0].text)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" "}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`
  • ${t}
  • \n`}checkbox({checked:e}){return"'}paragraph({tokens:e}){return`

    ${this.parser.parseInline(e)}

    \n`}table(e){let t="",n="";for(let t=0;t${s}`),"\n\n"+t+"\n"+s+"
    \n"}tablerow({text:e}){return`\n${e}\n`}tablecell(e){const t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`\n`}strong({tokens:e}){return`${this.parser.parseInline(e)}`}em({tokens:e}){return`${this.parser.parseInline(e)}`}codespan({text:e}){return`${e}`}br(e){return"
    "}del({tokens:e}){return`${this.parser.parseInline(e)}`}link({href:e,title:t,tokens:n}){const s=this.parser.parseInline(n),r=u(e);if(null===r)return s;let i='
    ",i}image({href:e,title:t,text:n}){const s=u(e);if(null===s)return n;let r=`${n}{const r=e[s].flat(1/0);n=n.concat(this.walkTokens(r,t))})):e.tokens&&(n=n.concat(this.walkTokens(e.tokens,t)))}}return n}use(...e){const t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach((e=>{const n={...e};if(n.async=this.defaults.async||n.async||!1,e.extensions&&(e.extensions.forEach((e=>{if(!e.name)throw new Error("extension name required");if("renderer"in e){const n=t.renderers[e.name];t.renderers[e.name]=n?function(...t){let s=e.renderer.apply(this,t);return!1===s&&(s=n.apply(this,t)),s}:e.renderer}if("tokenizer"in e){if(!e.level||"block"!==e.level&&"inline"!==e.level)throw new Error("extension level must be 'block' or 'inline'");const n=t[e.level];n?n.unshift(e.tokenizer):t[e.level]=[e.tokenizer],e.start&&("block"===e.level?t.startBlock?t.startBlock.push(e.start):t.startBlock=[e.start]:"inline"===e.level&&(t.startInline?t.startInline.push(e.start):t.startInline=[e.start]))}"childTokens"in e&&e.childTokens&&(t.childTokens[e.name]=e.childTokens)})),n.extensions=t),e.renderer){const t=this.defaults.renderer||new te(this.defaults);for(const n in e.renderer){if(!(n in t))throw new Error(`renderer '${n}' does not exist`);if(["options","parser"].includes(n))continue;const s=n,r=e.renderer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n||""}}n.renderer=t}if(e.tokenizer){const t=this.defaults.tokenizer||new x(this.defaults);for(const n in e.tokenizer){if(!(n in t))throw new Error(`tokenizer '${n}' does not exist`);if(["options","rules","lexer"].includes(n))continue;const s=n,r=e.tokenizer[s],i=t[s];t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.tokenizer=t}if(e.hooks){const t=this.defaults.hooks||new re;for(const n in e.hooks){if(!(n in t))throw new Error(`hook '${n}' does not exist`);if(["options","block"].includes(n))continue;const s=n,r=e.hooks[s],i=t[s];re.passThroughHooks.has(n)?t[s]=e=>{if(this.defaults.async)return Promise.resolve(r.call(t,e)).then((e=>i.call(t,e)));const n=r.call(t,e);return i.call(t,n)}:t[s]=(...e)=>{let n=r.apply(t,e);return!1===n&&(n=i.apply(t,e)),n}}n.hooks=t}if(e.walkTokens){const t=this.defaults.walkTokens,s=e.walkTokens;n.walkTokens=function(e){let n=[];return n.push(s.call(this,e)),t&&(n=n.concat(t.call(this,e))),n}}this.defaults={...this.defaults,...n}})),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return ee.lex(e,t??this.defaults)}parser(e,t){return se.parse(e,t??this.defaults)}parseMarkdown(e){return(t,n)=>{const s={...n},r={...this.defaults,...s},i=this.onError(!!r.silent,!!r.async);if(!0===this.defaults.async&&!1===s.async)return i(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(null==t)return i(new Error("marked(): input parameter is undefined or null"));if("string"!=typeof t)return i(new Error("marked(): input parameter is of type "+Object.prototype.toString.call(t)+", string expected"));r.hooks&&(r.hooks.options=r,r.hooks.block=e);const l=r.hooks?r.hooks.provideLexer():e?ee.lex:ee.lexInline,o=r.hooks?r.hooks.provideParser():e?se.parse:se.parseInline;if(r.async)return Promise.resolve(r.hooks?r.hooks.preprocess(t):t).then((e=>l(e,r))).then((e=>r.hooks?r.hooks.processAllTokens(e):e)).then((e=>r.walkTokens?Promise.all(this.walkTokens(e,r.walkTokens)).then((()=>e)):e)).then((e=>o(e,r))).then((e=>r.hooks?r.hooks.postprocess(e):e)).catch(i);try{r.hooks&&(t=r.hooks.preprocess(t));let e=l(t,r);r.hooks&&(e=r.hooks.processAllTokens(e)),r.walkTokens&&this.walkTokens(e,r.walkTokens);let n=o(e,r);return r.hooks&&(n=r.hooks.postprocess(n)),n}catch(e){return i(e)}}}onError(e,t){return n=>{if(n.message+="\nPlease report this to https://github.com/markedjs/marked.",e){const e="

    An error occurred:

    "+c(n.message+"",!0)+"
    ";return t?Promise.resolve(e):e}if(t)return Promise.reject(n);throw n}}}const le=new ie;function oe(e,t){return le.parse(e,t)}oe.options=oe.setOptions=function(e){return le.setOptions(e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.getDefaults=t,oe.defaults=e.defaults,oe.use=function(...e){return le.use(...e),oe.defaults=le.defaults,n(oe.defaults),oe},oe.walkTokens=function(e,t){return le.walkTokens(e,t)},oe.parseInline=le.parseInline,oe.Parser=se,oe.parser=se.parse,oe.Renderer=te,oe.TextRenderer=ne,oe.Lexer=ee,oe.lexer=ee.lex,oe.Tokenizer=x,oe.Hooks=re,oe.parse=oe;const ae=oe.options,ce=oe.setOptions,he=oe.use,pe=oe.walkTokens,ue=oe.parseInline,ke=oe,ge=se.parse,fe=ee.lex;e.Hooks=re,e.Lexer=ee,e.Marked=ie,e.Parser=se,e.Renderer=te,e.TextRenderer=ne,e.Tokenizer=x,e.getDefaults=t,e.lexer=fe,e.marked=oe,e.options=ae,e.parse=ke,e.parseInline=ue,e.parser=ge,e.setOptions=ce,e.use=he,e.walkTokens=pe})); 7 | -------------------------------------------------------------------------------- /src/frontend/assets/components/modal-eq.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 18 | 19 | 20 | 21 |
    22 | 23 |
    24 | 25 |
    26 |

    均衡器

    27 | 31 |
    32 |
    33 |
    34 | 38 |
    39 | 40 | 41 | 42 | 43 | 119 | 120 | -------------------------------------------------------------------------------- /src/frontend/assets/components/modal-update.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 10 | 11 | 12 | 13 |
    14 | 15 |
    16 | 17 |
    18 |

    版本更新

    19 |
    正在连接下载源...
    20 |
    21 |
    22 | 23 | 24 | 25 | 94 | 95 | -------------------------------------------------------------------------------- /src/frontend/assets/components/modal.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:"icon";src:url("../icon.woff2");} 2 | @font-face{font-family:"windowsicon";src:url("../windowsicon.ttf");} 3 | @font-face{font-family:"font";src:url("../font.ttf");} 4 | html{font-size:16px;user-select:none;font-family:"font","微软雅黑";background:#F9F9FB;} 5 | body{margin:0;} 6 | i{font-family:"icon";font-style:normal;} 7 | img{pointer-events:none;} 8 | *{box-sizing:border-box;outline:none;} 9 | *[hidden]{display:none!important;} 10 | ::-webkit-scrollbar{display:none;} 11 | button{background:#1E9FFF;color:white;border:0;border-radius:5px;padding:0 20px;font-size:16px;transition:filter .2s;font-family:inherit;height:30px;margin-left:5px;} 12 | button.sub{background:#DAEFFF;color:#1E9FFF;} 13 | button.square{width:30px;padding:0;} 14 | button:hover{filter:brightness(.95);} 15 | button:active{filter:brightness(.9);} 16 | button:disabled{filter:grayscale(1)!important;} 17 | header{position:fixed;top:0;height:35px;display:flex;justify-content:right;width:100%;z-index:100;-webkit-app-region:drag;} 18 | 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;} 19 | header i:hover{background:rgba(0,0,0,.05);} 20 | header i:active{background:rgba(0,0,0,.1);opacity:.8;} 21 | header i#closeBtn:hover{background:#E81123;color:white;} 22 | header i#closeBtn:active{background:#E81123;color:rgba(255,255,255,.8);} 23 | main{position:fixed;inset:30px 40px;} 24 | main h1{margin-top:0;margin-bottom:10px;font-size:1.8em;} 25 | main .buttons{position:absolute;bottom:0;right:0;} 26 | input,select,textarea{background:white;padding:8px 10px;border-radius:5px;border:0;font-size:1rem;font-family:inherit;width:100%;margin:5px 0;resize:none;} -------------------------------------------------------------------------------- /src/frontend/assets/components/require.js: -------------------------------------------------------------------------------- 1 | const {ipcRenderer, shell} = require("electron"); 2 | const fs = require("fs"); 3 | const path = require("path"); 4 | const musicMetadata = require("music-metadata"); 5 | const flacTagger = require("flac-tagger"); 6 | const nodeId3 = require("node-id3"); 7 | const fflate = require("fflate"); 8 | -------------------------------------------------------------------------------- /src/frontend/assets/components/shutdown.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SimMusicShutdownUI 6 | 17 | 18 | 19 |
    20 |
    21 | 设备将在 60 秒后 22 | 按任意键可取消操作。晚安,做个好梦~ 23 |
    24 | 25 | 61 | 62 | -------------------------------------------------------------------------------- /src/frontend/assets/components/webview.css: -------------------------------------------------------------------------------- 1 | @font-face{font-family:"icon";src:url("../icon.woff2");} 2 | @font-face{font-family:"windowsicon";src:url("../windowsicon.ttf");} 3 | @font-face{font-family:"font";src:url("../font.ttf");} 4 | html{font-size:16px;user-select:none;font-family:"font","微软雅黑";background:#F9F9FB;} 5 | body{margin:0;} 6 | i{font-family:"icon";font-style:normal;} 7 | img{pointer-events:none;} 8 | *{box-sizing:border-box;outline:none;} 9 | *[hidden]{display:none!important;} 10 | ::-webkit-scrollbar{display:none;} 11 | button{background:#1E9FFF;color:white;border:0;border-radius:5px;padding:0 20px;font-size:16px;transition:filter .2s;font-family:inherit;height:30px;margin-left:5px;} 12 | button.sub{background:#DAEFFF;color:#1E9FFF;} 13 | button.square{width:30px;padding:0;} 14 | button:hover{filter:brightness(.95);} 15 | button:active{filter:brightness(.9);} 16 | button:disabled{filter:grayscale(1)!important;} 17 | header{position:fixed;top:0;height:35px;display:flex;align-items:center;justify-content:right;width:100%;z-index:100;-webkit-app-region:drag;} 18 | 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;} 19 | header i:hover{background:rgba(0,0,0,.05);} 20 | header i:active{background:rgba(0,0,0,.1);opacity:.8;} 21 | header i#closeBtn:hover{background:#E81123;color:white;} 22 | header i#closeBtn:active{background:#E81123;color:rgba(255,255,255,.8);} 23 | 24 | 25 | header>div{width:calc(100% - 40px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-left:15px;} 26 | #progressBar{top:33px;height:2px;border-radius:2px;left:-5px;width:0px;background:#1E9FFF;position:fixed;transition:width .5s linear;} 27 | #progressBar.finished{animation:finished .8s;display:none;} 28 | @keyframes finished{0%{opacity:1;display:block;}50%{width:97vw;opacity:1;display:block;}100%{width:calc(100vw + 5px);opacity:0;display:block;}} 29 | 30 | webview{position:fixed;top:35px;width:100vw;height:calc(100vh - 35px);border-top:1px solid rgba(0,0,0,.1);background:white;} 31 | .buttons{display:none;} 32 | .showBtn webview{height:calc(100vh - 90px);} 33 | .showBtn .buttons{position:fixed;display:flex;align-items:center;justify-content:center;height:55px;width:100%;bottom:0;border-top:1px solid rgba(0,0,0,.1);} -------------------------------------------------------------------------------- /src/frontend/assets/components/webview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |
    13 |
    SimMusic
    14 | 15 | 16 |
    17 | 18 |
    19 | 20 | 21 | 22 |
    23 | 24 | 25 |
    26 | 27 | 28 | 29 | 80 | 81 | -------------------------------------------------------------------------------- /src/frontend/assets/font-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/font-bold.woff2 -------------------------------------------------------------------------------- /src/frontend/assets/font.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/font.woff2 -------------------------------------------------------------------------------- /src/frontend/assets/icon-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/icon-blue.png -------------------------------------------------------------------------------- /src/frontend/assets/icon-error.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/assets/icon-grey.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/assets/icon.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/icon.woff2 -------------------------------------------------------------------------------- /src/frontend/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/assets/main.css: -------------------------------------------------------------------------------- 1 | 2 | @font-face{font-family:"icon";src:url("icon.woff2");} 3 | @font-face{font-family:"windowsicon";src:url("windowsicon.ttf");} 4 | @font-face{font-family:"font";font-weight:400;src:url("font.woff2");} 5 | @font-face{font-family:"font";font-weight:700;src:url("font-bold.woff2");} 6 | html{font-size:16px;font-family:"font","微软雅黑";} 7 | body{margin:0;user-select:none;} 8 | body:not(.appLoading){background:#F9F9FB;} 9 | i{font-family:"icon";font-style:normal;} 10 | img{pointer-events:none;} 11 | *{box-sizing:border-box;outline:none;} 12 | *[hidden]{display:none!important;} 13 | ::-webkit-scrollbar{display:none;} 14 | button{background:#1E9FFF;color:white;border:0;border-radius:5px;padding:0 20px;font-size:15px;transition:filter .2s;font-family:inherit;height:30px;} 15 | button.sub{background:#DAEFFF;color:#1E9FFF;} 16 | button.square{width:30px;padding:0;} 17 | button:hover{filter:brightness(.95);} 18 | button:active{filter:brightness(.9);} 19 | button:disabled{filter:grayscale(1)!important;} 20 | .buttonGroup{height:30px;display:flex;border-radius:5px;overflow:hidden;width:fit-content;} 21 | .buttonGroup button{border-radius:0;} 22 | a{color:#1E9FFF;text-decoration:none;} 23 | a:hover{text-decoration:underline;} 24 | a:active{opacity:.8;text-decoration:underline;} 25 | input,select{background:rgba(0,0,0,.03);padding:8px 10px;border-radius:5px;border:0;font-size:1rem;font-family:inherit;width:100%;margin:5px 0;} 26 | ::selection{background:#DAEFFF;color:black;} 27 | 28 | 29 | .left .leftBar div,.right .musicListTitle section b, 30 | .right .musicListTitle section .details, 31 | .bottom .info .musicInfoBottom, 32 | .bottom .info .musicInfoBottom>div>*, 33 | .controls .musicInfo, 34 | .list>div>div>*, 35 | .right #downloadContainer>div>.info>.music>b{width:100%;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;} 36 | 37 | 38 | /* 主窗体 - 加载动画 */ 39 | .loadingSplash{transition:opacity .2s;position:fixed;z-index:200;inset:0;display:flex;align-items:center;justify-content:center;transition:opacity .2s;background:#1E9FFF;} 40 | .loadingSplash>img{width:250px;height:250px;} 41 | body:not(.appLoading) .loadingSplash{opacity:0;pointer-events:none;} 42 | 43 | 44 | /* 主窗体 - 顶栏图标 */ 45 | header{position:fixed;top:0;height:35px;display:flex;justify-content:right;width:calc(100% - 138px);z-index:100;-webkit-app-region:drag;font-size:.95em;} 46 | header i{font-family:"windowsicon";height:100%;width:46px;display:flex;align-items:center;justify-content:center;transition:background .2s,opacity .2s,color .2s;padding-bottom:1px;-webkit-app-region:no-drag;} 47 | header i#lyricsBtn{font-size:.95em;padding-bottom:1px;font-family:"微软雅黑"!important;} 48 | header i#miniBtn{transform:scaleY(-1);font-size:.95em;} 49 | header i#devBtn{padding-top:2px;} 50 | header i:hover{background:rgba(0,0,0,.07);} 51 | header i:active{background:rgba(0,0,0,.2);opacity:.8;} 52 | header i#closeBtn:hover{background:#E81123;color:white;} 53 | header i#closeBtn:active{background:#E81123;color:rgba(252,252,252,.8);} 54 | header i.active{color:#1E9FFF;} 55 | .playerShown #miniBtn,.playerShown #lyricsBtn,.playerShown #devBtn{display:none;} 56 | body:not(.playerShown) #hidePlayerBtn{display:none;} 57 | .fullscreen header{display:none;} 58 | 59 | 60 | 61 | /* 主窗体 - 左右容器 */ 62 | #homePage{height:100vh;transition:all .2s;} 63 | .playerShown #homePage{transform:scale(.98);border-radius:20px;transition:all .5s;} 64 | 65 | 66 | /* 主窗体 - 左侧界面 */ 67 | .left{position:fixed;top:0;bottom:0;width:var(--leftBarWidth);padding:35px 10px 0 10px;text-align:center;} 68 | .left img{width:150px;max-width:calc(100% - 25px);} 69 | .left .leftBar{margin-top:5px;text-align:left;overflow-y:scroll;padding-bottom:100px;height:calc(100% - 45px);} 70 | .left .leftBar section{margin-top:2px;} 71 | .left .leftBar .title{opacity:.5;padding:0 8px 0 10px;font-size:.9em;display:flex;align-items:center;margin-top:20px;margin-bottom:4px;} 72 | .left .leftBar .title span{width:100%;} 73 | .left .leftBar .title i{transition:background .2s;padding:2px;border-radius:5px;} 74 | .left .leftBar .title i:hover{background:rgba(0,0,0,.1);} 75 | .left .leftBar .title i:active{background:rgba(0,0,0,.2);} 76 | .left .leftBar div{text-align:left;padding:4.5px 10px;height:30px;border-radius:5px;transition:all .2s,color .1s;position:relative;} 77 | .left .leftBar div:hover{background:rgba(0,0,0,.05);} 78 | .left .leftBar div:active{background:rgba(0,0,0,.1);} 79 | .left .leftBar div.active{color:white;background:transparent;} 80 | .left .leftBar div::before{content:"";background:linear-gradient(90deg,#1E9FFF,#1E9FFFBB);opacity:0;transition:opacity .2s;position:absolute;inset:0;z-index:-1;} 81 | .left .leftBar div.active::before{opacity:1;} 82 | .left .leftBar div[hidden]{display:block!important;pointer-events:none;transform:translateX(-100px) translateY(-4.5px);padding:0 10px;height:0;opacity:0;} 83 | 84 | 85 | 86 | /* 主窗体 - 右侧界面 */ 87 | .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);} 88 | .right #rightPlaceholder{position:absolute;inset:0;bottom:80px;display:flex;align-items:center;justify-content:center;} 89 | .right #rightPlaceholder img{width:150px;} 90 | .right #leftBarResizer{position:absolute;z-index:10;left:-2.5px;width:5px;height:100%;top:0;cursor:col-resize;} 91 | /* 歌单 */ 92 | .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;} 93 | .right .musicListTitle img{width:120px;height:120px;background:white;border-radius:5px;object-fit:cover;} 94 | .right .musicListTitle section{margin-left:30px;width:calc(100% - 150px);} 95 | .right .musicListTitle section b{font-size:1.3em;display:block;} 96 | .right .musicListTitle section .details{opacity:.8;font-size:.9em;margin:5px 0 10px 0;} 97 | .right #musicListContainer>div:not(.show){display:none;} 98 | .right #musicListContainer .tableContainer{width:100%;white-space:nowrap;padding:180px 0 100px 0;height:100vh;overflow-y:scroll;} 99 | .right #musicListContainer .tableContainer table{table-layout:fixed;width:100%;max-width:100%;border-collapse:collapse;} 100 | .right #musicListContainer .tableContainer table colgroup>col:nth-child(1){width:80px;} 101 | .right #musicListContainer .tableContainer table colgroup>col:nth-child(2){width:30%;} 102 | .right #musicListContainer .tableContainer table colgroup>col:nth-child(5){width:80px;} 103 | .right #musicListContainer .tableContainer table th{font-weight:normal;text-align:left;opacity:.5;font-size:.9em;padding:5px 0;transition:opacity .2s;} 104 | .right #musicListContainer .tableContainer table th:not(.disabled):hover,.right #musicListContainer .tableContainer table th:not(.disabled):active{opacity:.8;} 105 | .right #musicListContainer .tableContainer table th span{display:flex;align-items:center;} 106 | .right #musicListContainer .tableContainer table th span::after{font-family:"icon";content:"\EA4E";opacity:0;transition:opacity .2s,transform .2s;display:block;} 107 | .right #musicListContainer .tableContainer table th.positiveOrder span::after{opacity:.8;margin-top:1px;} 108 | .right #musicListContainer .tableContainer table th.reversedOrder span::after{opacity:.8;transform:rotate(180deg);margin-bottom:1px;} 109 | .right #musicListContainer .tableContainer table td{overflow:hidden;text-overflow:ellipsis;} 110 | body:not(.disableHighlight) .right #musicListContainer .tableContainer table td>m{background:#DAEFFF;} 111 | .right #musicListContainer .tableContainer table td:first-child{padding:7px 10px 7px 30px;font-size:0;} 112 | .right #musicListContainer .tableContainer table td:first-child img{width:35px;height:35px;background:white;border-radius:5px;} 113 | .right #musicListContainer .tableContainer table td:last-child{padding:0 30px 0 0;width:100px;} 114 | .right #musicListContainer .tableContainer table thead tr{background:transparent!important;} 115 | .right #musicListContainer .tableContainer table tr{transition:background .1s;} 116 | .right #musicListContainer .tableContainer table tr:hover{background:rgba(0,0,0,.025);} 117 | .right #musicListContainer .tableContainer table tr:active{background:rgba(0,0,0,.05);} 118 | .right #musicListContainer .tableContainer table tr.active{color:#1E9FFF;} 119 | .right #musicListContainer .tableContainer table tr.selected{background:#DAEFFF!important;} 120 | .right #musicListContainer .musicLocator{position:absolute;right:20px;bottom:100px;height:40px;width:40px;border:1px solid rgba(0,0,0,.1);border-radius:50%;background:rgba(252,252,252,.9);backdrop-filter:blur(20px);font-size:1.2em;display:flex;align-items:center;justify-content:center;transition:all .2s;} 121 | .right #musicListContainer .musicLocator:hover{color:#1E9FFF;} 122 | .right #musicListContainer .musicLocator:active{color:#1E9FFF;transform:scale(.95);filter:brightness(.95);} 123 | .right #musicListContainer .musicLocator.hidden{transform:scale(.2);opacity:0;pointer-events:none;} 124 | .right #musicListContainer .musicListErrorOverlay{position:absolute;inset:180px 0 80px 0;display:flex;align-items:center;justify-content:center;flex-direction:column;} 125 | .right #musicListContainer .musicListErrorOverlay img{width:120px;} 126 | .right #musicListContainer .musicListErrorOverlay div{opacity:.5;margin-top:10px;font-size:1.2em;text-align:center;} 127 | /* 搜索 */ 128 | .right .searchTitle{padding:30px;height:180px;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:5;} 129 | .right .searchTitle .searchTitleText{font-size:1.5em;} 130 | .right .searchTitle .inputGroup{margin-top:50px;display:flex;white-space:nowrap;align-items:center;justify-content:center;width:100%;} 131 | .right .searchTitle .inputGroup select{height:37px;border-radius:5px 0 0 5px;margin-right:2px;width:fit-content;padding:0 10px;} 132 | .right .searchTitle .inputGroup input{height:37px;margin-right:10px;max-width:500px;width:100%;border-radius:0 5px 5px 0;padding:0 10px;} 133 | .right .searchTitle .inputGroup button{height:37px;font-size:1.05em;} 134 | .right #searchBottomIndicator{text-align:center;opacity:.8;margin-top:50px;margin:30px 0 10px 0;font-size:.9em;} 135 | /* 下载 */ 136 | .right #downloadContainer{padding-top:90px!important;} 137 | .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;} 138 | .right #downloadContainer>div[data-status="success"]{background:#EFF6EF;} 139 | .right #downloadContainer>div[data-status="error"]{background:#FCEFEF;} 140 | .right #downloadContainer>div>.info{display:flex;align-items:center;white-space:nowrap;position:absolute;inset:0;z-index:5;padding:0 15px;} 141 | .right #downloadContainer>div>.info>.music{display:flex;flex-direction:column;width:calc(100% - 150px);} 142 | .right #downloadContainer>div>.info>.music>span{display:block;opacity:.8;font-size:.9em;margin-top:3px;} 143 | .right #downloadContainer>div[data-status="pending"]>.info>.music>span>i::after{content:"\F337";} 144 | .right #downloadContainer>div[data-status="download"]>.info>.music>span>i::after{content:"\EC5A";} 145 | .right #downloadContainer>div[data-status="success"]>.info>.music>span>i::after{content:"\EB7B";} 146 | .right #downloadContainer>div[data-status="error"]>.info>.music>span>i::after{content:"\F4C8";} 147 | .right #downloadContainer>div>.info>.buttons{display:flex;justify-content:flex-end;width:150px;} 148 | .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;} 149 | .right #downloadContainer>div>.info>.buttons>i:hover{background:rgba(0,0,0,.05);} 150 | .right #downloadContainer>div>.info>.buttons>i:active{background:rgba(0,0,0,.1);} 151 | .right #downloadContainer>div:not([data-status="error"])>.info>.buttons>i.errorOnly{display:none;} 152 | .right #downloadContainer>div:not([data-status="success"])>.info>.buttons>i.successOnly{display:none;} 153 | .right #downloadContainer>div>.progressBar{transition:width .2s,opacity .2s;position:absolute;inset:0;right:unset;background:#DAEFFF;width:var(--progressWidth);} 154 | .right #downloadContainer>div:not([data-status="download"])>.progressBar{opacity:0;} 155 | /* 扩展 */ 156 | .right #extensionContainer{padding-top:90px!important;} 157 | .right #extensionContainer .block>div{white-space:nowrap;display:flex;zoom:.9;align-items:center;margin-right:15px;} 158 | .right #extensionContainer .block>div>span{margin-left:5px;} 159 | /* 设置 */ 160 | .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;} 161 | .right .page .header i{margin-right:5px;} 162 | .right .page .header small{font-size:.6em;margin-left:15px;opacity:.8;} 163 | .right .page .container{position:absolute;z-index:1;padding:70px 27.5px 100px 27.5px;width:100%;height:100%;overflow-y:scroll;} 164 | .right .page .title{font-weight:bold;margin:20px 2.5px 5px 2.5px;} 165 | .right .page .block{background:white;width:100%;border-radius:5px;margin-bottom:5px;padding:10px 15px;display:flex;align-items:center;} 166 | .right .page .block.highlight{background:#1E9FFF;color:white;} 167 | .right .page .block{transform-origin:bottom;transition:margin-top .2s,opacity .2s,transform .2s;} 168 | .right .page .block.folded{margin-top:calc(-5px - var(--height));opacity:0;pointer-events:none;transform:scaleY(0);} 169 | .right .page .block section{width:100%;margin-right:10px;} 170 | .right .page .block section div{font-size:1em;} 171 | .right .page .block section div badge{font-size:.8em;background:rgba(0,0,0,.1);opacity:.5;padding:0 7px;margin-left:5px;margin-top:2px;vertical-align:top;border-radius:10px;display:inline-block;} 172 | .right .page .block section span{display:block;font-size:.9em;opacity:.8;word-break:break-all;line-height:1.1em;margin-top:3px;} 173 | .right .page .block button{white-space:nowrap;} 174 | .right .page .block.highlight button{background:white!important;color:#1E9FFF;} 175 | .right .page .block .range{min-width:150px;max-width:150px;} 176 | .right .page .block .range>div{background:rgba(0,0,0,.05);} 177 | .right .page .block input,.right .page .block select{min-width:150px;max-width:150px;} 178 | .right .page .block .colorInput{background:rgba(0,0,0,.03);position:relative;border-radius:5px;} 179 | .right .page .block .colorInput>input{opacity:0;} 180 | .right .page .block .colorInput>span{position:absolute;inset:0;height:fit-content;margin:auto;text-align:center;text-transform:uppercase;} 181 | .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;} 182 | .toggle::before{transition:margin-left .2s,transform .2s;transform:scale(.7);width:16px;height:16px;background:white;border-radius:50%;content:"";display:block;} 183 | .on .toggle{background:#1E9FFF;} 184 | .on .toggle::before{margin-left:15px;transform:scale(.9);} 185 | /* 关于 */ 186 | .right #aboutPage{padding:30px;overflow-y:scroll;} 187 | .right #aboutPage .top{display:flex;align-items:center;} 188 | .right #aboutPage .top>img{width:100px;height:100px;border-radius:5px;background:#1E9FFF;} 189 | .right #aboutPage .top>div{margin-left:15px;} 190 | .right #aboutPage .top>div>b{display:block;font-size:1.5em;} 191 | .right #aboutPage .top>div>div{opacity:.8;font-size:.9em;} 192 | .right #aboutPage .top>div>div>img{height:1.1em;margin-top:.2em;} 193 | .right #aboutPage .main{display:flex;margin-top:50px;} 194 | .right #aboutPage .main>div{width:100%;margin-right:10px;} 195 | .right #aboutPage .main>div>div>b{font-size:1.2em;display:block;} 196 | .right #aboutPage .main>div>div>span{font-size:.9em;opacity:.8;display:block;margin-bottom:5px;} 197 | .right #aboutPage .main>div a{display:block;;} 198 | .right #aboutPage .main>div section{background:white;display:flex;align-items:center;border-radius:5px;margin:15px 0;overflow:hidden;transition:background .2s;} 199 | .right #aboutPage .main>div section:hover{background:rgba(0,0,0,.025);} 200 | .right #aboutPage .main>div section:active{background:rgba(0,0,0,.05);} 201 | .right #aboutPage .main>div section>img{width:80px;height:80px;} 202 | .right #aboutPage .main>div section>div{margin-left:10px;} 203 | .right #aboutPage .main>div section>div>b{display:block;font-size:1.2em;} 204 | .right #aboutPage .main>div section>div>span{font-size:.9em;opacity:.8;display:block;margin-bottom:5px;} 205 | /* 更新 */ 206 | .right #updateContainer{padding-top:90px!important;} 207 | .right #updateInfo{background:white;margin:0 -2.5px;padding:5px 20px 15px 20px;} 208 | .right #updateInfo p{margin:3px 5px;} 209 | .right #updateInfo h1,.right #updateInfo h2,.right #updateInfo h3{margin:15px 5px 5px 5px;zoom:.9;} 210 | .right #updateInfo h1{border-bottom:1px solid rgba(0,0,0,.05);padding-bottom:5px;} 211 | .right #updateInfo ul,.right #updateInfo ol{margin:0;padding-left:23px;} 212 | .right #updateInfo blockquote{background:rgba(0,0,0,.025);margin:5px 0;padding:7px 10px;border-radius:10px;} 213 | .right #updateInfo blockquote p{margin:3px 0;} 214 | .right #updateInfo img{margin-top:5px;background:white;border-radius:5px;width:100%;display:block;} 215 | .right #updateInfo code{font-family:"Source Code Pro", "Consolas", "font";} 216 | .right .operations{margin:0 -2.5px;padding-top:10px;text-align:right;} 217 | 218 | 219 | /* 主窗体 - 文件拖入 */ 220 | #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;} 221 | #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;} 222 | #dropTipContainer #dropTip>i{color:#1E9FFF;} 223 | .dragOver #dropTipContainer{opacity:1;} 224 | 225 | 226 | /* 主窗体 - 主题图片 */ 227 | #themeImage{position:fixed;inset:0;width:100%;height:100%;object-fit:cover;opacity:.05;z-index:40;transition:opacity .2s;} 228 | body:not(.themeImage) #themeImage{opacity:0;} 229 | .themeImage .right #rightPlaceholder img{display:none;} 230 | .themeImage #bottom,.themeImage .musicListTitle,.themeImage .right .page .header,.themeImage .musicLocator{background:rgba(255,255,255,.9);} 231 | 232 | 233 | /* 主窗体 - 底部控件 */ 234 | .bottom{position:fixed;bottom:-90px;height:80px;width:100%;background:rgba(252,252,252,.9);backdrop-filter:blur(20px);transition:bottom .2s;} 235 | .withCurrentMusic:not(.playerShown) .bottom{bottom:0;} 236 | .bottom #bottomProgressBar{width:calc(100% - 10px);height:5px;position:absolute;left:5px;top:0;z-index:2;} 237 | .bottom #bottomProgressBar>div{border-radius:0;height:6px;} 238 | .bottom .progressBefore,.bottom .progressAfter{position:absolute;content:"";width:5px;height:6px;background:#1E9FFF;left:0;z-index:1;} 239 | .bottom .progressAfter{background:rgba(0,0,0,.1);left:unset;right:0;} 240 | .bottom .info{position:absolute;top:15px;bottom:8px;left:10px;display:flex;align-items:center;width:300px;} 241 | .bottom .info .img{width:54px;height:54px;border-radius:5px;background:white;overflow:hidden;margin-right:10px;position:relative;transition:transform .2s;} 242 | .bottom .info .img:active{transform:scale(.95);} 243 | .bottom .info .img::after{content:"\EA78";font-size:2.5em;background:rgba(0,0,0,.2);position:absolute;inset:0;font-family:"icon";display:flex;align-items:center;justify-content:center;color:white;opacity:0;transition:opacity .2s;} 244 | .bottom .info .img:hover::after,.bottom .info .img:active::after{opacity:1;} 245 | .bottom .info .img>img{width:100%;height:100%;object-fit:cover;} 246 | .bottom .info .musicInfoBottom{max-width:200px;} 247 | .bottom .info .musicInfoBottom b{font-size:1.1em;} 248 | .bottom .info .musicInfoBottom>div{position:relative;} 249 | .bottom .info .musicInfoBottom>div div{transition:opacity .2s,transform .2s;} 250 | .bottom .info .musicInfoBottom>div #miniModeStatus{position:absolute;top:0;left:0;width:100%;} 251 | .miniModeStatus .bottom .info .musicInfoBottom>div #bottomArtist{transform:translateY(-3px);opacity:0;} 252 | body:not(.miniModeStatus) .bottom .info .musicInfoBottom>div #miniModeStatus{transform:translateY(3px);opacity:0;} 253 | .bottom .center{position:absolute;top:0;bottom:0;left:300px;right:300px;} 254 | .bottom .center>div,.bottom .volBtnBottom>div{width:40px;height:40px;} 255 | .bottom .center>div.play{width:55px;height:55px;} 256 | .bottom .volBtnBottom{width:150px;position:absolute;top:5px;bottom:0;right:30px;display:flex;align-items:center;} 257 | .bottom .volBtnBottom>div{opacity:.7;margin-right:5px;min-width:40px;height:40px;} 258 | .playBtnColor .bottom .center>.play{background:#1e9fff;color:white;font-size:1.6em;width:50px;height:50px;margin: 0 5px;} 259 | .playBtnColor .bottom .center>.play:hover,.playBtnColor .bottom .center>.play:active{filter:brightness(.95);} 260 | .playBtnColor .bottom .center>div{opacity:.5;} 261 | 262 | 263 | /* 主窗体 - 播放内页 */ 264 | #playPage{position:fixed;top:120vh;left:0;width:100%;height:100%;background:white;transition:top .3s;overflow:hidden;z-index:50;} 265 | .playerShown #playPage{top:0;} 266 | .darkPlayer{--SimAPTheme:rgba(255,255,255,.8)!important;} 267 | .darkPlayer #playPage{background:black;color:rgba(255,255,255,.9);} 268 | .darkPlayer #playPage b,.darkPlayer .lyrics>div>div.active{font-weight:normal;} 269 | .darkPlayer #playPage .SimProgress{--SimProgressBackground:rgba(255,255,255,.1);} 270 | .darkPlayer #playPage .SimProgress::after{background:white;} 271 | .darkPlayer #playPage .SimLRC{--normalColor:rgba(255,255,255,.2)!important;} 272 | .darkPlayer #playPage #background{opacity:.35;} 273 | .darkPlayer #playPage #background canvas{opacity:.5;} 274 | .darkPlayer.playerShown header i{color:rgba(255,255,255,.8);} 275 | .darkPlayer .controls #album,.darkPlayer .list>div>img{background:rgba(0,0,0,.1);} 276 | .darkPlayer .controls .infoBar i{background:rgba(255,255,255,.025);color:rgba(255,255,255,.6);} 277 | .darkPlayer .controls .infoBar i:hover,.darkPlayer .controls .infoBar i:active{background:rgba(255,255,255,.05);color:rgba(255,255,255,.8);} 278 | .darkPlayer .controls .buttons>div:hover,.darkPlayer .controls .buttons>div:active{background:rgba(255,255,255,.05);} 279 | .darkPlayer.musicLoading #progressBar>div{background-image:linear-gradient(-45deg,rgba(255,255,255,.05) 25%,transparent 0,transparent 50%,rgba(255,255,255,.05) 0,rgba(255,255,255,.05) 75%,transparent 0,transparent)!important;} 280 | .darkPlayer.volume .controls .buttons>.volBtn{background:rgba(255,255,255,.05)!important;color:rgba(255,255,255,.8);} 281 | .darkPlayer .list>div:hover{background:rgba(255,255,255,.025);} 282 | .darkPlayer .list>div.active,.darkPlayer .list>div:active{background:rgba(255,255,255,.05);} 283 | .darkPlayer.playerShown header i:hover{background:rgba(255,255,255,.05);} 284 | 285 | 286 | 287 | /* 主窗体 - 迷你模式 */ 288 | .miniMode{background:transparent;} 289 | .miniMode header{-webkit-app-region:no-drag;} 290 | .miniMode .left,.miniMode .right{display:none;} 291 | .miniMode .bottom{z-index:300;height:60px;background:white;} 292 | .miniMode .bottom .progressBefore,.miniMode .bottom .progressAfter{display:none;} 293 | .miniMode .bottom #bottomProgressBar{bottom:0;left:60px;top:unset;width:calc(100% - 60px);z-index:5;opacity:.8;;transition:opacity .2s,height .2s;} 294 | .miniMode .bottom:hover #bottomProgressBar{height:7px;} 295 | .miniMode .bottom #bottomProgressBar:hover,.miniMode .bottom #bottomProgressBar:active{height:7px;opacity:1;} 296 | .miniMode .bottom #bottomProgressBar::after{display:none;} 297 | .miniMode .bottom .info{top:0;bottom:0!important;left:0;width:170px;bottom:10px;} 298 | .miniMode .bottom .info .img{width:60px;height:60px;border-radius:0;margin-right:10px;-webkit-app-region:drag;} 299 | .miniMode .bottom .info .img::after{display:none;} 300 | .miniMode .bottom .info .musicInfoBottom{font-size:1.1em;max-width:calc(100px / .75);zoom:.75;margin-bottom:5px;} 301 | .miniMode .bottom .info .musicInfoBottom>div{margin-top:-1px;} 302 | .miniMode .bottom .center{right:15px;zoom:.7;bottom:7.5px;width:220px;left:unset;} 303 | .miniMode .bottom .center .bottomListBtn{display:none;} 304 | body:not(.miniMode) .bottom .center .miniModeBtn{display:none;} 305 | .miniMode .bottom .volume{display:none;} 306 | -------------------------------------------------------------------------------- /src/frontend/assets/misc/recommend-musictag.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/recommend-musictag.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/recommend-salt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/recommend-salt.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/recommend-vnimusic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/recommend-vnimusic.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/taskbar-next.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/taskbar-next.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/taskbar-pause.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/taskbar-pause.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/taskbar-play.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/taskbar-play.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/taskbar-prev.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/taskbar-prev.png -------------------------------------------------------------------------------- /src/frontend/assets/misc/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/misc/text.png -------------------------------------------------------------------------------- /src/frontend/assets/placeholder.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/frontend/assets/windowsicon.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Simsv-Software/SimMusic2024-Windows/3fd79f1d420dff61ed00a7ae996b7465baf22175/src/frontend/assets/windowsicon.ttf -------------------------------------------------------------------------------- /src/frontend/lrc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SimMusicDesktopLyrics 5 | 6 | 7 | 8 | 9 | 25 | 26 |
    27 | 28 |
    29 |
    30 | 31 | 32 | 104 | 105 | -------------------------------------------------------------------------------- /src/frontend/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SimMusic 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 |
    17 | 18 | 19 | 20 | 21 |
    22 | 23 | 24 |
    25 | 26 |
    27 | 28 | 29 | 30 |
    31 | 32 |
    33 | 34 |
    35 | 36 |
    歌单
    37 |
    38 |
    功能
    39 |
    搜索
    40 | 41 |
    扩展
    42 |
    设置
    43 |
    关于
    44 | 45 |
    46 |
    47 | 48 |
    49 |
    50 | 51 |
    52 | 53 |
    54 | 55 | 103 | 104 | 110 | 111 | 123 | 124 | 128 | 129 |
    172 | 173 | 186 |
    187 |
    188 | 189 |
    190 | 191 |
    192 |
    193 |
    194 |
    195 |
    未在播放
    未知艺术家
    196 |
    197 |
    198 |
    199 |
    200 |
    201 |
    202 |
    203 |
    204 |
    205 |
    206 |
    207 |
    208 |
    209 |
    210 |
    211 |
    212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | 2 | // © 2020 - 2024 Simsv Studio 3 | 4 | const {app, BrowserWindow, ipcMain, dialog, nativeImage, Tray, Menu, screen, session, webContents, desktopCapturer} = require("electron"); 5 | const {exec} = require("child_process"); 6 | const path = require("path"); 7 | const fs = require("fs"); 8 | const os = require("os"); 9 | 10 | app.commandLine.appendSwitch("enable-smooth-scrolling"); 11 | app.commandLine.appendSwitch("enable-features", "WindowsScrollingPersonality,FluentScrollbar,ParallelDownloading"); 12 | 13 | // 创建窗口 14 | const SimMusicWindows = {}; 15 | let isMainWinLoaded; 16 | let pendingOpenFile = []; 17 | let tray; 18 | function showMainWin() { 19 | SimMusicWindows.mainWin.show(); 20 | if (SimMusicWindows.mainWin.isMinimized()) {SimMusicWindows.mainWin.restore();} 21 | SimMusicWindows.mainWin.focus(); 22 | } 23 | const createWindow = () => { 24 | // 主窗体 25 | SimMusicWindows.mainWin = new BrowserWindow({ 26 | width: 1000, 27 | height: 700, 28 | minWidth: 1000, 29 | minHeight: 700, 30 | frame: false, 31 | resizable: true, 32 | show: false, 33 | backgroundColor: "#1E9FFF", 34 | title: "SimMusic", 35 | titleBarStyle: "hidden", 36 | titleBarOverlay: { 37 | color: "rgba(0,0,0,0)", 38 | symbolColor: "white", 39 | height: 35, 40 | }, 41 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } 42 | }); 43 | SimMusicWindows.mainWin.loadURL(path.join(__dirname, "frontend/main.html")); 44 | setTimeout(() => {SimMusicWindows.mainWin.show();}, 50); 45 | SimMusicWindows.mainWin.on("close", e => { 46 | e.preventDefault(); 47 | SimMusicWindows.mainWin.webContents.executeJavaScript("WindowOps.close()", true); 48 | }); 49 | // 歌词窗体 50 | SimMusicWindows.lrcWin = new BrowserWindow({ 51 | width: 0, 52 | height: 0, 53 | frame: false, 54 | resizable: false, 55 | show: false, 56 | transparent: true, 57 | focusable: false, 58 | alwaysOnTop: true, 59 | backgroundThrottling: false, 60 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } 61 | }); 62 | SimMusicWindows.lrcWin.loadURL(path.join(__dirname, "frontend/lrc.html")); 63 | SimMusicWindows.lrcWin.maximize(); 64 | } 65 | app.whenReady().then(() => { 66 | tray = new Tray(nativeImage.createFromPath(path.join(__dirname, "frontend/assets/icon-blue.png"))); 67 | tray.on("click", () => { showMainWin(); }); 68 | tray.setToolTip("SimMusic"); 69 | createWindow(); 70 | if (!app.requestSingleInstanceLock()) { 71 | app.exit(); 72 | return; 73 | } 74 | const initOpenFile = process.argv[process.argv.length - 1]; 75 | if (process.argv.length != 1 && initOpenFile && fs.existsSync(initOpenFile)) pendingOpenFile.push(initOpenFile); 76 | app.on("second-instance", (_event, argv) => { 77 | const openFile = argv[argv.length - 1]; 78 | if (openFile && fs.existsSync(openFile)) { 79 | if (!isMainWinLoaded) pendingOpenFile.push(openFile); 80 | else { 81 | showMainWin(); 82 | SimMusicWindows.mainWin.webContents.send("fileLaunch", openFile); 83 | } 84 | } else { 85 | showMainWin(); 86 | } 87 | }); 88 | session.defaultSession.setDisplayMediaRequestHandler((request, callback) => { 89 | desktopCapturer.getSources({ types: ["screen"] }).then((sources) => { 90 | callback({ video: sources[0], audio: "loopback" }) 91 | }); 92 | }); 93 | }); 94 | ipcMain.handle("mainWinLoaded", () => { 95 | if (isMainWinLoaded) return []; 96 | isMainWinLoaded = true; 97 | setTimeout(() => { 98 | SimMusicWindows.mainWin.setTitleBarOverlay({color: "rgba(255,255,255,0)", symbolColor: "black", height: 35}); 99 | }, 500); 100 | return pendingOpenFile; 101 | }); 102 | ipcMain.handle("overlayBlack", () => { 103 | SimMusicWindows.mainWin.setTitleBarOverlay({color: "rgba(255,255,255,0)", symbolColor: "black", height: 35}); 104 | }); 105 | ipcMain.handle("overlayWhite", () => { 106 | SimMusicWindows.mainWin.setTitleBarOverlay({color: "rgba(0,0,0,0)", symbolColor: "rgba(255,255,255,.8)", height: 35}); 107 | }); 108 | 109 | 110 | // 处理窗口事件 111 | ipcMain.handle("winOps", (_event, args) => { 112 | return SimMusicWindows[args[0]][args[1]](); 113 | }); 114 | ipcMain.handle("restart", () => { 115 | app.exit(); 116 | app.relaunch(); 117 | }); 118 | ipcMain.handle("quitApp", () => { 119 | app.exit(); 120 | }); 121 | 122 | 123 | // 对话框 124 | ipcMain.handle("dialog", (_event, type, txt, parent, dialogId) => { 125 | const dialogWindow = new BrowserWindow({ 126 | parent: SimMusicWindows[parent], 127 | modal: true, 128 | width: 500, 129 | height: 200, 130 | frame: false, 131 | resizable: false, 132 | show: false, 133 | maximizable: false, 134 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false, devTools: false } 135 | }); 136 | dialogWindow.loadURL(path.join(__dirname, `frontend/assets/components/dialog.html?type=${type}&txt=${encodeURIComponent(txt)}&parent=${parent}&dialogId=${dialogId}`)); 137 | dialogWindow.once("ready-to-show", () => { dialogWindow.show(); }); 138 | }); 139 | ipcMain.handle("dialogSubmit", async (_event, parent, dialogId, txt) => { 140 | if (dialogId.startsWith("wv")) { 141 | try { 142 | const cookies = await session.fromPartition("dialog-" + dialogId).cookies.get({}); 143 | const json = JSON.stringify({ 144 | cookies: cookies, 145 | url: txt, 146 | }); 147 | SimMusicWindows[parent].webContents.send("dialogSubmit", dialogId, json); 148 | } catch { 149 | SimMusicWindows[parent].webContents.send("dialogSubmit", dialogId, "{}"); 150 | } 151 | } else { 152 | SimMusicWindows[parent].webContents.send("dialogSubmit", dialogId, txt); 153 | } 154 | }); 155 | ipcMain.handle("dialogCancel", (_event, parent) => { 156 | SimMusicWindows[parent].webContents.send("dialogCancel"); 157 | }); 158 | ipcMain.handle("webview", (_event, url, parent, dialogId, width, height, showFinishBtn) => { 159 | const dialogWindow = new BrowserWindow({ 160 | parent: SimMusicWindows[parent], 161 | modal: true, 162 | width: width ?? 600, 163 | height: height ?? 500, 164 | minWidth: 600, 165 | minHeight: 500, 166 | frame: false, 167 | resizable: true, 168 | show: false, 169 | maximizable: true, 170 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false, webviewTag: true, devTools: false } 171 | }); 172 | dialogWindow.loadURL(path.join(__dirname, `frontend/assets/components/webview.html?url=${encodeURIComponent(url)}&showFinishBtn=${showFinishBtn}&parent=${parent}&dialogId=${dialogId}`)); 173 | dialogWindow.center(); 174 | dialogWindow.once("ready-to-show", () => { dialogWindow.show(); }); 175 | }); 176 | ipcMain.handle("webviewDialogLoaded", (_event, wcId) => { 177 | const wc = webContents.fromId(wcId); 178 | wc.setWindowOpenHandler(({ url }) => { 179 | wc.loadURL(url); 180 | return { action: "deny" } 181 | }); 182 | }); 183 | ipcMain.handle("modal", (_event, url, height, parent) => { 184 | const dialogWindow = new BrowserWindow({ 185 | parent: SimMusicWindows[parent], 186 | modal: true, 187 | width: 500, 188 | height: height, 189 | frame: false, 190 | resizable: false, 191 | show: false, 192 | maximizable: false, 193 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false, devTools: false } 194 | }); 195 | dialogWindow.loadURL(path.join(__dirname, "frontend/assets/components/", url)); 196 | dialogWindow.once("ready-to-show", () => { dialogWindow.show(); }); 197 | }); 198 | 199 | 200 | // 任务栏控件 201 | const createTaskbarButtons = (isPlay) => { 202 | SimMusicWindows.mainWin.setThumbarButtons([ 203 | { 204 | tooltip: "上一首", 205 | icon: nativeImage.createFromPath(path.join(__dirname, "frontend/assets/misc/taskbar-prev.png")), 206 | click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.prev(true)", true);} 207 | }, { 208 | tooltip: isPlay ? "暂停" : "播放", 209 | icon: nativeImage.createFromPath(path.join(__dirname, isPlay ? "frontend/assets/misc/taskbar-pause.png" : "frontend/assets/misc/taskbar-play.png")), 210 | click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.togglePlay(true)", true);} 211 | }, { 212 | tooltip: "下一首", 213 | icon: nativeImage.createFromPath(path.join(__dirname, "frontend/assets/misc/taskbar-next.png")), 214 | click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.next(true)", true);} 215 | } 216 | ]); 217 | const menu = Menu.buildFromTemplate([ 218 | { label: "SimMusic", type: "normal", enabled: false}, 219 | { type: "separator" }, 220 | { label: "显示主窗口", type: "normal", click() { showMainWin(); }}, 221 | { label: isPlay ? "暂停" : "播放", type: "normal", click () {SimMusicWindows.mainWin.webContents.executeJavaScript("SimAPControls.togglePlay()", true);}}, 222 | { type: "separator" }, 223 | { label: "退出应用", type: "normal", click: app.exit}, 224 | ]); 225 | tray.setContextMenu(menu); 226 | } 227 | ipcMain.handle("musicPlay", () => { 228 | if (lyricsShowing) SimMusicWindows.lrcWin.webContents.send("setHidden", "inside", false); 229 | createTaskbarButtons(true); 230 | }); 231 | ipcMain.handle("musicPause", () => { 232 | SimMusicWindows.lrcWin.webContents.send("setHidden", "inside", true); 233 | createTaskbarButtons(false); 234 | }); 235 | 236 | 237 | 238 | // 桌面歌词 239 | let lyricsShowing = false; 240 | ipcMain.handle("toggleLyrics", (_event, isShow) => { 241 | if (isShow || isShow === false) {lyricsShowing = !isShow;} 242 | if (lyricsShowing) { 243 | SimMusicWindows.lrcWin.webContents.send("setHidden", "text", true); 244 | setTimeout(() => {SimMusicWindows.lrcWin.hide();}, 100); 245 | lyricsShowing = false; 246 | } else { 247 | SimMusicWindows.lrcWin.show(); 248 | SimMusicWindows.lrcWin.setIgnoreMouseEvents("true", {forward: true}); 249 | SimMusicWindows.lrcWin.setSkipTaskbar(true); 250 | SimMusicWindows.lrcWin.setAlwaysOnTop(false); 251 | SimMusicWindows.lrcWin.setAlwaysOnTop(true); 252 | lyricsShowing = true; 253 | setTimeout(() => {SimMusicWindows.lrcWin.webContents.send("setHidden", "text", false);}, 400); 254 | } 255 | return lyricsShowing; 256 | }); 257 | ipcMain.handle("lrcUpdate", (_event, lrc) => { 258 | SimMusicWindows.lrcWin.webContents.send("lrcUpdate", lrc); 259 | }); 260 | ipcMain.handle("focusDesktopLyrics", () => { 261 | SimMusicWindows.lrcWin.setIgnoreMouseEvents(false); 262 | }); 263 | ipcMain.handle("unfocusDesktopLyrics", () => { 264 | SimMusicWindows.lrcWin.setIgnoreMouseEvents(true, {forward: true}); 265 | }); 266 | ipcMain.handle("updateDesktopLyricsConfig", (_event, isProtected) => { 267 | SimMusicWindows.lrcWin.webContents.send("lrcWinReload"); 268 | SimMusicWindows.lrcWin.setContentProtection(isProtected); 269 | }); 270 | 271 | 272 | 273 | // 迷你模式 274 | let isMiniMode = false; 275 | ipcMain.handle("toggleMini", () => { 276 | const { width, height } = screen.getPrimaryDisplay().workAreaSize; 277 | SimMusicWindows.mainWin.setOpacity(0); 278 | if (isMiniMode) { 279 | setTimeout(() => { 280 | SimMusicWindows.mainWin.setMinimumSize(1000, 700); 281 | SimMusicWindows.mainWin.setSize(1000, 700); 282 | SimMusicWindows.mainWin.setPosition(parseInt(width / 2 - 500), parseInt(height / 2 - 350)); 283 | SimMusicWindows.mainWin.setResizable(true); 284 | SimMusicWindows.mainWin.setHasShadow(true); 285 | SimMusicWindows.mainWin.setAlwaysOnTop(false); 286 | SimMusicWindows.mainWin.setSkipTaskbar(false); 287 | SimMusicWindows.mainWin.setOpacity(1); 288 | SimMusicWindows.mainWin.setMinimizable(true); 289 | SimMusicWindows.mainWin.setClosable(true); 290 | SimMusicWindows.mainWin.setTitleBarOverlay({color: "rgba(255,255,255,0)", symbolColor: "black", height: 35}); 291 | }, 50); 292 | return isMiniMode = false; 293 | } else { 294 | setTimeout(() => { 295 | SimMusicWindows.mainWin.unmaximize(); 296 | SimMusicWindows.mainWin.setMinimumSize(340, 60); 297 | SimMusicWindows.mainWin.setSize(340, 60); 298 | SimMusicWindows.mainWin.setResizable(false); 299 | SimMusicWindows.mainWin.setHasShadow(false); 300 | SimMusicWindows.mainWin.setAlwaysOnTop(true); 301 | SimMusicWindows.mainWin.setSkipTaskbar(true); 302 | SimMusicWindows.mainWin.setPosition(width - 360, height - 90); 303 | SimMusicWindows.mainWin.setOpacity(.98); 304 | SimMusicWindows.mainWin.setMinimizable(false); 305 | SimMusicWindows.mainWin.setClosable(false); 306 | SimMusicWindows.mainWin.setTitleBarOverlay({color: "rgba(0,0,0,0)", symbolColor: "rgba(255,255,255,0)", height: 10}); 307 | }, 50); 308 | return isMiniMode = true; 309 | } 310 | }); 311 | 312 | 313 | 314 | 315 | // 文件格式关联 316 | const fileRegAppId = "com.simsv.music"; 317 | const fileRegFileExt = [".mp3", ".flac", ".wav"]; 318 | const appPath = process.execPath; 319 | const batchPath = path.join(os.tmpdir(), "sim-music-operations.bat"); 320 | const requestAdminCmd = ` 321 | @echo off 322 | net session >nul 2>&1 323 | if %errorLevel% neq 0 ( 324 | powershell.exe -Command "Start-Process '%~0' -Verb RunAs" 325 | exit /B 326 | ) 327 | `; 328 | function registerFileExt(isReg) { 329 | let commands = requestAdminCmd; 330 | if (isReg) { 331 | commands += `REG ADD "HKEY_CLASSES_ROOT\\${fileRegAppId}\\shell\\open\\command" /ve /d "\\"${appPath}\\" \\"%%1\\"" /f\n`; 332 | commands += `REG ADD "HKEY_CLASSES_ROOT\\${fileRegAppId}\\DefaultIcon" /ve /d "\\"${path.dirname(appPath)}\\resources\\file-icon.ico\\",0" /f\n`; 333 | fileRegFileExt.forEach(ext => { 334 | commands += `REG ADD "HKEY_CLASSES_ROOT\\${ext}" /ve /d "${fileRegAppId}" /f\n`; 335 | }); 336 | } else { 337 | commands += `REG DELETE "HKEY_CLASSES_ROOT\\${fileRegAppId}\\shell\\open\\command" /ve /f\n`; 338 | } 339 | fs.writeFileSync(batchPath, commands, { encoding: "utf-8" }); 340 | try { exec(`cmd.exe /c "${batchPath}"`); } catch {} 341 | } 342 | ipcMain.handle("regFileExt", (_event, isReg) => { 343 | return registerFileExt(isReg); 344 | }); 345 | 346 | 347 | 348 | 349 | // 本体更新 350 | ipcMain.handle("appUpdate", () => { 351 | let commands = ` 352 | ${requestAdminCmd} 353 | title SimMusic Updater 354 | echo Updating SimMusic, Please wait ... 355 | echo The updating process will be finished in a few seconds. 356 | timeout /t 2 /nobreak 357 | taskkill /im sim-music.exe 358 | taskkill /im sim-music-dev.exe 359 | timeout /t 2 /nobreak 360 | move /Y "${path.join(os.tmpdir(), "sim-music-update.simtemp")}" "${path.dirname(appPath)}\\resources\\app.asar" 361 | timeout /t 2 /nobreak 362 | start "" "${appPath}"`; 363 | fs.writeFileSync(batchPath, commands, { encoding: "utf-8" }); 364 | try { exec(`cmd.exe /c "${batchPath}"`); } catch {} 365 | setTimeout(() => {app.exit();}, 1000); 366 | }); 367 | 368 | 369 | 370 | // 主窗口其他调用 371 | ipcMain.handle("pickFolder", () => { 372 | return dialog.showOpenDialogSync(SimMusicWindows.mainWin, { 373 | title: "选择目录 - SimMusic", 374 | defaultPath: "C:\\", 375 | buttonLabel: "使用此目录", 376 | properties: ["openDirectory"], 377 | }); 378 | }); 379 | ipcMain.handle("shutdownCountdown", () => { 380 | const countdown = new BrowserWindow({ 381 | frame: false, 382 | resizable: false, 383 | kiosk: true, 384 | transparent: true, 385 | alwaysOnTop: true, 386 | skipTaskbar: true, 387 | parent: SimMusicWindows.mainWin, 388 | modal: true, 389 | webPreferences: { webSecurity: false, nodeIntegration: true, contextIsolation: false } 390 | }); 391 | countdown.loadURL(path.join(__dirname, "frontend/assets/components/shutdown.html")); 392 | }); 393 | ipcMain.handle("cmd", (_event, cmd) => { 394 | exec(cmd); 395 | }); 396 | ipcMain.handle("mainWinExec", (_event, js) => { 397 | SimMusicWindows.mainWin.webContents.executeJavaScript(js); 398 | }); 399 | ipcMain.handle("openDevtools", () => { 400 | SimMusicWindows.mainWin.webContents.openDevTools(); 401 | // 傻逼谷歌搞个宋体当默认代码字体 怎么想的 给你眼珠子扣下来踩两脚 402 | SimMusicWindows.mainWin.webContents.once("devtools-opened", () => { 403 | const css = ` 404 | :root { 405 | --sys-color-base: var(--ref-palette-neutral100); 406 | --source-code-font-family: consolas; 407 | --source-code-font-size: 12px; 408 | --monospace-font-family: consolas; 409 | --monospace-font-size: 12px; 410 | --default-font-family: system-ui, sans-serif; 411 | --default-font-size: 12px; 412 | } 413 | .-theme-with-dark-background { 414 | --sys-color-base: var(--ref-palette-secondary25); 415 | } 416 | body { 417 | --default-font-family: system-ui,sans-serif; 418 | }`; 419 | SimMusicWindows.mainWin.webContents.devToolsWebContents.executeJavaScript(` 420 | const overriddenStyle = document.createElement('style'); 421 | overriddenStyle.innerHTML = '${css.replaceAll('\n', ' ')}'; 422 | document.body.append(overriddenStyle); 423 | document.body.classList.remove('platform-windows');`); 424 | }); 425 | }); 426 | 427 | 428 | 429 | -------------------------------------------------------------------------------- /src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sim-music", 3 | "version": "1.0.0", 4 | "description": "SimMusic", 5 | "main": "main.js", 6 | "author": "Simsv Studio", 7 | "dependencies": { 8 | "fflate": "^0.8.2", 9 | "flac-tagger": "^1.0.7", 10 | "music-metadata": "^7.13.5", 11 | "node-id3": "^0.2.6" 12 | } 13 | } 14 | --------------------------------------------------------------------------------