mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-09-20 21:50:11 +08:00
Merge branch 'Erotica' of gitee.com:xiyidaiwa/spark-store into Erotica
# Conflicts: # src/App.vue
This commit is contained in:
@@ -1269,7 +1269,6 @@ ipcMain.handle("show-apm-install-dialog", async (event) => {
|
|||||||
return { success: false, cancelled: true };
|
return { success: false, cancelled: true };
|
||||||
});
|
});
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"uninstall-installed",
|
"uninstall-installed",
|
||||||
async (
|
async (
|
||||||
@@ -1311,14 +1310,14 @@ ipcMain.handle(
|
|||||||
? "卸载完成"
|
? "卸载完成"
|
||||||
: stderr || stdout || `卸载失败,退出码 ${code}`,
|
: stderr || stdout || `卸载失败,退出码 ${code}`,
|
||||||
};
|
};
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
interface LaunchAppPayload {
|
interface LaunchAppPayload {
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
origin?: "spark" | "apm";
|
origin?: "spark" | "apm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
ipcMain.handle(
|
ipcMain.handle(
|
||||||
"launch-app",
|
"launch-app",
|
||||||
async (
|
async (
|
||||||
|
|||||||
@@ -357,6 +357,16 @@ function saveWindowState(state: WindowState): void {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let saveBoundsTimer: NodeJS.Timeout | null = null;
|
let saveBoundsTimer: NodeJS.Timeout | null = null;
|
||||||
|
function flushSaveBounds(): void {
|
||||||
|
if (saveBoundsTimer) {
|
||||||
|
clearTimeout(saveBoundsTimer);
|
||||||
|
saveBoundsTimer = null;
|
||||||
|
}
|
||||||
|
if (win && !win.isDestroyed()) {
|
||||||
|
const { width, height, x, y } = win.getBounds();
|
||||||
|
saveWindowState({ width, height, x, y, maximized: win.isMaximized() });
|
||||||
|
}
|
||||||
|
}
|
||||||
function scheduleSaveBounds(winInstance: BrowserWindow): void {
|
function scheduleSaveBounds(winInstance: BrowserWindow): void {
|
||||||
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
|
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
|
||||||
saveBoundsTimer = setTimeout(() => {
|
saveBoundsTimer = setTimeout(() => {
|
||||||
@@ -372,6 +382,11 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void {
|
|||||||
}, 400);
|
}, 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 应用退出前立即持久化(防抖 400ms 可能在快速关闭时丢失最后一次状态)
|
||||||
|
app.on("before-quit", () => {
|
||||||
|
flushSaveBounds();
|
||||||
|
});
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
const saved = loadWindowState();
|
const saved = loadWindowState();
|
||||||
// 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中,
|
// 恢复时:若上次窗口过大(>1600x900,通常为全屏/最大化),回退到默认尺寸并居中,
|
||||||
|
|||||||
+237
-173
@@ -897,10 +897,24 @@ const fetchAppFromStore = async (
|
|||||||
try {
|
try {
|
||||||
const arch = window.apm_store.arch || "amd64";
|
const arch = window.apm_store.arch || "amd64";
|
||||||
const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
const finalArch = origin === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${category}/${pkgname}/app.json`;
|
// 路径参数需编码,避免特殊字符破坏请求路径或造成路径穿越
|
||||||
|
const appJsonUrl = `${APM_STORE_BASE_URL}/${finalArch}/${encodeURIComponent(
|
||||||
|
category,
|
||||||
|
)}/${encodeURIComponent(pkgname)}/app.json`;
|
||||||
const response = await fetch(appJsonUrl);
|
const response = await fetch(appJsonUrl);
|
||||||
if (!response.ok) return null;
|
if (!response.ok) return null;
|
||||||
const appJson = await response.json();
|
const appJson = await response.json();
|
||||||
|
// img_urls 可能为字符串形式的 JSON,解析失败时安全回退为空数组
|
||||||
|
const parsedImgUrls = (() => {
|
||||||
|
if (typeof appJson.img_urls === "string") {
|
||||||
|
try {
|
||||||
|
return JSON.parse(appJson.img_urls) as string[];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (appJson.img_urls as string[]) || [];
|
||||||
|
})();
|
||||||
return {
|
return {
|
||||||
name: appJson.Name || "",
|
name: appJson.Name || "",
|
||||||
pkgname: appJson.Pkgname || pkgname,
|
pkgname: appJson.Pkgname || pkgname,
|
||||||
@@ -914,10 +928,7 @@ const fetchAppFromStore = async (
|
|||||||
size: appJson.Size || "",
|
size: appJson.Size || "",
|
||||||
more: appJson.More || "",
|
more: appJson.More || "",
|
||||||
tags: appJson.Tags || "",
|
tags: appJson.Tags || "",
|
||||||
img_urls:
|
img_urls: parsedImgUrls,
|
||||||
typeof appJson.img_urls === "string"
|
|
||||||
? (JSON.parse(appJson.img_urls) as string[])
|
|
||||||
: appJson.img_urls || [],
|
|
||||||
icons: appJson.icons || "",
|
icons: appJson.icons || "",
|
||||||
category: category,
|
category: category,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
@@ -944,6 +955,31 @@ interface OpenDetailInput extends Partial<App> {
|
|||||||
origin?: "spark" | "apm";
|
origin?: "spark" | "apm";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取远程/本地均无匹配时的回退 App 构造(两处兜底分支共用,避免重复字段映射)
|
||||||
|
const createFallbackApp = (
|
||||||
|
raw: Record<string, unknown>,
|
||||||
|
pkgname: string,
|
||||||
|
category: string,
|
||||||
|
): App => ({
|
||||||
|
name: (raw.name as string) || "",
|
||||||
|
pkgname,
|
||||||
|
version: (raw.version as string) || "",
|
||||||
|
filename: (raw.filename as string) || "",
|
||||||
|
category,
|
||||||
|
torrent_address: "",
|
||||||
|
author: "",
|
||||||
|
contributor: "",
|
||||||
|
website: "",
|
||||||
|
update: "",
|
||||||
|
size: "",
|
||||||
|
more: (raw.more as string) || "",
|
||||||
|
tags: "",
|
||||||
|
img_urls: [],
|
||||||
|
icons: "",
|
||||||
|
origin: (raw.origin as "spark" | "apm") || "apm",
|
||||||
|
currentStatus: "not-installed",
|
||||||
|
});
|
||||||
|
|
||||||
const openDetail = async (app: App | OpenDetailInput) => {
|
const openDetail = async (app: App | OpenDetailInput) => {
|
||||||
// 提取 pkgname 和 category(必须存在)
|
// 提取 pkgname 和 category(必须存在)
|
||||||
const pkgname = app?.pkgname;
|
const pkgname = app?.pkgname;
|
||||||
@@ -1029,26 +1065,11 @@ const openDetail = async (app: App | OpenDetailInput) => {
|
|||||||
finalApp = fullApp;
|
finalApp = fullApp;
|
||||||
} else {
|
} else {
|
||||||
// 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象
|
// 两个仓库都没有找到,且本地也没有,构造一个最小可用的 App 对象
|
||||||
finalApp = {
|
finalApp = createFallbackApp(
|
||||||
name: ((app as Record<string, unknown>).name as string) || "",
|
app as Record<string, unknown>,
|
||||||
pkgname: pkgname,
|
pkgname,
|
||||||
version: ((app as Record<string, unknown>).version as string) || "",
|
category,
|
||||||
filename: ((app as Record<string, unknown>).filename as string) || "",
|
);
|
||||||
category: category,
|
|
||||||
torrent_address: "",
|
|
||||||
author: "",
|
|
||||||
contributor: "",
|
|
||||||
website: "",
|
|
||||||
update: "",
|
|
||||||
size: "",
|
|
||||||
more: ((app as Record<string, unknown>).more as string) || "",
|
|
||||||
tags: "",
|
|
||||||
img_urls: [],
|
|
||||||
icons: "",
|
|
||||||
origin:
|
|
||||||
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
|
|
||||||
currentStatus: "not-installed",
|
|
||||||
} as App;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 非 HomeView 来源,使用原来的逻辑
|
// 非 HomeView 来源,使用原来的逻辑
|
||||||
@@ -1056,26 +1077,11 @@ const openDetail = async (app: App | OpenDetailInput) => {
|
|||||||
finalApp = fullApp;
|
finalApp = fullApp;
|
||||||
} else {
|
} else {
|
||||||
// 构造一个最小可用的 App 对象
|
// 构造一个最小可用的 App 对象
|
||||||
finalApp = {
|
finalApp = createFallbackApp(
|
||||||
name: ((app as Record<string, unknown>).name as string) || "",
|
app as Record<string, unknown>,
|
||||||
pkgname: pkgname,
|
pkgname,
|
||||||
version: ((app as Record<string, unknown>).version as string) || "",
|
category,
|
||||||
filename: ((app as Record<string, unknown>).filename as string) || "",
|
);
|
||||||
category: category,
|
|
||||||
torrent_address: "",
|
|
||||||
author: "",
|
|
||||||
contributor: "",
|
|
||||||
website: "",
|
|
||||||
update: "",
|
|
||||||
size: "",
|
|
||||||
more: ((app as Record<string, unknown>).more as string) || "",
|
|
||||||
tags: "",
|
|
||||||
img_urls: [],
|
|
||||||
icons: "",
|
|
||||||
origin:
|
|
||||||
((app as Record<string, unknown>).origin as "spark" | "apm") || "apm",
|
|
||||||
currentStatus: "not-installed",
|
|
||||||
} as App;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1324,7 +1330,7 @@ const loadRanking = async () => {
|
|||||||
const gen = ++rankingGeneration;
|
const gen = ++rankingGeneration;
|
||||||
rankingLoading.value = true;
|
rankingLoading.value = true;
|
||||||
const all = apps.value.slice();
|
const all = apps.value.slice();
|
||||||
const CONCURRENCY = 64;
|
const CONCURRENCY = 15;
|
||||||
const results: App[] = [];
|
const results: App[] = [];
|
||||||
for (let i = 0; i < all.length; i += CONCURRENCY) {
|
for (let i = 0; i < all.length; i += CONCURRENCY) {
|
||||||
if (gen !== rankingGeneration) {
|
if (gen !== rankingGeneration) {
|
||||||
@@ -1367,17 +1373,30 @@ const loadHome = async () => {
|
|||||||
// 按名称去重,spark 优先:同名链接 spark 覆盖 apm
|
// 按名称去重,spark 优先:同名链接 spark 覆盖 apm
|
||||||
const seenNames = new Set<string>();
|
const seenNames = new Set<string>();
|
||||||
|
|
||||||
for (const mode of modes) {
|
// 并行请求各来源的 homelinks.json,缩短首页加载耗时
|
||||||
|
const modeResults = await Promise.all(
|
||||||
|
modes.map(async (mode) => {
|
||||||
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
const finalArch = mode === "spark" ? `${arch}-store` : `${arch}-apm`;
|
||||||
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
||||||
|
|
||||||
// homelinks.json
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/homelinks.json`);
|
const res = await fetch(`${base}/homelinks.json`);
|
||||||
if (res.ok) {
|
if (res.ok) return { mode, raw: (await res.json()) as unknown };
|
||||||
const raw = await res.json();
|
} catch (e) {
|
||||||
|
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
||||||
|
}
|
||||||
|
return { mode, raw: undefined };
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const { mode, raw } of modeResults) {
|
||||||
|
if (!raw) continue;
|
||||||
// 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误)
|
// 校验 links 为数组,且每项均为对象(避免后端返回异常结构导致运行时错误)
|
||||||
const links = Array.isArray(raw) ? (raw.filter((x) => x && typeof x === "object") as Record<string, unknown>[]) : [];
|
const links = Array.isArray(raw)
|
||||||
|
? (raw.filter((x) => x && typeof x === "object") as Record<
|
||||||
|
string,
|
||||||
|
unknown
|
||||||
|
>[])
|
||||||
|
: [];
|
||||||
for (const l of links) {
|
for (const l of links) {
|
||||||
const name = (l.Name as string) || (l.name as string) || "";
|
const name = (l.Name as string) || (l.name as string) || "";
|
||||||
if (!name) continue; // 跳过空名称,避免空字符串污染 seenNames 与去重逻辑
|
if (!name) continue; // 跳过空名称,避免空字符串污染 seenNames 与去重逻辑
|
||||||
@@ -1401,10 +1420,6 @@ const loadHome = async () => {
|
|||||||
homeLinks.value.push(safeLink);
|
homeLinks.value.push(safeLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
homeError.value = (error as Error)?.message || "加载首页失败";
|
homeError.value = (error as Error)?.message || "加载首页失败";
|
||||||
} finally {
|
} finally {
|
||||||
@@ -1554,7 +1569,10 @@ const loadHomeListApps = async (entryId: string) => {
|
|||||||
try {
|
try {
|
||||||
const path = `/${finalArch}${jsonUrl}`;
|
const path = `/${finalArch}${jsonUrl}`;
|
||||||
const rawApps =
|
const rawApps =
|
||||||
(await fetchWithRetry<Record<string, string>[]>(path, rootAbortController.signal)) || [];
|
(await fetchWithRetry<Record<string, string>[]>(
|
||||||
|
path,
|
||||||
|
rootAbortController.signal,
|
||||||
|
)) || [];
|
||||||
const apps = parseAppList(rawApps, mode);
|
const apps = parseAppList(rawApps, mode);
|
||||||
for (const app of apps) {
|
for (const app of apps) {
|
||||||
if (!app.pkgname || seenPkgnames.has(app.pkgname)) continue;
|
if (!app.pkgname || seenPkgnames.has(app.pkgname)) continue;
|
||||||
@@ -2967,10 +2985,16 @@ const normalizeAppJson = (
|
|||||||
size: appJson.Size,
|
size: appJson.Size,
|
||||||
more: appJson.More,
|
more: appJson.More,
|
||||||
tags: appJson.Tags,
|
tags: appJson.Tags,
|
||||||
img_urls:
|
img_urls: (() => {
|
||||||
typeof appJson.img_urls === "string"
|
if (typeof appJson.img_urls === "string") {
|
||||||
? (JSON.parse(appJson.img_urls) as string[])
|
try {
|
||||||
: appJson.img_urls,
|
return JSON.parse(appJson.img_urls) as string[];
|
||||||
|
} catch {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (appJson.img_urls as string[]) || [];
|
||||||
|
})(),
|
||||||
icons: appJson.icons,
|
icons: appJson.icons,
|
||||||
category: category,
|
category: category,
|
||||||
origin: origin,
|
origin: origin,
|
||||||
@@ -3144,7 +3168,10 @@ const loadApps = async (onFirstBatch?: () => void) => {
|
|||||||
const path = `/${finalArch}/${category}/applist.json`;
|
const path = `/${finalArch}/${category}/applist.json`;
|
||||||
|
|
||||||
logger.info(`加载分类: ${category} (来源: ${mode})`);
|
logger.info(`加载分类: ${category} (来源: ${mode})`);
|
||||||
const categoryApps = await fetchWithRetry<AppJson[]>(path, rootAbortController.signal);
|
const categoryApps = await fetchWithRetry<AppJson[]>(
|
||||||
|
path,
|
||||||
|
rootAbortController.signal,
|
||||||
|
);
|
||||||
|
|
||||||
const normalizedApps = (categoryApps || []).map((appJson) =>
|
const normalizedApps = (categoryApps || []).map((appJson) =>
|
||||||
normalizeAppJson(appJson, category, mode as "spark" | "apm"),
|
normalizeAppJson(appJson, category, mode as "spark" | "apm"),
|
||||||
@@ -3209,15 +3236,135 @@ const handleWindowResize = () => {
|
|||||||
}, 400);
|
}, 400);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// —— 以下为可复用的事件/IPC 监听处理器(命名函数,便于 onUnmounted 统一移除)——
|
||||||
|
// 收集"等待 loading 完成后执行一次"的 watcher,便于卸载时统一停止,避免泄漏
|
||||||
|
const pendingWatchers: Array<() => void> = [];
|
||||||
|
const registerOnceWatcher = (stop: () => void): void => {
|
||||||
|
pendingWatchers.push(stop);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleHashChange = () => {
|
||||||
|
isSubmitterView.value = window.location.hash === "#submitter";
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (showPreview.value) {
|
||||||
|
if (e.key === "Escape") closeScreenPreview();
|
||||||
|
if (e.key === "ArrowLeft") prevScreen();
|
||||||
|
if (e.key === "ArrowRight") nextScreen();
|
||||||
|
}
|
||||||
|
if (showModal.value && e.key === "Escape") {
|
||||||
|
closeDetail();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeepLinkUpdate = () => {
|
||||||
|
if (loading.value) {
|
||||||
|
const stop = watch(loading, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
openUpdateModal();
|
||||||
|
stop();
|
||||||
|
const idx = pendingWatchers.indexOf(stop);
|
||||||
|
if (idx >= 0) pendingWatchers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerOnceWatcher(stop);
|
||||||
|
} else {
|
||||||
|
openUpdateModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeepLinkInstalled = () => {
|
||||||
|
if (loading.value) {
|
||||||
|
const stop = watch(loading, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
openInstalledModal();
|
||||||
|
stop();
|
||||||
|
const idx = pendingWatchers.indexOf(stop);
|
||||||
|
if (idx >= 0) pendingWatchers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerOnceWatcher(stop);
|
||||||
|
} else {
|
||||||
|
openInstalledModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTriggerApmInstallDialog = () => {
|
||||||
|
showApmInstallDialog.value = true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeepLinkInstall = (_event: IpcRendererEvent, pkgname: string) => {
|
||||||
|
const tryOpen = () => {
|
||||||
|
const target = apps.value.find((a) => a.pkgname === pkgname);
|
||||||
|
if (target) {
|
||||||
|
openDetail(target);
|
||||||
|
} else {
|
||||||
|
logger.warn(`Deep link: app ${pkgname} not found`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (loading.value) {
|
||||||
|
const stop = watch(loading, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
tryOpen();
|
||||||
|
stop();
|
||||||
|
const idx = pendingWatchers.indexOf(stop);
|
||||||
|
if (idx >= 0) pendingWatchers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerOnceWatcher(stop);
|
||||||
|
} else {
|
||||||
|
tryOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDeepLinkSearch = (
|
||||||
|
_event: IpcRendererEvent,
|
||||||
|
data: { pkgname: string },
|
||||||
|
) => {
|
||||||
|
const tryOpen = () => {
|
||||||
|
currentView.value = "default";
|
||||||
|
activeTab.value = "all";
|
||||||
|
const target = apps.value.find((a) => a.pkgname === data.pkgname);
|
||||||
|
if (target) {
|
||||||
|
openDetail({ ...target, _fromDeepLink: true });
|
||||||
|
} else {
|
||||||
|
searchQuery.value = data.pkgname;
|
||||||
|
logger.warn(
|
||||||
|
`Deep link: app ${data.pkgname} not found, fallback to search`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if (loading.value) {
|
||||||
|
const stop = watch(loading, (val) => {
|
||||||
|
if (!val) {
|
||||||
|
tryOpen();
|
||||||
|
stop();
|
||||||
|
const idx = pendingWatchers.indexOf(stop);
|
||||||
|
if (idx >= 0) pendingWatchers.splice(idx, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
registerOnceWatcher(stop);
|
||||||
|
} else {
|
||||||
|
tryOpen();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveComplete = (
|
||||||
|
_event: IpcRendererEvent,
|
||||||
|
payload: ChannelPayload,
|
||||||
|
) => {
|
||||||
|
const pkgname = currentApp.value?.pkgname;
|
||||||
|
if (payload.success && pkgname) {
|
||||||
|
removeDownloadItem(pkgname);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// 生命周期钩子
|
// 生命周期钩子
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
initTheme();
|
initTheme();
|
||||||
updateCenterStore.bind();
|
updateCenterStore.bind();
|
||||||
|
|
||||||
const handleHashChange = () => {
|
|
||||||
isSubmitterView.value = window.location.hash === "#submitter";
|
|
||||||
};
|
|
||||||
|
|
||||||
handleHashChange();
|
handleHashChange();
|
||||||
window.addEventListener("hashchange", handleHashChange);
|
window.addEventListener("hashchange", handleHashChange);
|
||||||
|
|
||||||
@@ -3277,121 +3424,23 @@ onMounted(async () => {
|
|||||||
logger.info("所有应用数据加载完成");
|
logger.info("所有应用数据加载完成");
|
||||||
|
|
||||||
// 设置键盘导航
|
// 设置键盘导航
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", handleKeydown);
|
||||||
if (showPreview.value) {
|
|
||||||
if (e.key === "Escape") closeScreenPreview();
|
|
||||||
if (e.key === "ArrowLeft") prevScreen();
|
|
||||||
if (e.key === "ArrowRight") nextScreen();
|
|
||||||
}
|
|
||||||
if (showModal.value && e.key === "Escape") {
|
|
||||||
closeDetail();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Deep link Handlers
|
// Deep link Handlers
|
||||||
window.ipcRenderer.on("deep-link-update", () => {
|
window.ipcRenderer.on("deep-link-update", handleDeepLinkUpdate);
|
||||||
if (loading.value) {
|
window.ipcRenderer.on("deep-link-installed", handleDeepLinkInstalled);
|
||||||
const stop = watch(loading, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
openUpdateModal();
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
openUpdateModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.on("deep-link-installed", () => {
|
|
||||||
if (loading.value) {
|
|
||||||
const stop = watch(loading, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
openInstalledModal();
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
openInstalledModal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.on("trigger-apm-install-dialog", () => {
|
|
||||||
showApmInstallDialog.value = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.on(
|
window.ipcRenderer.on(
|
||||||
"deep-link-install",
|
"trigger-apm-install-dialog",
|
||||||
(_event: IpcRendererEvent, pkgname: string) => {
|
handleTriggerApmInstallDialog,
|
||||||
const tryOpen = () => {
|
|
||||||
const target = apps.value.find((a) => a.pkgname === pkgname);
|
|
||||||
if (target) {
|
|
||||||
openDetail(target);
|
|
||||||
} else {
|
|
||||||
logger.warn(`Deep link: app ${pkgname} not found`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading.value) {
|
|
||||||
const stop = watch(loading, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
tryOpen();
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tryOpen();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
window.ipcRenderer.on(
|
|
||||||
"deep-link-search",
|
|
||||||
(_event: IpcRendererEvent, data: { pkgname: string }) => {
|
|
||||||
// 根据包名直接打开应用详情
|
|
||||||
const tryOpen = () => {
|
|
||||||
// 先切换到"全部应用"分类
|
|
||||||
currentView.value = "default";
|
|
||||||
activeTab.value = "all";
|
|
||||||
// 使用类似 HomeView 的方式打开应用,从两个仓库获取完整信息
|
|
||||||
const target = apps.value.find((a) => a.pkgname === data.pkgname);
|
|
||||||
if (target) {
|
|
||||||
openDetail({ ...target, _fromDeepLink: true });
|
|
||||||
} else {
|
|
||||||
// 如果找不到应用,回退到搜索模式
|
|
||||||
searchQuery.value = data.pkgname;
|
|
||||||
logger.warn(
|
|
||||||
`Deep link: app ${data.pkgname} not found, fallback to search`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading.value) {
|
|
||||||
const stop = watch(loading, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
tryOpen();
|
|
||||||
stop();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
tryOpen();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
window.ipcRenderer.on("deep-link-install", handleDeepLinkInstall);
|
||||||
|
window.ipcRenderer.on("deep-link-search", handleDeepLinkSearch);
|
||||||
|
|
||||||
window.ipcRenderer.on(
|
window.ipcRenderer.on(
|
||||||
"install-complete",
|
"install-complete",
|
||||||
handleInstallCompleteForDownloadRecord,
|
handleInstallCompleteForDownloadRecord,
|
||||||
);
|
);
|
||||||
|
window.ipcRenderer.on("remove-complete", handleRemoveComplete);
|
||||||
window.ipcRenderer.on(
|
|
||||||
"remove-complete",
|
|
||||||
(_event: IpcRendererEvent, payload: ChannelPayload) => {
|
|
||||||
const pkgname = currentApp.value?.pkgname;
|
|
||||||
if (payload.success && pkgname) {
|
|
||||||
removeDownloadItem(pkgname);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
window.ipcRenderer.send("renderer-ready", { status: true });
|
window.ipcRenderer.send("renderer-ready", { status: true });
|
||||||
logger.info("Renderer process is ready!");
|
logger.info("Renderer process is ready!");
|
||||||
@@ -3400,11 +3449,26 @@ onMounted(async () => {
|
|||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
rootAbortController.abort();
|
rootAbortController.abort();
|
||||||
updateCenterStore.unbind();
|
updateCenterStore.unbind();
|
||||||
|
// 停止仍挂起的"等待 loading 完成后执行一次"的 watcher,避免泄漏
|
||||||
|
for (const stop of pendingWatchers.splice(0)) stop();
|
||||||
|
// 移除 window / document 监听
|
||||||
|
window.removeEventListener("hashchange", handleHashChange);
|
||||||
|
window.removeEventListener("resize", handleWindowResize);
|
||||||
|
document.removeEventListener("keydown", handleKeydown);
|
||||||
|
// 移除所有 IPC 监听
|
||||||
|
window.ipcRenderer.off("deep-link-update", handleDeepLinkUpdate);
|
||||||
|
window.ipcRenderer.off("deep-link-installed", handleDeepLinkInstalled);
|
||||||
|
window.ipcRenderer.off(
|
||||||
|
"trigger-apm-install-dialog",
|
||||||
|
handleTriggerApmInstallDialog,
|
||||||
|
);
|
||||||
|
window.ipcRenderer.off("deep-link-install", handleDeepLinkInstall);
|
||||||
|
window.ipcRenderer.off("deep-link-search", handleDeepLinkSearch);
|
||||||
window.ipcRenderer.off(
|
window.ipcRenderer.off(
|
||||||
"install-complete",
|
"install-complete",
|
||||||
handleInstallCompleteForDownloadRecord,
|
handleInstallCompleteForDownloadRecord,
|
||||||
);
|
);
|
||||||
window.removeEventListener("resize", handleWindowResize);
|
window.ipcRenderer.off("remove-complete", handleRemoveComplete);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 观察器
|
// 观察器
|
||||||
|
|||||||
@@ -226,7 +226,8 @@ export interface CategoryInfo {
|
|||||||
zh: string;
|
zh: string;
|
||||||
origins?: string[];
|
origins?: string[];
|
||||||
origin?: "spark" | "apm";
|
origin?: "spark" | "apm";
|
||||||
[k: string]: unknown;
|
// 保留扩展点,避免使用宽泛的索引签名削弱类型安全
|
||||||
|
extra?: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface HomeLink {
|
export interface HomeLink {
|
||||||
@@ -238,7 +239,6 @@ export interface HomeLink {
|
|||||||
imgUrl?: string;
|
imgUrl?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
origin?: "spark" | "apm";
|
origin?: "spark" | "apm";
|
||||||
[k: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SidebarEntry {
|
export interface SidebarEntry {
|
||||||
|
|||||||
@@ -24,13 +24,14 @@ export interface ContributorRank {
|
|||||||
* 聚合与展示统一去除 `<...>` 取显示名。
|
* 聚合与展示统一去除 `<...>` 取显示名。
|
||||||
*/
|
*/
|
||||||
export function parseContributors(raw: string | undefined): string[] {
|
export function parseContributors(raw: string | undefined): string[] {
|
||||||
if (!raw) return [];
|
if (!raw || typeof raw !== "string") return [];
|
||||||
const names: string[] = [];
|
const names: string[] = [];
|
||||||
for (const part of raw.split(/[;;]/)) {
|
for (const part of raw.split(/[;;]/)) {
|
||||||
const trimmed = part.trim();
|
const trimmed = part.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
|
// 去除 <...> 包裹的邮箱,取显示名;长度 1~50 过滤空字符串/异常超长
|
||||||
const name = trimmed.replace(/<[^>]*>/g, "").trim();
|
const name = trimmed.replace(/<[^>]*>/g, "").trim();
|
||||||
if (name) names.push(name);
|
if (name.length > 0 && name.length <= 50) names.push(name);
|
||||||
}
|
}
|
||||||
return names;
|
return names;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user