diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index daa1225e..bc0b7300 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -2,7 +2,7 @@ declare namespace NodeJS { interface ProcessEnv { - VSCODE_DEBUG?: 'true' + VSCODE_DEBUG?: "true"; /** * The built directory structure * @@ -16,8 +16,8 @@ declare namespace NodeJS { * │ └── index.html > Electron-Renderer * ``` */ - APP_ROOT: string + APP_ROOT: string; /** /dist/ or /public/ */ - VITE_PUBLIC: string + VITE_PUBLIC: string; } } diff --git a/electron/global.ts b/electron/global.ts index 4d0437bb..c5f3aeb4 100644 --- a/electron/global.ts +++ b/electron/global.ts @@ -1,2 +1,2 @@ -import { ref } from 'vue'; -export const isLoaded = ref(false); \ No newline at end of file +import { ref } from "vue"; +export const isLoaded = ref(false); diff --git a/electron/main/backend/install-manager.ts b/electron/main/backend/install-manager.ts index 09887094..dfd09ac3 100644 --- a/electron/main/backend/install-manager.ts +++ b/electron/main/backend/install-manager.ts @@ -1,11 +1,11 @@ -import { ipcMain, WebContents } from 'electron'; -import { spawn, ChildProcess, exec } from 'node:child_process'; -import { promisify } from 'node:util'; -import pino from 'pino'; +import { ipcMain, WebContents } from "electron"; +import { spawn, ChildProcess, exec } from "node:child_process"; +import { promisify } from "node:util"; +import pino from "pino"; -import { ChannelPayload, InstalledAppInfo } from '../../typedefinition'; +import { ChannelPayload, InstalledAppInfo } from "../../typedefinition"; -const logger = pino({ 'name': 'install-manager' }); +const logger = pino({ name: "install-manager" }); type InstallTask = { id: number; @@ -16,67 +16,69 @@ type InstallTask = { webContents: WebContents | null; }; -const SHELL_CALLER_PATH = '/opt/apm-store/extras/shell-caller.sh'; +const SHELL_CALLER_PATH = "/opt/apm-store/extras/shell-caller.sh"; export const tasks = new Map(); let idle = true; // Indicates if the installation manager is idle const checkSuperUserCommand = async (): Promise => { - let superUserCmd = ''; + let superUserCmd = ""; const execAsync = promisify(exec); if (process.getuid && process.getuid() !== 0) { - const { stdout, stderr } = await execAsync('which /usr/bin/pkexec'); + const { stdout, stderr } = await execAsync("which /usr/bin/pkexec"); if (stderr) { - logger.error('没有找到 pkexec 命令'); + logger.error("没有找到 pkexec 命令"); return; } logger.info(`找到提升权限命令: ${stdout.trim()}`); superUserCmd = stdout.trim(); if (superUserCmd.length === 0) { - logger.error('没有找到提升权限的命令 pkexec!'); + logger.error("没有找到提升权限的命令 pkexec!"); } } return superUserCmd; -} +}; const runCommandCapture = async (execCommand: string, execParams: string[]) => { - return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => { - const child = spawn(execCommand, execParams, { - shell: true, - env: process.env - }); + return await new Promise<{ code: number; stdout: string; stderr: string }>( + (resolve) => { + const child = spawn(execCommand, execParams, { + shell: true, + env: process.env, + }); - let stdout = ''; - let stderr = ''; + let stdout = ""; + let stderr = ""; - child.stdout?.on('data', (data) => { - stdout += data.toString(); - }); + child.stdout?.on("data", (data) => { + stdout += data.toString(); + }); - child.stderr?.on('data', (data) => { - stderr += data.toString(); - }); + child.stderr?.on("data", (data) => { + stderr += data.toString(); + }); - child.on('error', (err) => { - resolve({ code: -1, stdout, stderr: err.message }); - }); + child.on("error", (err) => { + resolve({ code: -1, stdout, stderr: err.message }); + }); - child.on('close', (code) => { - resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr }); - }); - }); + child.on("close", (code) => { + resolve({ code: typeof code === "number" ? code : -1, stdout, stderr }); + }); + }, + ); }; const parseInstalledList = (output: string) => { const apps: Array = []; - const lines = output.split('\n'); + const lines = output.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - if (trimmed.startsWith('Listing')) continue; - if (trimmed.startsWith('[INFO]')) continue; + if (trimmed.startsWith("Listing")) continue; + if (trimmed.startsWith("[INFO]")) continue; const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/); if (!match) continue; @@ -85,32 +87,40 @@ const parseInstalledList = (output: string) => { version: match[2], arch: match[3], flags: match[4], - raw: trimmed + raw: trimmed, }); } return apps; }; const parseUpgradableList = (output: string) => { - const apps: Array<{ pkgname: string; newVersion: string; currentVersion: string; raw: string }> = []; - const lines = output.split('\n'); + const apps: Array<{ + pkgname: string; + newVersion: string; + currentVersion: string; + raw: string; + }> = []; + const lines = output.split("\n"); for (const line of lines) { const trimmed = line.trim(); if (!trimmed) continue; - if (trimmed.startsWith('Listing')) continue; - if (trimmed.startsWith('[INFO]')) continue; - if (trimmed.includes('=') && !trimmed.includes('/')) continue; + if (trimmed.startsWith("Listing")) continue; + if (trimmed.startsWith("[INFO]")) continue; + if (trimmed.includes("=") && !trimmed.includes("/")) continue; - if (!trimmed.includes('/')) continue; + if (!trimmed.includes("/")) continue; const tokens = trimmed.split(/\s+/); if (tokens.length < 2) continue; const pkgToken = tokens[0]; - const pkgname = pkgToken.split('/')[0]; - const newVersion = tokens[1] || ''; - const currentMatch = trimmed.match(/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i); - const currentToken = tokens[5] || ''; - const currentVersion = currentMatch?.[1] || currentToken.replace('[', '').replace(']', ''); + const pkgname = pkgToken.split("/")[0]; + const newVersion = tokens[1] || ""; + const currentMatch = trimmed.match( + /\[(?:upgradable from|from):\s*([^\]\s]+)\]/i, + ); + const currentToken = tokens[5] || ""; + const currentVersion = + currentMatch?.[1] || currentToken.replace("[", "").replace("]", ""); if (!pkgname) continue; apps.push({ pkgname, newVersion, currentVersion, raw: trimmed }); @@ -119,30 +129,30 @@ const parseUpgradableList = (output: string) => { }; // Listen for download requests from renderer process -ipcMain.on('queue-install', async (event, download_json) => { +ipcMain.on("queue-install", async (event, download_json) => { const download = JSON.parse(download_json); const { id, pkgname } = download || {}; if (!id || !pkgname) { - logger.warn('passed arguments missing id or pkgname'); + logger.warn("passed arguments missing id or pkgname"); return; } logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`); - + // 避免重复添加同一任务,但允许重试下载 if (tasks.has(id) && !download.retry) { - tasks.get(id)?.webContents.send('install-log', { + tasks.get(id)?.webContents.send("install-log", { id, time: Date.now(), - message: `任务id: ${id} 已在列表中,忽略重复添加` + message: `任务id: ${id} 已在列表中,忽略重复添加`, }); - tasks.get(id)?.webContents.send('install-complete', { + tasks.get(id)?.webContents.send("install-complete", { id: id, success: false, time: Date.now(), exitCode: -1, - message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}` + message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}`, }); return; } @@ -151,7 +161,7 @@ ipcMain.on('queue-install', async (event, download_json) => { // 开始组装安装命令 const superUserCmd = await checkSuperUserCommand(); - let execCommand = ''; + let execCommand = ""; const execParams = []; if (superUserCmd.length > 0) { execCommand = superUserCmd; @@ -159,7 +169,7 @@ ipcMain.on('queue-install', async (event, download_json) => { } else { execCommand = SHELL_CALLER_PATH; } - execParams.push('apm', 'install', '-y', pkgname); + execParams.push("apm", "install", "-y", pkgname); const task: InstallTask = { id, @@ -167,7 +177,7 @@ ipcMain.on('queue-install', async (event, download_json) => { execCommand, execParams, process: null, - webContents + webContents, }; tasks.set(id, task); if (idle) processNextInQueue(0); @@ -179,53 +189,53 @@ function processNextInQueue(index: number) { idle = false; const task = Array.from(tasks.values())[index]; const webContents = task.webContents; - let stdoutData = ''; - let stderrData = ''; + let stdoutData = ""; + let stderrData = ""; - webContents.send('install-status', { + webContents.send("install-status", { id: task.id, time: Date.now(), - message: 'installing' - }) - webContents.send('install-log', { - id: task.id, - time: Date.now(), - message: `开始执行: ${task.execCommand} ${task.execParams.join(' ')}` + message: "installing", }); - logger.info(`启动安装命令: ${task.execCommand} ${task.execParams.join(' ')}`); + webContents.send("install-log", { + id: task.id, + time: Date.now(), + message: `开始执行: ${task.execCommand} ${task.execParams.join(" ")}`, + }); + logger.info(`启动安装命令: ${task.execCommand} ${task.execParams.join(" ")}`); const child = spawn(task.execCommand, task.execParams, { shell: true, - env: process.env + env: process.env, }); task.process = child; // 监听 stdout - child.stdout.on('data', (data) => { + child.stdout.on("data", (data) => { stdoutData += data.toString(); - webContents.send('install-log', { + webContents.send("install-log", { id: task.id, time: Date.now(), - message: data.toString() + message: data.toString(), }); }); // 监听 stderr - child.stderr.on('data', (data) => { + child.stderr.on("data", (data) => { stderrData += data.toString(); - webContents.send('install-log', { + webContents.send("install-log", { id: task.id, time: Date.now(), - message: data.toString() + message: data.toString(), }); }); - child.on('close', (code) => { + child.on("close", (code) => { const success = code === 0; // 拼接json消息 const messageJSONObj = { - message: success ? '安装完成' : `安装失败,退出码 ${code}`, + message: success ? "安装完成" : `安装失败,退出码 ${code}`, stdout: stdoutData, - stderr: stderrData + stderr: stderrData, }; if (success) { @@ -234,42 +244,45 @@ function processNextInQueue(index: number) { logger.error(messageJSONObj); } - webContents.send('install-complete', { + webContents.send("install-complete", { id: task.id, success: success, time: Date.now(), exitCode: code, - message: JSON.stringify(messageJSONObj) + message: JSON.stringify(messageJSONObj), }); tasks.delete(task.id); idle = true; - if (tasks.size > 0) - processNextInQueue(0); + if (tasks.size > 0) processNextInQueue(0); }); } -ipcMain.handle('check-installed', async (_event, pkgname: string) => { +ipcMain.handle("check-installed", async (_event, pkgname: string) => { if (!pkgname) { - logger.warn('check-installed missing pkgname'); + logger.warn("check-installed missing pkgname"); return false; } let isInstalled = false; logger.info(`检查应用是否已安装: ${pkgname}`); - const child = spawn(SHELL_CALLER_PATH, ['apm', 'list', '--installed', pkgname], { - shell: true, - env: process.env - }); + const child = spawn( + SHELL_CALLER_PATH, + ["apm", "list", "--installed", pkgname], + { + shell: true, + env: process.env, + }, + ); - let output = ''; - - child.stdout.on('data', (data) => { + let output = ""; + + child.stdout.on("data", (data) => { output += data.toString(); }); - + await new Promise((resolve) => { - child.on('close', (code) => { + child.on("close", (code) => { if (code === 0 && output.includes(pkgname)) { isInstalled = true; logger.info(`应用已安装: ${pkgname}`); @@ -282,16 +295,16 @@ ipcMain.handle('check-installed', async (_event, pkgname: string) => { return isInstalled; }); -ipcMain.on('remove-installed', async (_event, pkgname: string) => { +ipcMain.on("remove-installed", async (_event, pkgname: string) => { const webContents = _event.sender; if (!pkgname) { - logger.warn('remove-installed missing pkgname'); + logger.warn("remove-installed missing pkgname"); return; } logger.info(`卸载已安装应用: ${pkgname}`); - + const superUserCmd = await checkSuperUserCommand(); - let execCommand = ''; + let execCommand = ""; const execParams = []; if (superUserCmd.length > 0) { execCommand = superUserCmd; @@ -299,25 +312,29 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => { } else { execCommand = SHELL_CALLER_PATH; } - const child = spawn(execCommand, [...execParams, 'apm', 'remove', '-y', pkgname], { - shell: true, - env: process.env - }); - let output = ''; - - child.stdout.on('data', (data) => { + const child = spawn( + execCommand, + [...execParams, "apm", "remove", "-y", pkgname], + { + shell: true, + env: process.env, + }, + ); + let output = ""; + + child.stdout.on("data", (data) => { const chunk = data.toString(); output += chunk; - webContents.send('remove-progress', chunk); + webContents.send("remove-progress", chunk); }); - child.on('close', (code) => { + child.on("close", (code) => { const success = code === 0; // 拼接json消息 const messageJSONObj = { - message: success ? '卸载完成' : `卸载失败,退出码 ${code}`, + message: success ? "卸载完成" : `卸载失败,退出码 ${code}`, stdout: output, - stderr: '' + stderr: "", }; if (success) { @@ -326,26 +343,28 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => { logger.error(messageJSONObj); } - webContents.send('remove-complete', { + webContents.send("remove-complete", { id: 0, success: success, time: Date.now(), exitCode: code, - message: JSON.stringify(messageJSONObj) + message: JSON.stringify(messageJSONObj), } satisfies ChannelPayload); }); }); -ipcMain.handle('list-upgradable', async () => { - const { code, stdout, stderr } = await runCommandCapture( - SHELL_CALLER_PATH, - ['apm', 'list', '--upgradable']); +ipcMain.handle("list-upgradable", async () => { + const { code, stdout, stderr } = await runCommandCapture(SHELL_CALLER_PATH, [ + "apm", + "list", + "--upgradable", + ]); if (code !== 0) { logger.error(`list-upgradable failed: ${stderr || stdout}`); return { success: false, message: stderr || stdout || `list-upgradable failed with code ${code}`, - apps: [] + apps: [], }; } @@ -353,20 +372,25 @@ ipcMain.handle('list-upgradable', async () => { return { success: true, apps }; }); -ipcMain.handle('list-installed', async () => { +ipcMain.handle("list-installed", async () => { const superUserCmd = await checkSuperUserCommand(); - const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; - const execParams = superUserCmd.length > 0 - ? [SHELL_CALLER_PATH, 'apm', 'list', '--installed'] - : ['apm', 'list', '--installed']; + const execCommand = + superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; + const execParams = + superUserCmd.length > 0 + ? [SHELL_CALLER_PATH, "apm", "list", "--installed"] + : ["apm", "list", "--installed"]; - const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams); + const { code, stdout, stderr } = await runCommandCapture( + execCommand, + execParams, + ); if (code !== 0) { logger.error(`list-installed failed: ${stderr || stdout}`); return { success: false, message: stderr || stdout || `list-installed failed with code ${code}`, - apps: [] + apps: [], }; } @@ -374,19 +398,24 @@ ipcMain.handle('list-installed', async () => { return { success: true, apps }; }); -ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => { +ipcMain.handle("uninstall-installed", async (_event, pkgname: string) => { if (!pkgname) { - logger.warn('uninstall-installed missing pkgname'); - return { success: false, message: 'missing pkgname' }; + logger.warn("uninstall-installed missing pkgname"); + return { success: false, message: "missing pkgname" }; } const superUserCmd = await checkSuperUserCommand(); - const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; - const execParams = superUserCmd.length > 0 - ? [SHELL_CALLER_PATH, 'apm', 'remove', '-y', pkgname] - : ['apm', 'remove', '-y', pkgname]; + const execCommand = + superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH; + const execParams = + superUserCmd.length > 0 + ? [SHELL_CALLER_PATH, "apm", "remove", "-y", pkgname] + : ["apm", "remove", "-y", pkgname]; - const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams); + const { code, stdout, stderr } = await runCommandCapture( + execCommand, + execParams, + ); const success = code === 0; if (success) { @@ -397,24 +426,28 @@ ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => { return { success, - message: success ? '卸载完成' : (stderr || stdout || `卸载失败,退出码 ${code}`) + message: success + ? "卸载完成" + : stderr || stdout || `卸载失败,退出码 ${code}`, }; }); -ipcMain.handle('launch-app', async (_event, pkgname: string) => { +ipcMain.handle("launch-app", async (_event, pkgname: string) => { if (!pkgname) { - logger.warn('No pkgname provided for launch-app'); + logger.warn("No pkgname provided for launch-app"); } const execCommand = "/opt/apm-store/extras/host-spawn"; - const execParams = ['/opt/apm-store/extras/apm-launcher', 'launch', pkgname ]; + const execParams = ["/opt/apm-store/extras/apm-launcher", "launch", pkgname]; - logger.info(`Launching app: ${pkgname} with command: ${execCommand} ${execParams.join(' ')}`); + logger.info( + `Launching app: ${pkgname} with command: ${execCommand} ${execParams.join(" ")}`, + ); spawn(execCommand, execParams, { shell: false, env: process.env, detached: true, - stdio: 'ignore' + stdio: "ignore", }).unref(); -}); \ No newline at end of file +}); diff --git a/electron/main/deeplink.ts b/electron/main/deeplink.ts index 11d02718..e6afdcbc 100644 --- a/electron/main/deeplink.ts +++ b/electron/main/deeplink.ts @@ -5,7 +5,7 @@ import { app } from "electron"; import pino from "pino"; -const logger = pino({ 'name': 'deeplink.ts' }); +const logger = pino({ name: "deeplink.ts" }); type Query = Record; export type Listener = (query: Query) => void; @@ -52,13 +52,13 @@ export const deepLink = { on: (event: string, listener: Listener) => { const count = listeners.add(event, listener); logger.info( - `Deep link: listener added for event ${event}. Total event listeners: ${count}` + `Deep link: listener added for event ${event}. Total event listeners: ${count}`, ); }, off: (event: string, listener: Listener) => { const count = listeners.remove(event, listener); logger.info( - `Deep link: listener removed for event ${event}. Total event listeners: ${count}` + `Deep link: listener removed for event ${event}. Total event listeners: ${count}`, ); }, once: (event: string, listener: Listener) => { @@ -72,7 +72,7 @@ export const deepLink = { export function handleCommandLine(commandLine: string[]) { const target = commandLine.find((arg) => - protocols.some((protocol) => arg.startsWith(protocol + "://")) + protocols.some((protocol) => arg.startsWith(protocol + "://")), ); if (!target) return; diff --git a/electron/main/handle-url-scheme.ts b/electron/main/handle-url-scheme.ts index b468d75d..4a3f1e42 100644 --- a/electron/main/handle-url-scheme.ts +++ b/electron/main/handle-url-scheme.ts @@ -1,9 +1,9 @@ -import { BrowserWindow } from 'electron'; -import { deepLink } from './deeplink'; -import { isLoaded } from '../global'; -import pino from 'pino'; +import { BrowserWindow } from "electron"; +import { deepLink } from "./deeplink"; +import { isLoaded } from "../global"; +import pino from "pino"; -const logger = pino({ 'name': 'handle-url-scheme.ts' }); +const logger = pino({ name: "handle-url-scheme.ts" }); const pendingActions: Array<() => void> = []; @@ -24,22 +24,26 @@ new Promise((resolve) => { }); deepLink.on("event", (query) => { - logger.info(`Deep link: event "event" fired with query: ${JSON.stringify(query)}`); + logger.info( + `Deep link: event "event" fired with query: ${JSON.stringify(query)}`, + ); }); deepLink.on("action", (query) => { - logger.info(`Deep link: event "action" fired with query: ${JSON.stringify(query)}`); - + logger.info( + `Deep link: event "action" fired with query: ${JSON.stringify(query)}`, + ); + const action = () => { const win = BrowserWindow.getAllWindows()[0]; if (!win) return; - if (query.cmd === 'update') { - win.webContents.send('deep-link-update'); + if (query.cmd === "update") { + win.webContents.send("deep-link-update"); if (win.isMinimized()) win.restore(); win.focus(); - } else if (query.cmd === 'list') { - win.webContents.send('deep-link-installed'); + } else if (query.cmd === "list") { + win.webContents.send("deep-link-installed"); if (win.isMinimized()) win.restore(); win.focus(); } @@ -55,14 +59,16 @@ deepLink.on("action", (query) => { }); deepLink.on("install", (query) => { - logger.info(`Deep link: event "install" fired with query: ${JSON.stringify(query)}`); - + logger.info( + `Deep link: event "install" fired with query: ${JSON.stringify(query)}`, + ); + const action = () => { const win = BrowserWindow.getAllWindows()[0]; if (!win) return; if (query.pkg) { - win.webContents.send('deep-link-install', query.pkg); + win.webContents.send("deep-link-install", query.pkg); if (win.isMinimized()) win.restore(); win.focus(); } diff --git a/electron/main/index.ts b/electron/main/index.ts index ad831982..a141dcae 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,24 +1,23 @@ -import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from 'electron' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import os from 'node:os' -import fs from 'node:fs' -import pino from 'pino' -import { handleCommandLine } from './deeplink.js' -import { isLoaded } from '../global.js' -import { tasks } from './backend/install-manager.js' - +import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from "electron"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import os from "node:os"; +import fs from "node:fs"; +import pino from "pino"; +import { handleCommandLine } from "./deeplink.js"; +import { isLoaded } from "../global.js"; +import { tasks } from "./backend/install-manager.js"; // Assure single instance application if (!app.requestSingleInstanceLock()) { app.exit(0); } -import './backend/install-manager.js' -import './handle-url-scheme.js' +import "./backend/install-manager.js"; +import "./handle-url-scheme.js"; -const logger = pino({ 'name': 'index.ts' }); -const __dirname = path.dirname(fileURLToPath(import.meta.url)) +const logger = pino({ name: "index.ts" }); +const __dirname = path.dirname(fileURLToPath(import.meta.url)); // The built directory structure // @@ -30,38 +29,38 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)) // ├─┬ dist // │ └── index.html > Electron-Renderer // -process.env.APP_ROOT = path.join(__dirname, '../..') +process.env.APP_ROOT = path.join(__dirname, "../.."); -export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') -export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') -export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL +export const MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron"); +export const RENDERER_DIST = path.join(process.env.APP_ROOT, "dist"); +export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL; process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL - ? path.join(process.env.APP_ROOT, 'public') - : RENDERER_DIST + ? path.join(process.env.APP_ROOT, "public") + : RENDERER_DIST; // Disable GPU Acceleration for Windows 7 -if (os.release().startsWith('6.1')) app.disableHardwareAcceleration() +if (os.release().startsWith("6.1")) app.disableHardwareAcceleration(); // Set application name for Windows 10+ notifications -if (process.platform === 'win32') app.setAppUserModelId(app.getName()) +if (process.platform === "win32") app.setAppUserModelId(app.getName()); if (!app.requestSingleInstanceLock()) { - app.quit() - process.exit(0) + app.quit(); + process.exit(0); } -let win: BrowserWindow | null = null -const preload = path.join(__dirname, '../preload/index.mjs') -const indexHtml = path.join(RENDERER_DIST, 'index.html') +let win: BrowserWindow | null = null; +const preload = path.join(__dirname, "../preload/index.mjs"); +const indexHtml = path.join(RENDERER_DIST, "index.html"); async function createWindow() { win = new BrowserWindow({ - title: 'APM AppStore', + title: "APM AppStore", width: 1366, height: 768, autoHideMenuBar: true, - icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'), + icon: path.join(process.env.VITE_PUBLIC, "favicon.ico"), webPreferences: { preload, // Warning: Enable nodeIntegration and disable contextIsolation is not secure in production @@ -71,30 +70,31 @@ async function createWindow() { // Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation // contextIsolation: false, }, - }) + }); - if (VITE_DEV_SERVER_URL) { // #298 - win.loadURL(VITE_DEV_SERVER_URL) + if (VITE_DEV_SERVER_URL) { + // #298 + win.loadURL(VITE_DEV_SERVER_URL); // Open devTool if the app is not packaged - win.webContents.openDevTools({mode:'detach'}) + win.webContents.openDevTools({ mode: "detach" }); } else { - win.loadFile(indexHtml) + win.loadFile(indexHtml); } // Test actively push message to the Electron-Renderer - win.webContents.on('did-finish-load', () => { - win?.webContents.send('main-process-message', new Date().toLocaleString()) - logger.info('Renderer process is ready.'); - }) + win.webContents.on("did-finish-load", () => { + win?.webContents.send("main-process-message", new Date().toLocaleString()); + logger.info("Renderer process is ready."); + }); // Make all links open with the browser, not with the application win.webContents.setWindowOpenHandler(({ url }) => { - if (url.startsWith('https:')) shell.openExternal(url) - return { action: 'deny' } - }) + if (url.startsWith("https:")) shell.openExternal(url); + return { action: "deny" }; + }); // win.webContents.on('will-navigate', (event, url) => { }) #344 - win.on('close', (event) => { + win.on("close", (event) => { // 截获 close 默认行为 event.preventDefault(); // 点击关闭时触发close事件,我们按照之前的思路在关闭时,隐藏窗口,隐藏任务栏窗口 @@ -103,50 +103,52 @@ async function createWindow() { win.setSkipTaskbar(true); } else { // 如果没有下载任务,才允许关闭窗口 - win.destroy() + win.destroy(); } - - }) + }); } -ipcMain.on('renderer-ready', (event, args) => { - logger.info('Received renderer-ready event with args: ' + JSON.stringify(args)); +ipcMain.on("renderer-ready", (event, args) => { + logger.info( + "Received renderer-ready event with args: " + JSON.stringify(args), + ); isLoaded.value = args.status; logger.info(`isLoaded set to: ${isLoaded.value}`); -}) +}); app.whenReady().then(() => { - createWindow() - handleCommandLine(process.argv) -}) + createWindow(); + handleCommandLine(process.argv); +}); -app.on('window-all-closed', () => { - win = null - if (process.platform !== 'darwin') app.quit() -}) +app.on("window-all-closed", () => { + win = null; + if (process.platform !== "darwin") app.quit(); +}); -app.on('second-instance', () => { +app.on("second-instance", () => { if (win) { // Focus on the main window if the user tried to open another - if (win.isMinimized()) win.restore() - win.focus() + if (win.isMinimized()) win.restore(); + win.focus(); } -}) +}); -app.on('activate', () => { - const allWindows = BrowserWindow.getAllWindows() +app.on("activate", () => { + const allWindows = BrowserWindow.getAllWindows(); if (allWindows.length) { - allWindows[0].focus() + allWindows[0].focus(); } else { - createWindow() + createWindow(); } -}) +}); // 设置托盘 // 获取图标路径 function getIconPath() { let iconPath = ""; - const iconFile = process.platform === "win32" ? "amber-pm-logo.ico" : "amber-pm-logo.png"; // 图标文件名,linux下需要png格式,不然会不显示 + const iconFile = + process.platform === "win32" ? "amber-pm-logo.ico" : "amber-pm-logo.png"; // 图标文件名,linux下需要png格式,不然会不显示 // 判断是否在打包模式 if (app.isPackaged) { // 打包模式 @@ -170,31 +172,35 @@ function getIconPath() { let tray = null; app.whenReady().then(() => { - tray = new Tray(getIconPath()); - const contextMenu = Menu.buildFromTemplate([{ - label: '显示主界面', - click: () => { win.show() } + tray = new Tray(getIconPath()); + const contextMenu = Menu.buildFromTemplate([ + { + label: "显示主界面", + click: () => { + win.show(); + }, }, { - label: '退出程序', - click: () => { win.destroy() } + label: "退出程序", + click: () => { + win.destroy(); + }, + }, + ]); + tray.setToolTip("APM 应用商店"); + tray.setContextMenu(contextMenu); + // 双击触发 + tray.on("click", () => { + // 双击通知区图标实现应用的显示或隐藏 + if (win.isVisible()) { + win.hide(); + win.setSkipTaskbar(true); + } else { + win.show(); + win.setSkipTaskbar(false); } - ]); - tray.setToolTip('APM 应用商店') - tray.setContextMenu(contextMenu) - // 双击触发 - tray.on('click', () => { - // 双击通知区图标实现应用的显示或隐藏 - if (win.isVisible()) { - win.hide(); - win.setSkipTaskbar(true); - } else { - win.show(); - win.setSkipTaskbar(false); - } - }); -}) - + }); +}); // New window example arg: new windows url // ipcMain.handle('open-win', (_, arg) => { diff --git a/electron/preload/index.ts b/electron/preload/index.ts index f2f77cd1..99af614d 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,66 +1,70 @@ -import { ipcRenderer, contextBridge } from 'electron' +import { ipcRenderer, contextBridge } from "electron"; // --------- Expose some API to the Renderer process --------- -contextBridge.exposeInMainWorld('ipcRenderer', { +contextBridge.exposeInMainWorld("ipcRenderer", { on(...args: Parameters) { - const [channel, listener] = args - return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args)) + const [channel, listener] = args; + return ipcRenderer.on(channel, (event, ...args) => + listener(event, ...args), + ); }, off(...args: Parameters) { - const [channel, ...omit] = args - return ipcRenderer.off(channel, ...omit) + const [channel, ...omit] = args; + return ipcRenderer.off(channel, ...omit); }, send(...args: Parameters) { - const [channel, ...omit] = args - return ipcRenderer.send(channel, ...omit) + const [channel, ...omit] = args; + return ipcRenderer.send(channel, ...omit); }, invoke(...args: Parameters) { - const [channel, ...omit] = args - return ipcRenderer.invoke(channel, ...omit) + const [channel, ...omit] = args; + return ipcRenderer.invoke(channel, ...omit); }, // You can expose other APTs you need here. // ... -}) +}); -contextBridge.exposeInMainWorld('apm_store', { +contextBridge.exposeInMainWorld("apm_store", { arch: (() => { const arch = process.arch; - if (arch === 'x64') { - return 'amd64' + '-apm'; + if (arch === "x64") { + return "amd64" + "-apm"; } else { - return arch + '-apm'; + return arch + "-apm"; } - })() + })(), }); // --------- Preload scripts loading --------- -function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) { +function domReady( + condition: DocumentReadyState[] = ["complete", "interactive"], +) { return new Promise((resolve) => { if (condition.includes(document.readyState)) { - resolve(true) + resolve(true); } else { - document.addEventListener('readystatechange', () => { + document.addEventListener("readystatechange", () => { if (condition.includes(document.readyState)) { - resolve(true) + resolve(true); } - }) + }); } - }) + }); } const safeDOM = { append(parent: HTMLElement, child: HTMLElement) { - if (!Array.from(parent.children).find(e => e === child)) { - return parent.appendChild(child) + if (!Array.from(parent.children).find((e) => e === child)) { + return parent.appendChild(child); } }, remove(parent: HTMLElement, child: HTMLElement) { - if (Array.from(parent.children).find(e => e === child)) { - return parent.removeChild(child) + if (Array.from(parent.children).find((e) => e === child)) { + return parent.removeChild(child); } }, -} +}; /** * https://tobiasahlin.com/spinkit @@ -69,7 +73,7 @@ const safeDOM = { * https://matejkustec.github.io/SpinThatShit */ function useLoading() { - const className = `loaders-css__square-spin` + const className = `loaders-css__square-spin`; const styleContent = ` @keyframes square-spin { 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); } @@ -96,35 +100,34 @@ function useLoading() { background: #282c34; z-index: 9; } - ` - const oStyle = document.createElement('style') - const oDiv = document.createElement('div') + `; + const oStyle = document.createElement("style"); + const oDiv = document.createElement("div"); - oStyle.id = 'app-loading-style' - oStyle.innerHTML = styleContent - oDiv.className = 'app-loading-wrap' - oDiv.innerHTML = `
` + oStyle.id = "app-loading-style"; + oStyle.innerHTML = styleContent; + oDiv.className = "app-loading-wrap"; + oDiv.innerHTML = `
`; return { appendLoading() { - safeDOM.append(document.head, oStyle) - safeDOM.append(document.body, oDiv) + safeDOM.append(document.head, oStyle); + safeDOM.append(document.body, oDiv); }, removeLoading() { - safeDOM.remove(document.head, oStyle) - safeDOM.remove(document.body, oDiv) + safeDOM.remove(document.head, oStyle); + safeDOM.remove(document.body, oDiv); }, - } + }; } // ---------------------------------------------------------------------- -const { appendLoading, removeLoading } = useLoading() -domReady().then(appendLoading) +const { appendLoading, removeLoading } = useLoading(); +domReady().then(appendLoading); window.onmessage = (ev) => { - if (ev.data.payload === 'removeLoading') - removeLoading() -} + if (ev.data.payload === "removeLoading") removeLoading(); +}; -setTimeout(removeLoading, 4999) +setTimeout(removeLoading, 4999); diff --git a/electron/typedefinition.ts b/electron/typedefinition.ts index 08cf3b2b..70d96476 100644 --- a/electron/typedefinition.ts +++ b/electron/typedefinition.ts @@ -1,14 +1,13 @@ export interface InstalledAppInfo { - pkgname: string; - version: string; - arch: string; - flags: string; - raw: string; + pkgname: string; + version: string; + arch: string; + flags: string; + raw: string; } - export type ChannelPayload = { - success: boolean; - message: string; - [k: string]: unknown; -}; \ No newline at end of file + success: boolean; + message: string; + [k: string]: unknown; +}; diff --git a/package.json b/package.json index f7607349..588b58bc 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "build:rpm": "vue-tsc --noEmit && vite build --mode production && electron-builder --config electron-builder.yml --linux rpm", "build:deb": "vue-tsc --noEmit && vite build --mode production && electron-builder --config electron-builder.yml --linux deb", "preview": "vite preview --mode debug", - "lint": "eslint --ext .ts,.vue src electron" + "lint": "eslint --ext .ts,.vue src electron", + "lint:fix": "eslint --ext .ts,.vue src electron --fix", + "format": "prettier --write \"src/**/*.{ts,vue}\" \"electron/**/*.{ts,vue}\"" }, "devDependencies": { "@dotenvx/dotenvx": "^1.51.4", diff --git a/src/App.vue b/src/App.vue index b8df9995..f5c0195a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,67 +1,148 @@ diff --git a/src/components/AppCard.vue b/src/components/AppCard.vue index aee8316b..676d5e96 100644 --- a/src/components/AppCard.vue +++ b/src/components/AppCard.vue @@ -1,47 +1,67 @@ diff --git a/src/components/AppGrid.vue b/src/components/AppGrid.vue index d220c1d9..19aee8d8 100644 --- a/src/components/AppGrid.vue +++ b/src/components/AppGrid.vue @@ -1,22 +1,42 @@ diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 8d061d87..1c8dd00a 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -5,10 +5,16 @@
- - + + placeholder="搜索应用名 / 包名 / 标签" + @input="debounceSearch" + />
@@ -19,8 +25,8 @@ diff --git a/src/components/AppSidebar.vue b/src/components/AppSidebar.vue index a30e813c..a6a840d5 100644 --- a/src/components/AppSidebar.vue +++ b/src/components/AppSidebar.vue @@ -1,41 +1,71 @@ diff --git a/src/components/DownloadDetail.vue b/src/components/DownloadDetail.vue index a6d26f60..1b5a00f0 100644 --- a/src/components/DownloadDetail.vue +++ b/src/components/DownloadDetail.vue @@ -1,72 +1,123 @@ diff --git a/src/components/DownloadQueue.vue b/src/components/DownloadQueue.vue index 10ce3313..f7a4f7a8 100644 --- a/src/components/DownloadQueue.vue +++ b/src/components/DownloadQueue.vue @@ -1,65 +1,121 @@ diff --git a/src/components/InstalledAppsModal.vue b/src/components/InstalledAppsModal.vue index f79ac483..d5206823 100644 --- a/src/components/InstalledAppsModal.vue +++ b/src/components/InstalledAppsModal.vue @@ -1,67 +1,108 @@