mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
feat: enhance UI and functionality across components
- Added Google Fonts preconnect and stylesheet link in `index.html` for improved typography. - Updated version in `package.json` to `4.9.9alpha3`. - Refined launch configuration by removing deprecated arguments. - Improved app detail modal and card components for better accessibility and visual consistency. - Enhanced download queue and sidebar components with updated styles and functionality. - Implemented new utility classes for better styling control in CSS. - Adjusted various components for improved responsiveness and user experience.
This commit is contained in:
@@ -251,7 +251,8 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
||||
type: "info",
|
||||
title: "APM 安装成功",
|
||||
message: "恭喜您,APM 已成功安装",
|
||||
detail: "APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。",
|
||||
detail:
|
||||
"APM 应用需重启后方可展示和使用,若完成安装后无法在应用列表中展示,请重启电脑后继续。",
|
||||
buttons: ["确定"],
|
||||
defaultId: 0,
|
||||
});
|
||||
@@ -670,7 +671,6 @@ ipcMain.handle("list-upgradable", async () => {
|
||||
return { success: true, apps };
|
||||
});
|
||||
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
ipcMain.handle("uninstall-installed", async (_event, payload: any) => {
|
||||
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
|
||||
|
||||
139
electron/main/backend/telemetry.ts
Normal file
139
electron/main/backend/telemetry.ts
Normal file
@@ -0,0 +1,139 @@
|
||||
/**
|
||||
* 启动时遥测:收集系统与商店版本信息并上报至 status.deepinos.org.cn
|
||||
* 仅在 Linux 下执行一次,不阻塞启动,失败静默记录日志。
|
||||
*/
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import pino from "pino";
|
||||
|
||||
const logger = pino({ name: "telemetry" });
|
||||
const TELEMETRY_URL = "https://status.spark-app.store/upload";
|
||||
|
||||
interface TelemetryPayload {
|
||||
"Distributor ID": string;
|
||||
Release: string;
|
||||
Architecture: string;
|
||||
Store_Version: string;
|
||||
UUID: string;
|
||||
TIME: string;
|
||||
}
|
||||
|
||||
function readFileSafe(path: string): string {
|
||||
try {
|
||||
return fs.readFileSync(path, "utf8").trim();
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
/** 解析 /etc/os-release 的 KEY="value" 行 */
|
||||
function parseOsRelease(content: string): Record<string, string> {
|
||||
const out: Record<string, string> = {};
|
||||
for (const line of content.split("\n")) {
|
||||
const m = line.match(/^([A-Z_][A-Z0-9_]*)=(?:")?([^"]*)(?:")?$/);
|
||||
if (m) out[m[1]] = m[2].replace(/\\"/g, '"');
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function getDistroInfo(): { distributorId: string; release: string } {
|
||||
const osReleasePath = "/etc/os-release";
|
||||
const redhatPath = "/etc/redhat-release";
|
||||
const debianPath = "/etc/debian_version";
|
||||
|
||||
if (fs.existsSync(osReleasePath)) {
|
||||
const content = readFileSafe(osReleasePath);
|
||||
const parsed = parseOsRelease(content);
|
||||
const name = parsed.NAME ?? "Unknown";
|
||||
const versionId = parsed.VERSION_ID ?? "Unknown";
|
||||
return { distributorId: name, release: versionId };
|
||||
}
|
||||
|
||||
if (fs.existsSync(redhatPath)) {
|
||||
const content = readFileSafe(redhatPath);
|
||||
const distributorId = content.split(/\s+/)[0] ?? "Unknown";
|
||||
const releaseMatch = content.match(/release\s+([0-9][0-9.]*)/i);
|
||||
const release = releaseMatch ? releaseMatch[1] : "Unknown";
|
||||
return { distributorId, release };
|
||||
}
|
||||
|
||||
if (fs.existsSync(debianPath)) {
|
||||
const release = readFileSafe(debianPath) || "Unknown";
|
||||
return { distributorId: "Debian", release };
|
||||
}
|
||||
|
||||
return { distributorId: "Unknown", release: "Unknown" };
|
||||
}
|
||||
|
||||
function getUuid(): string {
|
||||
const content = readFileSafe("/etc/machine-id");
|
||||
return content || "unknown";
|
||||
}
|
||||
|
||||
/** 架构:与 uname -m 一致,使用 Node 的 os.machine() */
|
||||
function getArchitecture(): string {
|
||||
if (typeof os.machine === "function") {
|
||||
return os.machine();
|
||||
}
|
||||
const arch = process.arch;
|
||||
if (arch === "x64") return "x86_64";
|
||||
if (arch === "arm64") return "aarch64";
|
||||
return arch;
|
||||
}
|
||||
|
||||
function buildPayload(storeVersion: string): TelemetryPayload {
|
||||
const { distributorId, release } = getDistroInfo();
|
||||
const time = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
||||
|
||||
return {
|
||||
"Distributor ID": distributorId,
|
||||
Release: release,
|
||||
Architecture: getArchitecture(),
|
||||
Store_Version: storeVersion,
|
||||
UUID: getUuid(),
|
||||
TIME: time,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送遥测数据。仅在 Linux 下执行;非 Linux 直接返回。
|
||||
* 不抛出异常,错误仅写日志。
|
||||
*/
|
||||
export function sendTelemetryOnce(storeVersion: string): void {
|
||||
if (process.platform !== "linux") {
|
||||
logger.debug("Telemetry skipped: not Linux");
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = buildPayload(storeVersion);
|
||||
const body = JSON.stringify(payload);
|
||||
|
||||
fetch(TELEMETRY_URL, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body,
|
||||
})
|
||||
.then((res) => {
|
||||
const code = res.status;
|
||||
if (code === 200) {
|
||||
logger.debug("Telemetry sent successfully");
|
||||
return;
|
||||
}
|
||||
if (code === 400) {
|
||||
logger.warn("Telemetry: 客户端请求错误,请检查 JSON 或接口逻辑");
|
||||
return;
|
||||
}
|
||||
if (code === 422) {
|
||||
logger.warn("Telemetry: 请求数据无效,请检查字段值");
|
||||
return;
|
||||
}
|
||||
if (code === 500) {
|
||||
logger.warn("Telemetry: 服务器内部错误");
|
||||
return;
|
||||
}
|
||||
logger.warn(`Telemetry: 未处理的响应码 ${code}`);
|
||||
})
|
||||
.catch((err) => {
|
||||
logger.warn({ err }, "Telemetry request failed");
|
||||
});
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import pino from "pino";
|
||||
import { handleCommandLine } from "./deeplink.js";
|
||||
import { isLoaded } from "../global.js";
|
||||
import { tasks } from "./backend/install-manager.js";
|
||||
import { sendTelemetryOnce } from "./backend/telemetry.js";
|
||||
|
||||
// Assure single instance application
|
||||
if (!app.requestSingleInstanceLock()) {
|
||||
@@ -64,13 +65,21 @@ let win: BrowserWindow | null = null;
|
||||
const preload = path.join(__dirname, "../preload/index.mjs");
|
||||
const indexHtml = path.join(RENDERER_DIST, "index.html");
|
||||
|
||||
// Use app.getVersion() when the app is packaged.
|
||||
/** 与项目 package.json 一致的版本号:打包用 app.getVersion(),未打包时读 package.json */
|
||||
function getAppVersion(): string {
|
||||
if (app.isPackaged) return app.getVersion();
|
||||
const pkgPath = path.join(process.env.APP_ROOT ?? __dirname, "package.json");
|
||||
try {
|
||||
const raw = fs.readFileSync(pkgPath, "utf8");
|
||||
const pkg = JSON.parse(raw) as { version?: string };
|
||||
return typeof pkg.version === "string" ? pkg.version : "dev";
|
||||
} catch {
|
||||
return "dev";
|
||||
}
|
||||
}
|
||||
|
||||
const getUserAgent = (): string => {
|
||||
const version =
|
||||
app && app.isPackaged
|
||||
? app.getVersion()
|
||||
: process.env.npm_package_version || "dev";
|
||||
return `Spark-Store/${version}`;
|
||||
return `Spark-Store/${getAppVersion()}`;
|
||||
};
|
||||
|
||||
logger.info("User Agent: " + getUserAgent());
|
||||
@@ -86,9 +95,8 @@ function getStoreFilterFromArgv(): "spark" | "apm" | "both" {
|
||||
return "both";
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
"get-store-filter",
|
||||
(): "spark" | "apm" | "both" => getStoreFilterFromArgv(),
|
||||
ipcMain.handle("get-store-filter", (): "spark" | "apm" | "both" =>
|
||||
getStoreFilterFromArgv(),
|
||||
);
|
||||
|
||||
async function createWindow() {
|
||||
@@ -204,6 +212,8 @@ app.whenReady().then(() => {
|
||||
});
|
||||
createWindow();
|
||||
handleCommandLine(process.argv);
|
||||
// 启动后执行一次遥测(仅 Linux,不阻塞)
|
||||
sendTelemetryOnce(getAppVersion());
|
||||
});
|
||||
|
||||
app.on("window-all-closed", () => {
|
||||
@@ -247,9 +257,7 @@ function resolveIconPath(filename: string): string {
|
||||
/** 按优先级返回托盘图标路径:spark-store(.png|.ico) → amber-pm-logo.png。托盘不支持 SVG,故不尝试 spark-store.svg */
|
||||
function getTrayIconPath(): string | null {
|
||||
const ext = process.platform === "win32" ? ".ico" : ".png";
|
||||
const candidates = [
|
||||
`spark-store${ext}`
|
||||
];
|
||||
const candidates = [`spark-store${ext}`];
|
||||
for (const name of candidates) {
|
||||
const iconPath = resolveIconPath(name);
|
||||
if (fs.existsSync(iconPath)) {
|
||||
@@ -265,7 +273,9 @@ function getTrayIconPath(): string | null {
|
||||
const FALLBACK_TRAY_PNG =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAHklEQVQ4T2NkYGD4z0ABYBwNwMAwGoChNQAAAABJRU5ErkJggg==";
|
||||
|
||||
function getTrayImage(): string | ReturnType<typeof nativeImage.createFromDataURL> {
|
||||
function getTrayImage():
|
||||
| string
|
||||
| ReturnType<typeof nativeImage.createFromDataURL> {
|
||||
const iconPath = getTrayIconPath();
|
||||
if (iconPath) return iconPath;
|
||||
return nativeImage.createFromDataURL(FALLBACK_TRAY_PNG);
|
||||
|
||||
Reference in New Issue
Block a user