Compare commits

..
26 Commits
Author SHA1 Message Date
xiyidaiwaandshenmo7192 77b4244f1d !408 refactor(已安装应用): 合并 APM/Spark 分页为列表标签并新增统计
Merge pull request !408 from xiyidaiwa/Erotica
2026-07-29 06:47:41 +00:00
gfdgd-xi c1b89cab1a 支持从git提取贡献者信息
Signed-off-by: gfdgd_xi <3025613752@qq.com>
2026-07-27 16:22:57 +08:00
shenmo7192andGitee 17cbb7cff2 !407 style: 统一滚动条外观并将投稿应用窗口改为圆角
Merge pull request !407 from xiyidaiwa/Erotica
2026-07-24 14:13:49 +00:00
shenmo7192 bcf3252e4a style(app-detail): add tooltip text for install buttons
为应用详情页和详情弹窗的两个安装按钮分别添加了对应的提示文案,提升用户体验
2026-07-24 10:26:40 +08:00
xiyidaiwa 910aec6cce fix(app): 修复应用计数统计问题
- 将应用总数统计从 apps.value.length 改为 baseApps.value.length
- 确保分类统计基于正确的基础应用数据
- 解决了应用过滤后计数不准确的问题
2026-07-23 18:41:19 +08:00
xiyidaiwa 3d28010797 fix: 处理 PR 评审意见(副作用清理、滚动条分层与变量化)
- SubmitterWindow: onMounted 修改 body/html 背景后,在 onUnmounted 中恢复原始值,避免副作用泄漏。
- appstyle.css: 全局滚动条基础几何/颜色移至 @layer base,使 .scrollbar-muted/.scrollbar-nowidth 工具类优先级更高;
  暗色 thumb 改用专用变量 --color-scrollbar-thumb-dark (#15203b),保留比 surface 略亮的可辨识反差(未改用 --color-surface-dark)。
- 评审建议"margin 改 padding"与"改用 .scrollbar-muted"经分析不成立:padding 不会内移滚动条、无法避开圆角,margin 才能把容器连同滚动条推离圆角;
  .scrollbar-muted 为 0.4rem 细条,与其他页面 0.75rem 全局不一致,故保持 mr-4 mb-4 全局样式。
2026-07-22 01:39:52 +08:00
xiyidaiwa df8c2d7eaa style: 统一滚动条外观并将投稿应用窗口改为圆角
- 滚动条:主进程对 Linux 关闭 overlay 滚动条
  (disable-features=OverlayScrollbar),使 CSS ::-webkit-scrollbar 自定义生效;
  全局统一定义亮色 #94a3b8 / 暗色 #15203b(带极淡描边),宽度 0.75rem,亮暗一致。
- 投稿页:修复滚动条贯穿整个窗口、侵入标题栏(改为 flex 分栏,
  标题栏 shrink-0,内容区单独滚动)。
- 应用管理页:移除 scrollbar-nowidth 隐藏与 scrollbar-thumb-* 覆盖色,
  改由全局规则统一,与其他页面一致。
- 圆角页面:投稿页与已安装应用页滚动容器内缩 (mr-4 mb-4),
  避免滚动条滚入圆角被裁切。
- 投稿应用窗口:独立窗口改为透明圆角窗口 (transparent + rounded-3xl + shadow-2xl),
  与弹框风格统一;挂载时 body 背景透明以露出圆角。
- 标题栏:左上角蓝色圆点替换为软件图标 spark-store.svg。
- 更新中心:搜索框对齐主页样式(左侧图标、统一圆角与配色)。
2026-07-22 01:19:52 +08:00
shenmo7192 e857ce67b3 move spark-store launcher from extra to tools 2026-07-20 11:20:20 +08:00
shenmo7192andGitee 98296fa8a3 update tool/ssinstall.
Signed-off-by: shenmo <jifengshenmo@outlook.com>
2026-07-20 03:11:55 +00:00
shenmo7192andGitee 96541cc13e update tool/aptss.
Signed-off-by: shenmo <jifengshenmo@outlook.com>
2026-07-20 03:09:13 +00:00
shenmo7192andGitee b84f82e7bb 上游标准化
Signed-off-by: shenmo <jifengshenmo@outlook.com>
2026-07-20 03:07:55 +00:00
shenmo7192andGitee e7476f4bc3 update tool/aptss.
Signed-off-by: shenmo <jifengshenmo@outlook.com>
2026-07-20 03:05:13 +00:00
shenmo7192 0a6fcd95c7 feat(install-manager): 安装成功时跳过APM快捷方式创建,仅针对升级任务 2026-07-19 23:27:19 +08:00
shenmo7192 99c6048295 修复 UOS 不识别 oneshot 的问题 2026-07-18 23:02:40 +08:00
shenmo7192 3b7f14862d refactor(DownloadQueue): 移除无用的滚轮隐藏队列逻辑与相关依赖
移除了页面滚轮事件监听、isHidden状态和相关的DOM引用,简化组件逻辑
2026-07-18 22:11:54 +08:00
shenmo7192 367828f1f4 chore(vscode & download queue): 修复调试配置和下载面板交互问题
1. 给VSCode调试配置添加--no-sandbox参数以解决容器/权限问题
2. 修复下载队列面板收起时点击无响应的bug,调整隐藏状态的位移参数
3. 优化展开/收起切换逻辑,隐藏状态下点击会自动展开面板
2026-07-18 22:04:23 +08:00
shenmo7192 70250acc76 fix(submitter): 捕获打开输出文件夹失败的错误
新增错误捕获逻辑,避免打开打包完成的文件夹失败时导致程序报错
2026-07-18 12:48:49 +08:00
shenmo7192andGitee 3157714776 !406 fix: 仅在使用loonggpu时禁用显卡加速
Merge pull request !406 from AAA Elysia 猫猫侠 ⁧~喵/N/A
2026-07-18 04:30:26 +00:00
AAA Elysia 猫猫侠 ⁧~喵andGitee bfd7626b8f fix: 仅在使用loonggpu时禁用显卡加速
Signed-off-by: AAA Elysia 猫猫侠 ⁧~喵 <elysia-best@simplelinux.cn.eu.org>
2026-07-18 04:27:53 +00:00
shenmo7192 5d512c4e86 add: 龙芯禁用GPU 2026-07-18 11:34:59 +08:00
shenmo7192 9323c368d6 style: 统一文件名小写处理并格式化代码
1. 对deb包名、打包文件名统一转为小写格式,避免大小写不一致问题
2. 格式化多处代码换行和缩进,提升代码可读性
2026-07-18 10:37:52 +08:00
shenmo7192 1f9971f7aa chore(gitignore): ignore Debian build intermediates under debian/ 2026-07-18 10:34:56 +08:00
shenmo7192 d67f7b3666 feat(submitter): 完善提交表单功能,优化交互与体验
1.  新增electronUtils接口用于获取文件路径,支持拖拽上传deb包
2.  交换贡献者与官网地址表单字段
3.  重构架构选择UI,按来源和架构排序并添加来源标识
4.  新增截图和图标上传格式校验,限制仅支持PNG
5.  优化截图上传区域样式,支持拖放上传
6.  拆分重置表单和提交成功后的状态清理逻辑
7.  重构架构排序逻辑,兼容新旧存储格式
2026-07-18 00:22:45 +08:00
shenmo7192 c2ba273ca2 update 5.2.1.0 2026-07-17 23:47:22 +08:00
shenmo7192 361dad1a64 feat(submitter): 新增自动获取git用户名并填充提交信息
扩展了提交器的自动填充能力,新增通过git config获取本地git用户名的能力,和原有邮箱获取逻辑结合,自动按照格式填充贡献者字段,优化用户提交体验
2026-07-17 23:12:10 +08:00
shenmo7192 5730c0bfea feat(SubmitterWindow): add view submission queue button
add a button to open the submission queue page in new tab, alongside the close button in the header
2026-07-17 22:52:03 +08:00
25 changed files with 1300 additions and 482 deletions
+10
View File
@@ -55,3 +55,13 @@ test-results.json
# VSCode CMake Extension
build/*
# Debian build intermediates
debian/*.log
debian/*.substvars
debian/*.debhelper
debian/debhelper-build-stamp
debian/files
debian/tmp/
debian/.debhelper/
debian/spark-store/
+1
View File
@@ -30,6 +30,7 @@
// },
"runtimeArgs": [
"--remote-debugging-port=9229",
"--no-sandbox",
"."
],
"envFile": "${workspaceFolder}/.vscode/.debug.env",
+19
View File
@@ -1,3 +1,22 @@
## [未发布 / Unreleased] (Erotica 分支,基于 1e60a4c2 之后)
本批改动汇总(自提交 `1e60a4c2` 起):
1. **已安装应用新增搜索框 + 窗口尺寸/位置持久化**:补齐已安装面板的应用搜索框;主窗口大小、位置、最大化状态写入本地 `window-state.json` 持久化,并限制最小尺寸 800×500。
2. **已安装列表健壮性加固**:修复异步竞态、错误 UX、加载状态处理与刷新防抖(多轮 AI 审查改进),并清理对应代码缩进与对齐。
3. **各页面滚动条贴边修复**:避免滚动栏被窗口圆角裁切。
4. **投稿应用窗口圆角虚影修复**:消除投稿弹窗圆角处的方角残影。
5. **已安装应用来源筛选与排序优化**:将头部 APM / Spark / 总数三段式统计徽章改为可点击,点击后按来源筛选(默认显示全部);列表默认 APM 应用置顶;新增"暂无已安装的 APM/Spark 应用"空状态提示,避免筛选无结果时出现空引号。
6. **AI 代码审查安全加固**
- `launch-app` IPC 移除 `any`,新增包名正则校验(`/^[a-zA-Z0-9._+-]+$/`),拦截非法输入防止命令注入。
- `getIconUrl` 本地图标路径增加白名单目录校验与路径遍历(`..`)防护。
- `fetchWithRetry` 仅对网络错误 / 5xx / 超时重试,4xx 快速失败。
- `AppGrid` 普通网格 `v-for``:key``index` 改为 `app.pkgname`
- `loong64` 架构下尊重 `--no-spark` 启动参数,不再硬编码覆盖。
- `openDownloadedApp` 的 IPC 调用补充 `.catch` 错误处理。
相关提交:`1d9d25d6``c75938aa``39a358bb``51473cac``cf0e72ff``b0d60737``d3705ed4`
## [1.1.1](https://github.com/elysia-best/apm-app-store/compare/v1.1.0...v1.1.1) (2026-02-17)
+1 -1
View File
@@ -47,4 +47,4 @@ install:
cp -rv pkg/usr/share/dsg/ $(DESTDIR)/usr/share/
cp -rv pkg/usr/share/bash-completion/ $(DESTDIR)/usr/share/
cp -rv tool/spark-store.asc $(DESTDIR)/opt/durapps/spark-store/bin/
ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
# ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
+1 -1
View File
@@ -1,4 +1,4 @@
spark-store (5.2.1-0) UNRELEASED; urgency=medium
spark-store (5.2.1.0) UNRELEASED; urgency=medium
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
+44 -12
View File
@@ -42,6 +42,7 @@ type InstallTask = {
metalinkUrl?: string;
filename?: string;
origin: "spark" | "apm";
upgradeOnly?: boolean;
cancelled?: boolean;
phase: "queued-download" | "downloading" | "queued-install" | "installing";
};
@@ -268,7 +269,8 @@ ipcMain.on("queue-install", async (event, download_json) => {
typeof download_json === "string"
? JSON.parse(download_json)
: download_json;
const { id, pkgname, metalinkUrl, filename, origin } = download || {};
const { id, pkgname, metalinkUrl, filename, origin, upgradeOnly } =
download || {};
if (!id || !pkgname) {
logger.warn("passed arguments missing id or pkgname");
@@ -376,6 +378,7 @@ ipcMain.on("queue-install", async (event, download_json) => {
metalinkUrl,
filename,
origin: origin || "apm",
upgradeOnly: Boolean(upgradeOnly),
phase: metalinkUrl ? "queued-download" : "queued-install",
};
tasks.set(id, task);
@@ -769,9 +772,9 @@ async function runInstallPhase(task: InstallTask) {
if (success) {
logger.info(msgObj);
// 安装成功后,如果是APM安装,就调用createApmDesktopShortcut
// 这个函数负责处理桌面快捷方式,它自己会读取设置并决定要不要创建
if (task.origin === "apm" && task.pkgname) {
// 安装成功后,如果是APM安装任务,就调用createApmDesktopShortcut
// 升级任务不创建桌面快捷方式;这个函数自己会读取设置并决定要不要创建
if (task.origin === "apm" && task.pkgname && !task.upgradeOnly) {
try {
await createApmDesktopShortcut(task.pkgname, sendLog);
} catch (err) {
@@ -994,8 +997,16 @@ ipcMain.handle(
}> = [];
if (origin === "spark") {
// 如果提供了包名列表只检查这些包的安装状态(优化版)
if (pkgnameList && pkgnameList.length > 0) {
// 显式传入了包名列表(可能是空数组):只检查这些包的安装状态(优化版)
if (Array.isArray(pkgnameList)) {
if (pkgnameList.length === 0) {
// 商店目录尚未加载或该来源没有任何可枚举的包时,
// 直接返回空列表,避免退化为“全量扫描整个系统”后再被渲染端全部跳过,
// 否则会误报“已安装应用为空”。
logger.info("Spark 包名列表为空,跳过已安装检查");
return { success: true, apps: [] };
}
logger.info(
`使用优化模式检查 ${pkgnameList.length} 个 Spark 包的安装状态`,
);
@@ -1037,7 +1048,7 @@ ipcMain.handle(
return { success: true, apps: installedApps };
}
// 回退到全量扫描模式(未提供包名列表时
// 回退到全量扫描模式(仅当调用方未传入 pkgnameList 时,例如旧版直接调用
logger.info("使用全量扫描模式获取所有 Spark 已安装包");
const { code, stdout } = await runCommandCapture("dpkg-query", [
"-W",
@@ -1271,13 +1282,31 @@ ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
ipcMain.handle("launch-app", async (_event, payload: any) => {
interface LaunchAppPayload {
pkgname: string;
origin?: "spark" | "apm";
}
// 合法包名字符(Debian 包名规范 + Spark 应用包名常见字符),
// 用于拦截包含特殊字符的非法输入,避免命令注入。
const PKGNAME_PATTERN = /^[a-zA-Z0-9._+-]+$/;
ipcMain.handle(
"launch-app",
async (
_event,
payload: LaunchAppPayload,
): Promise<{ success: boolean; message?: string }> => {
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
const origin = typeof payload === "string" ? "spark" : payload.origin;
if (!pkgname) {
logger.warn("No pkgname provided for launch-app");
if (
!pkgname ||
typeof pkgname !== "string" ||
!PKGNAME_PATTERN.test(pkgname)
) {
logger.warn(`Invalid pkgname provided for launch-app: ${pkgname}`);
return { success: false, message: "Invalid package name" };
}
let execCommand = "/opt/spark-store/extras/app-launcher";
@@ -1298,4 +1327,7 @@ ipcMain.handle("launch-app", async (_event, payload: any) => {
detached: true,
stdio: "ignore",
}).unref();
});
return { success: true };
},
);
+29 -6
View File
@@ -448,6 +448,7 @@ export function registerSubmitterHandlers(
width: 800,
height: 900,
frame: false,
transparent: true,
show: false,
autoHideMenuBar: true,
icon: path.join(process.env.VITE_PUBLIC!, "favicon.ico"),
@@ -972,6 +973,24 @@ export function registerSubmitterHandlers(
}
});
ipcMain.handle("get-git-name", async () => {
try {
const { exec } = await import("node:child_process");
const util = await import("util");
const execAsync = util.promisify(exec);
const { stdout } = await execAsync("git config user.name");
const name = stdout.trim();
logger.info({ name }, "[Submitter] Git name retrieved");
return { success: true, data: name || "" };
} catch (err) {
logger.warn(
{ err },
"[Submitter] Failed to get git name, not a git repo or git not installed",
);
return { success: false, data: "" };
}
});
ipcMain.handle("get-tags-list", async () => {
try {
const apiUrl = "https://upload.deepinos.org.cn/api/index/get_tags_list";
@@ -1402,8 +1421,9 @@ export function registerSubmitterHandlers(
// arch 由前端解析 deb 时获取并传入,无需再次调用 dpkg-deb
const debArch = String(dataObj.arch || "amd64");
const pkgVersion = String(dataObj.version || "0.0.0");
const debPkgName = String(dataObj.pkgname || "unknown");
const formFileName = `${debPkgName}_${pkgVersion}_${debArch}.deb`;
const debPkgName = String(dataObj.pkgname || "unknown").toLowerCase();
const formFileName =
`${debPkgName}_${pkgVersion}_${debArch}.deb`.toLowerCase();
logger.info(
{ formFileName, debArch },
"[Submitter] Constructed file_name",
@@ -1671,7 +1691,7 @@ export function registerSubmitterHandlers(
}
const dataObj = formData as Record<string, unknown>;
const pkgname = String(dataObj.pkgname || "");
const pkgname = String(dataObj.pkgname || "").toLowerCase();
const packageName = String(dataObj.name || "");
const packageVersion = String(dataObj.version || "");
const packageCategory = String(dataObj.category || "");
@@ -1862,7 +1882,8 @@ export function registerSubmitterHandlers(
"[Submitter] Deb metadata parsed",
);
const packDebFileName = `${debPkgName}_${debVersion}_${debArch}.deb`;
const packDebFileName =
`${debPkgName}_${debVersion}_${debArch}.deb`.toLowerCase();
logger.info(
{
from: debFilePath,
@@ -1913,7 +1934,7 @@ export function registerSubmitterHandlers(
sendPackageProgress("tar", 85, "正在打包 tar.gz...");
logger.info("[Submitter] Starting tar...");
const tarFileName = `${pkgname}-${storeArch}.tar.gz`;
const tarFileName = `${pkgname.toLowerCase()}-${storeArch.toLowerCase()}.tar.gz`;
const tarOutputPath = path.join(baseTempDir, tarFileName);
try {
await execAsync(
@@ -1938,7 +1959,9 @@ export function registerSubmitterHandlers(
sendPackageProgress("done", 100, "打包完成!");
logger.info({ baseTempDir }, "[Submitter] Opening folder");
await shell.openPath(baseTempDir);
shell.openPath(baseTempDir).catch((err) => {
logger.warn({ err }, "[Submitter] Failed to open folder");
});
const duration = Date.now() - startTime;
logger.info(
+132 -3
View File
@@ -7,6 +7,7 @@ import {
shell,
Tray,
nativeTheme,
screen,
session,
} from "electron";
import { fileURLToPath } from "node:url";
@@ -72,6 +73,13 @@ if (!app.requestSingleInstanceLock()) {
import "./backend/install-manager.js";
import "./handle-url-scheme.js";
// 关闭 Linux 的覆盖式(overlay)滚动条,强制使用经典滚动条,
// 否则 GTK overlay 滚动条会忽略渲染进程的 ::-webkit-scrollbar 颜色,
// 导致暗色模式下滚动条始终是原生灰色。必须在 app ready 前设置。
if (process.platform === "linux") {
app.commandLine.appendSwitch("disable-features", "OverlayScrollbar");
}
const logger = pino({ name: "index.ts" });
const FLARUM_TOKEN_URL = "https://bbs.spark-app.store/api/token";
@@ -129,7 +137,9 @@ logger.info("User Agent: " + getUserAgent());
/** 根据启动参数 --no-apm / --no-spark 决定只展示的来源 */
function getStoreFilterFromArgv(): "spark" | "apm" | "both" {
if (process.arch === "loong64") {
// Currently loong64 only have spark support
// Currently loong64 only have spark support,
// 但用户显式传入 --no-spark 时应允许回退到 apm
if (process.argv.includes("--no-spark")) return "apm";
return "spark";
} else {
const argv = process.argv;
@@ -146,6 +156,12 @@ ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" =>
getStoreFilterFromArgv(),
);
// 渲染端在窗口尺寸变化时(包括无边框窗口鼠标拉边角)经此保存当前窗口尺寸
ipcMain.handle("save-window-bounds", (): boolean => {
if (win && !win.isDestroyed()) scheduleSaveBounds(win);
return true;
});
ipcMain.handle("get-app-version", (): string => getAppVersion());
ipcMain.handle("get-system-info", (): { distro: string } => getSystemInfo());
@@ -270,11 +286,103 @@ const showAndFocusMainWindow = (): void => {
win.focus();
};
// 窗口尺寸持久化:保存/恢复上一次调整后的窗口大小,避免每次打开都使用默认尺寸
const DEFAULT_WINDOW_SIZE = { width: 1366, height: 768 };
const MIN_WINDOW_SIZE = { width: 800, height: 500 };
interface WindowState {
width?: number;
height?: number;
x?: number;
y?: number;
maximized?: boolean;
}
function getWindowStatePath(): string {
// 延迟到调用时再取 userData,避免在 app ready 之前调用 app.getPath 出错
return path.join(app.getPath("userData"), "window-state.json");
}
// 校验保存的窗口位置是否至少部分落在某个显示器可见区域内,避免窗口跑到屏幕外
function isVisible(bounds: WindowState): boolean {
if (
bounds.width === undefined ||
bounds.height === undefined ||
bounds.x === undefined ||
bounds.y === undefined
) {
return false;
}
const displays = screen.getAllDisplays();
return displays.some((display) => {
const { x, y, width, height } = display.workArea;
const horizontally =
bounds.x < x + width && bounds.x + bounds.width > x;
const vertically =
bounds.y < y + height && bounds.y + bounds.height > y;
return horizontally && vertically;
});
}
function loadWindowState(): WindowState {
try {
const file = getWindowStatePath();
if (fs.existsSync(file)) {
const parsed = JSON.parse(
fs.readFileSync(file, "utf-8"),
) as WindowState;
if (
parsed.width !== undefined &&
parsed.height !== undefined &&
parsed.width >= MIN_WINDOW_SIZE.width &&
parsed.height >= MIN_WINDOW_SIZE.height &&
isVisible(parsed)
) {
return parsed;
}
logger.warn({ parsed }, "已保存的窗口状态无效,使用默认尺寸");
}
} catch (err) {
logger.warn({ err }, "读取窗口状态失败,使用默认尺寸");
}
return {};
}
function saveWindowState(state: WindowState): void {
try {
fs.writeFileSync(getWindowStatePath(), JSON.stringify(state));
logger.info({ state }, "已保存窗口状态");
} catch (err) {
logger.warn({ err }, "保存窗口状态失败");
}
}
let saveBoundsTimer: NodeJS.Timeout | null = null;
function scheduleSaveBounds(winInstance: BrowserWindow): void {
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
saveBoundsTimer = setTimeout(() => {
if (winInstance.isDestroyed()) return;
const { width, height, x, y } = winInstance.getBounds();
saveWindowState({
width,
height,
x,
y,
maximized: winInstance.isMaximized(),
});
}, 400);
}
async function createWindow() {
const saved = loadWindowState();
const mainWindow = new BrowserWindow({
title: "星火应用商店",
width: 1366,
height: 768,
width: saved.width ?? DEFAULT_WINDOW_SIZE.width,
height: saved.height ?? DEFAULT_WINDOW_SIZE.height,
x: saved.x,
y: saved.y,
minWidth: MIN_WINDOW_SIZE.width,
minHeight: MIN_WINDOW_SIZE.height,
frame: false,
autoHideMenuBar: true,
icon: path.join(process.env.VITE_PUBLIC, "favicon.ico"),
@@ -290,6 +398,18 @@ async function createWindow() {
});
win = mainWindow;
// 恢复上一次的最大化状态
if (saved.maximized) {
mainWindow.maximize();
}
logger.info({ saved }, "已恢复窗口状态");
// 窗口大小/位置/最大化变化后防抖保存,下次启动时恢复
// 位置/最大化变化由主进程事件保存;尺寸变化由渲染端 DOM resize 经 IPC 兜底保存
mainWindow.on("moved", () => scheduleSaveBounds(mainWindow));
mainWindow.on("maximize", () => scheduleSaveBounds(mainWindow));
mainWindow.on("unmaximize", () => scheduleSaveBounds(mainWindow));
if (VITE_DEV_SERVER_URL) {
// #298
mainWindow.loadURL(VITE_DEV_SERVER_URL);
@@ -323,6 +443,15 @@ async function createWindow() {
mainWindow.on("close", (event) => {
if (allowAppExit) {
// 真正退出前同步保存最终窗口尺寸(防抖可能尚未触发)
const { width, height, x, y } = mainWindow.getBounds();
saveWindowState({
width,
height,
x,
y,
maximized: mainWindow.isMaximized(),
});
return;
}
+10 -1
View File
@@ -1,4 +1,9 @@
import { ipcRenderer, contextBridge, type IpcRendererEvent } from "electron";
import {
ipcRenderer,
contextBridge,
webUtils,
type IpcRendererEvent,
} from "electron";
type StoreFilter = "spark" | "apm" | "both";
@@ -97,6 +102,10 @@ contextBridge.exposeInMainWorld("apm_store", {
})(),
});
contextBridge.exposeInMainWorld("electronUtils", {
getPathForFile: (file: File): string => webUtils.getPathForFile(file),
});
contextBridge.exposeInMainWorld("windowControls", {
minimize: () => ipcRenderer.send("window-control-minimize"),
toggleMaximize: () => ipcRenderer.send("window-control-toggle-maximize"),
@@ -3,7 +3,7 @@ Description=Spark Store update notifier
After=apt-daily.service network.target network-online.target systemd-networkd.service NetworkManager.service connman.service
[Service]
Type=oneshot
Type=simple
RemainAfterExit=yes
ExecStart=/opt/durapps/spark-store/bin/update-upgrade/ss-update-notifier.sh
Restart=on-failure
+209 -73
View File
@@ -73,7 +73,9 @@
:category-counts="categoryCounts"
@select-category="selectSubCategory"
/>
<div class="flex min-h-0 flex-1 flex-col px-4 py-6 lg:px-10">
<div
class="flex min-h-0 flex-1 flex-col overflow-hidden px-4 py-6 lg:px-10"
>
<FavoriteFolderManager
v-if="currentView === 'favorites'"
:folders="favoriteFolders"
@@ -172,10 +174,7 @@
:apps="installedApps"
:loading="installedLoading"
:error="installedError"
:active-origin="activeInstalledOrigin"
:store-filter="storeFilter"
:spark-available="sparkAvailable"
:apm-available="apmAvailable"
:warning="installedWarning"
:logged-in="isLoggedIn"
:syncing="syncLoading"
:sync-message="syncStatusMessage"
@@ -184,7 +183,6 @@
@open-app="openDownloadedApp($event.pkgname, $event.origin)"
@open-detail="openDetail"
@uninstall="uninstallInstalledApp"
@switch-origin="handleSwitchOrigin"
@sync-to-account="syncInstalledAppsToAccount"
@restore-from-account="openRestoreFromAccount"
@request-login="requireLogin('云端同步需要登录星火账号。')"
@@ -287,7 +285,7 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from "vue";
import axios from "axios";
import axios, { AxiosError } from "axios";
import pino from "pino";
import AppSidebar from "./components/AppSidebar.vue";
import AppHeader from "./components/AppHeader.vue";
@@ -362,10 +360,9 @@ import {
setAuthSession,
} from "./global/authState";
import {
getAllowedInstalledOrigin,
getEffectiveStoreFilter,
getDefaultInstalledOrigin,
isOriginEnabled,
isOriginUsable,
} from "./modules/storeFilter";
import { createUpdateCenterStore } from "./modules/updateCenter";
import {
@@ -424,7 +421,16 @@ const fetchWithRetry = async <T,>(
const response = await axiosInstance.get<T>(url);
return response.data;
} catch (error) {
if (retries > 0) {
const axiosError = error as AxiosError;
const status = axiosError.response?.status;
// 仅对网络错误(无响应)、服务端 5xx 错误或超时进行重试;
// 4xx(如 404/400)属于明确的客户端错误,直接抛出以避免无谓重试。
const isNetworkError = status === undefined;
const isServerError = typeof status === "number" && status >= 500;
const isTimeout =
axiosError.code === "ECONNABORTED" || axiosError.code === "ETIMEDOUT";
if (retries > 0 && (isNetworkError || isServerError || isTimeout)) {
await new Promise((resolve) => setTimeout(resolve, delay));
return fetchWithRetry(url, retries - 1, delay * 2);
}
@@ -432,6 +438,44 @@ const fetchWithRetry = async <T,>(
}
};
// 渲染进程从 IPC 拿到的 result.apps 实际类型为 anyipcRenderer.invoke 返回 Promise<any>),
// 直接断言成 InstalledAppInfo[] 会绕过运行时类型检查。后端字段缺失时会引发运行时错误。
// 此守卫仅校验本项目实际使用的关键字段,后端字段缺失时跳过即可,避免整批失败。
const isInstalledAppInfo = (value: unknown): value is InstalledAppInfo => {
if (typeof value !== "object" || value === null) return false;
const v = value as Partial<InstalledAppInfo>;
return (
typeof v.pkgname === "string" &&
typeof v.name === "string" &&
typeof v.version === "string" &&
typeof v.arch === "string" &&
(v.origin === "spark" || v.origin === "apm") &&
typeof v.flags === "string" &&
typeof v.isDependency === "boolean" &&
// icon 在类型中为可选字段(string | undefined),需显式校验其类型,
// 避免非字符串值进入下游 app.icon || "" 触发隐式转换异常
(typeof v.icon === "string" || v.icon === undefined)
);
};
// 单来源已安装查询超时时间:某个来源(如 APM)响应极慢或挂起时,
// 不应阻塞其它来源整体返回,超时后该来源标记为失败并走 warning/error 流程。
const LIST_INSTALLED_TIMEOUT_MS = 15000;
// 为 Promise 增加超时控制:超时即 reject,配合 Promise.allSettled 让单来源失败不影响其它来源。
const withTimeout = <T,>(
promise: Promise<T>,
ms: number,
label: string,
): Promise<T> => {
return Promise.race([
promise,
new Promise<T>((_, reject) =>
setTimeout(() => reject(new Error(`${label} 超时(${ms}ms`)), ms),
),
]);
};
// 响应式状态
const themeMode = ref<"light" | "dark" | "auto">("auto");
const systemIsDark = ref(
@@ -468,10 +512,13 @@ const loading = ref(true);
const showDownloadDetailModal = ref(false);
const currentDownload: Ref<DownloadItem | null> = ref(null);
const showInstalledModal = ref(false);
const activeInstalledOrigin = ref<"apm" | "spark">("apm");
const installedApps = ref<App[]>([]);
const installedLoading = ref(false);
const installedError = ref("");
// 部分来源失败(如 Spark/APM 之一不可用)时使用更轻量的 warning 提示,避免与已有列表同时呈现红色致命错误条造成 UX 混淆
const installedWarning = ref("");
// refreshInstalledApps 的代次计数器,用于异步竞态防护:新进入一次刷新自增,await 后若代次已变则放弃本轮写入
const installedRefreshGeneration = ref(0);
const updateCenterStore = createUpdateCenterStore();
const showUninstallModal = ref(false);
const uninstallTargetApp: Ref<App | null> = ref(null);
@@ -613,7 +660,7 @@ const categoryCounts = computed(() => {
return { ...tabCounts, all: allCounts.all };
}
const counts: Record<string, number> = { all: apps.value.length };
const counts: Record<string, number> = { all: baseApps.value.length };
sourceApps.forEach((app) => {
if (!counts[app.category]) counts[app.category] = 0;
counts[app.category]++;
@@ -1379,81 +1426,104 @@ const confirmMigrationStart = async () => {
};
const openInstalledModal = () => {
const defaultOrigin = getDefaultInstalledOrigin(
storeFilter.value,
availableSources.value,
);
if (!defaultOrigin) {
if (
getEffectiveStoreFilter(storeFilter.value, availableSources.value) === null
) {
return;
}
showInstalledModal.value = true;
activeInstalledOrigin.value =
getAllowedInstalledOrigin(
storeFilter.value,
activeInstalledOrigin.value,
availableSources.value,
) ?? defaultOrigin;
refreshInstalledApps();
};
const closeInstalledModal = () => {
showInstalledModal.value = false;
// 关闭模态框时同步清空错误 / 警告,避免下次打开时残留过期状态
installedError.value = "";
installedWarning.value = "";
};
const handleSwitchOrigin = (origin: "apm" | "spark") => {
activeInstalledOrigin.value =
getAllowedInstalledOrigin(
storeFilter.value,
origin,
availableSources.value,
) ?? activeInstalledOrigin.value;
refreshInstalledApps();
// 根据当前启动模式和系统可用性,确定需要查询哪些 origin 的已安装应用
const resolveInstalledOrigins = (): Array<"spark" | "apm"> => {
const origins: Array<"spark" | "apm"> = [];
if (isOriginUsable(storeFilter.value, "spark", availableSources.value)) {
origins.push("spark");
}
if (isOriginUsable(storeFilter.value, "apm", availableSources.value)) {
origins.push("apm");
}
return origins;
};
const refreshInstalledApps = async () => {
// 异步竞态防护:进入时若模态框已关闭(极小概率),直接放弃本轮请求
if (!showInstalledModal.value) return;
installedLoading.value = true;
installedError.value = "";
installedWarning.value = "";
// 代次计数器:每次进入自增;await 之后若代次已变(新一轮刷新 / 关闭重开),丢弃本轮结果
const generation = ++installedRefreshGeneration.value;
try {
const origin = getAllowedInstalledOrigin(
storeFilter.value,
activeInstalledOrigin.value,
availableSources.value,
);
if (!origin) {
installedApps.value = [];
installedError.value = "当前系统不可用应用管理功能";
return;
}
activeInstalledOrigin.value = origin;
if (!isOriginEnabled(storeFilter.value, origin)) {
installedApps.value = [];
installedError.value = `当前启动模式已禁用 ${origin === "spark" ? "Spark" : "APM"} 软件管理`;
return;
}
// Spark 优化:只检查远端商店目录中的应用,避免全量扫描
let pkgnameList: string[] | undefined;
if (origin === "spark") {
pkgnameList = apps.value
const origins = resolveInstalledOrigins();
// Spark 已安装列表依赖商店目录(apps.value)来枚举包名。
// 仅当目录中存在可枚举的 Spark 包时才查询 Spark
// 否则空目录会触发"全量扫描整个系统"的陷阱导致列表被全部跳过而误报为空。
const sparkPkgnameList = apps.value
.filter((a) => a.origin === "spark")
.map((a) => a.pkgname);
}
const effectiveOrigins = origins.filter(
(o) => o !== "spark" || sparkPkgnameList.length > 0,
);
const result = await window.ipcRenderer.invoke("list-installed", {
origin,
pkgnameList,
});
if (!result?.success) {
if (effectiveOrigins.length === 0) {
installedApps.value = [];
installedError.value = result?.message || "读取已安装应用失败";
// 目录尚未加载完成(但来源可用)时给出过渡提示,待目录加载后会自动重查
installedError.value =
apps.value.length === 0
? "正在加载应用目录,请稍候…"
: "当前系统不可用应用管理功能";
return;
}
installedApps.value = [];
for (const app of result.apps) {
// 并行查询每个 origin 的已安装应用:
// 用 allSettled + 每源超时,避免单一来源(如 APM)响应慢/挂起阻塞整体;
// 超时或失败的来源在下方循环标记为 failedOrigin,不影响其它来源结果。
const results = await Promise.allSettled(
effectiveOrigins.map((origin) =>
withTimeout(
window.ipcRenderer.invoke("list-installed", {
origin,
pkgnameList: origin === "spark" ? sparkPkgnameList : undefined,
}),
LIST_INSTALLED_TIMEOUT_MS,
`${origin} list-installed`,
),
),
);
// 异步结束后的回调阶段重新校验代次与模态可见性,避免陈旧竞态写入 ref
if (generation !== installedRefreshGeneration.value) return;
if (!showInstalledModal.value) return;
const combinedApps: App[] = [];
const failedOrigins: string[] = [];
for (let i = 0; i < effectiveOrigins.length; i++) {
const origin = effectiveOrigins[i];
const settled = results[i];
// allSettled: rejected(超时/异常)或 success=false 都视为该来源失败
if (settled.status !== "fulfilled" || !settled.value?.success) {
failedOrigins.push(origin);
continue;
}
const result = settled.value;
const appList = Array.isArray(result?.apps) ? result.apps : [];
for (const rawApp of appList) {
// 运行时类型守卫,避免后端字段缺失造成下游访问 undefined 抛出
if (!isInstalledAppInfo(rawApp)) continue;
const app = rawApp;
// Find matching remote app to enrich data. We look exactly for that origin.
let appInfo = apps.value.find(
(a) => a.pkgname === app.pkgname && a.origin === origin,
@@ -1494,16 +1564,65 @@ const refreshInstalledApps = async () => {
isDependency: app.isDependency,
};
}
installedApps.value.push(appInfo);
combinedApps.push(appInfo);
}
}
installedApps.value = combinedApps;
// 部分来源失败使用轻量 warning(不与列表同时呈现红色致命错误条),致命/全失败仍用 error
if (failedOrigins.length > 0) {
const labels = failedOrigins
.map((o) => (o === "spark" ? "Spark" : "APM"))
.join("、");
if (combinedApps.length > 0) {
installedWarning.value = `部分来源加载失败(${labels}),已安装列表可能不完整`;
} else {
installedError.value = `读取${labels}已安装应用失败`;
}
}
} catch (error: unknown) {
if (generation !== installedRefreshGeneration.value) return;
if (!showInstalledModal.value) return;
installedApps.value = [];
installedError.value = (error as Error)?.message || "读取已安装应用失败";
} finally {
// 仅最末一代次负责清理 loading,防止陈旧代次提前关闭 loading 影响后续刷新
if (generation === installedRefreshGeneration.value) {
installedLoading.value = false;
}
}
};
// 应用目录(apps.value)可能在打开"已安装应用"模态框之后才加载完成。
// 当目录长度变化、且模态框处于打开状态时,自动重查已安装应用,避免列表一直为空。
// 注意:上一版仅在 prevLen===0→len>0 触发,会遗漏目录后续更新(例如分类切换触发目录重建)。
// 现改为 len 任意 >0 的正向变化都允许触发;密集分批推送由下方 300ms 防抖合并最后一次写入。
// 每次变化都先自增 installedRefreshGeneration:即使上一轮刷新仍在加载中,也会立即失效,
// 避免用陈旧目录数据覆盖已安装列表(清理不单纯依赖定时器,代次校验兜底)。
let refreshDebounceTimer: ReturnType<typeof setTimeout> | null = null;
watch(
() => apps.value.length,
(len) => {
if (!showInstalledModal.value || len <= 0) {
return;
}
installedRefreshGeneration.value++;
if (refreshDebounceTimer !== null) {
clearTimeout(refreshDebounceTimer);
}
refreshDebounceTimer = setTimeout(() => {
refreshDebounceTimer = null;
void refreshInstalledApps();
}, 300);
},
);
onUnmounted(() => {
if (refreshDebounceTimer !== null) {
clearTimeout(refreshDebounceTimer);
}
});
const mapInstalledAppToCatalogApp = (
app: InstalledAppInfo,
origin: "spark" | "apm",
@@ -1573,8 +1692,11 @@ const refreshFavoriteInstalledApps = async (): Promise<void> => {
});
if (!result?.success) return;
for (const app of result.apps as InstalledAppInfo[]) {
const appInfo = mapInstalledAppToCatalogApp(app, origin);
const appList = Array.isArray(result?.apps) ? result.apps : [];
for (const rawApp of appList) {
// 运行时类型守卫:避免后端字段缺失时下游访问 undefined
if (!isInstalledAppInfo(rawApp)) continue;
const appInfo = mapInstalledAppToCatalogApp(rawApp, origin);
if (appInfo) refreshedApps.push(appInfo);
}
}),
@@ -1916,8 +2038,11 @@ const refreshInstalledSyncCandidates = async (
});
if (!result?.success) return;
for (const app of result.apps as InstalledAppInfo[]) {
const appInfo = mapInstalledAppToCatalogApp(app, origin);
const appList = Array.isArray(result?.apps) ? result.apps : [];
for (const rawApp of appList) {
// 运行时类型守卫:避免后端字段缺失时下游访问 undefined
if (!isInstalledAppInfo(rawApp)) continue;
const appInfo = mapInstalledAppToCatalogApp(rawApp, origin);
if (appInfo) refreshedApps.push(appInfo);
}
}),
@@ -2450,7 +2575,9 @@ const openDownloadedApp = (pkgname: string, origin?: "spark" | "apm") => {
// openApmStoreUrl(`apmstore://launch?pkg=${encodedPkg}`, {
// fallbackText: `打开应用: ${download.pkgname}`
// });
window.ipcRenderer.invoke("launch-app", { pkgname, origin });
window.ipcRenderer
.invoke("launch-app", { pkgname, origin })
.catch((err) => logger.error("启动应用失败 (launch-app):", err));
};
const loadCategories = async () => {
@@ -2794,6 +2921,15 @@ const handleSearchFocus = () => {
if (activeTab.value === "home") activeTab.value = "all";
};
// 窗口尺寸变化(含无边框窗口鼠标拉边角)时,防抖通知主进程保存当前尺寸
let saveBoundsTimer: number | undefined;
const handleWindowResize = () => {
if (saveBoundsTimer) clearTimeout(saveBoundsTimer);
saveBoundsTimer = window.setTimeout(() => {
void window.ipcRenderer.invoke("save-window-bounds");
}, 400);
};
// 生命周期钩子
onMounted(async () => {
initTheme();
@@ -2806,6 +2942,9 @@ onMounted(async () => {
handleHashChange();
window.addEventListener("hashchange", handleHashChange);
// 窗口尺寸变化(含无边框窗口鼠标拉边角)时,防抖通知主进程保存当前尺寸
window.addEventListener("resize", handleWindowResize);
try {
systemInfo.value = await window.ipcRenderer.invoke("get-system-info");
} catch (error: unknown) {
@@ -2827,10 +2966,6 @@ onMounted(async () => {
apmAvailable.value = await window.ipcRenderer.invoke("check-apm-available");
}
activeInstalledOrigin.value =
getDefaultInstalledOrigin(storeFilter.value, availableSources.value) ??
"spark";
await loadCategories();
await loadSidebarConfig();
@@ -2988,6 +3123,7 @@ onUnmounted(() => {
"install-complete",
handleInstallCompleteForDownloadRecord,
);
window.removeEventListener("resize", handleWindowResize);
});
// 观察器
+130 -100
View File
@@ -26,21 +26,20 @@ const createApp = (overrides: Partial<App> = {}): App => ({
});
describe("InstalledAppsModal", () => {
it("keeps scroll chaining inside the modal list", () => {
const { container } = render(InstalledAppsModal, {
props: {
const baseProps = {
show: true,
apps: [],
apps: [] as App[],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
warning: "",
loggedIn: false,
syncing: false,
syncMessage: "",
},
};
it("keeps scroll chaining inside the modal list", () => {
const { container } = render(InstalledAppsModal, {
props: baseProps,
});
expect(screen.getByText("已安装应用")).toBeTruthy();
@@ -52,17 +51,8 @@ describe("InstalledAppsModal", () => {
it("renders open and detail actions for a store-backed installed app", () => {
render(InstalledAppsModal, {
props: {
show: true,
...baseProps,
apps: [createApp()],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
});
@@ -70,20 +60,42 @@ describe("InstalledAppsModal", () => {
expect(screen.getByRole("button", { name: "查看详情" })).toBeTruthy();
});
it("renders the spark origin tag for spark apps", () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [createApp({ origin: "spark", name: "Spark Notes" })],
},
});
// 精确匹配应用行内的来源标签(页头统计区也有 "Spark" 文案,getAllByText 过于宽泛)
expect(screen.getByTestId("origin-tag-spark")).toBeTruthy();
});
it("renders the APM origin tag for APM apps", () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [
createApp({
origin: "apm",
name: "APM Container",
pkgname: "amber-pm-container",
version: "1.0.0",
}),
],
},
});
// 精确匹配应用行内的来源标签(页头统计区也有 "APM" 文案,getAllByText 过于宽泛)
expect(screen.getByTestId("origin-tag-apm")).toBeTruthy();
});
it("emits open-app when clicking 打开", async () => {
const rendered = render(InstalledAppsModal, {
props: {
show: true,
...baseProps,
apps: [createApp()],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
});
@@ -98,17 +110,8 @@ describe("InstalledAppsModal", () => {
it("emits open-detail when clicking 查看详情", async () => {
const rendered = render(InstalledAppsModal, {
props: {
show: true,
...baseProps,
apps: [createApp()],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
});
@@ -123,17 +126,8 @@ describe("InstalledAppsModal", () => {
it("shows 查看详情 for metadata-rich unknown-category apps", () => {
render(InstalledAppsModal, {
props: {
show: true,
...baseProps,
apps: [createApp({ category: "unknown", more: "Has store metadata" })],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
});
@@ -143,17 +137,8 @@ describe("InstalledAppsModal", () => {
it("hides 查看详情 for unknown-category apps", () => {
render(InstalledAppsModal, {
props: {
show: true,
...baseProps,
apps: [createApp({ category: "unknown" })],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
});
@@ -162,19 +147,7 @@ describe("InstalledAppsModal", () => {
it("requests login for cloud actions when logged out", async () => {
const rendered = render(InstalledAppsModal, {
props: {
show: true,
apps: [],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
loggedIn: false,
syncing: false,
syncMessage: "",
},
props: baseProps,
});
await fireEvent.click(screen.getByRole("button", { name: "同步到账号" }));
@@ -186,17 +159,8 @@ describe("InstalledAppsModal", () => {
it("emits cloud sync and restore events when logged in", async () => {
const rendered = render(InstalledAppsModal, {
props: {
show: true,
apps: [],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
...baseProps,
loggedIn: true,
syncing: false,
syncMessage: "",
},
});
@@ -210,17 +174,9 @@ describe("InstalledAppsModal", () => {
it("disables sync button while syncing", () => {
render(InstalledAppsModal, {
props: {
show: true,
apps: [],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
...baseProps,
loggedIn: true,
syncing: true,
syncMessage: "",
},
});
@@ -230,20 +186,94 @@ describe("InstalledAppsModal", () => {
it("shows account sync feedback in the installed apps modal", () => {
render(InstalledAppsModal, {
props: {
show: true,
apps: [],
loading: false,
error: "",
activeOrigin: "spark",
storeFilter: "both",
sparkAvailable: true,
apmAvailable: true,
...baseProps,
loggedIn: true,
syncing: false,
syncMessage: "同步完成",
},
});
expect(screen.getByText("同步完成")).toBeTruthy();
});
it("filters installed apps by search query (name match)", async () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [
createApp({ name: "Spark Notes", pkgname: "spark-notes" }),
createApp({
name: "Visual Studio Code",
pkgname: "code",
category: "dev",
more: "https://code.visualstudio.com/",
}),
],
},
});
// 初始两条都在
expect(screen.getByText("Spark Notes")).toBeTruthy();
expect(screen.getByText("Visual Studio Code")).toBeTruthy();
const input = screen.getByPlaceholderText("搜索已安装应用…");
await fireEvent.update(input, "code");
// "code" 只匹配到 pkgname 为 "code" 的项(名称不区分大小写)
expect(screen.queryByText("Spark Notes")).toBeNull();
expect(screen.getByText("Visual Studio Code")).toBeTruthy();
});
it("filters installed apps by search query (case-insensitive)", async () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [
createApp({ name: "钉钉", pkgname: "com.alibaba.dingtalk" }),
],
},
});
const input = screen.getByPlaceholderText("搜索已安装应用…");
await fireEvent.update(input, "DINGTALK");
// 包名大写不区分大小写匹配
expect(screen.getByText("钉钉")).toBeTruthy();
});
it("shows a no-match hint when search query has no results", async () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [createApp({ name: "Spark Notes" })],
},
});
const input = screen.getByPlaceholderText("搜索已安装应用…");
await fireEvent.update(input, "不存在的关键字xyz");
expect(screen.queryByText("Spark Notes")).toBeNull();
expect(screen.getByText(/未找到匹配/)).toBeTruthy();
});
it("clears the search when clicking the clear button", async () => {
render(InstalledAppsModal, {
props: {
...baseProps,
apps: [createApp()],
},
});
const input = screen.getByPlaceholderText(
"搜索已安装应用…",
) as HTMLInputElement;
await fireEvent.update(input, "code");
expect(input.value).toBe("code");
// 清除按钮存在(仅在有内容时显示)
const clearBtn = screen.getByRole("button", { name: "清除搜索" });
await fireEvent.click(clearBtn);
expect(input.value).toBe("");
expect(screen.getByText("Spark Notes")).toBeTruthy();
});
});
+60 -2
View File
@@ -13,6 +13,9 @@
--color-card-light: #ffffff;
--color-card-dark: #151c2c;
--color-scrollbar-thumb-light: #94a3b8;
--color-scrollbar-thumb-dark: #15203b;
--shadow-glass: 0 10px 30px rgba(15,23,42,0.08);
--shadow-glassDark: 0 20px 45px rgba(0,0,0,0.45);
@@ -22,6 +25,14 @@
@variant dark (&:where(.dark, .dark *));
@layer base {
html {
color-scheme: light;
}
html.dark {
color-scheme: dark;
}
body {
font-family: var(--font-sans);
background-color: var(--color-surface-light);
@@ -38,12 +49,50 @@
#app {
min-height: 100vh;
}
/* 全局滚动条基础几何:置于 base,使 utilities 中的 .scrollbar-muted/.scrollbar-nowidth 可覆盖 */
*::-webkit-scrollbar {
width: 0.75rem;
height: 0.75rem;
}
*::-webkit-scrollbar-track {
background: transparent;
}
/* 亮色滚动条:与暗色保持相同几何(透明边距),确保亮/暗滚动块大小一致 */
html ::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb-light);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
}
html ::-webkit-scrollbar-track {
background: transparent;
}
/* 暗色滚动条:挂在 html.dark 上(root),比 surface 略亮以保留可辨识反差 (#15203b),
注意:必须用 html.dark 而非 .dark *,因为经典滚动条只作用于滚动容器自身,
.dark * 在亮色模式不命中会导致回退到原生宽度,亮/暗宽度不一致。 */
html.dark ::-webkit-scrollbar-thumb {
background-color: var(--color-scrollbar-thumb-dark);
border-radius: 9999px;
border: 2px solid transparent;
background-clip: padding-box;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.15);
}
html.dark ::-webkit-scrollbar-track {
background: transparent;
}
}
@layer utilities {
/* 弱化滚动条 */
.scrollbar-muted {
scrollbar-width: thin;
scrollbar-color: #94a3b8 transparent;
scrollbar-color: var(--color-scrollbar-thumb-light) transparent;
}
.scrollbar-muted::-webkit-scrollbar {
@@ -55,10 +104,19 @@
}
.scrollbar-muted::-webkit-scrollbar-thumb {
background-color: #94a3b8;
background-color: var(--color-scrollbar-thumb-light);
border-radius: 9999px;
}
html.dark .scrollbar-muted {
scrollbar-color: var(--color-surface-dark) transparent;
}
html.dark .scrollbar-muted::-webkit-scrollbar-thumb {
background-color: var(--color-surface-dark);
}
/* 完全隐藏滚动条 */
.scrollbar-nowidth {
scrollbar-width: none;
}
+2
View File
@@ -97,6 +97,7 @@
<button
v-if="app.sparkApp"
type="button"
title="原生安装,高手自定义"
class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class="
viewingOrigin === 'spark'
@@ -110,6 +111,7 @@
<button
v-if="app.apmApp"
type="button"
title="兼容安装,省心不折腾"
class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class="
viewingOrigin === 'apm'
+2
View File
@@ -51,6 +51,7 @@
<button
v-if="app.sparkApp"
type="button"
title="原生安装,高手自定义"
class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class="
viewingOrigin === 'spark'
@@ -64,6 +65,7 @@
<button
v-if="app.apmApp"
type="button"
title="兼容安装,省心不折腾"
class="px-3 py-1 text-xs font-medium uppercase tracking-wider transition-colors"
:class="
viewingOrigin === 'apm'
+7 -3
View File
@@ -21,8 +21,8 @@
<div v-else-if="!loading && apps.length <= 50" class="non-virtual-scroller">
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
<AppCard
v-for="(app, index) in apps"
:key="index"
v-for="app in apps"
:key="app.pkgname"
:app="app"
:show-origin="effectiveShowOrigin"
@open-detail="$emit('open-detail', app)"
@@ -182,12 +182,17 @@ const gridRows = computed(() => {
.non-virtual-scroller {
height: 100%;
overflow-y: auto;
margin: -24px -16px; /* 抵消父容器的 px-4 py-6 */
}
@media (min-width: 1024px) {
.scroller {
margin: -24px -40px; /* 抵消父容器的 lg:px-10 */
}
.non-virtual-scroller {
margin: -24px -40px; /* 抵消父容器的 lg:px-10 */
}
}
.grid-row {
@@ -205,7 +210,6 @@ const gridRows = computed(() => {
flex: 1;
box-sizing: border-box;
overflow-x: hidden;
overflow-y: auto;
}
:deep(.vue-recycle-scroller__item-view) {
+1 -31
View File
@@ -1,12 +1,6 @@
<template>
<div
ref="queueRef"
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white shadow-2xl transition-all duration-200 dark:border-slate-800/70 dark:bg-slate-900 sm:left-auto sm:right-6 sm:w-96"
:class="
isHidden
? 'pointer-events-none translate-y-[calc(100%+1rem)] opacity-0'
: 'translate-y-0 opacity-100'
"
>
<div
class="flex items-center justify-between px-5 py-4"
@@ -136,7 +130,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, ref } from "vue";
import type { DownloadItem } from "../global/typedefinition";
const props = defineProps<{
@@ -153,30 +147,6 @@ const emit = defineEmits<{
}>();
const isExpanded = ref(false);
const isHidden = ref(false);
const queueRef = ref<HTMLElement | null>(null);
const onWheel = (event: WheelEvent) => {
if (queueRef.value?.contains(event.target as Node) || event.deltaY === 0) {
return;
}
if (event.deltaY > 0) {
isExpanded.value = false;
isHidden.value = true;
return;
}
isHidden.value = false;
};
onMounted(() => {
document.addEventListener("wheel", onWheel, { passive: true, capture: true });
});
onUnmounted(() => {
document.removeEventListener("wheel", onWheel, { capture: true });
});
const completedDownloads = computed(() => {
return props.downloads.filter((d) => d.status === "completed").length;
+228 -74
View File
@@ -14,71 +14,142 @@
@wheel="onOverlayWheel"
>
<div
class="flex w-full max-w-4xl max-h-[85vh] flex-col rounded-3xl border border-white/10 bg-white/95 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
class="flex w-full max-w-4xl max-h-[85vh] flex-col overflow-hidden rounded-3xl border border-white/10 bg-white/95 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
>
<div
class="flex items-start justify-between border-b border-slate-200/70 p-6 dark:border-slate-800/70"
>
<div>
<div class="flex flex-col gap-2">
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
已安装应用
</p>
<p class="text-sm text-slate-500 dark:text-slate-400">
管理本机安装的应用程序
</p>
<div
v-if="!loading && !error"
class="mt-2 flex flex-wrap items-center gap-3"
>
<div
class="inline-flex flex-wrap items-stretch overflow-hidden rounded-2xl border border-slate-200/70 bg-slate-50/60 text-sm shadow-sm dark:border-slate-700/60 dark:bg-slate-800/40"
>
<!-- APM -->
<div
class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-amber-50/80 dark:hover:bg-amber-500/10"
:class="
filterOrigin === 'apm'
? 'bg-amber-100 ring-2 ring-amber-400/50 dark:bg-amber-500/20'
: ''
"
title="仅显示 APM 应用"
role="button"
@click="filterOrigin = 'apm'"
>
<span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-amber-400 to-orange-500 text-white shadow-sm shadow-amber-500/30"
>
<i class="fas fa-box-open text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-amber-700 dark:text-amber-300"
>{{ apmCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-amber-600/80 dark:text-amber-400/80"
>APM</span
>
</div>
</div>
<!-- 分隔线 -->
<span
class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- Spark -->
<div
class="group flex cursor-pointer items-center gap-2.5 px-3.5 py-2 transition hover:bg-sky-50/80 dark:hover:bg-sky-500/10"
:class="
filterOrigin === 'spark'
? 'bg-sky-100 ring-2 ring-sky-400/50 dark:bg-sky-500/20'
: ''
"
title="仅显示 Spark 应用"
role="button"
@click="filterOrigin = 'spark'"
>
<span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-sky-400 to-blue-500 text-white shadow-sm shadow-sky-500/30"
>
<i class="fas fa-bolt text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-sky-700 dark:text-sky-300"
>{{ sparkCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-sky-600/80 dark:text-sky-400/80"
>Spark</span
>
</div>
</div>
<!-- 分隔线 -->
<span
class="self-stretch w-px bg-slate-200/80 dark:bg-slate-700/80"
></span>
<!-- 总数 -->
<div
class="flex cursor-pointer items-center gap-2.5 px-3.5 py-2"
:class="
filterOrigin === 'all'
? 'bg-slate-100 ring-2 ring-slate-400/40 dark:bg-slate-700/40'
: ''
"
title="显示全部已安装应用"
role="button"
@click="filterOrigin = 'all'"
>
<span
class="flex h-7 w-7 items-center justify-center rounded-lg bg-gradient-to-br from-slate-500 to-slate-700 text-white shadow-sm dark:from-slate-400 dark:to-slate-600"
>
<i class="fas fa-cubes text-[12px]"></i>
</span>
<div class="flex flex-col leading-tight">
<span
class="text-[15px] font-bold tabular-nums text-slate-900 dark:text-white"
>{{ totalCount }}</span
>
<span
class="text-[10px] font-medium uppercase tracking-wider text-slate-500 dark:text-slate-400"
>总数</span
>
</div>
</div>
</div>
<!-- 搜索框 -->
<div class="relative flex-1 min-w-[200px]">
<i
class="fas fa-search pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-slate-400"
></i>
<input
v-model="searchQuery"
type="text"
placeholder="搜索已安装应用…"
class="w-full rounded-2xl border border-slate-200/70 bg-slate-50/60 py-2 pl-9 pr-9 text-sm text-slate-700 placeholder-slate-400 transition focus:border-brand/60 focus:bg-white focus:outline-none focus:ring-2 focus:ring-brand/20 dark:border-slate-700 dark:bg-slate-800/40 dark:text-slate-200 dark:placeholder-slate-500 dark:focus:bg-slate-800"
/>
<button
v-if="searchQuery"
type="button"
class="absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-full text-slate-400 transition hover:bg-slate-200/60 hover:text-slate-700 dark:hover:bg-slate-700 dark:hover:text-white"
aria-label="清除搜索"
@click="searchQuery = ''"
>
<i class="fas fa-xmark text-xs"></i>
</button>
</div>
</div>
</div>
<div class="flex items-center gap-3">
<!-- 云端同步功能暂时关闭
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-brand/30 px-4 py-2 text-sm font-semibold text-brand transition hover:bg-brand/10 disabled:opacity-40"
:disabled="syncing"
@click="handleSyncClick"
>
<i class="fas fa-cloud-arrow-up"></i>
{{ syncing ? "同步中" : "同步到账号" }}
</button>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
@click="handleRestoreClick"
>
<i class="fas fa-cloud-arrow-down"></i>
从账号恢复
</button>
-->
<div
v-if="showOriginSwitcher"
class="flex items-center rounded-2xl border border-slate-200/70 p-1 dark:border-slate-800/70"
>
<button
v-if="apmEnabled"
type="button"
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
:class="
activeOrigin === 'apm'
? 'bg-brand/10 text-brand dark:bg-brand/15'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
"
:disabled="!apmAvailable"
@click="$emit('switch-origin', 'apm')"
>
APM 软件
</button>
<button
v-if="sparkEnabled"
type="button"
class="rounded-xl px-4 py-1.5 text-sm font-semibold transition"
:class="
activeOrigin === 'spark'
? 'bg-brand/10 text-brand dark:bg-brand/15'
: 'text-slate-500 hover:text-slate-700 dark:text-slate-400 dark:hover:text-slate-200'
"
@click="$emit('switch-origin', 'spark')"
>
Spark 软件
</button>
</div>
<button
type="button"
class="inline-flex items-center gap-2 rounded-2xl border border-slate-200/70 px-4 py-2 text-sm font-semibold text-slate-600 transition hover:bg-slate-50 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
@@ -100,7 +171,7 @@
</div>
<div
class="flex-1 overflow-y-auto overscroll-contain scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 p-6 space-y-4"
class="flex-1 overflow-y-auto overscroll-contain p-6 space-y-4 mb-6"
>
<div
v-if="syncMessage"
@@ -120,15 +191,37 @@
>
{{ error }}
</div>
<div
v-else-if="warning"
class="rounded-2xl border border-amber-200/70 bg-amber-50/60 px-4 py-3 text-sm text-amber-700 dark:border-amber-500/40 dark:bg-amber-500/10 dark:text-amber-300"
>
<i class="fas fa-triangle-exclamation mr-1.5"></i>
{{ warning }}
</div>
<div
v-else-if="apps.length === 0"
class="rounded-2xl border border-slate-200/70 px-4 py-10 text-center text-slate-500 dark:border-slate-800/70 dark:text-slate-400"
>
暂无已安装应用
</div>
<div
v-else-if="filteredApps.length === 0"
class="rounded-2xl border border-slate-200/70 px-4 py-10 text-center text-slate-500 dark:border-slate-800/70 dark:text-slate-400"
>
<i class="fas fa-search mr-1.5 text-slate-400"></i>
<template v-if="searchQuery">
未找到匹配<span class="font-semibold text-slate-700 dark:text-slate-300">{{ searchQuery }}</span>的已安装应用
</template>
<template v-else-if="filterOrigin === 'apm'">
暂无已安装的 APM 应用
</template>
<template v-else-if="filterOrigin === 'spark'">
暂无已安装的 Spark 应用
</template>
</div>
<div v-else class="space-y-3">
<div
v-for="app in apps"
v-for="app in filteredApps"
:key="app.pkgname"
class="flex flex-col gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm dark:border-slate-800/70 dark:bg-slate-900/70 sm:flex-row sm:items-center sm:justify-between"
>
@@ -155,6 +248,22 @@
>
{{ app.name }}
</p>
<span
v-if="app.origin === 'apm'"
data-testid="origin-tag-apm"
class="rounded-md bg-amber-100 px-2 py-0.5 text-[11px] font-semibold text-amber-700 dark:bg-amber-500/20 dark:text-amber-400"
title="APM 软件"
>
APM
</span>
<span
v-else-if="app.origin === 'spark'"
data-testid="origin-tag-spark"
class="rounded-md bg-sky-100 px-2 py-0.5 text-[11px] font-semibold text-sky-700 dark:bg-sky-500/20 dark:text-sky-400"
title="Spark 软件"
>
Spark
</span>
<span
v-if="app.isDependency"
class="rounded-md bg-rose-100 px-2 py-0.5 text-[11px] font-semibold text-rose-600 dark:bg-rose-500/20 dark:text-rose-400"
@@ -212,14 +321,32 @@
</template>
<script setup lang="ts">
import { computed, reactive } from "vue";
import { computed, reactive, ref } from "vue";
import { App } from "../global/typedefinition";
import { APM_STORE_BASE_URL } from "../global/storeConfig";
const iconErrors = reactive<Record<string, boolean>>({});
// 仅允许从这些常见图标目录读取本地图标,避免通过 app.icons 读取任意本地文件
const ALLOWED_LOCAL_ICON_PREFIXES = [
"/usr/share/",
"/usr/lib/",
"/usr/local/share/",
"/opt/",
"/var/lib/apm/",
"/var/lib/",
];
const getIconUrl = (app: App) => {
if (app.icons && app.icons.startsWith("/")) return `file://${app.icons}`;
// 本地图标:仅允许以白名单目录开头、且不含路径遍历("..")的绝对路径
if (
app.icons &&
app.icons.startsWith("/") &&
!app.icons.includes("..") &&
ALLOWED_LOCAL_ICON_PREFIXES.some((prefix) => app.icons!.startsWith(prefix))
) {
return `file://${app.icons}`;
}
if (!app.category || app.category === "unknown") return "";
const arch = window.apm_store.arch || "amd64";
const finalArch = app.origin === "spark" ? `${arch}-store` : `${arch}-apm`;
@@ -241,21 +368,60 @@ const props = defineProps<{
apps: App[];
loading: boolean;
error: string;
activeOrigin: "apm" | "spark";
storeFilter: "spark" | "apm" | "both";
sparkAvailable: boolean;
apmAvailable: boolean;
warning: string;
loggedIn: boolean;
syncing: boolean;
syncMessage: string;
}>();
const apmCount = computed(
() => props.apps.filter((a) => a.origin === "apm").length,
);
const sparkCount = computed(
() => props.apps.filter((a) => a.origin === "spark").length,
);
const totalCount = computed(() => props.apps.length);
// 来源筛选:默认全部;点击统计徽章可在 all/apm/spark 间切换
const filterOrigin = ref<"all" | "apm" | "spark">("all");
// 搜索关键词(按名称/包名不区分大小写过滤已安装应用)
const searchQuery = ref("");
const filteredApps = computed(() => {
// 1. 先按搜索关键词过滤
const q = searchQuery.value.trim().toLowerCase();
let list = props.apps;
if (q) {
list = list.filter(
(a) =>
a.name.toLowerCase().includes(q) ||
a.pkgname.toLowerCase().includes(q),
);
}
// 2. 再按来源筛选(默认 all = 不过滤)
if (filterOrigin.value === "apm") {
list = list.filter((a) => a.origin === "apm");
} else if (filterOrigin.value === "spark") {
list = list.filter((a) => a.origin === "spark");
}
// 3. 排序:APM 应用始终排在前面(默认全部视图也遵守此规则)
// 返回新数组,避免修改原始 props.apps
return [...list].sort((a, b) => {
const aApm = a.origin === "apm" ? 0 : 1;
const bApm = b.origin === "apm" ? 0 : 1;
if (aApm !== bApm) return aApm - bApm;
// 同类内保持原有的字母序,体验更一致
return a.pkgname.localeCompare(b.pkgname);
});
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const emit = defineEmits<{
(e: "close"): void;
(e: "refresh"): void;
(e: "uninstall", app: App): void;
(e: "switch-origin", origin: "apm" | "spark"): void;
(e: "open-app", app: App): void;
(e: "open-detail", app: App): void;
(e: "sync-to-account"): void;
@@ -287,16 +453,4 @@ const onOverlayWheel = (e: WheelEvent) => {
if (target.closest(".overflow-y-auto, .overflow-auto")) return;
e.preventDefault();
};
const sparkEnabled = computed(() => {
return props.storeFilter !== "apm" && props.sparkAvailable;
});
const apmEnabled = computed(() => {
return props.storeFilter !== "spark" && props.apmAvailable;
});
const showOriginSwitcher = computed(() => {
return sparkEnabled.value && apmEnabled.value;
});
</script>
+301 -84
View File
@@ -1,9 +1,9 @@
<template>
<div
class="h-screen overflow-y-auto bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100"
class="flex h-screen flex-col overflow-hidden rounded-3xl bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100 ring-1 ring-black/5 dark:ring-white/5 shadow-[inset_0_1px_3px_rgba(0,0,0,0.25)]"
>
<div
class="submitter-titlebar sticky top-0 z-30 border-b border-slate-200/70 bg-white px-4 py-3 dark:border-slate-800/70 dark:bg-slate-900"
class="submitter-titlebar shrink-0 z-30 border-b border-slate-200/70 bg-white px-4 py-3 dark:border-slate-800/70 dark:bg-slate-900"
>
<div class="flex items-center justify-between">
<div class="flex items-center gap-3">
@@ -14,6 +14,15 @@
</div>
<h1 class="text-lg font-semibold">投稿应用</h1>
</div>
<div class="flex items-center gap-2">
<button
type="button"
class="submitter-close-button inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-slate-800"
@click="openSubmissionQueue"
>
<i class="fas fa-list"></i>
查看当前投稿队列
</button>
<button
type="button"
class="submitter-close-button inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
@@ -23,7 +32,9 @@
</button>
</div>
</div>
</div>
<div class="flex-1 overflow-y-auto mb-6">
<div class="p-6 max-w-2xl mx-auto">
<div class="space-y-6">
<div>
@@ -135,6 +146,19 @@
</div>
</div>
<div>
<label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
>官网地址</label
>
<input
v-model="formData.website"
type="url"
placeholder="https://example.com"
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
@@ -161,19 +185,6 @@
/>
</div>
<div>
<label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
>官网地址</label
>
<input
v-model="formData.website"
type="url"
placeholder="https://example.com"
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div>
<label
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
@@ -215,12 +226,16 @@
>截图最多5张</label
>
<p class="mb-2 text-xs text-slate-500 dark:text-slate-400">
可直接 Ctrl+V 粘贴截图
点击添加拖放或直接 Ctrl+V 粘贴 PNG 截图
</p>
<div
tabindex="0"
class="grid grid-cols-5 gap-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
class="grid grid-cols-5 gap-3 rounded-lg border-2 border-dashed border-transparent p-2 transition-colors hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
@paste="handleScreenshotPaste"
@drop="handleScreenshotDrop"
@dragover.prevent
@dragenter.prevent
@dragleave.prevent
>
<div
v-for="(screenshot, index) in formData.screenshots"
@@ -247,11 +262,17 @@
<input
ref="screenshotInput"
type="file"
accept=".png,.jpg,.jpeg"
accept=".png"
multiple
class="hidden"
@change="handleScreenshotSelect"
/>
<p
v-if="mediaError"
class="mt-2 text-sm text-red-600 dark:text-red-400"
>
{{ mediaError }}
</p>
</div>
<div>
@@ -536,7 +557,7 @@
<button
type="button"
class="flex-1 px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
@click="resetFormAndClose"
@click="continueSubmission"
>
继续投递
</button>
@@ -587,7 +608,7 @@
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div
class="absolute inset-0 bg-black/50"
class="absolute inset-0 bg-black/50 rounded-3xl"
@click="showArchPackDialog = false"
></div>
<div
@@ -620,11 +641,25 @@
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-emerald-500 transition-colors text-left"
@click="selectPackArch(arch)"
>
<div class="font-medium text-slate-900 dark:text-slate-100">
<div class="flex items-center gap-2">
<span
class="rounded-full px-2 py-0.5 text-xs font-medium"
:class="
isSparkHistoryStore(arch.store)
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
"
>
{{ isSparkHistoryStore(arch.store) ? "Spark" : "APM" }}
</span>
<span class="font-medium text-slate-900 dark:text-slate-100">
{{ getArchDisplayName(arch.store) }}
</span>
</div>
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
输出: {{ formData.pkgname }}-{{ arch.store }}.tar.gz
输出: {{ formData.pkgname.toLowerCase() }}-{{
arch.store
}}.tar.gz
</div>
</button>
</div>
@@ -649,7 +684,7 @@
class="fixed inset-0 z-50 flex items-center justify-center p-4"
>
<div
class="absolute inset-0 bg-black/50"
class="absolute inset-0 bg-black/50 rounded-3xl"
@click="showArchDialog = false"
></div>
<div
@@ -676,14 +711,23 @@
<div class="space-y-3 mb-6">
<button
v-for="arch in availableArchs"
v-for="arch in sortedArchs"
:key="arch.store"
type="button"
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-blue-500 transition-colors text-left"
class="w-full p-4 rounded-lg border-2 transition-colors text-left"
:class="archOriginBorderClass(arch.store)"
@click="selectArch(arch)"
>
<div class="font-medium text-slate-900 dark:text-slate-100">
<div class="flex items-center gap-2">
<span class="font-medium text-slate-900 dark:text-slate-100">
{{ getArchDisplayName(arch.store) }}
</span>
<span
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
:class="getOriginBadgeClass(arch.store)"
>
{{ getOriginLabel(arch.store) }}
</span>
</div>
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
版本: {{ arch.version }} | 分类: {{ arch.category }}
@@ -704,6 +748,7 @@
</div>
</Teleport>
</div>
</div>
</template>
<script setup lang="ts">
@@ -767,6 +812,7 @@ const isSearchingHistory = ref(false);
const showArchDialog = ref(false);
const availableArchs = ref<HistoryArchInfo[]>([]);
const currentDebArch = ref("");
const mediaError = ref("");
const iconPreview = ref("");
const iconFileName = ref("");
@@ -822,6 +868,23 @@ const isValidUrl = (url: string): boolean => {
}
};
const getContributorFromGit = async (): Promise<string> => {
try {
const nameResult = await window.ipcRenderer.invoke("get-git-name");
const emailResult = await window.ipcRenderer.invoke("get-git-email");
const name = nameResult?.success ? nameResult.data : "";
const email = emailResult?.success ? emailResult.data : "";
if (name && email) {
return `${name} <${email}>`;
}
} catch (error) {
console.warn("[Submitter] Failed to get git config:", error);
}
return "";
};
const isFormValid = computed(() => {
const hasRequiredFields =
formData.name.trim() &&
@@ -864,13 +927,70 @@ const hasUserStartedFilling = computed(() => {
);
});
const getArchDisplayName = (store: string): string => {
const archMap: Record<string, string> = {
store: "AMD64 (x86_64)",
"aarch64-store": "ARM64 (aarch64)",
"loong64-store": "LoongArch64",
const getHistoryArch = (
store: string,
): "amd64" | "arm64" | "loong64" | "other" => {
if (store === "store" || store.startsWith("amd64-")) return "amd64";
if (store === "aarch64-store" || store.startsWith("arm64-")) return "arm64";
if (store === "loong64-store" || store.startsWith("loong64-"))
return "loong64";
return "other";
};
const isSparkHistoryStore = (store: string): boolean => {
// 兼容新格式(amd64-store / arm64-store / loong64-store / amd64-apm ...
// 与旧格式(store / aarch64-store / loong64-store
return store === "store" || store.endsWith("-store");
};
const sortHistoryArchs = (archs: HistoryArchInfo[]): HistoryArchInfo[] => {
const archOrder: Record<ReturnType<typeof getHistoryArch>, number> = {
amd64: 0,
arm64: 1,
loong64: 2,
other: 3,
};
return archMap[store] || store;
return [...archs].sort((a, b) => {
const sourceOrder =
Number(isSparkHistoryStore(b.store)) -
Number(isSparkHistoryStore(a.store));
if (sourceOrder !== 0) return sourceOrder;
return (
archOrder[getHistoryArch(a.store)] - archOrder[getHistoryArch(b.store)]
);
});
};
const getArchDisplayName = (store: string): string => {
const arch = getHistoryArch(store);
const archMap: Record<typeof arch, string> = {
amd64: "AMD64 (x86_64)",
arm64: "ARM64 (aarch64)",
loong64: "LoongArch64",
other: store,
};
return archMap[arch];
};
const sortedArchs = computed(() => sortHistoryArchs(availableArchs.value));
const getOriginLabel = (store: string): string => {
return isSparkHistoryStore(store) ? "Spark" : "APM";
};
const getOriginBadgeClass = (store: string): string => {
if (isSparkHistoryStore(store)) {
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400";
}
return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400";
};
const archOriginBorderClass = (store: string): string => {
if (isSparkHistoryStore(store)) {
return "border-slate-200 dark:border-slate-700 hover:border-blue-500";
}
return "border-slate-200 dark:border-slate-700 hover:border-amber-400";
};
const loadCategoriesPromise = ref<Promise<void> | null>(null);
@@ -1042,6 +1162,14 @@ const updateFormTags = () => {
formData.tags = selectedTags.value.map((t) => t.value).join(";");
};
const openSubmissionQueue = (): void => {
window.open(
"https://upload.spark-app.store/",
"_blank",
"noopener,noreferrer",
);
};
const selectDebFile = async () => {
const result = await window.ipcRenderer.invoke("select-deb-file");
if (result?.success && result.filePath) {
@@ -1091,14 +1219,8 @@ const searchHistoryApp = async () => {
console.log("[Submitter] availableArchs before:", availableArchs.value);
console.log("[Submitter] showArchDialog before:", showArchDialog.value);
// 按 amd64 → arm64 → loong64 顺序排序
const archOrder: Record<string, number> = {
store: 0,
"aarch64-store": 1,
"loong64-store": 2,
};
availableArchs.value = [...historyResult.data].sort(
(a, b) => (archOrder[a.store] ?? 99) - (archOrder[b.store] ?? 99),
availableArchs.value = sortHistoryArchs(
historyResult.data as HistoryArchInfo[],
);
console.log("[Submitter] availableArchs after:", availableArchs.value);
console.log(
@@ -1112,7 +1234,7 @@ const searchHistoryApp = async () => {
}
// 从第一条历史记录预填名称和分类
const firstArch = historyResult.data[0];
const firstArch = availableArchs.value[0];
if (firstArch) {
formData.name = firstArch.name || formData.name;
formData.category = firstArch.category || formData.category;
@@ -1183,6 +1305,15 @@ const searchHistoryApp = async () => {
const parseDebFileAndSearchHistory = async (debPath: string) => {
isParsingDeb.value = true;
debParseError.value = "";
availableArchs.value = [];
showArchDialog.value = false;
formData.pkgname = "";
formData.version = "";
formData.author = "";
formData.contributor = "";
formData.website = "";
formData.description = "";
currentDebArch.value = "";
try {
console.log(
@@ -1231,6 +1362,12 @@ const parseDebFileAndSearchHistory = async (debPath: string) => {
JSON.stringify(formData, null, 2),
);
const gitContributor = await getContributorFromGit();
if (gitContributor) {
formData.contributor = gitContributor;
console.log("[Submitter] Contributor updated from git:", gitContributor);
}
if (formData.pkgname) {
isSearchingHistory.value = true;
try {
@@ -1294,13 +1431,20 @@ const handleDrop = async (event: DragEvent) => {
if (file.name.endsWith(".deb")) {
console.log("[Submitter] File is a deb package");
// 在 contextIsolation 环境下,File.path 不可用
// 使用 Electron 的 webUtils.getPathForFile() 获取真实文件系统路径
let filePath: string;
try {
filePath = window.electronUtils.getPathForFile(file);
console.log("[Submitter] File path from electronUtils:", filePath);
} catch {
console.warn(
"[Submitter] electronUtils.getPathForFile failed, trying fallback",
);
const textUriList = event.dataTransfer?.getData("text/uri-list");
console.log("[Submitter] text/uri-list:", textUriList);
const textPlain = event.dataTransfer?.getData("text/plain");
console.log("[Submitter] text/plain:", textPlain);
const filePath = file.path || textUriList || textPlain;
filePath = textUriList || textPlain || "";
}
console.log("[Submitter] Final filePath:", filePath);
if (filePath) {
@@ -1339,7 +1483,9 @@ const selectArch = async (arch: HistoryArchInfo) => {
formData.name = arch.name || formData.name;
formData.author = arch.author || formData.author;
formData.contributor = arch.contributor || formData.contributor;
if (!formData.contributor) {
formData.contributor = arch.contributor || "";
}
formData.website = arch.website || formData.website;
formData.category = arch.category || formData.category;
formData.description = arch.more || formData.description;
@@ -1397,10 +1543,13 @@ const selectIconFile = () => {
iconFileInput.value?.click();
};
const handleIconFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
const isPngFile = (file: File): boolean => {
return (
file.type === "image/png" || (file.type === "" && /\.png$/i.test(file.name))
);
};
const readIconFile = (file: File): void => {
iconFileName.value = file.name;
const reader = new FileReader();
reader.onload = (e) => {
@@ -1409,26 +1558,31 @@ const handleIconFileSelect = (event: Event) => {
iconPreview.value = dataUrl;
};
reader.readAsDataURL(file);
};
const handleIconFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
const file = target.files?.[0];
if (file) {
if (isPngFile(file)) {
mediaError.value = "";
readIconFile(file);
} else {
mediaError.value = "应用图标仅支持 PNG 格式。";
}
}
target.value = "";
};
const handleIconDrop = (event: DragEvent) => {
event.preventDefault();
const file = event.dataTransfer?.files?.[0];
if (
file &&
(file.name.endsWith(".png") ||
file.name.endsWith(".jpg") ||
file.name.endsWith(".jpeg"))
) {
iconFileName.value = file.name;
const reader = new FileReader();
reader.onload = (e) => {
const dataUrl = e.target?.result as string;
formData.iconPath = dataUrl;
iconPreview.value = dataUrl;
};
reader.readAsDataURL(file);
if (!file) return;
if (isPngFile(file)) {
mediaError.value = "";
readIconFile(file);
} else {
mediaError.value = "应用图标仅支持 PNG 格式。";
}
};
@@ -1437,7 +1591,14 @@ const addScreenshot = () => {
};
const importScreenshots = (files: File[]) => {
for (const file of files) {
const pngFiles = files.filter(isPngFile);
if (pngFiles.length !== files.length) {
mediaError.value = "截图仅支持 PNG 格式。";
} else if (pngFiles.length > 0) {
mediaError.value = "";
}
for (const file of pngFiles) {
if (formData.screenshots.length >= 5) break;
const reader = new FileReader();
@@ -1455,16 +1616,18 @@ const importScreenshots = (files: File[]) => {
const handleScreenshotSelect = (event: Event) => {
const target = event.target as HTMLInputElement;
const files = Array.from(target.files ?? []).filter((file) =>
/\.(png|jpe?g)$/i.test(file.name),
);
importScreenshots(files);
importScreenshots(Array.from(target.files ?? []));
target.value = "";
};
const handleScreenshotDrop = (event: DragEvent) => {
event.preventDefault();
importScreenshots(Array.from(event.dataTransfer?.files ?? []));
};
const handleScreenshotPaste = (event: ClipboardEvent) => {
const files = Array.from(event.clipboardData?.items ?? [])
.filter((item) => item.type.startsWith("image/"))
.filter((item) => item.kind === "file")
.map((item) => item.getAsFile())
.filter((file): file is File => file !== null);
@@ -1497,6 +1660,7 @@ const resetForm = () => {
submitSuccess.value = false;
submitError.value = "";
debParseError.value = "";
mediaError.value = "";
packageSuccess.value = false;
packageError.value = "";
packageResult.value = null;
@@ -1514,14 +1678,29 @@ const resetForm = () => {
isSearchingHistory.value = false;
};
const closeSubmitSuccessModal = () => {
const clearSubmitTransientState = (): void => {
showSubmitSuccessModal.value = false;
resetForm();
submitSuccess.value = false;
isSubmitting.value = false;
uploadProgress.value = 0;
uploadStage.value = "";
uploadStageMessage.value = "";
uploadStages.value = [];
};
const resetFormAndClose = () => {
showSubmitSuccessModal.value = false;
const closeSubmitSuccessModal = () => {
clearSubmitTransientState();
};
const continueSubmission = () => {
const shouldClear = window.confirm(
"是否清空当前表单内容后继续投递?选择“取消”将保留已填写内容。",
);
if (shouldClear) {
resetForm();
} else {
clearSubmitTransientState();
}
};
const submitForm = async () => {
@@ -1712,26 +1891,64 @@ const closeWindow = () => {
window.ipcRenderer.send("close-submitter-window");
};
import { onMounted, nextTick } from "vue";
import { onMounted, onUnmounted, nextTick } from "vue";
const getGitEmail = async () => {
const getGitInfo = async () => {
try {
const result = await window.ipcRenderer.invoke("get-git-email");
if (result?.success && result.data) {
formData.mail = result.data;
console.log("[Submitter] Git email auto-filled:", result.data);
const [nameResult, emailResult] = await Promise.all([
window.ipcRenderer.invoke("get-git-name"),
window.ipcRenderer.invoke("get-git-email"),
]);
let gitName = "";
let gitEmail = "";
if (nameResult?.success && nameResult.data) {
gitName = nameResult.data;
console.log("[Submitter] Git name auto-filled:", gitName);
}
if (emailResult?.success && emailResult.data) {
gitEmail = emailResult.data;
console.log("[Submitter] Git email auto-filled:", gitEmail);
}
// 将 git name 和 email 合并填入 contributor 字段,格式: Name <email>
if (gitName || gitEmail) {
if (gitName && gitEmail) {
formData.contributor = `${gitName} <${gitEmail}>`;
} else if (gitName) {
formData.contributor = gitName;
} else if (gitEmail) {
formData.contributor = gitEmail;
}
}
// 单独填入邮箱
if (gitEmail) {
formData.mail = gitEmail;
}
} catch (err) {
console.warn("[Submitter] Failed to get git email:", err);
console.warn("[Submitter] Failed to get git info:", err);
}
};
onMounted(async () => {
console.log("[Submitter] Component mounted, loading categories and tags");
// 透明窗口:让 body/html 背景透明,圆角才能透出,否则四角是方角。
// 保存原始值,在 onUnmounted 中恢复,避免副作用泄漏到其它视图。
const prevHtmlBg = document.documentElement.style.backgroundColor;
const prevBodyBg = document.body.style.backgroundColor;
document.documentElement.style.backgroundColor = "transparent";
document.body.style.backgroundColor = "transparent";
onUnmounted(() => {
document.documentElement.style.backgroundColor = prevHtmlBg;
document.body.style.backgroundColor = prevBodyBg;
});
// 先等待分类列表加载完成,避免后续竞态
await Promise.all([loadCategoriesList(), loadTagsList()]);
// 尝试从 git 配置读取邮箱
await getGitEmail();
// 尝试从 git 配置读取 name 和 email,填入 contributor 和 mail
await getGitInfo();
});
</script>
+5 -1
View File
@@ -3,7 +3,11 @@
class="window-titlebar sticky top-0 z-20 flex h-10 shrink-0 items-center justify-between border-b border-slate-200/70 bg-white px-3 text-slate-700 dark:border-slate-800/70 dark:bg-slate-950 dark:text-slate-200"
>
<div class="flex min-w-0 items-center gap-2">
<span class="h-3 w-3 rounded-full bg-[#2b7fff]"></span>
<img
src="../assets/imgs/spark-store.svg"
class="h-5 w-5 flex-shrink-0"
alt="星火应用商店"
/>
<span class="truncate text-sm font-semibold">星火应用商店</span>
</div>
@@ -61,11 +61,13 @@
<label class="block relative">
<span class="sr-only">搜索更新</span>
<i
class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"
></i>
<input
:value="searchQuery"
type="search"
placeholder="搜索应用或包名"
class="w-full rounded-2xl border border-slate-200 bg-slate-50 px-4 py-3 text-sm text-slate-900 outline-none transition focus:border-brand/60 focus:bg-white dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100 dark:focus:bg-slate-900"
class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-20 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"
@input="handleInput"
/>
<button
+3
View File
@@ -22,6 +22,9 @@ declare global {
};
windowControls: WindowControlBridge;
updateCenter: UpdateCenterBridge;
electronUtils: {
getPathForFile: (file: File) => string;
};
}
}
+5 -5
View File
@@ -16,11 +16,11 @@ load_transhell
if [[ "$IS_APM_ENV" = "" ]] ;then
case $(arch) in
x86_64 | i686 | i386)
STORE_URL="store"
STORE_URL="amd64-store"
STORE_LIST_URL=""
;;
aarch64)
STORE_URL="aarch64-store"
STORE_URL="arm64-store"
STORE_LIST_URL="-aarch64"
;;
loongarch64)
@@ -62,13 +62,13 @@ is_empty_dir(){
}
function update_list(){
curl --progress-bar -o /opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list "${SPARK_DOWNLOAD_SERVER_URL}/${UPSTREAM_CATOGARY}${STORE_LIST_URL}.list"
curl -L --progress-bar -o /opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list "${SPARK_DOWNLOAD_SERVER_URL}/${UPSTREAM_CATOGARY}${STORE_LIST_URL}.list"
log.info "${UPSTREAM_CATOGARY}${STORE_LIST_URL}.list update done"
}
function update_conf(){
mkdir -p /tmp/aptss-conf/
curl --progress-bar -o /tmp/aptss-conf/apt-fast.conf "${SPARK_DOWNLOAD_SERVER_URL}/apt-fast.conf"
curl -L --progress-bar -o /tmp/aptss-conf/apt-fast.conf "${SPARK_DOWNLOAD_SERVER_URL}/apt-fast.conf"
log.info "apt-fast.conf update done"
chmod -R 755 /tmp/aptss-conf
}
@@ -96,7 +96,7 @@ update_conf
fi
if [ ! -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_${STORE_URL}_Packages" ] && [ ! -e "/var/lib/aptss/lists/d.store.deepinos.org.cn_${STORE_URL}_Packages" ] && [ ! -e "/var/lib/aptss/lists/mirrors.sdu.edu.cn_spark-store-repository_${STORE_URL}_Packages" ];then
if [ ! -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_${STORE_URL}_Packages" ] && [ ! -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_aarch64-store_Packages" ] && [ ! -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_store_Packages" ];then
mkdir -p /tmp/aptss-conf/
echo -e "\e[1;32m${TRANSHELL_CONTENT_GETTING_SERVER_CONFIG_AND_MIRROR_LIST}\e[0m"
+10
View File
@@ -29,6 +29,16 @@ if grep -q "ID=aosc" /etc/os-release; then
ARGS="$ARGS --no-spark"
fi
# 检查龙GPU,添加 --disable-gpu
ARCH=$(uname -m)
if [ "$ARCH" = "loongarch64" ] || [ "$ARCH" = "loong64" ]; then
is_loonggpu=$(lspci -s $(basename $(readlink $(grep -l connected /sys/class/drm/card*/*/status 2>/dev/null | head -1 | grep -o 'card[0-9]*' | xargs -I{} echo /sys/class/drm/{}/device))) | grep -qi loongson && echo "Found" || echo "NotFound")
if [ "$is_loonggpu" = "Found" ]; then
echo "检测到龙GPU"
ARGS="$ARGS --disable-gpu"
fi
fi
# 注意:已移除原先针对 arm64 + wayland 添加 --disable-gpu 的逻辑,
# 现在 arm64 设备无论是否使用 wayland 均不再添加此参数。
+8 -5
View File
@@ -62,11 +62,11 @@ export DEBIAN_FRONTEND=noninteractive
if [[ "$IS_APM_ENV" = "" ]] ;then
case $(arch) in
x86_64 | i686 | i386)
STORE_URL="store"
STORE_URL="amd64-store"
STORE_LIST_URL=""
;;
aarch64)
STORE_URL="aarch64-store"
STORE_URL="arm64-store"
STORE_LIST_URL="-aarch64"
;;
loongarch64)
@@ -195,10 +195,13 @@ function hash_check() {
# 检查可能的仓库位置
if [ -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_${STORE_URL}_Packages" ]; then
PACKAGES_DATA_PATH="/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_${STORE_URL}_Packages"
elif [ -e "/var/lib/aptss/lists/d.store.deepinos.org.cn_${STORE_URL}_Packages" ]; then
PACKAGES_DATA_PATH="/var/lib/aptss/lists/d.store.deepinos.org.cn_${STORE_URL}_Packages"
elif [ -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_store_Packages" ]; then
PACKAGES_DATA_PATH="/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_store_Packages"
elif [ -e "/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_aarch64-store_Packages" ]; then
PACKAGES_DATA_PATH="/var/lib/aptss/lists/${SPARK_DOWNLOAD_SERVER_URL_NO_PROTOCOL}_aarch64-store_Packages"
else
PACKAGES_DATA_PATH="/var/lib/aptss/lists/mirrors.sdu.edu.cn_spark-store-repository_${STORE_URL}_Packages"
echo "ERROR:NO_PACKAGE_FILE_FOUND!"
exit 1
fi
echo "正在运行包验证..."