-
Notifications
You must be signed in to change notification settings - Fork 356
Commit
Next version
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -95,4 +95,8 @@ tmp | |
|
||
plugins | ||
|
||
.VSCodeCounter | ||
.VSCodeCounter | ||
|
||
.idea/ | ||
|
||
undefinedelectron-log-preload.js |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,16 @@ | ||
{ | ||
"version": "0.0.5", | ||
"version": "0.0.6", | ||
"changeLog": [ | ||
"【修复】修复重启软件后歌单丢失的问题;如果未出现上述问题可忽略此版本更新", | ||
"--- 以下为 0.0.4 更新 ---", | ||
"1. 【功能】播放列表支持拖拽排序", | ||
"2. 【功能】支持多语言。本次支持简体中文、繁体中文、英文、西班牙语", | ||
"3. 【功能】支持歌词翻译功能(需要插件实现getLyric方法)", | ||
"4. 【功能】新增最近播放,默认保存最近播放的 500 首歌", | ||
"5. 【功能】新增小窗模式", | ||
"6. 【功能】新增音频设备移除时的行为设置,现在可以让拔掉耳机的时候停止播放了", | ||
"7. 【功能】新增单独的主题页,可以在主题市场中直接使用主题;本地.mftheme主题可以直接拖拽到播放器安装", | ||
"8. 【优化】本地音乐会尝试读取本地路径下的同名 .lrc 文件作为歌词;同时会读取同名 -tr.lrc 文件作为翻译", | ||
"9. 【修复】修复部分情况下本地歌词无法读取的问题", | ||
"10. 【修复】修复 linux 托盘点击无效的问题" | ||
"⚠ 此版本有大量代码重构,小伙伴们谨慎更新", | ||
"1. 【优化】大量代码重构", | ||
"2. 【功能】新增播放失败时不寻找其他音质版本的配置", | ||
"3. 【功能】歌单内支持通过 ctrl 键盘多选歌曲批量操作", | ||
"4. 【功能】支持自定义主窗口大小", | ||
"5. 【功能】支持自定义歌词窗口大小", | ||
"6. 【功能】调整播放详情页面的样式", | ||
"7. 【功能】支持了评论功能(需要插件支持)", | ||
"8. 【修复】修复了歌单id无法带特殊字符的问题", | ||
"9. 【修复】修复了音源太多时布局异常的问题" | ||
], | ||
"download": ["https://www.123pan.com/s/grz2jv-CjmAA.html"] | ||
"download": ["https://r0rvr854dd1.feishu.cn/drive/folder/IrVEfD67KlWZGkdqwjecLHFNnBb?from=from_copylink"] | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
const http = require("http"); | ||
const https = require("https"); | ||
|
||
|
||
const defaultPort = 52735; | ||
const maxRetries = 20; | ||
|
||
let retryCount = 0; | ||
|
||
function forwardRequest(clientRes, url, method, headers) { | ||
const options = { | ||
method: method, | ||
headers: headers, | ||
}; | ||
|
||
const protocol = url.startsWith("https") ? https : http; | ||
|
||
const req = protocol.request(url, options, (targetRes) => { | ||
// 将目标响应的状态码和头部转发到客户端 | ||
clientRes.writeHead(targetRes.statusCode, targetRes.headers); | ||
|
||
// 将目标响应的数据流转发到客户端 | ||
targetRes.pipe(clientRes, { | ||
end: true, | ||
}); | ||
}); | ||
|
||
req.on("error", (error) => { | ||
console.error("Error forwarding request:", error); | ||
clientRes.writeHead(500, {"Content-Type": "text/plain"}); | ||
clientRes.end("Internal Server Error"); | ||
}); | ||
|
||
// 结束目标请求 | ||
req.end(); | ||
} | ||
|
||
|
||
function safeParse(data) { | ||
try { | ||
return JSON.parse(data) || {}; | ||
} catch (e) { | ||
return {}; | ||
} | ||
} | ||
|
||
|
||
function startServer(port) { | ||
|
||
// 创建一个 HTTP 服务器 | ||
const server = http.createServer((req, res) => { | ||
if (req.method !== "GET") { | ||
res.writeHead(405, {"Content-Type": "text/plain"}); | ||
return res.end("Only GET requests are allowed"); | ||
} | ||
|
||
if (req.url === "/heartbeat") { | ||
res.writeHead(200, {"Content-Type": "text/plain"}); | ||
return res.end("OK"); | ||
} | ||
|
||
const query = new URLSearchParams(req.url.slice(1)); | ||
|
||
|
||
const url = query.get("url"); | ||
const method = query.get("method") || "GET"; // 默认使用 GET 方法 | ||
const headers = safeParse(query.get("headers")); | ||
|
||
res.setHeader("Access-Control-Allow-Origin", "*"); // 允许所有源 | ||
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS"); // 允许的方法 | ||
|
||
if (!url) { | ||
res.writeHead(400, {"Content-Type": "text/plain"}); | ||
return res.end("Bad Request: Missing URL"); | ||
} | ||
|
||
forwardRequest(res, url, method, { | ||
...(req.headers || {}), | ||
...(headers || {}) | ||
}); | ||
}); | ||
|
||
server.listen(port, () => { | ||
process.send?.({ | ||
type: "port", | ||
port | ||
}); | ||
console.log(`Proxy server is running on http://localhost:${port}`); | ||
}); | ||
|
||
server.on("error", (err) => { | ||
console.error("Server error:", err); | ||
if (retryCount < maxRetries) { | ||
retryCount++; | ||
const newPort = port + 1; // 尝试下一个端口 | ||
console.log(`Retrying on port: ${newPort} (attempt ${retryCount})`); | ||
startServer(newPort); | ||
} else { | ||
process.send?.({type: "error", error: "Max retries reached"}); | ||
} | ||
}) | ||
} | ||
|
||
|
||
startServer(defaultPort); |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { app } from "electron"; | ||
import path from "path"; | ||
|
||
const resPath = app.isPackaged | ||
? path.resolve(process.resourcesPath, "res") | ||
: path.resolve(__dirname, "../../res"); | ||
|
||
export default (resourceName: string) => { | ||
return path.resolve(resPath, resourceName); | ||
}; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
import {BrowserWindow, nativeImage} from "electron"; | ||
import getResourcePath from "@/common/main/get-resource-path"; | ||
import {t} from "@shared/i18n/main"; | ||
import {ResourceName} from "@/common/constant"; | ||
import asyncMemoize from "@/common/async-memoize"; | ||
import fs from "fs/promises"; | ||
import logger from "@shared/logger/main"; | ||
import axios from "axios"; | ||
import messageBus from "@shared/message-bus/main"; | ||
|
||
/** | ||
* 设置缩略图按钮 | ||
* @param window 当前窗口 | ||
* @param isPlaying 当前是否正在播放音乐 | ||
*/ | ||
function setThumbBarButtons(window: BrowserWindow, isPlaying?: boolean) { | ||
if (!window) { | ||
return; | ||
} | ||
|
||
window.setThumbarButtons([ | ||
{ | ||
icon: nativeImage.createFromPath(getResourcePath(ResourceName.SKIP_LEFT_ICON)), | ||
tooltip: t("main.previous_music"), | ||
click() { | ||
messageBus.sendCommand("SkipToPrevious"); | ||
}, | ||
}, | ||
{ | ||
icon: nativeImage.createFromPath( | ||
getResourcePath(isPlaying ? ResourceName.PAUSE_ICON : ResourceName.PLAY_ICON) | ||
), | ||
tooltip: isPlaying | ||
? t("media.music_state_pause") | ||
: t("media.music_state_play"), | ||
click() { | ||
messageBus.sendCommand( | ||
"TogglePlayerState" | ||
); | ||
}, | ||
}, | ||
{ | ||
icon: nativeImage.createFromPath(getResourcePath(ResourceName.SKIP_RIGHT_ICON)), | ||
tooltip: t("main.next_music"), | ||
click() { | ||
messageBus.sendCommand("SkipToNext"); | ||
}, | ||
}, | ||
]); | ||
|
||
} | ||
|
||
|
||
// 获取默认的图片 | ||
const getDefaultAlbumCoverImage = asyncMemoize(async () => { | ||
return await fs.readFile((getResourcePath(ResourceName.DEFAULT_ALBUM_COVER_IMAGE))); | ||
}) | ||
|
||
let hookedFlag = false; | ||
|
||
/** | ||
* 设置缩略图 | ||
* @param window 窗口 | ||
* @param src 图片url | ||
*/ | ||
async function setThumbImage(window: BrowserWindow, src: string) { | ||
if (!window) { | ||
return; | ||
} | ||
|
||
// only support windows | ||
if (process.platform !== "win32") { | ||
return; | ||
} | ||
|
||
try { | ||
const hwnd = window.getNativeWindowHandle().readBigUInt64LE(0); | ||
|
||
const taskBarThumbManager = (await import("@native/TaskbarThumbnailManager/TaskbarThumbnailManager.node")).default; | ||
|
||
if (!hookedFlag) { | ||
taskBarThumbManager.config(hwnd); | ||
hookedFlag = true; | ||
} | ||
|
||
let buffer: Buffer; | ||
if (!src) { | ||
buffer = await getDefaultAlbumCoverImage(); | ||
} else if (src.startsWith("http")) { | ||
try { | ||
buffer = ( | ||
await axios.get(src, { | ||
responseType: "arraybuffer", | ||
}) | ||
).data; | ||
} catch { | ||
buffer = await getDefaultAlbumCoverImage(); | ||
} | ||
} else if (src.startsWith("data:image")) { | ||
buffer = Buffer.from(src.split(";base64,").pop(), "base64"); | ||
} else { | ||
buffer = await getDefaultAlbumCoverImage(); | ||
} | ||
|
||
const size = 106; | ||
|
||
const sharp = (await import("sharp")).default; | ||
const result = await sharp(buffer) | ||
.resize(size, size, { | ||
fit: "cover", | ||
}) | ||
.png() | ||
.ensureAlpha(1) | ||
.raw() | ||
.toBuffer({ | ||
resolveWithObject: true, | ||
}); | ||
|
||
taskBarThumbManager.sendIconicRepresentation( | ||
hwnd, | ||
{ | ||
width: size, | ||
height: size, | ||
}, | ||
result.data | ||
); | ||
} catch (ex) { | ||
logger.logError("Fail to setThumbImage", ex); | ||
} | ||
|
||
|
||
} | ||
|
||
|
||
const ThumbBarManager = { | ||
setThumbBarButtons, | ||
setThumbImage | ||
}; | ||
|
||
export default ThumbBarManager; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// eslint-disable-next-line @typescript-eslint/no-empty-function | ||
export default () => {}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import AppConfig from "@shared/app-config/renderer"; | ||
import {IAppConfig} from "@/types/app-config"; | ||
import {useEffect, useState} from "react"; | ||
|
||
|
||
export default function useAppConfig<K extends keyof IAppConfig>(configKey: K): IAppConfig[K] { | ||
const [state, setState] = useState<IAppConfig[K]>(AppConfig.getConfig(configKey)); | ||
|
||
useEffect(() => { | ||
const callback = (patch: IAppConfig, fullConfig: IAppConfig) => { | ||
if (configKey in patch) { | ||
setState(fullConfig[configKey]); | ||
} | ||
}; | ||
|
||
AppConfig.onConfigUpdate(callback); | ||
|
||
return () => { | ||
AppConfig.offConfigUpdate(callback); | ||
}; | ||
}, []); | ||
|
||
|
||
return state; | ||
} |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
import {supportLocalMediaType} from "@/common/constant"; | ||
import {parseLocalMusicItem, safeStat} from "@/common/file-util"; | ||
import PluginManager from "@shared/plugin-manager/main"; | ||
import voidCallback from "@/common/void-callback"; | ||
import messageBus from "@shared/message-bus/main"; | ||
|
||
export function handleDeepLink(url: string) { | ||
if (!url) { | ||
return; | ||
} | ||
|
||
try { | ||
const urlObj = new URL(url); | ||
if (urlObj.protocol === "musicfree:") { | ||
handleMusicFreeScheme(urlObj); | ||
} | ||
} catch { | ||
// pass | ||
} | ||
} | ||
|
||
async function handleMusicFreeScheme(url: URL) { | ||
const hostname = url.hostname; | ||
if (hostname === "install") { | ||
try { | ||
const pluginUrlStr = | ||
url.pathname.slice(1) || url.searchParams.get("plugin"); | ||
const pluginUrls = pluginUrlStr.split(",").map(decodeURIComponent); | ||
await Promise.all( | ||
pluginUrls.map((it) => PluginManager.installPluginFromRemoteUrl(it).catch(voidCallback)) | ||
); | ||
} catch { | ||
// pass | ||
} | ||
} | ||
} | ||
|
||
async function handleBareUrl(url: string) { | ||
try { | ||
if ( | ||
(await safeStat(url)).isFile() && | ||
supportLocalMediaType.some((postfix) => url.endsWith(postfix)) | ||
) { | ||
const musicItem = await parseLocalMusicItem(url); | ||
messageBus.sendCommand("PlayMusic", musicItem); | ||
} | ||
} catch { | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,165 +1,279 @@ | ||
import { app, BrowserWindow, globalShortcut } from "electron"; | ||
import { | ||
createLyricWindow, | ||
createMainWindow, | ||
getLyricWindow, | ||
getMainWindow, | ||
showMainWindow, | ||
} from "./window"; | ||
import setupIpcMain, { handleProxy } from "./ipc"; | ||
import { setupPluginManager } from "./core/plugin-manager"; | ||
import { setupTray, setupTrayMenu } from "./tray"; | ||
import { setupGlobalShortCut } from "./core/global-short-cut"; | ||
import {app, BrowserWindow, globalShortcut} from "electron"; | ||
import fs from "fs"; | ||
import path from "path"; | ||
import { setAutoFreeze } from "immer"; | ||
import setThumbbarBtns from "./utils/set-thumbbar-btns"; | ||
import { currentMusicInfoStore } from "./store/current-music"; | ||
// TODO: Main process should not depends on files in renderer process | ||
import { PlayerState } from "@/renderer/core/track-player/enum"; | ||
|
||
import { | ||
getAppConfigPath, | ||
getAppConfigPathSync, | ||
setAppConfigPath, | ||
setupMainAppConfig, | ||
} from "@/shared/app-config/main"; | ||
import { setupGlobalContext } from "@/shared/global-context/main"; | ||
import { setupI18n } from "@/shared/i18n/main"; | ||
import { | ||
createMiniModeWindow, | ||
getMinimodeWindow, | ||
showMinimodeWindow, | ||
} from "./window/minimode-window"; | ||
import { handleDeepLink } from "./utils/deep-link"; | ||
|
||
// Handle creating/removing shortcuts on Windows when installing/uninstalling. | ||
// if (require("electron-squirrel-startup")) { | ||
// app.quit(); | ||
// } | ||
import {setAutoFreeze} from "immer"; | ||
import {setupGlobalContext} from "@/shared/global-context/main"; | ||
import {setupI18n} from "@/shared/i18n/main"; | ||
import {handleDeepLink} from "./deep-link"; | ||
import logger from "@shared/logger/main"; | ||
import {PlayerState} from "@/common/constant"; | ||
import ThumbBarUtil from "@/common/main/thumb-bar-util"; | ||
import windowManager from "@main/window-manager"; | ||
import AppConfig from "@shared/app-config/main"; | ||
import TrayManager from "@main/tray-manager"; | ||
import WindowDrag from "@shared/window-drag/main"; | ||
import {IAppConfig} from "@/types/app-config"; | ||
import axios from "axios"; | ||
import {HttpsProxyAgent} from "https-proxy-agent"; | ||
import PluginManager from "@shared/plugin-manager/main"; | ||
import ServiceManager from "@shared/service-manager/main"; | ||
import utils from "@shared/utils/main"; | ||
import messageBus from "@shared/message-bus/main"; | ||
import shortCut from "@shared/short-cut/main"; | ||
import voidCallback from "@/common/void-callback"; | ||
|
||
// portable | ||
if (process.platform === "win32") { | ||
try { | ||
const appPath = app.getPath("exe"); | ||
const portablePath = path.resolve(appPath, "../portable"); | ||
const portableFolderStat = fs.statSync(portablePath); | ||
if (portableFolderStat.isDirectory()) { | ||
const appPathNames = ["appData", "userData"]; | ||
appPathNames.forEach((it) => { | ||
app.setPath(it, path.resolve(portablePath, it)); | ||
}); | ||
try { | ||
const appPath = app.getPath("exe"); | ||
const portablePath = path.resolve(appPath, "../portable"); | ||
const portableFolderStat = fs.statSync(portablePath); | ||
if (portableFolderStat.isDirectory()) { | ||
const appPathNames = ["appData", "userData"]; | ||
appPathNames.forEach((it) => { | ||
app.setPath(it, path.resolve(portablePath, it)); | ||
}); | ||
} | ||
} catch (e) { | ||
// pass | ||
} | ||
} catch (e) { | ||
// console.log(e) | ||
} | ||
} | ||
|
||
setAutoFreeze(false); | ||
|
||
|
||
if (process.defaultApp) { | ||
if (process.argv.length >= 2) { | ||
app.setAsDefaultProtocolClient("musicfree", process.execPath, [ | ||
path.resolve(process.argv[1]), | ||
]); | ||
} | ||
if (process.argv.length >= 2) { | ||
app.setAsDefaultProtocolClient("musicfree", process.execPath, [ | ||
path.resolve(process.argv[1]), | ||
]); | ||
} | ||
} else { | ||
app.setAsDefaultProtocolClient("musicfree"); | ||
app.setAsDefaultProtocolClient("musicfree"); | ||
} | ||
|
||
// Quit when all windows are closed, except on macOS. There, it's common | ||
// for applications and their menu bar to stay active until the user quits | ||
// explicitly with Cmd + Q. | ||
app.on("window-all-closed", () => { | ||
if (process.platform !== "darwin") { | ||
app.quit(); | ||
} | ||
if (process.platform !== "darwin") { | ||
app.quit(); | ||
} | ||
}); | ||
|
||
app.on("activate", () => { | ||
// On OS X it's common to re-create a window in the app when the | ||
// dock icon is clicked and there are no other windows open. | ||
if (BrowserWindow.getAllWindows().length === 0) { | ||
createMainWindow(); | ||
} | ||
// On OS X it's common to re-create a window in the app when the | ||
// dock icon is clicked and there are no other windows open. | ||
if (BrowserWindow.getAllWindows().length === 0) { | ||
windowManager.showMainWindow(); | ||
} | ||
}); | ||
|
||
if (!app.requestSingleInstanceLock()) { | ||
app.exit(0); | ||
app.exit(0); | ||
} | ||
|
||
app.on("second-instance", (_evt, commandLine) => { | ||
if (getMainWindow()) { | ||
showMainWindow(); | ||
} | ||
if (windowManager.mainWindow) { | ||
windowManager.showMainWindow(); | ||
} | ||
|
||
if (process.platform !== "darwin") { | ||
handleDeepLink(commandLine.pop()); | ||
} | ||
if (process.platform !== "darwin") { | ||
handleDeepLink(commandLine.pop()); | ||
} | ||
}); | ||
|
||
app.on("open-url", (_evt, url) => { | ||
handleDeepLink(url); | ||
handleDeepLink(url); | ||
}); | ||
|
||
app.on("will-quit", () => { | ||
globalShortcut.unregisterAll(); | ||
globalShortcut.unregisterAll(); | ||
}); | ||
|
||
// In this file you can include the rest of your app's specific main process | ||
// code. You can also put them in separate files and import them here. | ||
app.whenReady().then(async () => { | ||
await Promise.allSettled([setupGlobalContext(), setupMainAppConfig()]); | ||
setupI18n({ | ||
getDefaultLang() { | ||
return getAppConfigPathSync("normal.language"); | ||
}, | ||
onLanguageChanged(lang) { | ||
setAppConfigPath("normal.language", lang); | ||
setupTrayMenu(); | ||
if (process.platform === "win32") { | ||
setThumbbarBtns( | ||
currentMusicInfoStore.getValue().currentPlayerState === | ||
PlayerState.Playing | ||
); | ||
} | ||
}, | ||
}); | ||
setupIpcMain(); | ||
setupPluginManager(); | ||
setupTray(); | ||
bootstrap(); | ||
setupGlobalShortCut(); | ||
createMainWindow(); | ||
logger.logPerf("App Ready"); | ||
setupGlobalContext(); | ||
await AppConfig.setup(windowManager); | ||
|
||
await setupI18n({ | ||
getDefaultLang() { | ||
return AppConfig.getConfig("normal.language"); | ||
}, | ||
onLanguageChanged(lang) { | ||
AppConfig.setConfig({ | ||
"normal.language": lang | ||
}); | ||
if (process.platform === "win32") { | ||
|
||
ThumbBarUtil.setThumbBarButtons(windowManager.mainWindow, messageBus.getAppState().playerState === PlayerState.Playing) | ||
} | ||
}, | ||
}); | ||
utils.setup(windowManager); | ||
PluginManager.setup(windowManager); | ||
TrayManager.setup(windowManager); | ||
WindowDrag.setup(); | ||
shortCut.setup().then(voidCallback); | ||
logger.logPerf("Create Main Window"); | ||
// Setup message bus & app state | ||
messageBus.onAppStateChange((_, patch) => { | ||
if ("musicItem" in patch) { | ||
TrayManager.buildTrayMenu(); | ||
const musicItem = patch.musicItem; | ||
const mainWindow = windowManager.mainWindow; | ||
|
||
if (mainWindow) { | ||
const thumbStyle = AppConfig.getConfig("normal.taskbarThumb"); | ||
if (process.platform === "win32" && thumbStyle === "artwork") { | ||
ThumbBarUtil.setThumbImage(mainWindow, musicItem?.artwork); | ||
} | ||
if (musicItem) { | ||
mainWindow.setTitle( | ||
musicItem.title + (musicItem.artist ? ` - ${musicItem.artist}` : "") | ||
); | ||
} else { | ||
mainWindow.setTitle(app.name); | ||
} | ||
} | ||
} else if ("playerState" in patch) { | ||
TrayManager.buildTrayMenu(); | ||
const playerState = patch.playerState; | ||
|
||
if (process.platform === "win32") { | ||
ThumbBarUtil.setThumbBarButtons(windowManager.mainWindow, playerState === PlayerState.Playing) | ||
} | ||
} else if ("repeatMode" in patch) { | ||
TrayManager.buildTrayMenu(); | ||
} else if ("lyricText" in patch && process.platform === "darwin") { | ||
if (AppConfig.getConfig("lyric.enableStatusBarLyric")) { | ||
TrayManager.setTitle(patch.lyricText); | ||
} else { | ||
TrayManager.setTitle(""); | ||
} | ||
} | ||
}) | ||
|
||
messageBus.setup(windowManager); | ||
|
||
windowManager.showMainWindow(); | ||
|
||
bootstrap(); | ||
|
||
}); | ||
|
||
async function bootstrap() { | ||
const downloadPath = await getAppConfigPath("download.path"); | ||
if (!downloadPath) { | ||
setAppConfigPath("download.path", app.getPath("downloads")); | ||
} | ||
|
||
/** 一些初始化设置 */ | ||
// 初始化桌面歌词 | ||
getAppConfigPath("lyric.enableDesktopLyric").then((result) => { | ||
if (result) { | ||
if (!getLyricWindow()) { | ||
createLyricWindow(); | ||
} | ||
ServiceManager.setup(windowManager); | ||
|
||
const downloadPath = AppConfig.getConfig("download.path"); | ||
if (!downloadPath) { | ||
AppConfig.setConfig({ | ||
"download.path": app.getPath("downloads") | ||
}); | ||
} | ||
}); | ||
|
||
getAppConfigPath("private.minimode").then((enabled) => { | ||
if (enabled) { | ||
if (!getMinimodeWindow()) { | ||
showMinimodeWindow(); | ||
} | ||
const minimodeEnabled = AppConfig.getConfig("private.minimode"); | ||
|
||
if (minimodeEnabled) { | ||
windowManager.showMiniModeWindow(); | ||
} | ||
}); | ||
|
||
getAppConfigPath("network.proxy").then((result) => { | ||
if (result) { | ||
handleProxy(result); | ||
/** 一些初始化设置 */ | ||
// 初始化桌面歌词 | ||
const desktopLyricEnabled = AppConfig.getConfig("lyric.enableDesktopLyric"); | ||
|
||
if (desktopLyricEnabled) { | ||
windowManager.showLyricWindow(); | ||
} | ||
|
||
AppConfig.onConfigUpdated((patch) => { | ||
// 桌面歌词锁定状态 | ||
if ("lyric.lockLyric" in patch) { | ||
const lyricWindow = windowManager.lyricWindow; | ||
const lockState = patch["lyric.lockLyric"]; | ||
|
||
if (!lyricWindow) { | ||
return; | ||
} | ||
if (lockState) { | ||
lyricWindow.setIgnoreMouseEvents(true, { | ||
forward: true, | ||
}); | ||
} else { | ||
lyricWindow.setIgnoreMouseEvents(false); | ||
} | ||
} | ||
if ("shortCut.enableGlobal" in patch) { | ||
const enableGlobal = patch["shortCut.enableGlobal"]; | ||
if (enableGlobal) { | ||
shortCut.registerAllGlobalShortCuts(); | ||
} else { | ||
shortCut.unregisterAllGlobalShortCuts(); | ||
} | ||
} | ||
}) | ||
|
||
|
||
// 初始化代理 | ||
const proxyConfigKeys: Array<keyof IAppConfig> = [ | ||
"network.proxy.enabled", | ||
"network.proxy.host", | ||
"network.proxy.port", | ||
"network.proxy.username", | ||
"network.proxy.password" | ||
]; | ||
|
||
AppConfig.onConfigUpdated((patch, config) => { | ||
let proxyUpdated = false; | ||
for (const proxyConfigKey of proxyConfigKeys) { | ||
if (proxyConfigKey in patch) { | ||
proxyUpdated = true; | ||
break; | ||
} | ||
} | ||
|
||
if (proxyUpdated) { | ||
if (config["network.proxy.enabled"]) { | ||
handleProxy(true, config["network.proxy.host"], config["network.proxy.port"], config["network.proxy.username"], config["network.proxy.password"]); | ||
} else { | ||
handleProxy(false); | ||
} | ||
} | ||
}); | ||
|
||
handleProxy( | ||
AppConfig.getConfig("network.proxy.enabled"), | ||
AppConfig.getConfig("network.proxy.host"), | ||
AppConfig.getConfig("network.proxy.port"), | ||
AppConfig.getConfig("network.proxy.username"), | ||
AppConfig.getConfig("network.proxy.password") | ||
); | ||
|
||
|
||
} | ||
|
||
|
||
function handleProxy(enabled: boolean, host?: string | null, port?: string | null, username?: string | null, password?: string | null) { | ||
try { | ||
if (!enabled) { | ||
axios.defaults.httpAgent = undefined; | ||
axios.defaults.httpsAgent = undefined; | ||
} else if (host) { | ||
const proxyUrl = new URL(host); | ||
proxyUrl.port = port; | ||
proxyUrl.username = username; | ||
proxyUrl.password = password; | ||
const agent = new HttpsProxyAgent(proxyUrl); | ||
|
||
axios.defaults.httpAgent = agent; | ||
axios.defaults.httpsAgent = agent; | ||
} else { | ||
throw new Error("Unknown Host"); | ||
} | ||
} catch (e) { | ||
axios.defaults.httpAgent = undefined; | ||
axios.defaults.httpsAgent = undefined; | ||
} | ||
}); | ||
} |
This file was deleted.
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,288 @@ | ||
import {app, Menu, MenuItem, MenuItemConstructorOptions, nativeImage, Tray} from "electron"; | ||
import {t} from "@shared/i18n/main"; | ||
import {IWindowManager} from "@/types/main/window-manager"; | ||
import getResourcePath from "@/common/main/get-resource-path"; | ||
import {PlayerState, RepeatMode, ResourceName} from "@/common/constant"; | ||
import AppConfig from "@shared/app-config/main"; | ||
import windowManager from "@main/window-manager"; | ||
import {IAppConfig} from "@/types/app-config"; | ||
import messageBus from "@shared/message-bus/main"; | ||
|
||
if (process.platform === "darwin") { | ||
Menu.setApplicationMenu( | ||
Menu.buildFromTemplate([ | ||
{ | ||
label: app.getName(), | ||
submenu: [ | ||
{ | ||
label: t("common.about"), | ||
role: "about", | ||
}, | ||
{ | ||
label: t("common.exit"), | ||
click() { | ||
app.quit(); | ||
}, | ||
}, | ||
], | ||
}, | ||
{ | ||
label: t("common.edit"), | ||
submenu: [ | ||
{ | ||
label: t("common.undo"), | ||
accelerator: "Command+Z", | ||
role: "undo", | ||
}, | ||
{ | ||
label: t("common.redo"), | ||
accelerator: "Shift+Command+Z", | ||
role: "redo", | ||
}, | ||
{type: "separator"}, | ||
{label: t("common.cut"), accelerator: "Command+X", role: "cut"}, | ||
{label: t("common.copy"), accelerator: "Command+C", role: "copy"}, | ||
{label: t("common.cut"), accelerator: "Command+V", role: "paste"}, | ||
{type: "separator"}, | ||
{ | ||
label: t("common.select_all"), | ||
accelerator: "Command+A", | ||
role: "selectAll", | ||
}, | ||
], | ||
}, | ||
]) | ||
); | ||
} else { | ||
Menu.setApplicationMenu(null); | ||
} | ||
|
||
class TrayManager { | ||
private static trayInstance: Tray | null = null; | ||
private windowManager: IWindowManager; | ||
|
||
private observedKey: Array<keyof IAppConfig> = [ | ||
"lyric.lockLyric", | ||
"lyric.enableDesktopLyric", | ||
"normal.language" | ||
] | ||
|
||
public setup(windowManager: IWindowManager) { | ||
this.windowManager = windowManager; | ||
const tray = new Tray( | ||
nativeImage.createFromPath(getResourcePath(ResourceName.LOGO_IMAGE)).resize({ | ||
width: 32, | ||
height: 32, | ||
}) | ||
); | ||
|
||
if (process.platform === "linux") { | ||
tray.on("click", () => { | ||
windowManager.showMainWindow(); | ||
}); | ||
} else { | ||
tray.on("double-click", () => { | ||
windowManager.showMainWindow(); | ||
}); | ||
} | ||
|
||
// 配置变化时更新菜单 | ||
AppConfig.onConfigUpdated((changedConfig) => { | ||
for (const k of this.observedKey) { | ||
if (k in changedConfig) { | ||
this.buildTrayMenu(); | ||
return; | ||
} | ||
} | ||
}) | ||
|
||
TrayManager.trayInstance = tray; | ||
this.buildTrayMenu(); | ||
} | ||
|
||
private openMusicDetail() { | ||
this.windowManager.showMainWindow(); | ||
messageBus.sendCommand("OpenMusicDetailPage"); | ||
} | ||
|
||
public async buildTrayMenu() { | ||
if (!TrayManager.trayInstance) { | ||
return; | ||
} | ||
const ctxMenu: Array<MenuItemConstructorOptions | MenuItem> = []; | ||
|
||
const tray = TrayManager.trayInstance; | ||
|
||
/********* 音乐信息 **********/ | ||
const {musicItem, playerState, repeatMode} = | ||
messageBus.getAppState(); | ||
// 更新一下tooltip | ||
if (musicItem) { | ||
tray.setToolTip( | ||
`${musicItem.title ?? t("media.unknown_title")}${ | ||
musicItem.artist ? ` - ${musicItem.artist}` : "" | ||
}` | ||
); | ||
} else { | ||
tray.setToolTip("MusicFree"); | ||
} | ||
if (musicItem) { | ||
const fullName = `${musicItem.title ?? t("media.unknown_title")}${ | ||
musicItem.artist ? ` - ${musicItem.artist}` : "" | ||
}`; | ||
ctxMenu.push( | ||
{ | ||
label: fullName.length > 12 ? fullName.slice(0, 12) + "..." : fullName, | ||
click: this.openMusicDetail.bind(this), | ||
}, | ||
{ | ||
label: `${t("media.media_platform")}: ${musicItem.platform}`, | ||
click: this.openMusicDetail.bind(this), | ||
} | ||
); | ||
} else { | ||
ctxMenu.push({ | ||
label: t("main.no_playing_music"), | ||
enabled: false, | ||
}); | ||
} | ||
|
||
ctxMenu.push( | ||
{ | ||
label: musicItem | ||
? playerState === PlayerState.Playing | ||
? t("media.music_state_pause") | ||
: t("media.music_state_play") | ||
: t("media.music_state_play_or_pause"), | ||
enabled: !!musicItem, | ||
click() { | ||
if (!musicItem) { | ||
return; | ||
} | ||
messageBus.sendCommand("TogglePlayerState"); | ||
}, | ||
}, | ||
{ | ||
label: t("main.previous_music"), | ||
enabled: !!musicItem, | ||
click() { | ||
messageBus.sendCommand("SkipToPrevious"); | ||
}, | ||
}, | ||
{ | ||
label: t("main.next_music"), | ||
enabled: !!musicItem, | ||
click() { | ||
messageBus.sendCommand("SkipToNext"); | ||
}, | ||
} | ||
); | ||
|
||
ctxMenu.push({ | ||
label: t("media.music_repeat_mode"), | ||
type: "submenu", | ||
submenu: Menu.buildFromTemplate([ | ||
{ | ||
label: t("media.music_repeat_mode_loop"), | ||
id: RepeatMode.Loop, | ||
type: "radio", | ||
checked: repeatMode === RepeatMode.Loop, | ||
click() { | ||
messageBus.sendCommand("SetRepeatMode", RepeatMode.Loop); | ||
}, | ||
}, | ||
{ | ||
label: t("media.music_repeat_mode_queue"), | ||
id: RepeatMode.Queue, | ||
type: "radio", | ||
checked: repeatMode === RepeatMode.Queue, | ||
click() { | ||
messageBus.sendCommand("SetRepeatMode", RepeatMode.Queue); | ||
}, | ||
}, | ||
{ | ||
label: t("media.music_repeat_mode_shuffle"), | ||
id: RepeatMode.Shuffle, | ||
type: "radio", | ||
checked: repeatMode === RepeatMode.Shuffle, | ||
click() { | ||
messageBus.sendCommand("SetRepeatMode", RepeatMode.Shuffle); | ||
}, | ||
}, | ||
]), | ||
}); | ||
|
||
ctxMenu.push({ | ||
type: "separator", | ||
}); | ||
/** TODO: 桌面歌词 */ | ||
// const lyricConfig = await getAppConfigPath("lyric"); | ||
// if (lyricConfig?.enableDesktopLyric) { | ||
// ctxMenu.push({ | ||
// label: t("main.close_desktop_lyric"), | ||
// click() { | ||
// setLyricWindow(false); | ||
// }, | ||
// }); | ||
// } else { | ||
// ctxMenu.push({ | ||
// label: t("main.open_desktop_lyric"), | ||
// click() { | ||
// setLyricWindow(true); | ||
// }, | ||
// }); | ||
// } | ||
// | ||
// if (lyricConfig?.lockLyric) { | ||
// ctxMenu.push({ | ||
// label: t("main.unlock_desktop_lyric"), | ||
// click() { | ||
// setDesktopLyricLock(false); | ||
// }, | ||
// }); | ||
// } else { | ||
// ctxMenu.push({ | ||
// label: t("main.lock_desktop_lyric"), | ||
// click() { | ||
// setDesktopLyricLock(true); | ||
// }, | ||
// }); | ||
// } | ||
|
||
ctxMenu.push({ | ||
type: "separator", | ||
}); | ||
/********* 其他操作 **********/ | ||
ctxMenu.push({ | ||
label: t("app_header.settings"), | ||
click() { | ||
windowManager.showMainWindow(); | ||
messageBus.sendCommand("Navigate", "/main/setting"); | ||
}, | ||
}); | ||
ctxMenu.push({ | ||
label: t("common.exit"), | ||
role: "quit", | ||
click() { | ||
app.exit(0); | ||
}, | ||
}); | ||
|
||
TrayManager.trayInstance.setContextMenu(Menu.buildFromTemplate(ctxMenu)); | ||
} | ||
|
||
public setTitle(title: string) { | ||
if (!title || !title.length) { | ||
TrayManager.trayInstance?.setTitle(""); | ||
return; | ||
} | ||
if (title.length > 7) { | ||
TrayManager.trayInstance?.setTitle(" " + title.slice(0) + "..."); | ||
} else { | ||
TrayManager.trayInstance?.setTitle(" " + title); | ||
} | ||
} | ||
|
||
} | ||
|
||
export default new TrayManager(); |
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.
This file was deleted.