From 6ea628d869cf2ee7c42c12240636b9589a0efe79 Mon Sep 17 00:00:00 2001 From: momen Date: Fri, 27 Feb 2026 23:03:10 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=E4=BF=AE=E6=94=B9=E5=BA=94?= =?UTF-8?q?=E7=94=A8=E4=BF=A1=E6=81=AF=E7=95=8C=E9=9D=A2=EF=BC=8C=E5=AF=B9?= =?UTF-8?q?=E4=BA=8E=E5=B7=B2=E5=AE=89=E8=A3=85=E5=BA=94=E7=94=A8=EF=BC=8C?= =?UTF-8?q?=E5=AE=89=E8=A3=85=E6=8C=89=E9=92=AE=E5=8F=98=E4=B8=BA=E6=89=93?= =?UTF-8?q?=E5=BC=80=E6=8C=89=E9=92=AE=EF=BC=8C=E5=B9=B6=E6=B7=BB=E5=8A=A0?= =?UTF-8?q?=E5=8D=B8=E8=BD=BD=E6=8C=89=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- electron/main/backend/install-manager.ts | 56 +++++++++++++++--------- src/App.vue | 4 +- src/components/AppDetailModal.vue | 4 +- src/global/downloadStatus.ts | 4 +- 4 files changed, 42 insertions(+), 26 deletions(-) diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index f9db98c9..949f123f 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -412,27 +412,41 @@ ipcMain.handle("check-installed", async (_event, pkgname: string) => { const checkScript = "/opt/spark-store/extras/check-is-installed"; let isInstalled = false; - const child = spawn(checkScript, [pkgname], { - shell: false, - env: process.env, - }); - - await new Promise((resolve) => { - child.on("error", (err) => { - logger.error(`check-installed 执行失败: ${err?.message || err}`); - resolve(); + // 首先尝试使用内置脚本 + if (fs.existsSync(checkScript)) { + const child = spawn(checkScript, [pkgname], { + shell: false, + env: process.env, }); - child.on("close", (code) => { - if (code === 0) { - isInstalled = true; - logger.info(`应用已安装: ${pkgname}`); - } else { - logger.info(`应用未安装: ${pkgname} (exit ${code})`); - } - resolve(); + await new Promise((resolve) => { + child.on("error", (err) => { + logger.error(`check-installed 脚本执行失败: ${err?.message || err}`); + resolve(); + }); + + child.on("close", (code) => { + if (code === 0) { + isInstalled = true; + logger.info(`应用已安装 (脚本检测): ${pkgname}`); + } + resolve(); + }); }); - }); + + if (isInstalled) return true; + } + + // 如果脚本不存在或检测不到,使用 dpkg-query 作为后备 + logger.info(`尝试使用 dpkg-query 检测: ${pkgname}`); + const { code } = await runCommandCapture("dpkg-query", ["-W", "-f='${Status}'", pkgname]); + + if (code === 0) { + isInstalled = true; + logger.info(`应用已安装 (dpkg-query 检测): ${pkgname}`); + } else { + logger.info(`应用未安装: ${pkgname}`); + } return isInstalled; }); @@ -456,7 +470,7 @@ ipcMain.on("remove-installed", async (_event, pkgname: string) => { } const child = spawn( execCommand, - [...execParams, "aptss", "remove", pkgname ], + [...execParams, "aptss", "remove", pkgname], { shell: true, env: process.env, @@ -579,8 +593,8 @@ ipcMain.handle("launch-app", async (_event, pkgname: string) => { logger.warn("No pkgname provided for launch-app"); } - const execCommand = "/opt/spark-store/extras/host-spawn"; - const execParams = ["/opt/spark-store/extras/app-launcher", "launch", pkgname]; + const execCommand = "/opt/spark-store/extras/app-launcher"; + const execParams = ["start", pkgname]; logger.info( `Launching app: ${pkgname} with command: ${execCommand} ${execParams.join(" ")}`, diff --git a/src/App.vue b/src/App.vue index 736c4b7e..16616fad 100644 --- a/src/App.vue +++ b/src/App.vue @@ -633,8 +633,8 @@ const onUninstallSuccess = () => { } }; -const installCompleteCallback = () => { - if (currentApp.value) { +const installCompleteCallback = (pkgname?: string) => { + if (currentApp.value && (!pkgname || currentApp.value.pkgname === pkgname)) { checkAppInstalled(currentApp.value); } }; diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index a910cd08..7017bd67 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -236,8 +236,10 @@ const activeDownload = computed(() => { const { installFeedback } = useInstallFeedback(appPkgname); const { isCompleted } = useDownloadItemStatus(appPkgname); const installBtnText = computed(() => { + if (props.isinstalled) { + return "已安装"; + } if (isCompleted.value) { - // TODO: 似乎有一个时间差,安装好了之后并不是立马就可以从已安装列表看见 return "已安装"; } if (installFeedback.value) { diff --git a/src/global/downloadStatus.ts b/src/global/downloadStatus.ts index 1d32c7ca..202333bc 100644 --- a/src/global/downloadStatus.ts +++ b/src/global/downloadStatus.ts @@ -12,7 +12,7 @@ export function removeDownloadItem(pkgname: string) { } } -export function watchDownloadsChange(cb: () => void) { +export function watchDownloadsChange(cb: (pkgname: string) => void) { const statusById = new Map(); for (const item of downloads.value) { @@ -25,7 +25,7 @@ export function watchDownloadsChange(cb: () => void) { for (const item of list) { const prevStatus = statusById.get(item.id); if (item.status === "completed" && prevStatus !== "completed") { - cb(); + cb(item.pkgname); } statusById.set(item.id, item.status); } From 88670be15ef419fead504b8dd9681d2b7001f050 Mon Sep 17 00:00:00 2001 From: momen Date: Fri, 27 Feb 2026 23:18:06 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=E9=92=88=E5=AF=B9=E5=BC=B1=E7=BD=91?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=EF=BC=8C=E4=BE=A7=E8=BE=B9=E6=A0=8F=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=B9=B6=E8=A1=8C=E5=8A=A0=E8=BD=BD=EF=BC=8C=E9=87=8D?= =?UTF-8?q?=E8=AF=95=E6=9C=BA=E5=88=B6=EF=BC=8C=E5=88=9D=E5=A7=8B=E5=8C=96?= =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 128 ++++++++++++++++++++++++++++++++-------------------- 1 file changed, 78 insertions(+), 50 deletions(-) diff --git a/src/App.vue b/src/App.vue index 16616fad..077924e0 100644 --- a/src/App.vue +++ b/src/App.vue @@ -179,6 +179,24 @@ const axiosInstance = axios.create({ baseURL: APM_STORE_BASE_URL, timeout: 5000, // 增加到 5 秒,避免网络波动导致的超时 }); + +const fetchWithRetry = async ( + url: string, + retries = 3, + delay = 1000, +): Promise => { + try { + const response = await axiosInstance.get(url); + return response.data; + } catch (error) { + if (retries > 0) { + await new Promise((resolve) => setTimeout(resolve, delay)); + return fetchWithRetry(url, retries - 1, delay * 2); + } + throw error; + } +}; + const cacheBuster = (url: string) => `${url}?cb=${Date.now()}`; // 响应式状态 @@ -739,56 +757,62 @@ const loadCategories = async () => { const loadApps = async (onFirstBatch?: () => void) => { try { - logger.info("开始加载应用数据(并发分批)..."); + logger.info("开始加载应用数据(全并发带重试)..."); const categoriesList = Object.keys(categories.value || {}); - const concurrency = 4; // 同时并发请求数量,可根据网络条件调整 + let firstBatchCallDone = false; - for (let i = 0; i < categoriesList.length; i += concurrency) { - const batch = categoriesList.slice(i, i + concurrency); - await Promise.all( - batch.map(async (category) => { - try { - logger.info(`加载分类: ${category}`); - const response = await axiosInstance.get( - cacheBuster(`/${window.apm_store.arch}/${category}/applist.json`), - ); - const categoryApps = response.status === 200 ? response.data : []; - categoryApps.forEach((appJson) => { - const normalizedApp: App = { - name: appJson.Name, - pkgname: appJson.Pkgname, - version: appJson.Version, - filename: appJson.Filename, - torrent_address: appJson.Torrent_address, - author: appJson.Author, - contributor: appJson.Contributor, - website: appJson.Website, - update: appJson.Update, - size: appJson.Size, - more: appJson.More, - tags: appJson.Tags, - img_urls: - typeof appJson.img_urls === "string" - ? JSON.parse(appJson.img_urls) - : appJson.img_urls, - icons: appJson.icons, - category: category, - currentStatus: "not-installed", - }; - apps.value.push(normalizedApp); - }); - } catch (error) { - logger.warn(`加载分类 ${category} 失败: ${error}`); + // 并发加载所有分类,每个分类自带重试机制 + await Promise.all( + categoriesList.map(async (category) => { + try { + logger.info(`加载分类: ${category}`); + const categoryApps = await fetchWithRetry( + cacheBuster(`/${window.apm_store.arch}/${category}/applist.json`), + ); + + const normalizedApps = (categoryApps || []).map((appJson) => ({ + name: appJson.Name, + pkgname: appJson.Pkgname, + version: appJson.Version, + filename: appJson.Filename, + torrent_address: appJson.Torrent_address, + author: appJson.Author, + contributor: appJson.Contributor, + website: appJson.Website, + update: appJson.Update, + size: appJson.Size, + more: appJson.More, + tags: appJson.Tags, + img_urls: + typeof appJson.img_urls === "string" + ? JSON.parse(appJson.img_urls) + : appJson.img_urls, + icons: appJson.icons, + category: category, + currentStatus: "not-installed" as const, + })); + + // 增量式更新,让用户尽快看到部分数据 + apps.value.push(...normalizedApps); + + // 只要有一个分类加载成功,就可以考虑关闭整体 loading(如果是首批逻辑) + if (!firstBatchCallDone && typeof onFirstBatch === "function") { + firstBatchCallDone = true; + onFirstBatch(); } - }), - ); + } catch (error) { + logger.warn(`加载分类 ${category} 最终失败: ${error}`); + } + }), + ); - // 首批完成回调(用于隐藏首屏 loading) - if (i === 0 && typeof onFirstBatch === "function") onFirstBatch(); + // 确保即使全部失败也结束 loading + if (!firstBatchCallDone && typeof onFirstBatch === "function") { + onFirstBatch(); } } catch (error) { - logger.error(`加载应用数据失败: ${error}`); + logger.error(`加载应用数据流程异常: ${error}`); } }; @@ -805,14 +829,18 @@ onMounted(async () => { initTheme(); await loadCategories(); - // 默认加载主页数据 - await loadHome(); - // 先显示 loading,并异步开始分批加载应用列表。 + + // 分类目录加载后,并行加载主页数据和所有应用列表 loading.value = true; - loadApps(() => { - // 当第一批分类加载完成后,隐藏首屏 loading - loading.value = false; - }); + await Promise.all([ + loadHome(), + new Promise((resolve) => { + loadApps(() => { + loading.value = false; + resolve(); + }); + }), + ]); // 设置键盘导航 document.addEventListener("keydown", (e) => { From 749cf3d3bf59d0b70d48256369f358d5940da510 Mon Sep 17 00:00:00 2001 From: momen Date: Fri, 27 Feb 2026 23:25:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat:=E5=BC=95=E5=85=A5=E6=87=92=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=EF=BC=8C=E9=98=B2=E6=AD=A2=E5=BC=B1=E7=BD=91=E6=83=85?= =?UTF-8?q?=E5=86=B5=E4=B8=8B=E6=97=A0=E6=B3=95=E6=AD=A3=E5=B8=B8=E5=8A=A0?= =?UTF-8?q?=E8=BD=BD=E5=BA=94=E7=94=A8=E5=9B=BE=E6=A0=87=E6=88=96=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E5=9B=BE=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 6 +----- src/components/AppDetailModal.vue | 27 ++++++++++++++++++++------- src/components/HomeView.vue | 6 +++++- 3 files changed, 26 insertions(+), 13 deletions(-) diff --git a/src/App.vue b/src/App.vue index 077924e0..c4937498 100644 --- a/src/App.vue +++ b/src/App.vue @@ -349,11 +349,7 @@ const loadScreenshots = (app: App) => { screenshots.value = []; for (let i = 1; i <= 5; i++) { const screenshotUrl = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${app.category}/${app.pkgname}/screen_${i}.png`; - const img = new Image(); - img.src = screenshotUrl; - img.onload = () => { - screenshots.value.push(screenshotUrl); - }; + screenshots.value.push(screenshotUrl); } }; diff --git a/src/components/AppDetailModal.vue b/src/components/AppDetailModal.vue index 7017bd67..ff578346 100644 --- a/src/components/AppDetailModal.vue +++ b/src/components/AppDetailModal.vue @@ -21,12 +21,15 @@
- icon + icon
@@ -108,7 +111,8 @@ :key="index" :src="screen" alt="screenshot" - class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60" + class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition-all duration-300 hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60" + loading="lazy" @click="openPreview(index)" @error="hideImage" /> @@ -229,6 +233,15 @@ const emit = defineEmits<{ const appPkgname = computed(() => props.app?.pkgname); +const isIconLoaded = ref(false); + +watch( + () => props.app, + () => { + isIconLoaded.value = false; + }, +); + const activeDownload = computed(() => { return downloads.value.find((d) => d.pkgname === props.app?.pkgname); }); diff --git a/src/components/HomeView.vue b/src/components/HomeView.vue index 668b3e53..ba95fd96 100644 --- a/src/components/HomeView.vue +++ b/src/components/HomeView.vue @@ -12,7 +12,11 @@ class="flex flex-col items-start gap-2 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm hover:shadow-lg transition" :title="link.more" > - +
{{ link.name }}
{{ link.more }}
From a9a6b6bdc643eb0ddc1077f18af8067a14b4a09a Mon Sep 17 00:00:00 2001 From: momen Date: Sat, 28 Feb 2026 02:28:50 +0800 Subject: [PATCH 4/4] =?UTF-8?q?fix:=E9=A6=96=E9=A1=B5=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E8=BD=AF=E4=BB=B6=E5=AE=89=E8=A3=85=E5=A4=B1=E8=B4=A5=E9=97=AE?= =?UTF-8?q?=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.vue | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/src/App.vue b/src/App.vue index c4937498..1f7057aa 100644 --- a/src/App.vue +++ b/src/App.vue @@ -403,13 +403,38 @@ const loadHome = async () => { const r = await fetch(url); if (r.ok) { const appsJson = await r.json(); - const apps = (appsJson || []).map((a: any) => ({ - name: a.Name || a.name || a.Pkgname || a.PkgName || "", - pkgname: a.Pkgname || a.pkgname || "", - category: a.Category || a.category || "unknown", - more: a.More || a.more || "", - version: a.Version || "", - })); + const rawApps = appsJson || []; + const apps = await Promise.all( + rawApps.map(async (a: any) => { + const baseApp = { + name: a.Name || a.name || a.Pkgname || a.PkgName || "", + pkgname: a.Pkgname || a.pkgname || "", + category: a.Category || a.category || "unknown", + more: a.More || a.more || "", + version: a.Version || "", + filename: a.Filename || a.filename || "", + }; + + // 根据官网的要求,读取Category和Pkgname,拼接出 源地址/架构/Category/Pkgname/app.json来获取对应的真实json + try { + const realAppUrl = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${baseApp.category}/${baseApp.pkgname}/app.json`; + const realRes = await fetch(realAppUrl); + if (realRes.ok) { + const realApp = await realRes.json(); + // 用真实json的filename字段和More字段来增补和覆盖当前的json + if (realApp.Filename) baseApp.filename = realApp.Filename; + if (realApp.More) baseApp.more = realApp.More; + if (realApp.Name) baseApp.name = realApp.Name; + } + } catch (e) { + console.warn( + `Failed to fetch real app.json for ${baseApp.pkgname}`, + e, + ); + } + return baseApp; + }), + ); homeLists.value.push({ title: item.name || "推荐", apps }); } } catch (e) {