Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,26 +22,18 @@

<!-- 这个 PR 做了什么、为什么这么做(动机 + 主要变更),尽量描述清楚 -->



## 关联 Issue

<!-- 如有关联,填 #编号;写 "Closes #编号" 可在合并时自动关闭对应 Issue -->



## 测试情况

<!-- 如何验证这些改动?手动复现步骤、覆盖到的平台(Windows / macOS / Linux)等 -->



## 截图 / 录屏

<!-- 涉及 UI 改动请附上;无则可删除本节 -->



## 自查清单

- [ ] 本 PR 只包含**一个主要功能 / 修复**,没有夹带无关改动
Expand Down
18 changes: 12 additions & 6 deletions electron/main/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ const configureMemoryOptimizations = (): void => {
const MEMORY_LOG_INTERVAL_MS = 10 * 60 * 1000;
/** 启动后首次采样延迟,避开启动期波动 */
const MEMORY_LOG_FIRST_DELAY_MS = 60 * 1000;
/** 启动后自动更新延迟,避免抢占首屏资源 */
const UPDATER_INIT_DELAY_MS = 2 * 60 * 1000;
/** 主窗口完成加载后恢复歌词辅助窗口,避免任务栏嵌入阻塞首屏 */
const LYRIC_WINDOWS_RESTORE_DELAY_MS = 1000;
/** 主窗口加载后再初始化系统媒体控制 */
const MEDIA_INIT_DELAY_MS = 500;

/** 记录各进程内存工作集,用于量化内存表现与防劣化对比 */
const logProcessMemory = (): void => {
Expand Down Expand Up @@ -107,7 +113,11 @@ export const initApp = (): void => {
// 注册 IPC
registerIpcHandlers();
// 创建主窗口
createMainWindow();
const mainWin = createMainWindow();
mainWin.webContents.once("did-finish-load", () => {
setTimeout(() => initMedia(), MEDIA_INIT_DELAY_MS);
setTimeout(() => restoreLyricWindows(), LYRIC_WINDOWS_RESTORE_DELAY_MS);
});
// 注册 orpheus 协议并处理冷启动唤起
initOrpheusRegistration();
const coldOrpheusUrl = extractOrpheusUrl(process.argv);
Expand All @@ -118,21 +128,17 @@ export const initApp = (): void => {
void initSongCache();
// 启动下载服务
void initDownload();
initMedia();
// 初始化 Last.fm 集成
initLastfm();
// 初始化插件系统
pluginRegistry.init();
// 初始化播放事件桥(需在 pluginRegistry.init 之后,读 hasEnabledControlPlugin)
initPlaybackBridge();
// 恢复歌词相关窗口
restoreLyricWindows();
// 注册全局快捷键
initGlobalHotkey();
// 启动外部 API 服务
void startServer();
// 初始化自动更新
initUpdater();
setTimeout(initUpdater, UPDATER_INIT_DELAY_MS);
// 周期记录各进程内存
setTimeout(logProcessMemory, MEMORY_LOG_FIRST_DELAY_MS);
setInterval(logProcessMemory, MEMORY_LOG_INTERVAL_MS);
Expand Down
15 changes: 12 additions & 3 deletions electron/main/window/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { store } from "@main/store";
import { handleCacheProtocolOnPartition } from "@main/utils/protocol";
import { isAppQuitting } from "@main/utils/lifecycle";
import { broadcast } from "@main/utils/broadcast";
import { coreLog } from "@main/utils/logger";

/** 主窗口 session */
const MAIN_PARTITION = "persist:main";
Expand All @@ -26,6 +27,7 @@ let mainWindow: BrowserWindow | null = null;
* 创建主窗口
*/
export const createMainWindow = (): BrowserWindow => {
const createdAt = Date.now();
const remember = store.get("system.rememberWindowState") ?? true;
const saved = remember ? store.get("windowStates.main") : undefined;

Expand All @@ -51,14 +53,21 @@ export const createMainWindow = (): BrowserWindow => {
// 初始化托盘
initTray();

// 自定义任务栏缩略图
enableTaskbarThumbnail(mainWindow);

// 缩略图工具栏
mainWindow.webContents.once("did-finish-load", () => {
coreLog.info(`主窗口 did-finish-load,用时 ${Date.now() - createdAt}ms`);
enableTaskbarThumbnail(mainWindow!);
initThumbar(mainWindow!);
});

mainWindow.webContents.once("dom-ready", () => {
coreLog.info(`主窗口 dom-ready,用时 ${Date.now() - createdAt}ms`);
});

mainWindow.webContents.once("did-fail-load", (_event, errorCode, errorDescription) => {
coreLog.warn(`主窗口加载失败 ${errorCode}: ${errorDescription}`);
});

// 每次加载完成应用界面缩放
mainWindow.webContents.on("did-finish-load", () => {
applyMainWindowZoom();
Expand Down
9 changes: 6 additions & 3 deletions scripts/build-native.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { spawnSync } from "node:child_process";
import { join } from "node:path";
import process from "node:process";

interface NativeModule {
Expand Down Expand Up @@ -80,6 +81,9 @@ const parseArgs = () => {

const napiArgs = ["--no-const-enum"];
const options = parseArgs();
const buildType = options.isDev ? "debug" : "release";
const projectRoot = process.cwd();
const napiCliPath = join(projectRoot, "node_modules", "@napi-rs", "cli", "dist", "cli.js");

if (!options.isDev) napiArgs.push("--release");
if (options.passing) napiArgs.push(...options.passing);
Expand All @@ -90,12 +94,11 @@ for (const mod of modules) {
}
const cwd = `native/${mod.name}`;

const buildType = options.isDev ? "debug" : "release";
console.log(`[BuildNative] 构建 ${mod.name} (${buildType})`);

const result = spawnSync("napi", ["build", ...napiArgs], {
const result = spawnSync(process.execPath, [napiCliPath, "build", ...napiArgs], {
stdio: "inherit",
shell: process.platform === "win32",
shell: false,
cwd,
});

Expand Down
13 changes: 12 additions & 1 deletion src/core/player/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -805,22 +805,31 @@ export const moveInQueue = (fromIndex: number, toIndex: number): void => {
let unsubscribe: (() => void) | null = null;
let initialized = false;

const logInitStep = (startedAt: number, step: string): void => {
console.info(`[player] init ${step} ${Math.round(performance.now() - startedAt)}ms`);
};

/** 初始化播放器 */
export const initPlayer = async (): Promise<void> => {
if (initialized) return;
initialized = true;
console.log("[player] init");
const startedAt = performance.now();
console.info("[player] init");
// 先从主进程同步后端配置,确保 system 设置可用
const settings = useSettingsStore();
await settings.syncSystem();
logInitStep(startedAt, "settings synced");
// 流媒体 store 必须在恢复队列前就绪,否则队列里的 streaming track 拿不到 cfg
await useStreamingStore().init();
logInitStep(startedAt, "streaming initialized");
// 插件 store 同理:在线歌曲 URL 兜底走插件,列表必须在 loadTrack 前就绪
void usePluginsStore().load();
await queue.restoreQueue();
logInitStep(startedAt, "queue restored");
const status = useStatusStore();
// 恢复上次的音量和播放模式到主进程
await window.api.player.setVolume(status.volume);
logInitStep(startedAt, "player backend ready");
syncPlayMode();
// 应用渐入渐出配置
const { fadeEnabled, fadeDuration, loudnessNormalization, equalizer } = settings.system.player;
Expand All @@ -835,6 +844,7 @@ export const initPlayer = async (): Promise<void> => {
}
// 刷新设备列表并恢复上次选择的输出设备
await refreshDevices();
logInitStep(startedAt, "devices refreshed");
if (settings.player.outputDevice) {
await window.api.player.setOutputDevice(settings.player.outputDevice);
}
Expand Down Expand Up @@ -883,6 +893,7 @@ export const initPlayer = async (): Promise<void> => {
} else {
status.state = "idle";
}
logInitStep(startedAt, "done");
};

/** 清理事件订阅 */
Expand Down
11 changes: 11 additions & 0 deletions src/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@ import { useMediaStore } from "@/stores/media";
import { useSettingsStore } from "@/stores/settings";
import { useOrpheusProtocol } from "@/composables/useOrpheusProtocol";

const PlayerBar = defineAsyncComponent(() => import("@/components/player/PlayerBar.vue"));
const FullPlayer = defineAsyncComponent(() => import("@/components/player/FullPlayer/index.vue"));
const SettingsDialog = defineAsyncComponent(
() => import("@/components/settings/SettingsDialog.vue"),
);
const UpdateDialog = defineAsyncComponent(() => import("@/components/modals/UpdateDialog.vue"));
const SPerformanceMonitor = defineAsyncComponent(
() => import("@/components/SPerformanceMonitor.vue"),
);
const SDialogProvider = defineAsyncComponent(() => import("@/components/ui/SDialogProvider.vue"));

const route = useRoute();
const status = useStatusStore();
const settings = useSettingsStore();
Expand Down
12 changes: 11 additions & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ import { initPlayer } from "./core/player";
import { installHotkeyManager } from "./core/hotkey/manager";
import { vRipple } from "./directives/ripple";

const bootStart = performance.now();
const logBoot = (stage: string): void => {
console.info(`[boot] ${stage} ${Math.round(performance.now() - bootStart)}ms`);
};

const pinia = createPinia();
pinia.use(piniaPersistedstate);

Expand Down Expand Up @@ -40,8 +45,10 @@ const SPLASH_ANIM_MS = 2050;

// 初始化程序
router.isReady().then(() => {
logBoot("router ready");
// 挂载应用
app.mount("#app");
logBoot("app mounted");
// 笔画播完即淡出
const remaining = Math.max(0, SPLASH_ANIM_MS - performance.now());
setTimeout(() => {
Expand All @@ -50,9 +57,12 @@ router.isReady().then(() => {
loading.classList.add("hidden");
loading.addEventListener("transitionend", () => loading.remove(), { once: true });
}
logBoot("splash hidden");
}, remaining);
// 初始化播放器
initPlayer().catch(console.error);
initPlayer()
.then(() => logBoot("player ready"))
.catch(console.error);
// 初始化快捷键
useHotkeyStore()
.init()
Expand Down
7 changes: 5 additions & 2 deletions src/stores/streaming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ export const useStreamingStore = defineStore("streaming", () => {
const playlists = shallowRef<Playlist[]>([]);
/** 缓存最后更新时间(ms) */
const lastFetchedAt = ref(0);
/** 是否已加载服务器配置 */
const initialized = ref(false);
/** 是否已从 IndexedDB 完成首次水合 */
const hydrated = ref(false);

Expand Down Expand Up @@ -568,14 +570,15 @@ export const useStreamingStore = defineStore("streaming", () => {
};

const init = async (): Promise<void> => {
if (hydrated.value) return;
if (initialized.value) return;
initialized.value = true;
const result = await window.api.streaming.loadServers();
servers.value = result.servers;
activeServerId.value = result.activeServerId;
if (activeServerId.value && !servers.value.find((s) => s.id === activeServerId.value)) {
activeServerId.value = null;
}
await hydrateFromCache();
void hydrateFromCache();
if (activeServerId.value) void connectToServer(activeServerId.value);
};

Expand Down
9 changes: 7 additions & 2 deletions src/stores/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import i18n from "@/i18n";

const { t } = i18n.global;

/** 启动后自动检查更新延迟,避免抢占首屏加载 */
const AUTO_CHECK_DELAY_MS = 2 * 60 * 1000;

export const useUpdateStore = defineStore("update", () => {
/** 当前阶段 */
const phase = ref<UpdatePhase>("idle");
Expand Down Expand Up @@ -56,8 +59,10 @@ export const useUpdateStore = defineStore("update", () => {
// 订阅主进程推送的更新事件
const unsubscribe = window.api.update.onEvent(handleEvent);
onScopeDispose(unsubscribe);
// 触发启动检查
void window.api.update.check(false);
const autoCheckTimer = window.setTimeout(() => {
void window.api.update.check(false);
}, AUTO_CHECK_DELAY_MS);
onScopeDispose(() => window.clearTimeout(autoCheckTimer));

/** 手动检查更新 */
const checkManually = (): void => {
Expand Down