diff --git a/AGENTS.md b/AGENTS.md index 713d8de0..db3d87a6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -325,6 +325,47 @@ export const APM_STORE_BASE_URL = 'https://erotica.spark-app.store'; // /{arch}/{category}/{pkgname}/icon.png - App icon // /{arch}/{category}/{pkgname}/screen_N.png - Screenshots (1-5) // /{arch}/categories.json - Categories mapping + +### Home (主页) 页面数据 + +The store may provide a special `home` directory under `{arch}` for a localized homepage. Two JSON files are expected: + +- `homelinks.json` — 用于构建首页的轮播或链接块。每个条目示例: + +```json +{ + "name": "交流反馈", + "more": "前往论坛交流讨论", + "imgUrl": "/home/links/bbs.png", + "type": "_blank", + "url": "https://bbs.spark-app.store/" +} +``` + +- `homelist.json` — 描述若干推荐应用列表,每项引用一个 JSON 列表(`jsonUrl`): + +```json +[ + { "name":"装机必备", "type":"appList", "jsonUrl":"/home/lists/NecessaryforInstallation.json" } +] +``` + +Parsing rules used by the app: +- Resolve `imgUrl` by prefixing: `${APM_STORE_BASE_URL}/{arch}${imgUrl}`. +- `type: _blank` → 使用系统浏览器打开链接;`type: _self` → 在当前页面打开。 +- For `homelist.json` entries with `type: "appList"`, fetch the referenced `jsonUrl` and map each item to the app shape used by the UI: + - `Name` → `name` + - `Pkgname` → `pkgname` + - `Category` → `category` + - `More` → `more` + +Where to implement: +- Renderer: `src/App.vue` loads and normalizes `homelinks.json` and `homelist.json` on selecting the `home` category and exposes data to a new `HomeView` component. +- Component: `src/components/HomeView.vue` renders link cards and recommended app sections (re-uses `AppCard.vue`). + +Notes: +- The `home` directory path is: `/{arch}/home/` under the configured `APM_STORE_BASE_URL`. +- Missing or partially invalid files are handled gracefully — individual failures don't block showing other home sections. ``` ### Axios Usage diff --git a/electron/main/index.ts b/electron/main/index.ts index 92687388..f5474054 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -160,6 +160,24 @@ ipcMain.handle("run-update-tool", async () => { } }); +// 启动安装设置脚本(可能需要提升权限) +ipcMain.handle("open-install-settings", async () => { + try { + const { spawn } = await import("node:child_process"); + const scriptPath = "/opt/durapps/spark-store/bin/update-upgrade/ss-update-controler.sh"; + const child = spawn("/opt/spark-store/extras/host-spawn", [scriptPath], { + detached: true, + stdio: "ignore", + }); + child.unref(); + logger.info(`Launched ${scriptPath}`); + return { success: true }; + } catch (err) { + logger.error({ err }, "Failed to launch install settings script"); + return { success: false, message: (err as Error)?.message || String(err) }; + } +}); + app.whenReady().then(() => { // Set User-Agent for client session.defaultSession.webRequest.onBeforeSendHeaders((details, callback) => { diff --git a/src/App.vue b/src/App.vue index 8941cd5e..736c4b7e 100644 --- a/src/App.vue +++ b/src/App.vue @@ -32,15 +32,30 @@ :active-category="activeCategory" :apps-count="filteredApps.length" @update-search="handleSearchInput" + @search-focus="handleSearchFocus" @update="handleUpdate" @list="handleList" + @open-install-settings="handleOpenInstallSettings" @toggle-sidebar="isSidebarOpen = !isSidebarOpen" /> - + + { const categories: Ref> = ref({}); const apps: Ref = ref([]); -const activeCategory = ref("all"); +const activeCategory = ref("home"); const searchQuery = ref(""); const isSidebarOpen = ref(false); const showModal = ref(false); @@ -279,6 +295,9 @@ const selectCategory = (category: string) => { activeCategory.value = category; searchQuery.value = ""; isSidebarOpen.value = false; + if (category === "home") { + loadHome(); + } }; const openDetail = (app: App) => { @@ -334,6 +353,67 @@ const closeScreenPreview = () => { showPreview.value = false; }; +// Home data +const homeLinks = ref([]); +const homeLists = ref>([]); +const homeLoading = ref(false); +const homeError = ref(""); + +const loadHome = async () => { + homeLoading.value = true; + homeError.value = ""; + homeLinks.value = []; + homeLists.value = []; + try { + const base = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/home`; + // homelinks.json + try { + const res = await fetch(`${base}/homelinks.json`); + if (res.ok) { + homeLinks.value = await res.json(); + } + } catch (e) { + // ignore single file failures + console.warn("Failed to load homelinks.json", e); + } + + // homelist.json + try { + const res2 = await fetch(`${base}/homelist.json`); + if (res2.ok) { + const lists = await res2.json(); + for (const item of lists) { + if (item.type === "appList" && item.jsonUrl) { + try { + const url = `${APM_STORE_BASE_URL}/${window.apm_store.arch}${item.jsonUrl}`; + 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 || "", + })); + homeLists.value.push({ title: item.name || "推荐", apps }); + } + } catch (e) { + console.warn("Failed to load home list", item, e); + } + } + } + } + } catch (e) { + console.warn("Failed to load homelist.json", e); + } + } catch (error: unknown) { + homeError.value = (error as Error)?.message || "加载首页失败"; + } finally { + homeLoading.value = false; + } +}; + const prevScreen = () => { if (currentScreenIndex.value > 0) { currentScreenIndex.value--; @@ -357,6 +437,17 @@ const handleUpdate = async () => { } }; +const handleOpenInstallSettings = async () => { + try { + const result = await window.ipcRenderer.invoke("open-install-settings"); + if (!result || !result.success) { + logger.warn(`启动安装设置失败: ${result?.message || "未知错误"}`); + } + } catch (error) { + logger.error(`调用安装设置时出错: ${error}`); + } +}; + const handleList = () => { openInstalledModal(); }; @@ -705,11 +796,17 @@ const handleSearchInput = (value: string) => { searchQuery.value = value; }; +const handleSearchFocus = () => { + if (activeCategory.value === "home") activeCategory.value = "all"; +}; + // 生命周期钩子 onMounted(async () => { initTheme(); await loadCategories(); + // 默认加载主页数据 + await loadHome(); // 先显示 loading,并异步开始分批加载应用列表。 loading.value = true; loadApps(() => { diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 514c8e0e..2941a683 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -10,7 +10,7 @@ > - +
@@ -24,12 +24,13 @@ class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-4 text-sm text-slate-700 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/50 focus:ring-4 focus:ring-brand/10 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-200" placeholder="搜索应用名 / 包名 / 标签,按回车键搜索" @keydown.enter="handleSearch" + @focus="handleSearchFocus" />
-
- 共 {{ appsCount }} 个应用 · 在任何主流 Linux 发行上安装应用 +
+ 共 {{ appsCount }} 个应用
@@ -48,6 +49,8 @@ const emit = defineEmits<{ (e: "update-search", query: string): void; (e: "update"): void; (e: "list"): void; + (e: "search-focus"): void; + (e: "open-install-settings"): void; (e: "toggle-sidebar"): void; }>(); @@ -57,6 +60,10 @@ const handleSearch = () => { emit("update-search", localSearchQuery.value); }; +const handleSearchFocus = () => { + emit("search-focus"); +}; + watch( () => props.searchQuery, (newVal) => { diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue index e8e8865b..3f7e1bb3 100644 --- a/src/components/AppSidebar.vue +++ b/src/components/AppSidebar.vue @@ -30,6 +30,19 @@
+ + - -
+ +
+ +
+ +
+ +
+ + +
+