diff --git a/debian/changelog b/debian/changelog index 8fb4c320..27ec7629 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -spark-store (5.3.0.1) UNRELEASED; urgency=medium +spark-store (5.3.0.7-test) UNRELEASED; urgency=medium * Initial release. diff --git a/electron/main/index.ts b/electron/main/index.ts index 6b514806..941d8a1e 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -170,6 +170,18 @@ ipcMain.handle("save-window-bounds", (): boolean => { return true; }); +// 渲染端(设置页)切换界面整体缩放系数,经此实时应用到主窗口。 +// factor 限定在 0.5~3 之间,避免极端值导致界面不可用。 +ipcMain.handle("set-zoom-factor", (_event, factor: unknown): boolean => { + if (typeof factor !== "number" || !Number.isFinite(factor)) return false; + const clamped = Math.min(3, Math.max(0.5, factor)); + if (win && !win.isDestroyed()) { + win.webContents.setZoomFactor(clamped); + return true; + } + return false; +}); + ipcMain.handle("get-app-version", (): string => getAppVersion()); ipcMain.handle("get-system-info", (): { distro: string } => getSystemInfo()); @@ -307,6 +319,8 @@ interface WindowState { x?: number; y?: number; maximized?: boolean; + /** 界面整体缩放系数(Electron webContents.setZoomFactor),默认 1 */ + zoomFactor?: number; } function getWindowStatePath(): string { @@ -375,7 +389,14 @@ function flushSaveBounds(): void { } if (win && !win.isDestroyed()) { const { width, height, x, y } = win.getBounds(); - saveWindowState({ width, height, x, y, maximized: win.isMaximized() }); + saveWindowState({ + width, + height, + x, + y, + maximized: win.isMaximized(), + zoomFactor: win.webContents.getZoomFactor(), + }); } } function scheduleSaveBounds(winInstance: BrowserWindow): void { @@ -389,6 +410,7 @@ function scheduleSaveBounds(winInstance: BrowserWindow): void { x, y, maximized: winInstance.isMaximized(), + zoomFactor: winInstance.webContents.getZoomFactor(), }); }, 400); } @@ -438,6 +460,17 @@ async function createWindow() { }); win = mainWindow; + // 启动即应用持久化的界面缩放系数(默认 1 = 100%)。 + // 必须在 loadURL/loadFile 前设置,使首帧即按目标缩放渲染,避免闪烁。 + const savedZoom = + typeof saved.zoomFactor === "number" && + saved.zoomFactor >= 0.5 && + saved.zoomFactor <= 3 + ? saved.zoomFactor + : 1; + mainWindow.webContents.setZoomFactor(savedZoom); + logger.info({ zoomFactor: savedZoom }, "已应用界面缩放系数"); + // 设计意图(非缺陷,勿改为自动恢复 maximized): // 启动时不自动恢复最大化状态。原因——最大化窗口在多数屏幕上 bounds 会超过 // OVERSIZED_WINDOW_THRESHOLD(1600x900),若强行恢复最大化会导致「启动即全屏、 diff --git a/scripts/test-build.sh b/scripts/test-build.sh index 8346bafe..2730ad1d 100755 --- a/scripts/test-build.sh +++ b/scripts/test-build.sh @@ -11,7 +11,12 @@ # 再运行一次 -> 5.2.1.2-test # # 注意: -# - 此脚本仅用于本地测试,不会修改 package.json(electron-builder 的 dir 产物版本)。 +# - 此脚本仅用于本地测试,不会修改 package.json(其 version 为 semver 格式, +# 而 deb 测试版本如 5.3.0.8-test 会被 electron-builder 拒绝)。 +# - 为保证打包后「关于」页面显示版本与 deb 版本一致,脚本会临时向 +# electron-builder.yml 追加 extraMetadata.version(electron-builder 会将其写入 +# app.asar 内的 package.json,Electron 的 app.getVersion() 即从中读取), +# 打包完成后自动恢复,异常退出时由 trap 兜底恢复。 # - 每次运行都会改写 debian/changelog,属于未跟踪的工作区改动,按需自行还原。 # - 如需恢复正式版本,将 debian/changelog 顶部版本改回即可。 @@ -71,7 +76,33 @@ sed -i -E "1s/\\(${old_ver_esc}\\)/(${new_ver})/" "$CHANGELOG" # Electron 二进制下载镜像(本机到 GitHub 不通,使用 npmmirror 镜像) export ELECTRON_MIRROR="${ELECTRON_MIRROR:-https://registry.npmmirror.com/-/binary/electron/}" +# 同步应用内版本号:临时向 electron-builder.yml 追加 extraMetadata.version。 +# electron-builder 会把它合并进 app.asar 内的 package.json, +# 「关于」页面经 IPC 调用的 app.getVersion() 即读取该值。 +EB_YML="electron-builder.yml" +EB_YML_BAK="${EB_YML}.bak-test-build" + +# 清理上次异常中断可能残留的 extraMetadata 块(追加内容始终位于文件末尾) +if grep -q "^extraMetadata:" "$EB_YML"; then + echo "清理 $EB_YML 中残留的 extraMetadata 块" + sed -i '/^extraMetadata:/,$d' "$EB_YML" +fi +cp "$EB_YML" "$EB_YML_BAK" +restore_eb_yml() { + mv -f "$EB_YML_BAK" "$EB_YML" 2>/dev/null || true +} +trap restore_eb_yml EXIT +printf 'extraMetadata:\n version: "%s"\n' "$new_ver" >> "$EB_YML" + +# 清理旧的 electron-builder 产物,避免 release/ 下多个版本目录被 make install +# 的 release/*/linux*-unpacked/* 通配一并拷入 deb,造成内容污染 +rm -rf release/ + echo "开始测试打包(版本 ${new_ver})..." dpkg-buildpackage -us -uc -b +# 打包成功,立即恢复 electron-builder.yml 并解除 trap +restore_eb_yml +trap - EXIT + echo "完成。产物: ../spark-store_${new_ver}_amd64.deb" diff --git a/src/App.vue b/src/App.vue index abd3b205..d5bb3b55 100644 --- a/src/App.vue +++ b/src/App.vue @@ -344,6 +344,7 @@ import ReviewUserProfileModal from "./components/ReviewUserProfileModal.vue"; import WindowTitleBar from "./components/WindowTitleBar.vue"; import SubmitterWindow from "./components/SubmitterWindow.vue"; import { initTagPriorityStrategy } from "./global/tagPriority"; +import { initUiScale } from "./global/displaySettings"; import { FLARUM_BASE_URL, FLARUM_REGISTER_URL, @@ -1052,6 +1053,8 @@ onMounted(async () => { // 应用启动即锁定标签优先显示策略的真实持久化值,避免依赖详情页挂载顺序 // 导致先读到内存默认值 "auto" 再懒加载的竞态窗口。 initTagPriorityStrategy(); + // 恢复并应用持久化的界面整体缩放(Electron setZoomFactor)。 + void initUiScale(); initTheme(); updateCenterStore.bind(); diff --git a/src/components/AccountQuickMenu.vue b/src/components/AccountQuickMenu.vue index d4437082..124018a2 100644 --- a/src/components/AccountQuickMenu.vue +++ b/src/components/AccountQuickMenu.vue @@ -52,10 +52,10 @@ const emit = defineEmits<{ display: flex; width: 100%; align-items: center; - gap: 0.75rem; + gap: 12px; min-width: 0; - border-radius: 0.75rem; - padding: 0.625rem 0.75rem; + border-radius: 12px; + padding: 10px 12px; text-align: left; font-size: 0.875rem; font-weight: 500; diff --git a/src/components/AppCard.vue b/src/components/AppCard.vue index 82de7541..82d521f0 100644 --- a/src/components/AppCard.vue +++ b/src/components/AppCard.vue @@ -153,12 +153,10 @@ const formatDownloads = (n?: number): string => { // 排名徽标配色(前 3 名金/银/铜,其余灰色) const rankClass = computed(() => { - if (props.rank === 1) - return "bg-amber-400 text-white shadow-amber-400/40"; + if (props.rank === 1) return "bg-amber-400 text-white shadow-amber-400/40"; if (props.rank === 2) return "bg-slate-300 text-slate-700 shadow-slate-400/40"; - if (props.rank === 3) - return "bg-amber-600 text-white shadow-amber-600/40"; + if (props.rank === 3) return "bg-amber-600 text-white shadow-amber-600/40"; return "bg-slate-500/90 text-white shadow-slate-500/30"; }); diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue index 8db4000a..5b18d253 100644 --- a/src/components/AppSidebar.vue +++ b/src/components/AppSidebar.vue @@ -161,10 +161,10 @@ const emitSidebarAction = (action: "list" | "update" | "submit") => { display: flex; width: 100%; align-items: center; - gap: 0.75rem; + gap: 12px; border: 1px solid transparent; - border-radius: 0.75rem; - padding: 0.625rem 0.875rem; + border-radius: 12px; + padding: 10px 0.875rem; text-align: left; font-size: 0.875rem; font-weight: 500; diff --git a/src/components/CategoryBar.vue b/src/components/CategoryBar.vue index 1aa90f14..d56cef15 100644 --- a/src/components/CategoryBar.vue +++ b/src/components/CategoryBar.vue @@ -71,7 +71,7 @@ const selectCategory = (category: string) => { display: flex; flex-wrap: wrap; gap: 0.5rem; - padding: 0.75rem 0; + padding: 12px 0; } .category-pill { @@ -81,7 +81,7 @@ const selectCategory = (category: string) => { white-space: nowrap; padding: 0.375rem 0.875rem; border-radius: 9999px; - font-size: 0.8125rem; + font-size: 13px; font-weight: 500; color: #64748b; background: #f1f5f9; @@ -126,7 +126,7 @@ const selectCategory = (category: string) => { } .category-pill-count { - font-size: 0.6875rem; + font-size: 11px; font-weight: 600; opacity: 0.75; } diff --git a/src/components/HomeView.vue b/src/components/HomeView.vue index 7bc53b7d..61c785c1 100644 --- a/src/components/HomeView.vue +++ b/src/components/HomeView.vue @@ -192,7 +192,7 @@ const onLinkClick = (link: HomeLink) => { @media (max-width: 640px) { .auto-fit-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); - gap: 0.75rem; + gap: 12px; } } diff --git a/src/components/RankingAppRow.vue b/src/components/RankingAppRow.vue index 812dc5bf..76a06a21 100644 --- a/src/components/RankingAppRow.vue +++ b/src/components/RankingAppRow.vue @@ -77,7 +77,8 @@ const iconFailed = ref(false); const iconPath = computed(() => { const arch = window.apm_store.arch || "amd64"; - const finalArch = props.app.origin === "spark" ? `${arch}-store` : `${arch}-apm`; + const finalArch = + props.app.origin === "spark" ? `${arch}-store` : `${arch}-apm`; return `${APM_STORE_BASE_URL}/${finalArch}/${props.app.category}/${props.app.pkgname}/icon.png`; }); diff --git a/src/components/SettingsModal.vue b/src/components/SettingsModal.vue index 7cc55d35..8207964a 100644 --- a/src/components/SettingsModal.vue +++ b/src/components/SettingsModal.vue @@ -13,7 +13,7 @@ @click.self="closeModal" >
- -
+ +
+ + +
+
+
+ +
+
+

+ 界面缩放 +

+

+ 整体放大或缩小界面(图标、间距、布局等比变化) +

+
+ +
+
+
+
@@ -185,6 +230,13 @@ import { setTagPriorityStrategy, type TagPriorityStrategy, } from "../global/tagPriority"; +import { + getUiScale, + setUiScale, + uiScaleToFactor, + UI_SCALE_OPTIONS, + type UiScaleOption, +} from "../global/displaySettings"; const props = defineProps<{ show: boolean; @@ -206,6 +258,9 @@ const strategyOptions: Array<{ value: TagPriorityStrategy; label: string }> = [ { value: "apm", label: "APM 优先" }, ]; +// 界面缩放档位选项(90% / 100% / 110% / 125% / 150%) +const uiScaleOptions = UI_SCALE_OPTIONS; + const tagPriorityStrategy = ref("auto"); // 加载标签优先显示策略(从持久化读取) @@ -219,6 +274,24 @@ const selectStrategy = (value: TagPriorityStrategy) => { setTagPriorityStrategy(value); }; +// 界面整体缩放档位(Electron setZoomFactor,连图标/间距/布局等比变化) +const uiScale = ref("100"); + +const loadUiScale = () => { + uiScale.value = getUiScale(); +}; + +// 选择并保存界面缩放档位(立即经 IPC 应用到主窗口) +const selectUiScale = async (value: UiScaleOption) => { + uiScale.value = value; + setUiScale(value); + try { + await window.ipcRenderer.invoke("set-zoom-factor", uiScaleToFactor(value)); + } catch (error) { + console.error("应用界面缩放失败:", error); + } +}; + const settings = ref({ enableUpdateCheck: true, enableCreateDesktop: true, @@ -279,6 +352,7 @@ watch( if (newVal) { loadSettings(); loadTagPriority(); + loadUiScale(); } }, ); diff --git a/src/components/update-center/UpdateCenterItem.vue b/src/components/update-center/UpdateCenterItem.vue index d7b81018..fd329c64 100644 --- a/src/components/update-center/UpdateCenterItem.vue +++ b/src/components/update-center/UpdateCenterItem.vue @@ -83,15 +83,13 @@ v-if="item.ignored !== true && !task" class="flex shrink-0 flex-col items-end gap-0.5" > - {{ - timeLabel - }} + {{ timeLabel }} {{ sizeLabel }}
-
+
{{ timeLabel }} @@ -181,7 +179,8 @@ defineEmits<{ // 被锁定且未强制时,复选框禁用(与「已忽略」一样不可被选中) const isSelectionLocked = computed( - () => props.item.ignored === true || (props.item.held === true && !props.forced), + () => + props.item.ignored === true || (props.item.held === true && !props.forced), ); const normalizeIconSrc = (icon: string): string => { diff --git a/src/global/displaySettings.ts b/src/global/displaySettings.ts new file mode 100644 index 00000000..ee66f808 --- /dev/null +++ b/src/global/displaySettings.ts @@ -0,0 +1,76 @@ +/** + * 显示设置:界面整体缩放(Electron webContents.setZoomFactor) + * + * 通过 Electron 的 setZoomFactor 整体缩放渲染内容,连图标、间距、布局 + * 一起等比放大/缩小(逻辑分辨率变化),不会破坏布局;与单纯改字号相比 + * 更稳定,故本应用只提供此一种显示放大入口。 + * + * 实际缩放由主进程 setZoomFactor 执行;渲染端仅负责持久化档位偏好, + * 并在启动时把档位换算的系数通过 IPC 告知主进程。 + */ + +export type UiScaleOption = "90" | "100" | "110" | "125" | "150"; + +// 档位 → 缩放系数(与 Electron setZoomFactor 一致,1 = 100%) +const UI_SCALE_MAP: Record = { + "90": 0.9, + "100": 1, + "110": 1.1, + "125": 1.25, + "150": 1.5, +}; + +export const UI_SCALE_OPTIONS: Array<{ + value: UiScaleOption; + label: string; +}> = [ + { value: "90", label: "90%" }, + { value: "100", label: "100%" }, + { value: "110", label: "110%" }, + { value: "125", label: "125%" }, + { value: "150", label: "150%" }, +]; + +const UI_SCALE_STORAGE_KEY = "spark-store-ui-scale"; +const DEFAULT_UI_SCALE: UiScaleOption = "100"; + +const isValidUiScale = (v: unknown): v is UiScaleOption => + typeof v === "string" && v in UI_SCALE_MAP; + +/** 读取持久化的界面缩放档位(无/非法时回退默认「100%」) */ +export const getUiScale = (): UiScaleOption => { + try { + const raw = localStorage.getItem(UI_SCALE_STORAGE_KEY); + if (isValidUiScale(raw)) return raw; + } catch { + // localStorage 不可用时忽略,使用默认 + } + return DEFAULT_UI_SCALE; +}; + +/** 将档位换算为 Electron 缩放系数 */ +export const uiScaleToFactor = (option: UiScaleOption): number => + UI_SCALE_MAP[option] ?? UI_SCALE_MAP[DEFAULT_UI_SCALE]; + +/** 选择并持久化界面缩放档位(不在此直接调用主进程,由调用方经 IPC 应用) */ +export const setUiScale = (option: UiScaleOption): void => { + try { + localStorage.setItem(UI_SCALE_STORAGE_KEY, option); + } catch { + // 持久化失败时仍已在会话内选定,忽略写入错误 + } +}; + +/** + * 应用启动时初始化:从持久化恢复档位,并经 IPC 告知主进程应用缩放。 + * 需在 App.vue onMounted 调用,主进程 createWindow 阶段已自有兜底(默认 1), + * 此处确保渲染端偏好生效。 + */ +export const initUiScale = async (): Promise => { + const factor = uiScaleToFactor(getUiScale()); + try { + await window.ipcRenderer.invoke("set-zoom-factor", factor); + } catch { + // 主进程 IPC 不可用时忽略(主进程启动兜底已设为 1) + } +};