mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 01:10:16 +08:00
refactor: improve code formatting and consistency across components
- Updated button and span elements in ThemeToggle.vue and TopActions.vue for better readability. - Enhanced UninstallConfirmModal.vue and UpdateAppsModal.vue with consistent indentation and spacing. - Refactored downloadStatus.ts and storeConfig.ts for improved code clarity. - Standardized string quotes and spacing in typedefinition.ts and processInstall.ts. - Ensured consistent use of arrow functions and improved variable declarations throughout the codebase.
This commit is contained in:
6
electron/electron-env.d.ts
vendored
6
electron/electron-env.d.ts
vendored
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
declare namespace NodeJS {
|
declare namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
VSCODE_DEBUG?: 'true'
|
VSCODE_DEBUG?: "true";
|
||||||
/**
|
/**
|
||||||
* The built directory structure
|
* The built directory structure
|
||||||
*
|
*
|
||||||
@@ -16,8 +16,8 @@ declare namespace NodeJS {
|
|||||||
* │ └── index.html > Electron-Renderer
|
* │ └── index.html > Electron-Renderer
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
APP_ROOT: string
|
APP_ROOT: string;
|
||||||
/** /dist/ or /public/ */
|
/** /dist/ or /public/ */
|
||||||
VITE_PUBLIC: string
|
VITE_PUBLIC: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
import { ref } from 'vue';
|
import { ref } from "vue";
|
||||||
export const isLoaded = ref(false);
|
export const isLoaded = ref(false);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { ipcMain, WebContents } from 'electron';
|
import { ipcMain, WebContents } from "electron";
|
||||||
import { spawn, ChildProcess, exec } from 'node:child_process';
|
import { spawn, ChildProcess, exec } from "node:child_process";
|
||||||
import { promisify } from 'node:util';
|
import { promisify } from "node:util";
|
||||||
import pino from 'pino';
|
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 = {
|
type InstallTask = {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -16,67 +16,69 @@ type InstallTask = {
|
|||||||
webContents: WebContents | null;
|
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<number, InstallTask>();
|
export const tasks = new Map<number, InstallTask>();
|
||||||
|
|
||||||
let idle = true; // Indicates if the installation manager is idle
|
let idle = true; // Indicates if the installation manager is idle
|
||||||
|
|
||||||
const checkSuperUserCommand = async (): Promise<string> => {
|
const checkSuperUserCommand = async (): Promise<string> => {
|
||||||
let superUserCmd = '';
|
let superUserCmd = "";
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
if (process.getuid && process.getuid() !== 0) {
|
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) {
|
if (stderr) {
|
||||||
logger.error('没有找到 pkexec 命令');
|
logger.error("没有找到 pkexec 命令");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`找到提升权限命令: ${stdout.trim()}`);
|
logger.info(`找到提升权限命令: ${stdout.trim()}`);
|
||||||
superUserCmd = stdout.trim();
|
superUserCmd = stdout.trim();
|
||||||
|
|
||||||
if (superUserCmd.length === 0) {
|
if (superUserCmd.length === 0) {
|
||||||
logger.error('没有找到提升权限的命令 pkexec!');
|
logger.error("没有找到提升权限的命令 pkexec!");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return superUserCmd;
|
return superUserCmd;
|
||||||
}
|
};
|
||||||
|
|
||||||
const runCommandCapture = async (execCommand: string, execParams: string[]) => {
|
const runCommandCapture = async (execCommand: string, execParams: string[]) => {
|
||||||
return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
return await new Promise<{ code: number; stdout: string; stderr: string }>(
|
||||||
const child = spawn(execCommand, execParams, {
|
(resolve) => {
|
||||||
shell: true,
|
const child = spawn(execCommand, execParams, {
|
||||||
env: process.env
|
shell: true,
|
||||||
});
|
env: process.env,
|
||||||
|
});
|
||||||
|
|
||||||
let stdout = '';
|
let stdout = "";
|
||||||
let stderr = '';
|
let stderr = "";
|
||||||
|
|
||||||
child.stdout?.on('data', (data) => {
|
child.stdout?.on("data", (data) => {
|
||||||
stdout += data.toString();
|
stdout += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.stderr?.on('data', (data) => {
|
child.stderr?.on("data", (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('error', (err) => {
|
child.on("error", (err) => {
|
||||||
resolve({ code: -1, stdout, stderr: err.message });
|
resolve({ code: -1, stdout, stderr: err.message });
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on("close", (code) => {
|
||||||
resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr });
|
resolve({ code: typeof code === "number" ? code : -1, stdout, stderr });
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseInstalledList = (output: string) => {
|
const parseInstalledList = (output: string) => {
|
||||||
const apps: Array<InstalledAppInfo> = [];
|
const apps: Array<InstalledAppInfo> = [];
|
||||||
const lines = output.split('\n');
|
const lines = output.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
if (trimmed.startsWith('Listing')) continue;
|
if (trimmed.startsWith("Listing")) continue;
|
||||||
if (trimmed.startsWith('[INFO]')) continue;
|
if (trimmed.startsWith("[INFO]")) continue;
|
||||||
|
|
||||||
const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
|
const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
|
||||||
if (!match) continue;
|
if (!match) continue;
|
||||||
@@ -85,32 +87,40 @@ const parseInstalledList = (output: string) => {
|
|||||||
version: match[2],
|
version: match[2],
|
||||||
arch: match[3],
|
arch: match[3],
|
||||||
flags: match[4],
|
flags: match[4],
|
||||||
raw: trimmed
|
raw: trimmed,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return apps;
|
return apps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseUpgradableList = (output: string) => {
|
const parseUpgradableList = (output: string) => {
|
||||||
const apps: Array<{ pkgname: string; newVersion: string; currentVersion: string; raw: string }> = [];
|
const apps: Array<{
|
||||||
const lines = output.split('\n');
|
pkgname: string;
|
||||||
|
newVersion: string;
|
||||||
|
currentVersion: string;
|
||||||
|
raw: string;
|
||||||
|
}> = [];
|
||||||
|
const lines = output.split("\n");
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const trimmed = line.trim();
|
const trimmed = line.trim();
|
||||||
if (!trimmed) continue;
|
if (!trimmed) continue;
|
||||||
if (trimmed.startsWith('Listing')) continue;
|
if (trimmed.startsWith("Listing")) continue;
|
||||||
if (trimmed.startsWith('[INFO]')) continue;
|
if (trimmed.startsWith("[INFO]")) continue;
|
||||||
if (trimmed.includes('=') && !trimmed.includes('/')) continue;
|
if (trimmed.includes("=") && !trimmed.includes("/")) continue;
|
||||||
|
|
||||||
if (!trimmed.includes('/')) continue;
|
if (!trimmed.includes("/")) continue;
|
||||||
|
|
||||||
const tokens = trimmed.split(/\s+/);
|
const tokens = trimmed.split(/\s+/);
|
||||||
if (tokens.length < 2) continue;
|
if (tokens.length < 2) continue;
|
||||||
const pkgToken = tokens[0];
|
const pkgToken = tokens[0];
|
||||||
const pkgname = pkgToken.split('/')[0];
|
const pkgname = pkgToken.split("/")[0];
|
||||||
const newVersion = tokens[1] || '';
|
const newVersion = tokens[1] || "";
|
||||||
const currentMatch = trimmed.match(/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i);
|
const currentMatch = trimmed.match(
|
||||||
const currentToken = tokens[5] || '';
|
/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i,
|
||||||
const currentVersion = currentMatch?.[1] || currentToken.replace('[', '').replace(']', '');
|
);
|
||||||
|
const currentToken = tokens[5] || "";
|
||||||
|
const currentVersion =
|
||||||
|
currentMatch?.[1] || currentToken.replace("[", "").replace("]", "");
|
||||||
|
|
||||||
if (!pkgname) continue;
|
if (!pkgname) continue;
|
||||||
apps.push({ pkgname, newVersion, currentVersion, raw: trimmed });
|
apps.push({ pkgname, newVersion, currentVersion, raw: trimmed });
|
||||||
@@ -119,30 +129,30 @@ const parseUpgradableList = (output: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Listen for download requests from renderer process
|
// 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 download = JSON.parse(download_json);
|
||||||
const { id, pkgname } = download || {};
|
const { id, pkgname } = download || {};
|
||||||
|
|
||||||
if (!id || !pkgname) {
|
if (!id || !pkgname) {
|
||||||
logger.warn('passed arguments missing id or pkgname');
|
logger.warn("passed arguments missing id or pkgname");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
||||||
|
|
||||||
// 避免重复添加同一任务,但允许重试下载
|
// 避免重复添加同一任务,但允许重试下载
|
||||||
if (tasks.has(id) && !download.retry) {
|
if (tasks.has(id) && !download.retry) {
|
||||||
tasks.get(id)?.webContents.send('install-log', {
|
tasks.get(id)?.webContents.send("install-log", {
|
||||||
id,
|
id,
|
||||||
time: Date.now(),
|
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,
|
id: id,
|
||||||
success: false,
|
success: false,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
exitCode: -1,
|
exitCode: -1,
|
||||||
message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}`
|
message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -151,7 +161,7 @@ ipcMain.on('queue-install', async (event, download_json) => {
|
|||||||
|
|
||||||
// 开始组装安装命令
|
// 开始组装安装命令
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
let execCommand = '';
|
let execCommand = "";
|
||||||
const execParams = [];
|
const execParams = [];
|
||||||
if (superUserCmd.length > 0) {
|
if (superUserCmd.length > 0) {
|
||||||
execCommand = superUserCmd;
|
execCommand = superUserCmd;
|
||||||
@@ -159,7 +169,7 @@ ipcMain.on('queue-install', async (event, download_json) => {
|
|||||||
} else {
|
} else {
|
||||||
execCommand = SHELL_CALLER_PATH;
|
execCommand = SHELL_CALLER_PATH;
|
||||||
}
|
}
|
||||||
execParams.push('apm', 'install', '-y', pkgname);
|
execParams.push("apm", "install", "-y", pkgname);
|
||||||
|
|
||||||
const task: InstallTask = {
|
const task: InstallTask = {
|
||||||
id,
|
id,
|
||||||
@@ -167,7 +177,7 @@ ipcMain.on('queue-install', async (event, download_json) => {
|
|||||||
execCommand,
|
execCommand,
|
||||||
execParams,
|
execParams,
|
||||||
process: null,
|
process: null,
|
||||||
webContents
|
webContents,
|
||||||
};
|
};
|
||||||
tasks.set(id, task);
|
tasks.set(id, task);
|
||||||
if (idle) processNextInQueue(0);
|
if (idle) processNextInQueue(0);
|
||||||
@@ -179,53 +189,53 @@ function processNextInQueue(index: number) {
|
|||||||
idle = false;
|
idle = false;
|
||||||
const task = Array.from(tasks.values())[index];
|
const task = Array.from(tasks.values())[index];
|
||||||
const webContents = task.webContents;
|
const webContents = task.webContents;
|
||||||
let stdoutData = '';
|
let stdoutData = "";
|
||||||
let stderrData = '';
|
let stderrData = "";
|
||||||
|
|
||||||
webContents.send('install-status', {
|
webContents.send("install-status", {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: 'installing'
|
message: "installing",
|
||||||
})
|
|
||||||
webContents.send('install-log', {
|
|
||||||
id: task.id,
|
|
||||||
time: Date.now(),
|
|
||||||
message: `开始执行: ${task.execCommand} ${task.execParams.join(' ')}`
|
|
||||||
});
|
});
|
||||||
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, {
|
const child = spawn(task.execCommand, task.execParams, {
|
||||||
shell: true,
|
shell: true,
|
||||||
env: process.env
|
env: process.env,
|
||||||
});
|
});
|
||||||
task.process = child;
|
task.process = child;
|
||||||
|
|
||||||
// 监听 stdout
|
// 监听 stdout
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
stdoutData += data.toString();
|
stdoutData += data.toString();
|
||||||
webContents.send('install-log', {
|
webContents.send("install-log", {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: data.toString()
|
message: data.toString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听 stderr
|
// 监听 stderr
|
||||||
child.stderr.on('data', (data) => {
|
child.stderr.on("data", (data) => {
|
||||||
stderrData += data.toString();
|
stderrData += data.toString();
|
||||||
webContents.send('install-log', {
|
webContents.send("install-log", {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: data.toString()
|
message: data.toString(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
child.on('close', (code) => {
|
child.on("close", (code) => {
|
||||||
const success = code === 0;
|
const success = code === 0;
|
||||||
// 拼接json消息
|
// 拼接json消息
|
||||||
const messageJSONObj = {
|
const messageJSONObj = {
|
||||||
message: success ? '安装完成' : `安装失败,退出码 ${code}`,
|
message: success ? "安装完成" : `安装失败,退出码 ${code}`,
|
||||||
stdout: stdoutData,
|
stdout: stdoutData,
|
||||||
stderr: stderrData
|
stderr: stderrData,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -234,42 +244,45 @@ function processNextInQueue(index: number) {
|
|||||||
logger.error(messageJSONObj);
|
logger.error(messageJSONObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
webContents.send('install-complete', {
|
webContents.send("install-complete", {
|
||||||
id: task.id,
|
id: task.id,
|
||||||
success: success,
|
success: success,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
exitCode: code,
|
exitCode: code,
|
||||||
message: JSON.stringify(messageJSONObj)
|
message: JSON.stringify(messageJSONObj),
|
||||||
});
|
});
|
||||||
tasks.delete(task.id);
|
tasks.delete(task.id);
|
||||||
idle = true;
|
idle = true;
|
||||||
if (tasks.size > 0)
|
if (tasks.size > 0) processNextInQueue(0);
|
||||||
processNextInQueue(0);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.handle('check-installed', async (_event, pkgname: string) => {
|
ipcMain.handle("check-installed", async (_event, pkgname: string) => {
|
||||||
if (!pkgname) {
|
if (!pkgname) {
|
||||||
logger.warn('check-installed missing pkgname');
|
logger.warn("check-installed missing pkgname");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
let isInstalled = false;
|
let isInstalled = false;
|
||||||
|
|
||||||
logger.info(`检查应用是否已安装: ${pkgname}`);
|
logger.info(`检查应用是否已安装: ${pkgname}`);
|
||||||
|
|
||||||
const child = spawn(SHELL_CALLER_PATH, ['apm', 'list', '--installed', pkgname], {
|
const child = spawn(
|
||||||
shell: true,
|
SHELL_CALLER_PATH,
|
||||||
env: process.env
|
["apm", "list", "--installed", pkgname],
|
||||||
});
|
{
|
||||||
|
shell: true,
|
||||||
|
env: process.env,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let output = '';
|
let output = "";
|
||||||
|
|
||||||
child.stdout.on('data', (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
output += data.toString();
|
output += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
child.on('close', (code) => {
|
child.on("close", (code) => {
|
||||||
if (code === 0 && output.includes(pkgname)) {
|
if (code === 0 && output.includes(pkgname)) {
|
||||||
isInstalled = true;
|
isInstalled = true;
|
||||||
logger.info(`应用已安装: ${pkgname}`);
|
logger.info(`应用已安装: ${pkgname}`);
|
||||||
@@ -282,16 +295,16 @@ ipcMain.handle('check-installed', async (_event, pkgname: string) => {
|
|||||||
return isInstalled;
|
return isInstalled;
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
ipcMain.on("remove-installed", async (_event, pkgname: string) => {
|
||||||
const webContents = _event.sender;
|
const webContents = _event.sender;
|
||||||
if (!pkgname) {
|
if (!pkgname) {
|
||||||
logger.warn('remove-installed missing pkgname');
|
logger.warn("remove-installed missing pkgname");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`卸载已安装应用: ${pkgname}`);
|
logger.info(`卸载已安装应用: ${pkgname}`);
|
||||||
|
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
let execCommand = '';
|
let execCommand = "";
|
||||||
const execParams = [];
|
const execParams = [];
|
||||||
if (superUserCmd.length > 0) {
|
if (superUserCmd.length > 0) {
|
||||||
execCommand = superUserCmd;
|
execCommand = superUserCmd;
|
||||||
@@ -299,25 +312,29 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
|||||||
} else {
|
} else {
|
||||||
execCommand = SHELL_CALLER_PATH;
|
execCommand = SHELL_CALLER_PATH;
|
||||||
}
|
}
|
||||||
const child = spawn(execCommand, [...execParams, 'apm', 'remove', '-y', pkgname], {
|
const child = spawn(
|
||||||
shell: true,
|
execCommand,
|
||||||
env: process.env
|
[...execParams, "apm", "remove", "-y", pkgname],
|
||||||
});
|
{
|
||||||
let output = '';
|
shell: true,
|
||||||
|
env: process.env,
|
||||||
child.stdout.on('data', (data) => {
|
},
|
||||||
|
);
|
||||||
|
let output = "";
|
||||||
|
|
||||||
|
child.stdout.on("data", (data) => {
|
||||||
const chunk = data.toString();
|
const chunk = data.toString();
|
||||||
output += chunk;
|
output += chunk;
|
||||||
webContents.send('remove-progress', chunk);
|
webContents.send("remove-progress", chunk);
|
||||||
});
|
});
|
||||||
|
|
||||||
child.on('close', (code) => {
|
child.on("close", (code) => {
|
||||||
const success = code === 0;
|
const success = code === 0;
|
||||||
// 拼接json消息
|
// 拼接json消息
|
||||||
const messageJSONObj = {
|
const messageJSONObj = {
|
||||||
message: success ? '卸载完成' : `卸载失败,退出码 ${code}`,
|
message: success ? "卸载完成" : `卸载失败,退出码 ${code}`,
|
||||||
stdout: output,
|
stdout: output,
|
||||||
stderr: ''
|
stderr: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -326,26 +343,28 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
|||||||
logger.error(messageJSONObj);
|
logger.error(messageJSONObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
webContents.send('remove-complete', {
|
webContents.send("remove-complete", {
|
||||||
id: 0,
|
id: 0,
|
||||||
success: success,
|
success: success,
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
exitCode: code,
|
exitCode: code,
|
||||||
message: JSON.stringify(messageJSONObj)
|
message: JSON.stringify(messageJSONObj),
|
||||||
} satisfies ChannelPayload);
|
} satisfies ChannelPayload);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('list-upgradable', async () => {
|
ipcMain.handle("list-upgradable", async () => {
|
||||||
const { code, stdout, stderr } = await runCommandCapture(
|
const { code, stdout, stderr } = await runCommandCapture(SHELL_CALLER_PATH, [
|
||||||
SHELL_CALLER_PATH,
|
"apm",
|
||||||
['apm', 'list', '--upgradable']);
|
"list",
|
||||||
|
"--upgradable",
|
||||||
|
]);
|
||||||
if (code !== 0) {
|
if (code !== 0) {
|
||||||
logger.error(`list-upgradable failed: ${stderr || stdout}`);
|
logger.error(`list-upgradable failed: ${stderr || stdout}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: stderr || stdout || `list-upgradable failed with code ${code}`,
|
message: stderr || stdout || `list-upgradable failed with code ${code}`,
|
||||||
apps: []
|
apps: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,20 +372,25 @@ ipcMain.handle('list-upgradable', async () => {
|
|||||||
return { success: true, apps };
|
return { success: true, apps };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('list-installed', async () => {
|
ipcMain.handle("list-installed", async () => {
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
|
const execCommand =
|
||||||
const execParams = superUserCmd.length > 0
|
superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
|
||||||
? [SHELL_CALLER_PATH, 'apm', 'list', '--installed']
|
const execParams =
|
||||||
: ['apm', 'list', '--installed'];
|
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) {
|
if (code !== 0) {
|
||||||
logger.error(`list-installed failed: ${stderr || stdout}`);
|
logger.error(`list-installed failed: ${stderr || stdout}`);
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
message: stderr || stdout || `list-installed failed with code ${code}`,
|
message: stderr || stdout || `list-installed failed with code ${code}`,
|
||||||
apps: []
|
apps: [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,19 +398,24 @@ ipcMain.handle('list-installed', async () => {
|
|||||||
return { success: true, apps };
|
return { success: true, apps };
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => {
|
ipcMain.handle("uninstall-installed", async (_event, pkgname: string) => {
|
||||||
if (!pkgname) {
|
if (!pkgname) {
|
||||||
logger.warn('uninstall-installed missing pkgname');
|
logger.warn("uninstall-installed missing pkgname");
|
||||||
return { success: false, message: 'missing pkgname' };
|
return { success: false, message: "missing pkgname" };
|
||||||
}
|
}
|
||||||
|
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
const execCommand = superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
|
const execCommand =
|
||||||
const execParams = superUserCmd.length > 0
|
superUserCmd.length > 0 ? superUserCmd : SHELL_CALLER_PATH;
|
||||||
? [SHELL_CALLER_PATH, 'apm', 'remove', '-y', pkgname]
|
const execParams =
|
||||||
: ['apm', 'remove', '-y', pkgname];
|
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;
|
const success = code === 0;
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@@ -397,24 +426,28 @@ ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
success,
|
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) {
|
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 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, {
|
spawn(execCommand, execParams, {
|
||||||
shell: false,
|
shell: false,
|
||||||
env: process.env,
|
env: process.env,
|
||||||
detached: true,
|
detached: true,
|
||||||
stdio: 'ignore'
|
stdio: "ignore",
|
||||||
}).unref();
|
}).unref();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import pino from "pino";
|
import pino from "pino";
|
||||||
|
|
||||||
const logger = pino({ 'name': 'deeplink.ts' });
|
const logger = pino({ name: "deeplink.ts" });
|
||||||
type Query = Record<string, string>;
|
type Query = Record<string, string>;
|
||||||
export type Listener = (query: Query) => void;
|
export type Listener = (query: Query) => void;
|
||||||
|
|
||||||
@@ -52,13 +52,13 @@ export const deepLink = {
|
|||||||
on: (event: string, listener: Listener) => {
|
on: (event: string, listener: Listener) => {
|
||||||
const count = listeners.add(event, listener);
|
const count = listeners.add(event, listener);
|
||||||
logger.info(
|
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) => {
|
off: (event: string, listener: Listener) => {
|
||||||
const count = listeners.remove(event, listener);
|
const count = listeners.remove(event, listener);
|
||||||
logger.info(
|
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) => {
|
once: (event: string, listener: Listener) => {
|
||||||
@@ -72,7 +72,7 @@ export const deepLink = {
|
|||||||
|
|
||||||
export function handleCommandLine(commandLine: string[]) {
|
export function handleCommandLine(commandLine: string[]) {
|
||||||
const target = commandLine.find((arg) =>
|
const target = commandLine.find((arg) =>
|
||||||
protocols.some((protocol) => arg.startsWith(protocol + "://"))
|
protocols.some((protocol) => arg.startsWith(protocol + "://")),
|
||||||
);
|
);
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { BrowserWindow } from 'electron';
|
import { BrowserWindow } from "electron";
|
||||||
import { deepLink } from './deeplink';
|
import { deepLink } from "./deeplink";
|
||||||
import { isLoaded } from '../global';
|
import { isLoaded } from "../global";
|
||||||
import pino from 'pino';
|
import pino from "pino";
|
||||||
|
|
||||||
const logger = pino({ 'name': 'handle-url-scheme.ts' });
|
const logger = pino({ name: "handle-url-scheme.ts" });
|
||||||
|
|
||||||
const pendingActions: Array<() => void> = [];
|
const pendingActions: Array<() => void> = [];
|
||||||
|
|
||||||
@@ -24,22 +24,26 @@ new Promise<void>((resolve) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
deepLink.on("event", (query) => {
|
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) => {
|
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 action = () => {
|
||||||
const win = BrowserWindow.getAllWindows()[0];
|
const win = BrowserWindow.getAllWindows()[0];
|
||||||
if (!win) return;
|
if (!win) return;
|
||||||
|
|
||||||
if (query.cmd === 'update') {
|
if (query.cmd === "update") {
|
||||||
win.webContents.send('deep-link-update');
|
win.webContents.send("deep-link-update");
|
||||||
if (win.isMinimized()) win.restore();
|
if (win.isMinimized()) win.restore();
|
||||||
win.focus();
|
win.focus();
|
||||||
} else if (query.cmd === 'list') {
|
} else if (query.cmd === "list") {
|
||||||
win.webContents.send('deep-link-installed');
|
win.webContents.send("deep-link-installed");
|
||||||
if (win.isMinimized()) win.restore();
|
if (win.isMinimized()) win.restore();
|
||||||
win.focus();
|
win.focus();
|
||||||
}
|
}
|
||||||
@@ -55,14 +59,16 @@ deepLink.on("action", (query) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
deepLink.on("install", (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 action = () => {
|
||||||
const win = BrowserWindow.getAllWindows()[0];
|
const win = BrowserWindow.getAllWindows()[0];
|
||||||
if (!win) return;
|
if (!win) return;
|
||||||
|
|
||||||
if (query.pkg) {
|
if (query.pkg) {
|
||||||
win.webContents.send('deep-link-install', query.pkg);
|
win.webContents.send("deep-link-install", query.pkg);
|
||||||
if (win.isMinimized()) win.restore();
|
if (win.isMinimized()) win.restore();
|
||||||
win.focus();
|
win.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from 'electron'
|
import { app, BrowserWindow, ipcMain, Menu, shell, Tray } from "electron";
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from "node:url";
|
||||||
import path from 'node:path'
|
import path from "node:path";
|
||||||
import os from 'node:os'
|
import os from "node:os";
|
||||||
import fs from 'node:fs'
|
import fs from "node:fs";
|
||||||
import pino from 'pino'
|
import pino from "pino";
|
||||||
import { handleCommandLine } from './deeplink.js'
|
import { handleCommandLine } from "./deeplink.js";
|
||||||
import { isLoaded } from '../global.js'
|
import { isLoaded } from "../global.js";
|
||||||
import { tasks } from './backend/install-manager.js'
|
import { tasks } from "./backend/install-manager.js";
|
||||||
|
|
||||||
|
|
||||||
// Assure single instance application
|
// Assure single instance application
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
import './backend/install-manager.js'
|
import "./backend/install-manager.js";
|
||||||
import './handle-url-scheme.js'
|
import "./handle-url-scheme.js";
|
||||||
|
|
||||||
const logger = pino({ 'name': 'index.ts' });
|
const logger = pino({ name: "index.ts" });
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
// The built directory structure
|
// The built directory structure
|
||||||
//
|
//
|
||||||
@@ -30,38 +29,38 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|||||||
// ├─┬ dist
|
// ├─┬ dist
|
||||||
// │ └── index.html > Electron-Renderer
|
// │ └── 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 MAIN_DIST = path.join(process.env.APP_ROOT, "dist-electron");
|
||||||
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist')
|
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 VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
|
||||||
|
|
||||||
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
|
||||||
? path.join(process.env.APP_ROOT, 'public')
|
? path.join(process.env.APP_ROOT, "public")
|
||||||
: RENDERER_DIST
|
: RENDERER_DIST;
|
||||||
|
|
||||||
// Disable GPU Acceleration for Windows 7
|
// 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
|
// 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()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
app.quit()
|
app.quit();
|
||||||
process.exit(0)
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let win: BrowserWindow | null = null
|
let win: BrowserWindow | null = null;
|
||||||
const preload = path.join(__dirname, '../preload/index.mjs')
|
const preload = path.join(__dirname, "../preload/index.mjs");
|
||||||
const indexHtml = path.join(RENDERER_DIST, 'index.html')
|
const indexHtml = path.join(RENDERER_DIST, "index.html");
|
||||||
|
|
||||||
async function createWindow() {
|
async function createWindow() {
|
||||||
win = new BrowserWindow({
|
win = new BrowserWindow({
|
||||||
title: 'APM AppStore',
|
title: "APM AppStore",
|
||||||
width: 1366,
|
width: 1366,
|
||||||
height: 768,
|
height: 768,
|
||||||
autoHideMenuBar: true,
|
autoHideMenuBar: true,
|
||||||
icon: path.join(process.env.VITE_PUBLIC, 'favicon.ico'),
|
icon: path.join(process.env.VITE_PUBLIC, "favicon.ico"),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload,
|
preload,
|
||||||
// Warning: Enable nodeIntegration and disable contextIsolation is not secure in production
|
// 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
|
// Read more on https://www.electronjs.org/docs/latest/tutorial/context-isolation
|
||||||
// contextIsolation: false,
|
// contextIsolation: false,
|
||||||
},
|
},
|
||||||
})
|
});
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) { // #298
|
if (VITE_DEV_SERVER_URL) {
|
||||||
win.loadURL(VITE_DEV_SERVER_URL)
|
// #298
|
||||||
|
win.loadURL(VITE_DEV_SERVER_URL);
|
||||||
// Open devTool if the app is not packaged
|
// Open devTool if the app is not packaged
|
||||||
win.webContents.openDevTools({mode:'detach'})
|
win.webContents.openDevTools({ mode: "detach" });
|
||||||
} else {
|
} else {
|
||||||
win.loadFile(indexHtml)
|
win.loadFile(indexHtml);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test actively push message to the Electron-Renderer
|
// Test actively push message to the Electron-Renderer
|
||||||
win.webContents.on('did-finish-load', () => {
|
win.webContents.on("did-finish-load", () => {
|
||||||
win?.webContents.send('main-process-message', new Date().toLocaleString())
|
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||||
logger.info('Renderer process is ready.');
|
logger.info("Renderer process is ready.");
|
||||||
})
|
});
|
||||||
|
|
||||||
// Make all links open with the browser, not with the application
|
// Make all links open with the browser, not with the application
|
||||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||||
if (url.startsWith('https:')) shell.openExternal(url)
|
if (url.startsWith("https:")) shell.openExternal(url);
|
||||||
return { action: 'deny' }
|
return { action: "deny" };
|
||||||
})
|
});
|
||||||
// win.webContents.on('will-navigate', (event, url) => { }) #344
|
// win.webContents.on('will-navigate', (event, url) => { }) #344
|
||||||
|
|
||||||
win.on('close', (event) => {
|
win.on("close", (event) => {
|
||||||
// 截获 close 默认行为
|
// 截获 close 默认行为
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
// 点击关闭时触发close事件,我们按照之前的思路在关闭时,隐藏窗口,隐藏任务栏窗口
|
// 点击关闭时触发close事件,我们按照之前的思路在关闭时,隐藏窗口,隐藏任务栏窗口
|
||||||
@@ -103,50 +103,52 @@ async function createWindow() {
|
|||||||
win.setSkipTaskbar(true);
|
win.setSkipTaskbar(true);
|
||||||
} else {
|
} else {
|
||||||
// 如果没有下载任务,才允许关闭窗口
|
// 如果没有下载任务,才允许关闭窗口
|
||||||
win.destroy()
|
win.destroy();
|
||||||
}
|
}
|
||||||
|
});
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ipcMain.on('renderer-ready', (event, args) => {
|
ipcMain.on("renderer-ready", (event, args) => {
|
||||||
logger.info('Received renderer-ready event with args: ' + JSON.stringify(args));
|
logger.info(
|
||||||
|
"Received renderer-ready event with args: " + JSON.stringify(args),
|
||||||
|
);
|
||||||
isLoaded.value = args.status;
|
isLoaded.value = args.status;
|
||||||
logger.info(`isLoaded set to: ${isLoaded.value}`);
|
logger.info(`isLoaded set to: ${isLoaded.value}`);
|
||||||
})
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow()
|
createWindow();
|
||||||
handleCommandLine(process.argv)
|
handleCommandLine(process.argv);
|
||||||
})
|
});
|
||||||
|
|
||||||
app.on('window-all-closed', () => {
|
app.on("window-all-closed", () => {
|
||||||
win = null
|
win = null;
|
||||||
if (process.platform !== 'darwin') app.quit()
|
if (process.platform !== "darwin") app.quit();
|
||||||
})
|
});
|
||||||
|
|
||||||
app.on('second-instance', () => {
|
app.on("second-instance", () => {
|
||||||
if (win) {
|
if (win) {
|
||||||
// Focus on the main window if the user tried to open another
|
// Focus on the main window if the user tried to open another
|
||||||
if (win.isMinimized()) win.restore()
|
if (win.isMinimized()) win.restore();
|
||||||
win.focus()
|
win.focus();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
app.on('activate', () => {
|
app.on("activate", () => {
|
||||||
const allWindows = BrowserWindow.getAllWindows()
|
const allWindows = BrowserWindow.getAllWindows();
|
||||||
if (allWindows.length) {
|
if (allWindows.length) {
|
||||||
allWindows[0].focus()
|
allWindows[0].focus();
|
||||||
} else {
|
} else {
|
||||||
createWindow()
|
createWindow();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
// 设置托盘
|
// 设置托盘
|
||||||
// 获取图标路径
|
// 获取图标路径
|
||||||
function getIconPath() {
|
function getIconPath() {
|
||||||
let iconPath = "";
|
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) {
|
if (app.isPackaged) {
|
||||||
// 打包模式
|
// 打包模式
|
||||||
@@ -170,31 +172,35 @@ function getIconPath() {
|
|||||||
|
|
||||||
let tray = null;
|
let tray = null;
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
tray = new Tray(getIconPath());
|
tray = new Tray(getIconPath());
|
||||||
const contextMenu = Menu.buildFromTemplate([{
|
const contextMenu = Menu.buildFromTemplate([
|
||||||
label: '显示主界面',
|
{
|
||||||
click: () => { win.show() }
|
label: "显示主界面",
|
||||||
|
click: () => {
|
||||||
|
win.show();
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '退出程序',
|
label: "退出程序",
|
||||||
click: () => { win.destroy() }
|
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
|
// New window example arg: new windows url
|
||||||
// ipcMain.handle('open-win', (_, arg) => {
|
// ipcMain.handle('open-win', (_, arg) => {
|
||||||
|
|||||||
@@ -1,66 +1,70 @@
|
|||||||
import { ipcRenderer, contextBridge } from 'electron'
|
import { ipcRenderer, contextBridge } from "electron";
|
||||||
|
|
||||||
// --------- Expose some API to the Renderer process ---------
|
// --------- Expose some API to the Renderer process ---------
|
||||||
contextBridge.exposeInMainWorld('ipcRenderer', {
|
contextBridge.exposeInMainWorld("ipcRenderer", {
|
||||||
on(...args: Parameters<typeof ipcRenderer.on>) {
|
on(...args: Parameters<typeof ipcRenderer.on>) {
|
||||||
const [channel, listener] = args
|
const [channel, listener] = args;
|
||||||
return ipcRenderer.on(channel, (event, ...args) => listener(event, ...args))
|
return ipcRenderer.on(channel, (event, ...args) =>
|
||||||
|
listener(event, ...args),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
off(...args: Parameters<typeof ipcRenderer.off>) {
|
off(...args: Parameters<typeof ipcRenderer.off>) {
|
||||||
const [channel, ...omit] = args
|
const [channel, ...omit] = args;
|
||||||
return ipcRenderer.off(channel, ...omit)
|
return ipcRenderer.off(channel, ...omit);
|
||||||
},
|
},
|
||||||
send(...args: Parameters<typeof ipcRenderer.send>) {
|
send(...args: Parameters<typeof ipcRenderer.send>) {
|
||||||
const [channel, ...omit] = args
|
const [channel, ...omit] = args;
|
||||||
return ipcRenderer.send(channel, ...omit)
|
return ipcRenderer.send(channel, ...omit);
|
||||||
},
|
},
|
||||||
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
|
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
|
||||||
const [channel, ...omit] = args
|
const [channel, ...omit] = args;
|
||||||
return ipcRenderer.invoke(channel, ...omit)
|
return ipcRenderer.invoke(channel, ...omit);
|
||||||
},
|
},
|
||||||
|
|
||||||
// You can expose other APTs you need here.
|
// You can expose other APTs you need here.
|
||||||
// ...
|
// ...
|
||||||
})
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld('apm_store', {
|
contextBridge.exposeInMainWorld("apm_store", {
|
||||||
arch: (() => {
|
arch: (() => {
|
||||||
const arch = process.arch;
|
const arch = process.arch;
|
||||||
if (arch === 'x64') {
|
if (arch === "x64") {
|
||||||
return 'amd64' + '-apm';
|
return "amd64" + "-apm";
|
||||||
} else {
|
} else {
|
||||||
return arch + '-apm';
|
return arch + "-apm";
|
||||||
}
|
}
|
||||||
})()
|
})(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// --------- Preload scripts loading ---------
|
// --------- Preload scripts loading ---------
|
||||||
function domReady(condition: DocumentReadyState[] = ['complete', 'interactive']) {
|
function domReady(
|
||||||
|
condition: DocumentReadyState[] = ["complete", "interactive"],
|
||||||
|
) {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
if (condition.includes(document.readyState)) {
|
if (condition.includes(document.readyState)) {
|
||||||
resolve(true)
|
resolve(true);
|
||||||
} else {
|
} else {
|
||||||
document.addEventListener('readystatechange', () => {
|
document.addEventListener("readystatechange", () => {
|
||||||
if (condition.includes(document.readyState)) {
|
if (condition.includes(document.readyState)) {
|
||||||
resolve(true)
|
resolve(true);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const safeDOM = {
|
const safeDOM = {
|
||||||
append(parent: HTMLElement, child: HTMLElement) {
|
append(parent: HTMLElement, child: HTMLElement) {
|
||||||
if (!Array.from(parent.children).find(e => e === child)) {
|
if (!Array.from(parent.children).find((e) => e === child)) {
|
||||||
return parent.appendChild(child)
|
return parent.appendChild(child);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
remove(parent: HTMLElement, child: HTMLElement) {
|
remove(parent: HTMLElement, child: HTMLElement) {
|
||||||
if (Array.from(parent.children).find(e => e === child)) {
|
if (Array.from(parent.children).find((e) => e === child)) {
|
||||||
return parent.removeChild(child)
|
return parent.removeChild(child);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://tobiasahlin.com/spinkit
|
* https://tobiasahlin.com/spinkit
|
||||||
@@ -69,7 +73,7 @@ const safeDOM = {
|
|||||||
* https://matejkustec.github.io/SpinThatShit
|
* https://matejkustec.github.io/SpinThatShit
|
||||||
*/
|
*/
|
||||||
function useLoading() {
|
function useLoading() {
|
||||||
const className = `loaders-css__square-spin`
|
const className = `loaders-css__square-spin`;
|
||||||
const styleContent = `
|
const styleContent = `
|
||||||
@keyframes square-spin {
|
@keyframes square-spin {
|
||||||
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
|
25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
|
||||||
@@ -96,35 +100,34 @@ function useLoading() {
|
|||||||
background: #282c34;
|
background: #282c34;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
`
|
`;
|
||||||
const oStyle = document.createElement('style')
|
const oStyle = document.createElement("style");
|
||||||
const oDiv = document.createElement('div')
|
const oDiv = document.createElement("div");
|
||||||
|
|
||||||
oStyle.id = 'app-loading-style'
|
oStyle.id = "app-loading-style";
|
||||||
oStyle.innerHTML = styleContent
|
oStyle.innerHTML = styleContent;
|
||||||
oDiv.className = 'app-loading-wrap'
|
oDiv.className = "app-loading-wrap";
|
||||||
oDiv.innerHTML = `<div class="${className}"><div></div></div>`
|
oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
appendLoading() {
|
appendLoading() {
|
||||||
safeDOM.append(document.head, oStyle)
|
safeDOM.append(document.head, oStyle);
|
||||||
safeDOM.append(document.body, oDiv)
|
safeDOM.append(document.body, oDiv);
|
||||||
},
|
},
|
||||||
removeLoading() {
|
removeLoading() {
|
||||||
safeDOM.remove(document.head, oStyle)
|
safeDOM.remove(document.head, oStyle);
|
||||||
safeDOM.remove(document.body, oDiv)
|
safeDOM.remove(document.body, oDiv);
|
||||||
},
|
},
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// ----------------------------------------------------------------------
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
const { appendLoading, removeLoading } = useLoading()
|
const { appendLoading, removeLoading } = useLoading();
|
||||||
domReady().then(appendLoading)
|
domReady().then(appendLoading);
|
||||||
|
|
||||||
window.onmessage = (ev) => {
|
window.onmessage = (ev) => {
|
||||||
if (ev.data.payload === 'removeLoading')
|
if (ev.data.payload === "removeLoading") removeLoading();
|
||||||
removeLoading()
|
};
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(removeLoading, 4999)
|
setTimeout(removeLoading, 4999);
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
export interface InstalledAppInfo {
|
export interface InstalledAppInfo {
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
version: string;
|
version: string;
|
||||||
arch: string;
|
arch: string;
|
||||||
flags: string;
|
flags: string;
|
||||||
raw: string;
|
raw: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type ChannelPayload = {
|
export type ChannelPayload = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -27,7 +27,9 @@
|
|||||||
"build:rpm": "vue-tsc --noEmit && vite build --mode production && electron-builder --config electron-builder.yml --linux rpm",
|
"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",
|
"build:deb": "vue-tsc --noEmit && vite build --mode production && electron-builder --config electron-builder.yml --linux deb",
|
||||||
"preview": "vite preview --mode debug",
|
"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": {
|
"devDependencies": {
|
||||||
"@dotenvx/dotenvx": "^1.51.4",
|
"@dotenvx/dotenvx": "^1.51.4",
|
||||||
|
|||||||
462
src/App.vue
462
src/App.vue
@@ -1,67 +1,148 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="flex min-h-screen flex-col bg-slate-50 text-slate-900 transition-colors duration-300 dark:bg-slate-950 dark:text-slate-100 lg:flex-row">
|
class="flex min-h-screen flex-col bg-slate-50 text-slate-900 transition-colors duration-300 dark:bg-slate-950 dark:text-slate-100 lg:flex-row"
|
||||||
|
>
|
||||||
<aside
|
<aside
|
||||||
class="w-full border-b border-slate-200/70 bg-white/80 px-5 py-6 backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/70 lg:sticky lg:top-0 lg:flex lg:h-screen lg:w-72 lg:flex-col lg:border-b-0 lg:border-r">
|
class="w-full border-b border-slate-200/70 bg-white/80 px-5 py-6 backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/70 lg:sticky lg:top-0 lg:flex lg:h-screen lg:w-72 lg:flex-col lg:border-b-0 lg:border-r"
|
||||||
<AppSidebar :categories="categories" :active-category="activeCategory" :category-counts="categoryCounts"
|
>
|
||||||
:is-dark-theme="isDarkTheme" @toggle-theme="toggleTheme" @select-category="selectCategory" />
|
<AppSidebar
|
||||||
|
:categories="categories"
|
||||||
|
:active-category="activeCategory"
|
||||||
|
:category-counts="categoryCounts"
|
||||||
|
:is-dark-theme="isDarkTheme"
|
||||||
|
@toggle-theme="toggleTheme"
|
||||||
|
@select-category="selectCategory"
|
||||||
|
/>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="flex-1 px-4 py-6 lg:px-10">
|
<main class="flex-1 px-4 py-6 lg:px-10">
|
||||||
<AppHeader :search-query="searchQuery" :apps-count="filteredApps.length" @update-search="handleSearchInput"
|
<AppHeader
|
||||||
@update="handleUpdate" @list="handleList" />
|
:search-query="searchQuery"
|
||||||
<AppGrid :apps="filteredApps" :loading="loading" @open-detail="openDetail" />
|
:apps-count="filteredApps.length"
|
||||||
|
@update-search="handleSearchInput"
|
||||||
|
@update="handleUpdate"
|
||||||
|
@list="handleList"
|
||||||
|
/>
|
||||||
|
<AppGrid
|
||||||
|
:apps="filteredApps"
|
||||||
|
:loading="loading"
|
||||||
|
@open-detail="openDetail"
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AppDetailModal data-app-modal="detail" :show="showModal" :app="currentApp" :screenshots="screenshots"
|
<AppDetailModal
|
||||||
:isinstalled="currentAppIsInstalled" @close="closeDetail" @install="handleInstall" @remove="requestUninstallFromDetail"
|
data-app-modal="detail"
|
||||||
@open-preview="openScreenPreview" @open-app="openDownloadedApp" />
|
:show="showModal"
|
||||||
|
:app="currentApp"
|
||||||
|
:screenshots="screenshots"
|
||||||
|
:isinstalled="currentAppIsInstalled"
|
||||||
|
@close="closeDetail"
|
||||||
|
@install="handleInstall"
|
||||||
|
@remove="requestUninstallFromDetail"
|
||||||
|
@open-preview="openScreenPreview"
|
||||||
|
@open-app="openDownloadedApp"
|
||||||
|
/>
|
||||||
|
|
||||||
<ScreenPreview :show="showPreview" :screenshots="screenshots" :current-screen-index="currentScreenIndex"
|
<ScreenPreview
|
||||||
@close="closeScreenPreview" @prev="prevScreen" @next="nextScreen" />
|
:show="showPreview"
|
||||||
|
:screenshots="screenshots"
|
||||||
|
:current-screen-index="currentScreenIndex"
|
||||||
|
@close="closeScreenPreview"
|
||||||
|
@prev="prevScreen"
|
||||||
|
@next="nextScreen"
|
||||||
|
/>
|
||||||
|
|
||||||
<DownloadQueue :downloads="downloads" @pause="pauseDownload" @resume="resumeDownload"
|
<DownloadQueue
|
||||||
@cancel="cancelDownload" @retry="retryDownload" @clear-completed="clearCompletedDownloads"
|
:downloads="downloads"
|
||||||
@show-detail="showDownloadDetailModalFunc" />
|
@pause="pauseDownload"
|
||||||
|
@resume="resumeDownload"
|
||||||
|
@cancel="cancelDownload"
|
||||||
|
@retry="retryDownload"
|
||||||
|
@clear-completed="clearCompletedDownloads"
|
||||||
|
@show-detail="showDownloadDetailModalFunc"
|
||||||
|
/>
|
||||||
|
|
||||||
<DownloadDetail :show="showDownloadDetailModal" :download="currentDownload" @close="closeDownloadDetail"
|
<DownloadDetail
|
||||||
@pause="pauseDownload" @resume="resumeDownload" @cancel="cancelDownload" @retry="retryDownload"
|
:show="showDownloadDetailModal"
|
||||||
@open-app="openDownloadedApp" />
|
:download="currentDownload"
|
||||||
|
@close="closeDownloadDetail"
|
||||||
|
@pause="pauseDownload"
|
||||||
|
@resume="resumeDownload"
|
||||||
|
@cancel="cancelDownload"
|
||||||
|
@retry="retryDownload"
|
||||||
|
@open-app="openDownloadedApp"
|
||||||
|
/>
|
||||||
|
|
||||||
<InstalledAppsModal :show="showInstalledModal" :apps="installedApps" :loading="installedLoading"
|
<InstalledAppsModal
|
||||||
:error="installedError" @close="closeInstalledModal" @refresh="refreshInstalledApps"
|
:show="showInstalledModal"
|
||||||
@uninstall="uninstallInstalledApp" />
|
:apps="installedApps"
|
||||||
|
:loading="installedLoading"
|
||||||
|
:error="installedError"
|
||||||
|
@close="closeInstalledModal"
|
||||||
|
@refresh="refreshInstalledApps"
|
||||||
|
@uninstall="uninstallInstalledApp"
|
||||||
|
/>
|
||||||
|
|
||||||
<UpdateAppsModal :show="showUpdateModal" :apps="upgradableApps" :loading="updateLoading"
|
<UpdateAppsModal
|
||||||
:error="updateError" :has-selected="hasSelectedUpgrades" @close="closeUpdateModal"
|
:show="showUpdateModal"
|
||||||
@refresh="refreshUpgradableApps" @toggle-all="toggleAllUpgrades"
|
:apps="upgradableApps"
|
||||||
@upgrade-selected="upgradeSelectedApps" @upgrade-one="upgradeSingleApp" />
|
:loading="updateLoading"
|
||||||
|
:error="updateError"
|
||||||
|
:has-selected="hasSelectedUpgrades"
|
||||||
|
@close="closeUpdateModal"
|
||||||
|
@refresh="refreshUpgradableApps"
|
||||||
|
@toggle-all="toggleAllUpgrades"
|
||||||
|
@upgrade-selected="upgradeSelectedApps"
|
||||||
|
@upgrade-one="upgradeSingleApp"
|
||||||
|
/>
|
||||||
|
|
||||||
<UninstallConfirmModal :show="showUninstallModal" :app="uninstallTargetApp" @close="closeUninstallModal"
|
<UninstallConfirmModal
|
||||||
@success="onUninstallSuccess" />
|
:show="showUninstallModal"
|
||||||
|
:app="uninstallTargetApp"
|
||||||
|
@close="closeUninstallModal"
|
||||||
|
@success="onUninstallSuccess"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, watch, nextTick } from "vue";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import pino from 'pino';
|
import pino from "pino";
|
||||||
import AppSidebar from './components/AppSidebar.vue';
|
import AppSidebar from "./components/AppSidebar.vue";
|
||||||
import AppHeader from './components/AppHeader.vue';
|
import AppHeader from "./components/AppHeader.vue";
|
||||||
import AppGrid from './components/AppGrid.vue';
|
import AppGrid from "./components/AppGrid.vue";
|
||||||
import AppDetailModal from './components/AppDetailModal.vue';
|
import AppDetailModal from "./components/AppDetailModal.vue";
|
||||||
import ScreenPreview from './components/ScreenPreview.vue';
|
import ScreenPreview from "./components/ScreenPreview.vue";
|
||||||
import DownloadQueue from './components/DownloadQueue.vue';
|
import DownloadQueue from "./components/DownloadQueue.vue";
|
||||||
import DownloadDetail from './components/DownloadDetail.vue';
|
import DownloadDetail from "./components/DownloadDetail.vue";
|
||||||
import InstalledAppsModal from './components/InstalledAppsModal.vue';
|
import InstalledAppsModal from "./components/InstalledAppsModal.vue";
|
||||||
import UpdateAppsModal from './components/UpdateAppsModal.vue';
|
import UpdateAppsModal from "./components/UpdateAppsModal.vue";
|
||||||
import UninstallConfirmModal from './components/UninstallConfirmModal.vue';
|
import UninstallConfirmModal from "./components/UninstallConfirmModal.vue";
|
||||||
import { APM_STORE_BASE_URL, currentApp, currentAppIsInstalled } from './global/storeConfig';
|
import {
|
||||||
import { downloads, removeDownloadItem, watchDownloadsChange } from './global/downloadStatus';
|
APM_STORE_BASE_URL,
|
||||||
import { handleInstall, handleRetry, handleUpgrade } from './modeuls/processInstall';
|
currentApp,
|
||||||
import type { App, AppJson, DownloadItem, UpdateAppItem, ChannelPayload } from './global/typedefinition';
|
currentAppIsInstalled,
|
||||||
import type { Ref } from 'vue';
|
} from "./global/storeConfig";
|
||||||
import type { IpcRendererEvent } from 'electron';
|
import {
|
||||||
|
downloads,
|
||||||
|
removeDownloadItem,
|
||||||
|
watchDownloadsChange,
|
||||||
|
} from "./global/downloadStatus";
|
||||||
|
import {
|
||||||
|
handleInstall,
|
||||||
|
handleRetry,
|
||||||
|
handleUpgrade,
|
||||||
|
} from "./modeuls/processInstall";
|
||||||
|
import type {
|
||||||
|
App,
|
||||||
|
AppJson,
|
||||||
|
DownloadItem,
|
||||||
|
UpdateAppItem,
|
||||||
|
ChannelPayload,
|
||||||
|
} from "./global/typedefinition";
|
||||||
|
import type { Ref } from "vue";
|
||||||
|
import type { IpcRendererEvent } from "electron";
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
|
|
||||||
// Axios 全局配置
|
// Axios 全局配置
|
||||||
@@ -74,8 +155,8 @@ const axiosInstance = axios.create({
|
|||||||
const isDarkTheme = ref(false);
|
const isDarkTheme = ref(false);
|
||||||
const categories: Ref<Record<string, string>> = ref({});
|
const categories: Ref<Record<string, string>> = ref({});
|
||||||
const apps: Ref<App[]> = ref([]);
|
const apps: Ref<App[]> = ref([]);
|
||||||
const activeCategory = ref('all');
|
const activeCategory = ref("all");
|
||||||
const searchQuery = ref('');
|
const searchQuery = ref("");
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const showPreview = ref(false);
|
const showPreview = ref(false);
|
||||||
const currentScreenIndex = ref(0);
|
const currentScreenIndex = ref(0);
|
||||||
@@ -86,11 +167,13 @@ const currentDownload: Ref<DownloadItem | null> = ref(null);
|
|||||||
const showInstalledModal = ref(false);
|
const showInstalledModal = ref(false);
|
||||||
const installedApps = ref<App[]>([]);
|
const installedApps = ref<App[]>([]);
|
||||||
const installedLoading = ref(false);
|
const installedLoading = ref(false);
|
||||||
const installedError = ref('');
|
const installedError = ref("");
|
||||||
const showUpdateModal = ref(false);
|
const showUpdateModal = ref(false);
|
||||||
const upgradableApps = ref<(App & { selected: boolean; upgrading: boolean })[]>([]);
|
const upgradableApps = ref<(App & { selected: boolean; upgrading: boolean })[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
const updateLoading = ref(false);
|
const updateLoading = ref(false);
|
||||||
const updateError = ref('');
|
const updateError = ref("");
|
||||||
const showUninstallModal = ref(false);
|
const showUninstallModal = ref(false);
|
||||||
const uninstallTargetApp: Ref<App | null> = ref(null);
|
const uninstallTargetApp: Ref<App | null> = ref(null);
|
||||||
|
|
||||||
@@ -99,19 +182,21 @@ const filteredApps = computed(() => {
|
|||||||
let result = [...apps.value];
|
let result = [...apps.value];
|
||||||
|
|
||||||
// 按分类筛选
|
// 按分类筛选
|
||||||
if (activeCategory.value !== 'all') {
|
if (activeCategory.value !== "all") {
|
||||||
result = result.filter(app => app.category === activeCategory.value);
|
result = result.filter((app) => app.category === activeCategory.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按搜索关键词筛选
|
// 按搜索关键词筛选
|
||||||
if (searchQuery.value.trim()) {
|
if (searchQuery.value.trim()) {
|
||||||
const q = searchQuery.value.toLowerCase().trim();
|
const q = searchQuery.value.toLowerCase().trim();
|
||||||
result = result.filter(app => {
|
result = result.filter((app) => {
|
||||||
// 兼容可能为 undefined 的情况,虽然类型定义是 string
|
// 兼容可能为 undefined 的情况,虽然类型定义是 string
|
||||||
return ((app.name || '').toLowerCase().includes(q) ||
|
return (
|
||||||
(app.pkgname || '').toLowerCase().includes(q) ||
|
(app.name || "").toLowerCase().includes(q) ||
|
||||||
(app.tags || '').toLowerCase().includes(q) ||
|
(app.pkgname || "").toLowerCase().includes(q) ||
|
||||||
(app.more || '').toLowerCase().includes(q));
|
(app.tags || "").toLowerCase().includes(q) ||
|
||||||
|
(app.more || "").toLowerCase().includes(q)
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,7 +205,7 @@ const filteredApps = computed(() => {
|
|||||||
|
|
||||||
const categoryCounts = computed(() => {
|
const categoryCounts = computed(() => {
|
||||||
const counts: Record<string, number> = { all: apps.value.length };
|
const counts: Record<string, number> = { all: apps.value.length };
|
||||||
apps.value.forEach(app => {
|
apps.value.forEach((app) => {
|
||||||
if (!counts[app.category]) counts[app.category] = 0;
|
if (!counts[app.category]) counts[app.category] = 0;
|
||||||
counts[app.category]++;
|
counts[app.category]++;
|
||||||
});
|
});
|
||||||
@@ -128,17 +213,17 @@ const categoryCounts = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const hasSelectedUpgrades = computed(() => {
|
const hasSelectedUpgrades = computed(() => {
|
||||||
return upgradableApps.value.some(app => app.selected);
|
return upgradableApps.value.some((app) => app.selected);
|
||||||
});
|
});
|
||||||
|
|
||||||
// 方法
|
// 方法
|
||||||
const syncThemePreference = (enabled: boolean) => {
|
const syncThemePreference = (enabled: boolean) => {
|
||||||
document.documentElement.classList.toggle('dark', enabled);
|
document.documentElement.classList.toggle("dark", enabled);
|
||||||
};
|
};
|
||||||
|
|
||||||
const initTheme = () => {
|
const initTheme = () => {
|
||||||
const savedTheme = localStorage.getItem('theme');
|
const savedTheme = localStorage.getItem("theme");
|
||||||
isDarkTheme.value = savedTheme === 'dark';
|
isDarkTheme.value = savedTheme === "dark";
|
||||||
syncThemePreference(isDarkTheme.value);
|
syncThemePreference(isDarkTheme.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -162,15 +247,19 @@ const openDetail = (app: App) => {
|
|||||||
|
|
||||||
// 确保模态框显示后滚动到顶部
|
// 确保模态框显示后滚动到顶部
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const modal = document.querySelector('[data-app-modal="detail"] .modal-panel');
|
const modal = document.querySelector(
|
||||||
|
'[data-app-modal="detail"] .modal-panel',
|
||||||
|
);
|
||||||
if (modal) modal.scrollTop = 0;
|
if (modal) modal.scrollTop = 0;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkAppInstalled = (app: App) => {
|
const checkAppInstalled = (app: App) => {
|
||||||
window.ipcRenderer.invoke('check-installed', app.pkgname).then((isInstalled: boolean) => {
|
window.ipcRenderer
|
||||||
currentAppIsInstalled.value = isInstalled;
|
.invoke("check-installed", app.pkgname)
|
||||||
});
|
.then((isInstalled: boolean) => {
|
||||||
|
currentAppIsInstalled.value = isInstalled;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadScreenshots = (app: App) => {
|
const loadScreenshots = (app: App) => {
|
||||||
@@ -230,12 +319,12 @@ const closeUpdateModal = () => {
|
|||||||
|
|
||||||
const refreshUpgradableApps = async () => {
|
const refreshUpgradableApps = async () => {
|
||||||
updateLoading.value = true;
|
updateLoading.value = true;
|
||||||
updateError.value = '';
|
updateError.value = "";
|
||||||
try {
|
try {
|
||||||
const result = await window.ipcRenderer.invoke('list-upgradable');
|
const result = await window.ipcRenderer.invoke("list-upgradable");
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
upgradableApps.value = [];
|
upgradableApps.value = [];
|
||||||
updateError.value = result?.message || '检查更新失败';
|
updateError.value = result?.message || "检查更新失败";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -244,32 +333,34 @@ const refreshUpgradableApps = async () => {
|
|||||||
// Map properties if needed or assume main matches App interface except field names might differ
|
// Map properties if needed or assume main matches App interface except field names might differ
|
||||||
// For now assuming result.apps returns objects compatible with App for core fields,
|
// For now assuming result.apps returns objects compatible with App for core fields,
|
||||||
// but let's normalize just in case if main returns different structure.
|
// but let's normalize just in case if main returns different structure.
|
||||||
name: app.name || app.Name || '',
|
name: app.name || app.Name || "",
|
||||||
pkgname: app.pkgname || app.Pkgname || '',
|
pkgname: app.pkgname || app.Pkgname || "",
|
||||||
version: app.newVersion || app.version || '',
|
version: app.newVersion || app.version || "",
|
||||||
category: app.category || 'unknown',
|
category: app.category || "unknown",
|
||||||
selected: false,
|
selected: false,
|
||||||
upgrading: false
|
upgrading: false,
|
||||||
}));
|
}));
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
upgradableApps.value = [];
|
upgradableApps.value = [];
|
||||||
updateError.value = (error as Error)?.message || '检查更新失败';
|
updateError.value = (error as Error)?.message || "检查更新失败";
|
||||||
} finally {
|
} finally {
|
||||||
updateLoading.value = false;
|
updateLoading.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleAllUpgrades = () => {
|
const toggleAllUpgrades = () => {
|
||||||
const shouldSelectAll = !hasSelectedUpgrades.value || upgradableApps.value.some(app => !app.selected);
|
const shouldSelectAll =
|
||||||
upgradableApps.value = upgradableApps.value.map(app => ({
|
!hasSelectedUpgrades.value ||
|
||||||
|
upgradableApps.value.some((app) => !app.selected);
|
||||||
|
upgradableApps.value = upgradableApps.value.map((app) => ({
|
||||||
...app,
|
...app,
|
||||||
selected: shouldSelectAll ? true : false
|
selected: shouldSelectAll ? true : false,
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const upgradeSingleApp = (app: UpdateAppItem) => {
|
const upgradeSingleApp = (app: UpdateAppItem) => {
|
||||||
if (!app?.pkgname) return;
|
if (!app?.pkgname) return;
|
||||||
const target = apps.value.find(a => a.pkgname === app.pkgname);
|
const target = apps.value.find((a) => a.pkgname === app.pkgname);
|
||||||
if (target) {
|
if (target) {
|
||||||
handleUpgrade(target);
|
handleUpgrade(target);
|
||||||
} else {
|
} else {
|
||||||
@@ -278,28 +369,28 @@ const upgradeSingleApp = (app: UpdateAppItem) => {
|
|||||||
let minimalApp: App = {
|
let minimalApp: App = {
|
||||||
name: app.pkgname,
|
name: app.pkgname,
|
||||||
pkgname: app.pkgname,
|
pkgname: app.pkgname,
|
||||||
version: app.newVersion || '',
|
version: app.newVersion || "",
|
||||||
category: 'unknown',
|
category: "unknown",
|
||||||
tags: '',
|
tags: "",
|
||||||
more: '',
|
more: "",
|
||||||
filename: '',
|
filename: "",
|
||||||
torrent_address: '',
|
torrent_address: "",
|
||||||
author: '',
|
author: "",
|
||||||
contributor: '',
|
contributor: "",
|
||||||
website: '',
|
website: "",
|
||||||
update: '',
|
update: "",
|
||||||
size: '',
|
size: "",
|
||||||
img_urls: [],
|
img_urls: [],
|
||||||
icons: '',
|
icons: "",
|
||||||
currentStatus: 'installed'
|
currentStatus: "installed",
|
||||||
}
|
};
|
||||||
handleUpgrade(minimalApp);
|
handleUpgrade(minimalApp);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const upgradeSelectedApps = () => {
|
const upgradeSelectedApps = () => {
|
||||||
const selectedApps = upgradableApps.value.filter(app => app.selected);
|
const selectedApps = upgradableApps.value.filter((app) => app.selected);
|
||||||
selectedApps.forEach(app => {
|
selectedApps.forEach((app) => {
|
||||||
upgradeSingleApp(app);
|
upgradeSingleApp(app);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -315,50 +406,50 @@ const closeInstalledModal = () => {
|
|||||||
|
|
||||||
const refreshInstalledApps = async () => {
|
const refreshInstalledApps = async () => {
|
||||||
installedLoading.value = true;
|
installedLoading.value = true;
|
||||||
installedError.value = '';
|
installedError.value = "";
|
||||||
try {
|
try {
|
||||||
const result = await window.ipcRenderer.invoke('list-installed');
|
const result = await window.ipcRenderer.invoke("list-installed");
|
||||||
if (!result?.success) {
|
if (!result?.success) {
|
||||||
installedApps.value = [];
|
installedApps.value = [];
|
||||||
installedError.value = result?.message || '读取已安装应用失败';
|
installedError.value = result?.message || "读取已安装应用失败";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
installedApps.value = []
|
installedApps.value = [];
|
||||||
for (const app of result.apps) {
|
for (const app of result.apps) {
|
||||||
let appInfo = apps.value.find(a => a.pkgname === app.pkgname);
|
let appInfo = apps.value.find((a) => a.pkgname === app.pkgname);
|
||||||
if (appInfo) {
|
if (appInfo) {
|
||||||
appInfo.flags = app.flags;
|
appInfo.flags = app.flags;
|
||||||
appInfo.arch = app.arch;
|
appInfo.arch = app.arch;
|
||||||
appInfo.currentStatus = 'installed';
|
appInfo.currentStatus = "installed";
|
||||||
} else {
|
} else {
|
||||||
// 如果在当前应用列表中找不到该应用,创建一个最小的 App 对象
|
// 如果在当前应用列表中找不到该应用,创建一个最小的 App 对象
|
||||||
appInfo = {
|
appInfo = {
|
||||||
name: app.name || app.pkgname,
|
name: app.name || app.pkgname,
|
||||||
pkgname: app.pkgname,
|
pkgname: app.pkgname,
|
||||||
version: app.version,
|
version: app.version,
|
||||||
category: 'unknown',
|
category: "unknown",
|
||||||
tags: '',
|
tags: "",
|
||||||
more: '',
|
more: "",
|
||||||
filename: '',
|
filename: "",
|
||||||
torrent_address: '',
|
torrent_address: "",
|
||||||
author: '',
|
author: "",
|
||||||
contributor: '',
|
contributor: "",
|
||||||
website: '',
|
website: "",
|
||||||
update: '',
|
update: "",
|
||||||
size: '',
|
size: "",
|
||||||
img_urls: [],
|
img_urls: [],
|
||||||
icons: '',
|
icons: "",
|
||||||
currentStatus: 'installed',
|
currentStatus: "installed",
|
||||||
arch: app.arch,
|
arch: app.arch,
|
||||||
flags: app.flags
|
flags: app.flags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
installedApps.value.push(appInfo);
|
installedApps.value.push(appInfo);
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
installedApps.value = [];
|
installedApps.value = [];
|
||||||
installedError.value = (error as Error)?.message || '读取已安装应用失败';
|
installedError.value = (error as Error)?.message || "读取已安装应用失败";
|
||||||
} finally {
|
} finally {
|
||||||
installedLoading.value = false;
|
installedLoading.value = false;
|
||||||
}
|
}
|
||||||
@@ -366,7 +457,7 @@ const refreshInstalledApps = async () => {
|
|||||||
|
|
||||||
const requestUninstall = (app: App) => {
|
const requestUninstall = (app: App) => {
|
||||||
let target = null;
|
let target = null;
|
||||||
target = apps.value.find(a => a.pkgname === app.pkgname) || app;
|
target = apps.value.find((a) => a.pkgname === app.pkgname) || app;
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
uninstallTargetApp.value = target as App;
|
uninstallTargetApp.value = target as App;
|
||||||
@@ -399,10 +490,10 @@ const onUninstallSuccess = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const installCompleteCallback = () => {
|
const installCompleteCallback = () => {
|
||||||
if (currentApp.value) {
|
if (currentApp.value) {
|
||||||
checkAppInstalled(currentApp.value);
|
checkAppInstalled(currentApp.value);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
watchDownloadsChange(installCompleteCallback);
|
watchDownloadsChange(installCompleteCallback);
|
||||||
|
|
||||||
@@ -412,24 +503,25 @@ const uninstallInstalledApp = (app: App) => {
|
|||||||
|
|
||||||
// 目前 APM 商店不能暂停下载(因为 APM 本身不支持),但保留这些方法以备将来使用
|
// 目前 APM 商店不能暂停下载(因为 APM 本身不支持),但保留这些方法以备将来使用
|
||||||
const pauseDownload = (id: DownloadItem) => {
|
const pauseDownload = (id: DownloadItem) => {
|
||||||
const download = downloads.value.find(d => d.id === id.id);
|
const download = downloads.value.find((d) => d.id === id.id);
|
||||||
if (download && download.status === 'installing') { // 'installing' matches type definition, previously 'downloading'
|
if (download && download.status === "installing") {
|
||||||
download.status = 'paused';
|
// 'installing' matches type definition, previously 'downloading'
|
||||||
|
download.status = "paused";
|
||||||
download.logs.push({
|
download.logs.push({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: '下载已暂停'
|
message: "下载已暂停",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 同理
|
// 同理
|
||||||
const resumeDownload = (id: DownloadItem) => {
|
const resumeDownload = (id: DownloadItem) => {
|
||||||
const download = downloads.value.find(d => d.id === id.id);
|
const download = downloads.value.find((d) => d.id === id.id);
|
||||||
if (download && download.status === 'paused') {
|
if (download && download.status === "paused") {
|
||||||
download.status = 'installing'; // previously 'downloading'
|
download.status = "installing"; // previously 'downloading'
|
||||||
download.logs.push({
|
download.logs.push({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: '继续下载...'
|
message: "继续下载...",
|
||||||
});
|
});
|
||||||
// simulateDownload(download); // removed or undefined?
|
// simulateDownload(download); // removed or undefined?
|
||||||
}
|
}
|
||||||
@@ -437,14 +529,14 @@ const resumeDownload = (id: DownloadItem) => {
|
|||||||
|
|
||||||
// 同理
|
// 同理
|
||||||
const cancelDownload = (id: DownloadItem) => {
|
const cancelDownload = (id: DownloadItem) => {
|
||||||
const index = downloads.value.findIndex(d => d.id === id.id);
|
const index = downloads.value.findIndex((d) => d.id === id.id);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
const download = downloads.value[index];
|
const download = downloads.value[index];
|
||||||
// download.status = 'cancelled'; // 'cancelled' not in DownloadItem type union? Check type
|
// download.status = 'cancelled'; // 'cancelled' not in DownloadItem type union? Check type
|
||||||
download.status = 'failed'; // Use 'failed' or add 'cancelled' to type if needed. User asked to keep type simple.
|
download.status = "failed"; // Use 'failed' or add 'cancelled' to type if needed. User asked to keep type simple.
|
||||||
download.logs.push({
|
download.logs.push({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: '下载已取消'
|
message: "下载已取消",
|
||||||
});
|
});
|
||||||
// 延迟删除,让用户看到取消状态
|
// 延迟删除,让用户看到取消状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -455,21 +547,21 @@ const cancelDownload = (id: DownloadItem) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const retryDownload = (id: DownloadItem) => {
|
const retryDownload = (id: DownloadItem) => {
|
||||||
const download = downloads.value.find(d => d.id === id.id);
|
const download = downloads.value.find((d) => d.id === id.id);
|
||||||
if (download && download.status === 'failed') {
|
if (download && download.status === "failed") {
|
||||||
download.status = 'queued';
|
download.status = "queued";
|
||||||
download.progress = 0;
|
download.progress = 0;
|
||||||
download.downloadedSize = 0;
|
download.downloadedSize = 0;
|
||||||
download.logs.push({
|
download.logs.push({
|
||||||
time: Date.now(),
|
time: Date.now(),
|
||||||
message: '重新开始下载...'
|
message: "重新开始下载...",
|
||||||
});
|
});
|
||||||
handleRetry(download);
|
handleRetry(download);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCompletedDownloads = () => {
|
const clearCompletedDownloads = () => {
|
||||||
downloads.value = downloads.value.filter(d => d.status !== 'completed');
|
downloads.value = downloads.value.filter((d) => d.status !== "completed");
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDownloadDetailModalFunc = (download: DownloadItem) => {
|
const showDownloadDetailModalFunc = (download: DownloadItem) => {
|
||||||
@@ -487,12 +579,14 @@ const openDownloadedApp = (pkgname: string) => {
|
|||||||
// openApmStoreUrl(`apmstore://launch?pkg=${encodedPkg}`, {
|
// openApmStoreUrl(`apmstore://launch?pkg=${encodedPkg}`, {
|
||||||
// fallbackText: `打开应用: ${download.pkgname}`
|
// fallbackText: `打开应用: ${download.pkgname}`
|
||||||
// });
|
// });
|
||||||
window.ipcRenderer.invoke('launch-app', pkgname);
|
window.ipcRenderer.invoke("launch-app", pkgname);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(`/${window.apm_store.arch}/categories.json`);
|
const response = await axiosInstance.get(
|
||||||
|
`/${window.apm_store.arch}/categories.json`,
|
||||||
|
);
|
||||||
categories.value = response.data;
|
categories.value = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`读取 categories.json 失败: ${error}`);
|
logger.error(`读取 categories.json 失败: ${error}`);
|
||||||
@@ -502,10 +596,12 @@ const loadCategories = async () => {
|
|||||||
const loadApps = async () => {
|
const loadApps = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
logger.info('开始加载应用数据...');
|
logger.info("开始加载应用数据...");
|
||||||
const promises = Object.keys(categories.value).map(async category => {
|
const promises = Object.keys(categories.value).map(async (category) => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get<AppJson[]>(`/${window.apm_store.arch}/${category}/applist.json`);
|
const response = await axiosInstance.get<AppJson[]>(
|
||||||
|
`/${window.apm_store.arch}/${category}/applist.json`,
|
||||||
|
);
|
||||||
return response.status === 200 ? response.data : [];
|
return response.status === 200 ? response.data : [];
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
@@ -532,10 +628,13 @@ const loadApps = async () => {
|
|||||||
size: appJson.Size,
|
size: appJson.Size,
|
||||||
more: appJson.More,
|
more: appJson.More,
|
||||||
tags: appJson.Tags,
|
tags: appJson.Tags,
|
||||||
img_urls: typeof appJson.img_urls === 'string' ? JSON.parse(appJson.img_urls) : appJson.img_urls,
|
img_urls:
|
||||||
|
typeof appJson.img_urls === "string"
|
||||||
|
? JSON.parse(appJson.img_urls)
|
||||||
|
: appJson.img_urls,
|
||||||
icons: appJson.icons,
|
icons: appJson.icons,
|
||||||
category: category,
|
category: category,
|
||||||
currentStatus: 'not-installed',
|
currentStatus: "not-installed",
|
||||||
};
|
};
|
||||||
apps.value.push(normalizedApp);
|
apps.value.push(normalizedApp);
|
||||||
});
|
});
|
||||||
@@ -559,19 +658,19 @@ onMounted(async () => {
|
|||||||
await loadApps();
|
await loadApps();
|
||||||
|
|
||||||
// 设置键盘导航
|
// 设置键盘导航
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (showPreview.value) {
|
if (showPreview.value) {
|
||||||
if (e.key === 'Escape') closeScreenPreview();
|
if (e.key === "Escape") closeScreenPreview();
|
||||||
if (e.key === 'ArrowLeft') prevScreen();
|
if (e.key === "ArrowLeft") prevScreen();
|
||||||
if (e.key === 'ArrowRight') nextScreen();
|
if (e.key === "ArrowRight") nextScreen();
|
||||||
}
|
}
|
||||||
if (showModal.value && e.key === 'Escape') {
|
if (showModal.value && e.key === "Escape") {
|
||||||
closeDetail();
|
closeDetail();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deep link Handlers
|
// Deep link Handlers
|
||||||
window.ipcRenderer.on('deep-link-update', () => {
|
window.ipcRenderer.on("deep-link-update", () => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
const stop = watch(loading, (val) => {
|
const stop = watch(loading, (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
@@ -584,7 +683,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.ipcRenderer.on('deep-link-installed', () => {
|
window.ipcRenderer.on("deep-link-installed", () => {
|
||||||
if (loading.value) {
|
if (loading.value) {
|
||||||
const stop = watch(loading, (val) => {
|
const stop = watch(loading, (val) => {
|
||||||
if (!val) {
|
if (!val) {
|
||||||
@@ -597,43 +696,48 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.ipcRenderer.on('deep-link-install', (_event: IpcRendererEvent, pkgname: string) => {
|
window.ipcRenderer.on(
|
||||||
const tryOpen = () => {
|
"deep-link-install",
|
||||||
const target = apps.value.find(a => a.pkgname === pkgname);
|
(_event: IpcRendererEvent, pkgname: string) => {
|
||||||
if (target) {
|
const tryOpen = () => {
|
||||||
openDetail(target);
|
const target = apps.value.find((a) => a.pkgname === pkgname);
|
||||||
} else {
|
if (target) {
|
||||||
logger.warn(`Deep link: app ${pkgname} not found`);
|
openDetail(target);
|
||||||
}
|
} else {
|
||||||
};
|
logger.warn(`Deep link: app ${pkgname} not found`);
|
||||||
|
|
||||||
if (loading.value) {
|
|
||||||
const stop = watch(loading, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
tryOpen();
|
|
||||||
stop();
|
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
} else {
|
|
||||||
tryOpen();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.ipcRenderer.on('remove-complete', (_event: IpcRendererEvent, payload: ChannelPayload) => {
|
if (loading.value) {
|
||||||
const pkgname = currentApp.value?.pkgname
|
const stop = watch(loading, (val) => {
|
||||||
if(payload.success && pkgname){
|
if (!val) {
|
||||||
removeDownloadItem(pkgname);
|
tryOpen();
|
||||||
}
|
stop();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
tryOpen();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
window.ipcRenderer.on(
|
||||||
|
"remove-complete",
|
||||||
|
(_event: IpcRendererEvent, payload: ChannelPayload) => {
|
||||||
|
const pkgname = currentApp.value?.pkgname;
|
||||||
|
if (payload.success && pkgname) {
|
||||||
|
removeDownloadItem(pkgname);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
window.ipcRenderer.send('renderer-ready', { status: true });
|
window.ipcRenderer.send("renderer-ready", { status: true });
|
||||||
logger.info('Renderer process is ready!');
|
logger.info("Renderer process is ready!");
|
||||||
});
|
});
|
||||||
|
|
||||||
// 观察器
|
// 观察器
|
||||||
watch(isDarkTheme, (newVal) => {
|
watch(isDarkTheme, (newVal) => {
|
||||||
localStorage.setItem('theme', newVal ? 'dark' : 'light');
|
localStorage.setItem("theme", newVal ? "dark" : "light");
|
||||||
syncThemePreference(newVal);
|
syncThemePreference(newVal);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,47 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div @click="openDetail"
|
<div
|
||||||
class="group flex h-full cursor-pointer gap-4 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm transition hover:-translate-y-1 hover:border-brand/50 hover:shadow-lg dark:border-slate-800/60 dark:bg-slate-900/60">
|
@click="openDetail"
|
||||||
|
class="group flex h-full cursor-pointer gap-4 rounded-2xl border border-slate-200/70 bg-white/90 p-4 shadow-sm transition hover:-translate-y-1 hover:border-brand/50 hover:shadow-lg dark:border-slate-800/60 dark:bg-slate-900/60"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
class="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700">
|
class="flex h-16 w-16 items-center justify-center overflow-hidden rounded-2xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700"
|
||||||
<img ref="iconImg" :src="loadedIcon" alt="icon"
|
>
|
||||||
:class="['h-full w-full object-cover transition-opacity duration-300', isLoaded ? 'opacity-100' : 'opacity-0']" />
|
<img
|
||||||
|
ref="iconImg"
|
||||||
|
:src="loadedIcon"
|
||||||
|
alt="icon"
|
||||||
|
:class="[
|
||||||
|
'h-full w-full object-cover transition-opacity duration-300',
|
||||||
|
isLoaded ? 'opacity-100' : 'opacity-0',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-1 flex-col gap-1 overflow-hidden">
|
<div class="flex flex-1 flex-col gap-1 overflow-hidden">
|
||||||
<div class="truncate text-base font-semibold text-slate-900 dark:text-white">{{ app.name || '' }}</div>
|
<div
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">{{ app.pkgname || '' }} · {{ app.version || '' }}</div>
|
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">{{ description }}</div>
|
>
|
||||||
|
{{ app.name || "" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
||||||
|
</div>
|
||||||
|
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{{ description }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, onMounted, onBeforeUnmount, ref, watch } from "vue";
|
||||||
import { APM_STORE_BASE_URL } from '../global/storeConfig';
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
import type { App } from '../global/typedefinition';
|
import type { App } from "../global/typedefinition";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
app: App
|
app: App;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'open-detail', app: App): void
|
(e: "open-detail", app: App): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const iconImg = ref<HTMLImageElement | null>(null);
|
const iconImg = ref<HTMLImageElement | null>(null);
|
||||||
const isLoaded = ref(false);
|
const isLoaded = ref(false);
|
||||||
const loadedIcon = ref('data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E');
|
const loadedIcon = ref(
|
||||||
|
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E',
|
||||||
|
);
|
||||||
|
|
||||||
const iconPath = computed(() => {
|
const iconPath = computed(() => {
|
||||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const description = computed(() => {
|
const description = computed(() => {
|
||||||
const more = props.app.more || '';
|
const more = props.app.more || "";
|
||||||
return more.substring(0, 80) + (more.length > 80 ? '...' : '');
|
return more.substring(0, 80) + (more.length > 80 ? "..." : "");
|
||||||
});
|
});
|
||||||
|
|
||||||
const openDetail = () => {
|
const openDetail = () => {
|
||||||
emit('open-detail', props.app);
|
emit("open-detail", props.app);
|
||||||
};
|
};
|
||||||
|
|
||||||
let observer: IntersectionObserver | null = null;
|
let observer: IntersectionObserver | null = null;
|
||||||
@@ -57,24 +77,23 @@ onMounted(() => {
|
|||||||
img.onload = () => {
|
img.onload = () => {
|
||||||
loadedIcon.value = iconPath.value;
|
loadedIcon.value = iconPath.value;
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
if (observer)
|
if (observer) observer.unobserve(entry.target);
|
||||||
observer.unobserve(entry.target);
|
|
||||||
};
|
};
|
||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
// 加载失败时使用默认图标
|
// 加载失败时使用默认图标
|
||||||
loadedIcon.value = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23e0e0e0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3ENo Icon%3C/text%3E%3C/svg%3E';
|
loadedIcon.value =
|
||||||
|
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23e0e0e0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3ENo Icon%3C/text%3E%3C/svg%3E';
|
||||||
isLoaded.value = true;
|
isLoaded.value = true;
|
||||||
if (observer)
|
if (observer) observer.unobserve(entry.target);
|
||||||
observer.unobserve(entry.target);
|
|
||||||
};
|
};
|
||||||
img.src = iconPath.value;
|
img.src = iconPath.value;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
rootMargin: '50px', // 提前50px开始加载
|
rootMargin: "50px", // 提前50px开始加载
|
||||||
threshold: 0.01
|
threshold: 0.01,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// 观察图标元素
|
// 观察图标元素
|
||||||
@@ -85,7 +104,8 @@ onMounted(() => {
|
|||||||
|
|
||||||
// 当 app 变更时重置懒加载状态并重新观察
|
// 当 app 变更时重置懒加载状态并重新观察
|
||||||
watch(iconPath, () => {
|
watch(iconPath, () => {
|
||||||
loadedIcon.value = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E';
|
loadedIcon.value =
|
||||||
|
'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3C/svg%3E';
|
||||||
isLoaded.value = false;
|
isLoaded.value = false;
|
||||||
if (observer && iconImg.value) {
|
if (observer && iconImg.value) {
|
||||||
observer.unobserve(iconImg.value);
|
observer.unobserve(iconImg.value);
|
||||||
|
|||||||
@@ -1,107 +1,197 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 scale-95"
|
<Transition
|
||||||
enter-to-class="opacity-100 scale-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
enter-from-class="opacity-0 scale-95"
|
||||||
<div v-if="show" v-bind="attrs"
|
enter-to-class="opacity-100 scale-100"
|
||||||
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/70 p-4"
|
leave-active-class="duration-150 ease-in"
|
||||||
@click.self="closeModal">
|
leave-from-class="opacity-100 scale-100"
|
||||||
<div class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
leave-to-class="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
v-bind="attrs"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center overflow-hidden bg-slate-900/70 p-4"
|
||||||
|
@click.self="closeModal"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="modal-panel relative w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
>
|
||||||
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
<div class="flex flex-col gap-4 lg:flex-row lg:items-center">
|
||||||
<div class="flex flex-1 items-center gap-4">
|
<div class="flex flex-1 items-center gap-4">
|
||||||
<div
|
<div
|
||||||
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700">
|
class="flex h-20 w-20 items-center justify-center overflow-hidden rounded-3xl bg-gradient-to-b from-slate-100 to-slate-200 shadow-inner dark:from-slate-800 dark:to-slate-700"
|
||||||
<img v-if="app" :src="iconPath" alt="icon" class="h-full w-full object-cover" />
|
>
|
||||||
|
<img
|
||||||
|
v-if="app"
|
||||||
|
:src="iconPath"
|
||||||
|
alt="icon"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<p class="text-2xl font-bold text-slate-900 dark:text-white">{{ app?.name || '' }}</p>
|
<p class="text-2xl font-bold text-slate-900 dark:text-white">
|
||||||
|
{{ app?.name || "" }}
|
||||||
|
</p>
|
||||||
<!-- Close button for mobile layout could be considered here if needed, but for now sticking to desktop layout logic mainly -->
|
<!-- Close button for mobile layout could be considered here if needed, but for now sticking to desktop layout logic mainly -->
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">{{ app?.pkgname || '' }} · {{ app?.version || '' }}</p>
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{{ app?.pkgname || "" }} · {{ app?.version || "" }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2 lg:ml-auto">
|
<div class="flex flex-wrap gap-2 lg:ml-auto">
|
||||||
<button v-if="!isinstalled" type="button"
|
<button
|
||||||
|
v-if="!isinstalled"
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40 transition hover:-translate-y-0.5"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40 transition hover:-translate-y-0.5"
|
||||||
:class="installFeedback ? 'from-emerald-500 to-emerald-600' : 'from-brand to-brand-dark'"
|
:class="
|
||||||
|
installFeedback
|
||||||
|
? 'from-emerald-500 to-emerald-600'
|
||||||
|
: 'from-brand to-brand-dark'
|
||||||
|
"
|
||||||
@click="handleInstall"
|
@click="handleInstall"
|
||||||
:disabled="installFeedback || isCompleted">
|
:disabled="installFeedback || isCompleted"
|
||||||
<i class="fas" :class="installFeedback ? 'fa-check' : 'fa-download'"></i>
|
>
|
||||||
|
<i
|
||||||
|
class="fas"
|
||||||
|
:class="installFeedback ? 'fa-check' : 'fa-download'"
|
||||||
|
></i>
|
||||||
<span>{{ installBtnText }}</span>
|
<span>{{ installBtnText }}</span>
|
||||||
</button>
|
</button>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5"
|
||||||
@click="emit('open-app', app?.pkgname || '')">
|
@click="emit('open-app', app?.pkgname || '')"
|
||||||
|
>
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
<span>打开</span>
|
<span>打开</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-rose-500 to-rose-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 disabled:opacity-40 transition hover:-translate-y-0.5"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-rose-500 to-rose-600 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 disabled:opacity-40 transition hover:-translate-y-0.5"
|
||||||
@click="handleRemove">
|
@click="handleRemove"
|
||||||
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
<span>卸载</span>
|
<span>卸载</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
||||||
@click="closeModal" aria-label="关闭">
|
@click="closeModal"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="mt-4 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3 text-sm text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300">
|
class="mt-4 rounded-2xl border border-slate-200/60 bg-slate-50/70 px-4 py-3 text-sm text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
|
||||||
|
>
|
||||||
首次安装 APM 后需要重启系统以在启动器中看到应用入口。可前往
|
首次安装 APM 后需要重启系统以在启动器中看到应用入口。可前往
|
||||||
<a href="https://gitee.com/amber-ce/amber-pm/releases" target="_blank"
|
<a
|
||||||
class="font-semibold text-brand hover:underline">APM Releases</a>
|
href="https://gitee.com/amber-ce/amber-pm/releases"
|
||||||
|
target="_blank"
|
||||||
|
class="font-semibold text-brand hover:underline"
|
||||||
|
>APM Releases</a
|
||||||
|
>
|
||||||
获取 APM。
|
获取 APM。
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="screenshots.length" class="mt-6 grid gap-3 sm:grid-cols-2">
|
<div v-if="screenshots.length" class="mt-6 grid gap-3 sm:grid-cols-2">
|
||||||
<img v-for="(screen, index) in screenshots" :key="index" :src="screen" alt="screenshot"
|
<img
|
||||||
|
v-for="(screen, index) in screenshots"
|
||||||
|
:key="index"
|
||||||
|
:src="screen"
|
||||||
|
alt="screenshot"
|
||||||
class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60"
|
class="h-40 w-full cursor-pointer rounded-2xl border border-slate-200/60 object-cover shadow-sm transition hover:-translate-y-1 hover:shadow-lg dark:border-slate-800/60"
|
||||||
@click="openPreview(index)" @error="hideImage" />
|
@click="openPreview(index)"
|
||||||
|
@error="hideImage"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
<div class="mt-6 grid gap-4 sm:grid-cols-2">
|
||||||
<div v-if="app?.author" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.author"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">作者</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">作者</p>
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.author }}</p>
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.author }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.contributor" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.contributor"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">贡献者</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">贡献者</p>
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.contributor }}</p>
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.contributor }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.size" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.size"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">大小</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">大小</p>
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.size }}</p>
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.size }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.update" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">更新时间</p>
|
v-if="app?.update"
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.update }}</p>
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
|
<p class="text-xs uppercase tracking-wide text-slate-400">
|
||||||
|
更新时间
|
||||||
|
</p>
|
||||||
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.update }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.website" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.website"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">网站</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">网站</p>
|
||||||
<a :href="app.website" target="_blank"
|
<a
|
||||||
class="text-sm font-medium text-brand hover:underline">{{ app.website }}</a>
|
:href="app.website"
|
||||||
|
target="_blank"
|
||||||
|
class="text-sm font-medium text-brand hover:underline"
|
||||||
|
>{{ app.website }}</a
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.version" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.version"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">版本</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">版本</p>
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.version }}</p>
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.version }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="app?.tags" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="app?.tags"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<p class="text-xs uppercase tracking-wide text-slate-400">标签</p>
|
<p class="text-xs uppercase tracking-wide text-slate-400">标签</p>
|
||||||
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">{{ app.tags }}</p>
|
<p class="text-sm font-medium text-slate-800 dark:text-slate-200">
|
||||||
|
{{ app.tags }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="app?.more && app.more.trim() !== ''" class="mt-6 space-y-3">
|
<div v-if="app?.more && app.more.trim() !== ''" class="mt-6 space-y-3">
|
||||||
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">应用详情</h3>
|
<h3 class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||||
|
应用详情
|
||||||
|
</h3>
|
||||||
<div
|
<div
|
||||||
class="max-h-60 space-y-2 overflow-y-auto rounded-2xl border border-slate-200/60 bg-slate-50/80 p-4 text-sm leading-relaxed text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
|
class="max-h-60 space-y-2 overflow-y-auto rounded-2xl border border-slate-200/60 bg-slate-50/80 p-4 text-sm leading-relaxed text-slate-600 dark:border-slate-800/60 dark:bg-slate-900/60 dark:text-slate-300"
|
||||||
v-html="app.more.replace(/\n/g, '<br>')"></div>
|
v-html="app.more.replace(/\n/g, '<br>')"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,10 +199,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed,useAttrs } from 'vue';
|
import { computed, useAttrs } from "vue";
|
||||||
import { useDownloadItemStatus,useInstallFeedback } from '../global/downloadStatus';
|
import {
|
||||||
import { APM_STORE_BASE_URL } from '../global/storeConfig';
|
useDownloadItemStatus,
|
||||||
import type { App } from '../global/typedefinition';
|
useInstallFeedback,
|
||||||
|
} from "../global/downloadStatus";
|
||||||
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
|
import type { App } from "../global/typedefinition";
|
||||||
|
|
||||||
const attrs = useAttrs();
|
const attrs = useAttrs();
|
||||||
|
|
||||||
@@ -124,46 +217,45 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'install'): void;
|
(e: "install"): void;
|
||||||
(e: 'remove'): void;
|
(e: "remove"): void;
|
||||||
(e: 'open-preview', index: number): void;
|
(e: "open-preview", index: number): void;
|
||||||
(e: 'open-app', pkgname: string ): void;
|
(e: "open-app", pkgname: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const appPkgname = computed(() => props.app?.pkgname);
|
const appPkgname = computed(() => props.app?.pkgname);
|
||||||
const { installFeedback } = useInstallFeedback(appPkgname);
|
const { installFeedback } = useInstallFeedback(appPkgname);
|
||||||
const { isCompleted } = useDownloadItemStatus(appPkgname);
|
const { isCompleted } = useDownloadItemStatus(appPkgname);
|
||||||
const installBtnText = computed(() => {
|
const installBtnText = computed(() => {
|
||||||
if (isCompleted.value) {
|
if (isCompleted.value) {
|
||||||
// TODO: 似乎有一个时间差,安装好了之后并不是立马就可以从已安装列表看见
|
// TODO: 似乎有一个时间差,安装好了之后并不是立马就可以从已安装列表看见
|
||||||
return "已安装";
|
return "已安装";
|
||||||
}
|
}
|
||||||
return installFeedback.value ? "已加入队列" : "安装";
|
return installFeedback.value ? "已加入队列" : "安装";
|
||||||
});
|
});
|
||||||
const iconPath = computed(() => {
|
const iconPath = computed(() => {
|
||||||
if (!props.app) return '';
|
if (!props.app) return "";
|
||||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
emit('close');
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInstall = () => {
|
const handleInstall = () => {
|
||||||
emit('install');
|
emit("install");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRemove = () => {
|
const handleRemove = () => {
|
||||||
emit('remove');
|
emit("remove");
|
||||||
}
|
};
|
||||||
|
|
||||||
const openPreview = (index: number) => {
|
const openPreview = (index: number) => {
|
||||||
emit('open-preview', index);
|
emit("open-preview", index);
|
||||||
};
|
};
|
||||||
|
|
||||||
const hideImage = (e: Event) => {
|
const hideImage = (e: Event) => {
|
||||||
(e.target as HTMLElement).style.display = 'none';
|
(e.target as HTMLElement).style.display = "none";
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,22 +1,42 @@
|
|||||||
<template>
|
<template>
|
||||||
<div v-if="!loading" class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div
|
||||||
<AppCard v-for="(app, index) in apps" :key="index" :app="app" @open-detail="$emit('open-detail', app)" />
|
v-if="!loading"
|
||||||
|
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||||
|
>
|
||||||
|
<AppCard
|
||||||
|
v-for="(app, index) in apps"
|
||||||
|
:key="index"
|
||||||
|
:app="app"
|
||||||
|
@open-detail="$emit('open-detail', app)"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
<div
|
||||||
<div v-for="n in 8" :key="n"
|
v-else
|
||||||
class="flex gap-4 rounded-2xl border border-slate-200/60 bg-white/80 p-4 shadow-sm dark:border-slate-800/60 dark:bg-slate-900/50">
|
class="mt-6 grid gap-5 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4"
|
||||||
<div class="h-16 w-16 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-800"></div>
|
>
|
||||||
|
<div
|
||||||
|
v-for="n in 8"
|
||||||
|
:key="n"
|
||||||
|
class="flex gap-4 rounded-2xl border border-slate-200/60 bg-white/80 p-4 shadow-sm dark:border-slate-800/60 dark:bg-slate-900/50"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 animate-pulse rounded-2xl bg-slate-200 dark:bg-slate-800"
|
||||||
|
></div>
|
||||||
<div class="flex flex-1 flex-col justify-center gap-2">
|
<div class="flex flex-1 flex-col justify-center gap-2">
|
||||||
<div class="h-4 w-2/3 animate-pulse rounded-full bg-slate-200 dark:bg-slate-800"></div>
|
<div
|
||||||
<div class="h-3 w-1/2 animate-pulse rounded-full bg-slate-200/80 dark:bg-slate-800/80"></div>
|
class="h-4 w-2/3 animate-pulse rounded-full bg-slate-200 dark:bg-slate-800"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="h-3 w-1/2 animate-pulse rounded-full bg-slate-200/80 dark:bg-slate-800/80"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import AppCard from './AppCard.vue';
|
import AppCard from "./AppCard.vue";
|
||||||
import type { App } from '../global/typedefinition';
|
import type { App } from "../global/typedefinition";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
apps: App[];
|
apps: App[];
|
||||||
@@ -24,6 +44,6 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'open-detail', app: App): void;
|
(e: "open-detail", app: App): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -5,10 +5,16 @@
|
|||||||
<div class="w-full flex-1">
|
<div class="w-full flex-1">
|
||||||
<label for="searchBox" class="sr-only">搜索应用</label>
|
<label for="searchBox" class="sr-only">搜索应用</label>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<i class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
<i
|
||||||
<input id="searchBox" v-model="localSearchQuery"
|
class="fas fa-search pointer-events-none absolute left-4 top-1/2 -translate-y-1/2 text-slate-400"
|
||||||
|
></i>
|
||||||
|
<input
|
||||||
|
id="searchBox"
|
||||||
|
v-model="localSearchQuery"
|
||||||
class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-4 text-sm text-slate-700 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/50 focus:ring-4 focus:ring-brand/10 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-200"
|
class="w-full rounded-2xl border border-slate-200/70 bg-white/80 py-3 pl-12 pr-4 text-sm text-slate-700 shadow-sm outline-none transition placeholder:text-slate-400 focus:border-brand/50 focus:ring-4 focus:ring-brand/10 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-200"
|
||||||
placeholder="搜索应用名 / 包名 / 标签" @input="debounceSearch" />
|
placeholder="搜索应用名 / 包名 / 标签"
|
||||||
|
@input="debounceSearch"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -19,8 +25,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from "vue";
|
||||||
import TopActions from './TopActions.vue';
|
import TopActions from "./TopActions.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
searchQuery: string;
|
searchQuery: string;
|
||||||
@@ -28,22 +34,25 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update-search', query: string): void;
|
(e: "update-search", query: string): void;
|
||||||
(e: 'update'): void;
|
(e: "update"): void;
|
||||||
(e: 'list'): void;
|
(e: "list"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const localSearchQuery = ref(props.searchQuery || '');
|
const localSearchQuery = ref(props.searchQuery || "");
|
||||||
const timeoutId = ref<ReturnType<typeof setTimeout> | null>(null);
|
const timeoutId = ref<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
const debounceSearch = () => {
|
const debounceSearch = () => {
|
||||||
if (timeoutId.value) clearTimeout(timeoutId.value);
|
if (timeoutId.value) clearTimeout(timeoutId.value);
|
||||||
timeoutId.value = setTimeout(() => {
|
timeoutId.value = setTimeout(() => {
|
||||||
emit('update-search', localSearchQuery.value);
|
emit("update-search", localSearchQuery.value);
|
||||||
}, 220);
|
}, 220);
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.searchQuery, (newVal) => {
|
watch(
|
||||||
localSearchQuery.value = newVal || '';
|
() => props.searchQuery,
|
||||||
});
|
(newVal) => {
|
||||||
|
localSearchQuery.value = newVal || "";
|
||||||
|
},
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,41 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex h-full flex-col gap-6">
|
<div class="flex h-full flex-col gap-6">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<img :src="amberLogo" alt="Amber PM" class="h-11 w-11 rounded-2xl bg-white/70 p-2 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-800" />
|
<img
|
||||||
|
:src="amberLogo"
|
||||||
|
alt="Amber PM"
|
||||||
|
class="h-11 w-11 rounded-2xl bg-white/70 p-2 shadow-sm ring-1 ring-slate-900/5 dark:bg-slate-800"
|
||||||
|
/>
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<span class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400">APM Store</span>
|
<span
|
||||||
<span class="text-lg font-semibold text-slate-900 dark:text-white">APM 客户端商店</span>
|
class="text-xs uppercase tracking-[0.3em] text-slate-500 dark:text-slate-400"
|
||||||
|
>APM Store</span
|
||||||
|
>
|
||||||
|
<span class="text-lg font-semibold text-slate-900 dark:text-white"
|
||||||
|
>APM 客户端商店</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeToggle :is-dark="isDarkTheme" @toggle="toggleTheme" />
|
<ThemeToggle :is-dark="isDarkTheme" @toggle="toggleTheme" />
|
||||||
|
|
||||||
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
|
<div class="flex-1 space-y-2 overflow-y-auto scrollbar-muted pr-2">
|
||||||
<button type="button" class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
<button
|
||||||
:class="activeCategory === 'all' ? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15' : ''"
|
type="button"
|
||||||
@click="selectCategory('all')">
|
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||||
|
:class="
|
||||||
|
activeCategory === 'all'
|
||||||
|
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="selectCategory('all')"
|
||||||
|
>
|
||||||
<span>全部应用</span>
|
<span>全部应用</span>
|
||||||
<span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">{{ categoryCounts.all || 0 }}</span>
|
<span
|
||||||
|
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||||
|
>{{ categoryCounts.all || 0 }}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-for="(category, key) in categories" :key="key" type="button"
|
<button
|
||||||
|
v-for="(category, key) in categories"
|
||||||
|
:key="key"
|
||||||
|
type="button"
|
||||||
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
class="flex w-full items-center gap-3 rounded-2xl border border-transparent px-4 py-3 text-left text-sm font-medium text-slate-600 transition hover:border-brand/30 hover:bg-brand/5 hover:text-brand focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||||
:class="activeCategory === key ? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15' : ''"
|
:class="
|
||||||
@click="selectCategory(key)">
|
activeCategory === key
|
||||||
|
? 'border-brand/40 bg-brand/10 text-brand dark:bg-brand/15'
|
||||||
|
: ''
|
||||||
|
"
|
||||||
|
@click="selectCategory(key)"
|
||||||
|
>
|
||||||
<span class="flex flex-col">
|
<span class="flex flex-col">
|
||||||
<span>
|
<span>
|
||||||
<div class="text-left">{{ category.zh }}</div>
|
<div class="text-left">{{ category.zh }}</div>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">{{ categoryCounts[key] || 0 }}</span>
|
<span
|
||||||
|
class="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||||
|
>{{ categoryCounts[key] || 0 }}</span
|
||||||
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ThemeToggle from './ThemeToggle.vue';
|
import ThemeToggle from "./ThemeToggle.vue";
|
||||||
import amberLogo from '../assets/imgs/amber-pm-logo.png';
|
import amberLogo from "../assets/imgs/amber-pm-logo.png";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -46,15 +76,15 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'toggle-theme'): void;
|
(e: "toggle-theme"): void;
|
||||||
(e: 'select-category', category: string): void;
|
(e: "select-category", category: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const toggleTheme = () => {
|
const toggleTheme = () => {
|
||||||
emit('toggle-theme');
|
emit("toggle-theme");
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectCategory = (category: string) => {
|
const selectCategory = (category: string) => {
|
||||||
emit('select-category', category);
|
emit("select-category", category);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,72 +1,123 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 scale-95"
|
<Transition
|
||||||
enter-to-class="opacity-100 scale-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
enter-from-class="opacity-0 scale-95"
|
||||||
<div v-if="show"
|
enter-to-class="opacity-100 scale-100"
|
||||||
|
leave-active-class="duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100 scale-100"
|
||||||
|
leave-to-class="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10"
|
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10"
|
||||||
@click="handleOverlayClick">
|
@click="handleOverlayClick"
|
||||||
<div class="scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
>
|
||||||
@click.stop>
|
<div
|
||||||
|
class="scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 scrollbar-track-transparent w-full max-w-2xl max-h-[85vh] overflow-y-auto rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
@click.stop
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">下载详情</p>
|
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">实时了解安装进度</p>
|
下载详情
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
实时了解安装进度
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/60 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/60 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
||||||
@click="close">
|
@click="close"
|
||||||
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="download" class="mt-6 space-y-6">
|
<div v-if="download" class="mt-6 space-y-6">
|
||||||
<div class="flex items-center gap-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
<div class="h-16 w-16 overflow-hidden rounded-2xl bg-slate-100 dark:bg-slate-800">
|
class="flex items-center gap-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
<img :src="download.icon" :alt="download.name" class="h-full w-full object-cover" />
|
>
|
||||||
|
<div
|
||||||
|
class="h-16 w-16 overflow-hidden rounded-2xl bg-slate-100 dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="download.icon"
|
||||||
|
:alt="download.name"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-lg font-semibold text-slate-900 dark:text-white">{{ download.name }}</p>
|
<p class="text-lg font-semibold text-slate-900 dark:text-white">
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">{{ download.pkgname }} · {{ download.version }}</p>
|
{{ download.name }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
{{ download.pkgname }} · {{ download.version }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
class="space-y-4 rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<span class="text-sm font-medium text-slate-500">状态</span>
|
<span class="text-sm font-medium text-slate-500">状态</span>
|
||||||
<span
|
<span
|
||||||
class="rounded-full px-3 py-1 text-xs font-semibold"
|
class="rounded-full px-3 py-1 text-xs font-semibold"
|
||||||
:class="{
|
:class="{
|
||||||
'bg-blue-100 text-blue-700': download.status === 'downloading',
|
'bg-blue-100 text-blue-700':
|
||||||
'bg-amber-100 text-amber-600': download.status === 'installing',
|
download.status === 'downloading',
|
||||||
'bg-emerald-100 text-emerald-700': download.status === 'completed',
|
'bg-amber-100 text-amber-600':
|
||||||
|
download.status === 'installing',
|
||||||
|
'bg-emerald-100 text-emerald-700':
|
||||||
|
download.status === 'completed',
|
||||||
'bg-rose-100 text-rose-600': download.status === 'failed',
|
'bg-rose-100 text-rose-600': download.status === 'failed',
|
||||||
'bg-slate-200 text-slate-600': download.status === 'paused'
|
'bg-slate-200 text-slate-600': download.status === 'paused',
|
||||||
}">
|
}"
|
||||||
|
>
|
||||||
{{ getStatusText(download.status) }}
|
{{ getStatusText(download.status) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="download.status === 'downloading'" class="space-y-3">
|
<div v-if="download.status === 'downloading'" class="space-y-3">
|
||||||
<div class="h-2 rounded-full bg-slate-100 dark:bg-slate-800">
|
<div class="h-2 rounded-full bg-slate-100 dark:bg-slate-800">
|
||||||
<div class="h-full rounded-full bg-brand" :style="{ width: download.progress + '%' }"></div>
|
<div
|
||||||
|
class="h-full rounded-full bg-brand"
|
||||||
|
:style="{ width: download.progress + '%' }"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap items-center justify-between text-sm text-slate-500 dark:text-slate-400">
|
<div
|
||||||
|
class="flex flex-wrap items-center justify-between text-sm text-slate-500 dark:text-slate-400"
|
||||||
|
>
|
||||||
<span>{{ download.progress }}%</span>
|
<span>{{ download.progress }}%</span>
|
||||||
<span v-if="download.downloadedSize && download.totalSize">
|
<span v-if="download.downloadedSize && download.totalSize">
|
||||||
{{ formatSize(download.downloadedSize) }} / {{ formatSize(download.totalSize) }}
|
{{ formatSize(download.downloadedSize) }} /
|
||||||
|
{{ formatSize(download.totalSize) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="download.speed" class="flex flex-wrap gap-3 text-sm text-slate-500 dark:text-slate-300">
|
<div
|
||||||
<span class="flex items-center gap-2"><i class="fas fa-tachometer-alt"></i>{{ formatSpeed(download.speed) }}</span>
|
v-if="download.speed"
|
||||||
<span v-if="download.timeRemaining">剩余 {{ formatTime(download.timeRemaining) }}</span>
|
class="flex flex-wrap gap-3 text-sm text-slate-500 dark:text-slate-300"
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-2"
|
||||||
|
><i class="fas fa-tachometer-alt"></i
|
||||||
|
>{{ formatSpeed(download.speed) }}</span
|
||||||
|
>
|
||||||
|
<span v-if="download.timeRemaining"
|
||||||
|
>剩余 {{ formatTime(download.timeRemaining) }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="rounded-2xl border border-slate-200/60 p-4 text-sm text-slate-600 dark:border-slate-800/60 dark:text-slate-300">
|
<div
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 text-sm text-slate-600 dark:border-slate-800/60 dark:text-slate-300"
|
||||||
|
>
|
||||||
<div class="flex justify-between py-1">
|
<div class="flex justify-between py-1">
|
||||||
<span class="text-slate-400">下载源</span>
|
<span class="text-slate-400">下载源</span>
|
||||||
<span class="font-medium text-slate-900 dark:text-white">{{ download.source || 'APM Store' }}</span>
|
<span class="font-medium text-slate-900 dark:text-white">{{
|
||||||
|
download.source || "APM Store"
|
||||||
|
}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="download.startTime" class="flex justify-between py-1">
|
<div v-if="download.startTime" class="flex justify-between py-1">
|
||||||
<span class="text-slate-400">开始时间</span>
|
<span class="text-slate-400">开始时间</span>
|
||||||
@@ -76,40 +127,64 @@
|
|||||||
<span class="text-slate-400">完成时间</span>
|
<span class="text-slate-400">完成时间</span>
|
||||||
<span>{{ formatDate(download.endTime) }}</span>
|
<span>{{ formatDate(download.endTime) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="download.error" class="flex justify-between py-1 text-rose-500">
|
<div
|
||||||
|
v-if="download.error"
|
||||||
|
class="flex justify-between py-1 text-rose-500"
|
||||||
|
>
|
||||||
<span>错误信息</span>
|
<span>错误信息</span>
|
||||||
<span class="text-right">{{ download.error }}</span>
|
<span class="text-right">{{ download.error }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="download.logs && download.logs.length" class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60">
|
<div
|
||||||
|
v-if="download.logs && download.logs.length"
|
||||||
|
class="rounded-2xl border border-slate-200/60 p-4 dark:border-slate-800/60"
|
||||||
|
>
|
||||||
<div class="mb-3 flex items-center justify-between">
|
<div class="mb-3 flex items-center justify-between">
|
||||||
<span class="font-semibold text-slate-800 dark:text-slate-100">下载日志</span>
|
<span class="font-semibold text-slate-800 dark:text-slate-100"
|
||||||
<button type="button"
|
>下载日志</span
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300"
|
class="inline-flex items-center gap-2 rounded-full border border-slate-200 px-3 py-1 text-xs font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:text-slate-300"
|
||||||
@click="copyLogs">
|
@click="copyLogs"
|
||||||
|
>
|
||||||
<i class="fas fa-copy"></i>
|
<i class="fas fa-copy"></i>
|
||||||
复制日志
|
复制日志
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="max-h-48 space-y-2 overflow-y-auto rounded-2xl bg-slate-50/80 p-3 font-mono text-xs text-slate-600 dark:bg-slate-900/60 dark:text-slate-300">
|
<div
|
||||||
<div v-for="(log, index) in download.logs" :key="index" class="flex gap-3">
|
class="max-h-48 space-y-2 overflow-y-auto rounded-2xl bg-slate-50/80 p-3 font-mono text-xs text-slate-600 dark:bg-slate-900/60 dark:text-slate-300"
|
||||||
<span class="text-slate-400">{{ formatLogTime(log.time) }}</span>
|
>
|
||||||
|
<div
|
||||||
|
v-for="(log, index) in download.logs"
|
||||||
|
:key="index"
|
||||||
|
class="flex gap-3"
|
||||||
|
>
|
||||||
|
<span class="text-slate-400">{{
|
||||||
|
formatLogTime(log.time)
|
||||||
|
}}</span>
|
||||||
<span>{{ log.message }}</span>
|
<span>{{ log.message }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap justify-end gap-3">
|
<div class="flex flex-wrap justify-end gap-3">
|
||||||
<button v-if="download.status === 'failed'" type="button"
|
<button
|
||||||
|
v-if="download.status === 'failed'"
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-500 transition hover:bg-rose-50"
|
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-500 transition hover:bg-rose-50"
|
||||||
@click="retry">
|
@click="retry"
|
||||||
|
>
|
||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
重试下载
|
重试下载
|
||||||
</button>
|
</button>
|
||||||
<button v-if="download.status === 'completed'" type="button"
|
<button
|
||||||
|
v-if="download.status === 'completed'"
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg"
|
||||||
@click="openApp">
|
@click="openApp"
|
||||||
|
>
|
||||||
<i class="fas fa-external-link-alt"></i>
|
<i class="fas fa-external-link-alt"></i>
|
||||||
打开应用
|
打开应用
|
||||||
</button>
|
</button>
|
||||||
@@ -121,7 +196,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { DownloadItem } from '../global/typedefinition';
|
import type { DownloadItem } from "../global/typedefinition";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -129,17 +204,16 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'pause', download: DownloadItem): void;
|
(e: "pause", download: DownloadItem): void;
|
||||||
(e: 'resume', download: DownloadItem): void;
|
(e: "resume", download: DownloadItem): void;
|
||||||
(e: 'cancel', download: DownloadItem): void;
|
(e: "cancel", download: DownloadItem): void;
|
||||||
(e: 'retry', download: DownloadItem): void;
|
(e: "retry", download: DownloadItem): void;
|
||||||
(e: 'open-app', download: string): void;
|
(e: "open-app", download: string): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const close = () => {
|
const close = () => {
|
||||||
emit('close');
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleOverlayClick = () => {
|
const handleOverlayClick = () => {
|
||||||
@@ -148,38 +222,38 @@ const handleOverlayClick = () => {
|
|||||||
|
|
||||||
const retry = () => {
|
const retry = () => {
|
||||||
if (props.download) {
|
if (props.download) {
|
||||||
emit('retry', props.download);
|
emit("retry", props.download);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const openApp = () => {
|
const openApp = () => {
|
||||||
if (props.download) {
|
if (props.download) {
|
||||||
emit('open-app', props.download.pkgname);
|
emit("open-app", props.download.pkgname);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStatusText = (status: string) => {
|
const getStatusText = (status: string) => {
|
||||||
const statusMap: Record<string, string> = {
|
const statusMap: Record<string, string> = {
|
||||||
'pending': '等待中',
|
pending: "等待中",
|
||||||
'downloading': '下载中',
|
downloading: "下载中",
|
||||||
'installing': '安装中',
|
installing: "安装中",
|
||||||
'completed': '已完成',
|
completed: "已完成",
|
||||||
'failed': '失败',
|
failed: "失败",
|
||||||
'paused': '已暂停',
|
paused: "已暂停",
|
||||||
'cancelled': '已取消'
|
cancelled: "已取消",
|
||||||
};
|
};
|
||||||
return statusMap[status] || status;
|
return statusMap[status] || status;
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatSize = (bytes: number) => {
|
const formatSize = (bytes: number) => {
|
||||||
if (!bytes) return '0 B';
|
if (!bytes) return "0 B";
|
||||||
const units = ['B', 'KB', 'MB', 'GB'];
|
const units = ["B", "KB", "MB", "GB"];
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
return (bytes / Math.pow(1024, i)).toFixed(2) + ' ' + units[i];
|
return (bytes / Math.pow(1024, i)).toFixed(2) + " " + units[i];
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatSpeed = (bytesPerSecond: number) => {
|
const formatSpeed = (bytesPerSecond: number) => {
|
||||||
return formatSize(bytesPerSecond) + '/s';
|
return formatSize(bytesPerSecond) + "/s";
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
@@ -190,23 +264,26 @@ const formatTime = (seconds: number) => {
|
|||||||
|
|
||||||
const formatDate = (timestamp: number) => {
|
const formatDate = (timestamp: number) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleString('zh-CN');
|
return date.toLocaleString("zh-CN");
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatLogTime = (timestamp: number) => {
|
const formatLogTime = (timestamp: number) => {
|
||||||
const date = new Date(timestamp);
|
const date = new Date(timestamp);
|
||||||
return date.toLocaleTimeString('zh-CN');
|
return date.toLocaleTimeString("zh-CN");
|
||||||
};
|
};
|
||||||
|
|
||||||
const copyLogs = () => {
|
const copyLogs = () => {
|
||||||
if (!props.download?.logs) return;
|
if (!props.download?.logs) return;
|
||||||
const logsText = props.download.logs
|
const logsText = props.download.logs
|
||||||
.map(log => `[${formatLogTime(log.time)}] ${log.message}`)
|
.map((log) => `[${formatLogTime(log.time)}] ${log.message}`)
|
||||||
.join('\n');
|
.join("\n");
|
||||||
navigator.clipboard?.writeText(logsText).then(() => {
|
navigator.clipboard
|
||||||
alert('日志已复制到剪贴板');
|
?.writeText(logsText)
|
||||||
}).catch(() => {
|
.then(() => {
|
||||||
prompt('请手动复制日志:', logsText);
|
alert("日志已复制到剪贴板");
|
||||||
});
|
})
|
||||||
|
.catch(() => {
|
||||||
|
prompt("请手动复制日志:", logsText);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,65 +1,121 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white/95 shadow-2xl backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/90 sm:left-auto sm:right-6 sm:w-96">
|
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white/95 shadow-2xl backdrop-blur dark:border-slate-800/70 dark:bg-slate-900/90 sm:left-auto sm:right-6 sm:w-96"
|
||||||
<div class="flex items-center justify-between px-5 py-4" @click="toggleExpand">
|
>
|
||||||
<div class="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-200">
|
<div
|
||||||
|
class="flex items-center justify-between px-5 py-4"
|
||||||
|
@click="toggleExpand"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-2 text-sm font-semibold text-slate-700 dark:text-slate-200"
|
||||||
|
>
|
||||||
<i class="fas fa-download text-brand"></i>
|
<i class="fas fa-download text-brand"></i>
|
||||||
<span>下载队列</span>
|
<span>下载队列</span>
|
||||||
<span v-if="downloads.length"
|
<span
|
||||||
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300">
|
v-if="downloads.length"
|
||||||
|
class="rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500 dark:bg-slate-800/70 dark:text-slate-300"
|
||||||
|
>
|
||||||
({{ activeDownloads }}/{{ downloads.length }})
|
({{ activeDownloads }}/{{ downloads.length }})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button v-if="downloads.length" type="button"
|
<button
|
||||||
|
v-if="downloads.length"
|
||||||
|
type="button"
|
||||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
||||||
title="清除已完成" @click.stop="clearCompleted">
|
title="清除已完成"
|
||||||
|
@click.stop="clearCompleted"
|
||||||
|
>
|
||||||
<i class="fas fa-broom"></i>
|
<i class="fas fa-broom"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700 dark:text-slate-400"
|
||||||
@click.stop="toggleExpand">
|
@click.stop="toggleExpand"
|
||||||
<i class="fas" :class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"></i>
|
>
|
||||||
|
<i
|
||||||
|
class="fas"
|
||||||
|
:class="isExpanded ? 'fa-chevron-down' : 'fa-chevron-up'"
|
||||||
|
></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 -translate-y-2"
|
<Transition
|
||||||
enter-to-class="opacity-100 translate-y-0" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 translate-y-0" leave-to-class="opacity-0 -translate-y-2">
|
enter-from-class="opacity-0 -translate-y-2"
|
||||||
|
enter-to-class="opacity-100 translate-y-0"
|
||||||
|
leave-active-class="duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100 translate-y-0"
|
||||||
|
leave-to-class="opacity-0 -translate-y-2"
|
||||||
|
>
|
||||||
<div v-show="isExpanded" class="max-h-96 overflow-y-auto px-3 pb-4">
|
<div v-show="isExpanded" class="max-h-96 overflow-y-auto px-3 pb-4">
|
||||||
<div v-if="downloads.length === 0"
|
<div
|
||||||
class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200/80 px-4 py-12 text-slate-500 dark:border-slate-800/80 dark:text-slate-400">
|
v-if="downloads.length === 0"
|
||||||
|
class="flex flex-col items-center justify-center rounded-2xl border border-dashed border-slate-200/80 px-4 py-12 text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||||
|
>
|
||||||
<i class="fas fa-inbox text-3xl"></i>
|
<i class="fas fa-inbox text-3xl"></i>
|
||||||
<p class="mt-3 text-sm">暂无下载任务</p>
|
<p class="mt-3 text-sm">暂无下载任务</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="space-y-2">
|
<div v-else class="space-y-2">
|
||||||
<div v-for="download in downloads" :key="download.id"
|
<div
|
||||||
|
v-for="download in downloads"
|
||||||
|
:key="download.id"
|
||||||
class="flex cursor-pointer items-center gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-3 shadow-sm transition hover:border-brand/40 hover:shadow-lg dark:border-slate-800/70 dark:bg-slate-900"
|
class="flex cursor-pointer items-center gap-3 rounded-2xl border border-slate-200/70 bg-white/90 p-3 shadow-sm transition hover:border-brand/40 hover:shadow-lg dark:border-slate-800/70 dark:bg-slate-900"
|
||||||
:class="download.status === 'failed' ? 'border-rose-300/70 dark:border-rose-500/40' : ''"
|
:class="
|
||||||
@click="showDownloadDetail(download)">
|
download.status === 'failed'
|
||||||
<div class="h-12 w-12 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800">
|
? 'border-rose-300/70 dark:border-rose-500/40'
|
||||||
<img :src="download.icon" :alt="download.name" class="h-full w-full object-cover" />
|
: ''
|
||||||
|
"
|
||||||
|
@click="showDownloadDetail(download)"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-12 w-12 overflow-hidden rounded-xl bg-slate-100 dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
:src="download.icon"
|
||||||
|
:alt="download.name"
|
||||||
|
class="h-full w-full object-cover"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="truncate text-sm font-semibold text-slate-800 dark:text-slate-100">{{ download.name }}</p>
|
<p
|
||||||
|
class="truncate text-sm font-semibold text-slate-800 dark:text-slate-100"
|
||||||
|
>
|
||||||
|
{{ download.name }}
|
||||||
|
</p>
|
||||||
<p class="text-xs text-slate-500 dark:text-slate-400">
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||||
<span v-if="download.status === 'downloading'">下载中 {{ download.progress }}%</span>
|
<span v-if="download.status === 'downloading'"
|
||||||
<span v-else-if="download.status === 'installing'">安装中...</span>
|
>下载中 {{ download.progress }}%</span
|
||||||
|
>
|
||||||
|
<span v-else-if="download.status === 'installing'"
|
||||||
|
>安装中...</span
|
||||||
|
>
|
||||||
<span v-else-if="download.status === 'completed'">已完成</span>
|
<span v-else-if="download.status === 'completed'">已完成</span>
|
||||||
<span v-else-if="download.status === 'failed'">失败: {{ download.error }}</span>
|
<span v-else-if="download.status === 'failed'"
|
||||||
|
>失败: {{ download.error }}</span
|
||||||
|
>
|
||||||
<span v-else-if="download.status === 'paused'">已暂停</span>
|
<span v-else-if="download.status === 'paused'">已暂停</span>
|
||||||
<span v-else>等待中...</span>
|
<span v-else>等待中...</span>
|
||||||
</p>
|
</p>
|
||||||
<div v-if="download.status === 'downloading'"
|
<div
|
||||||
class="mt-2 h-1.5 rounded-full bg-slate-100 dark:bg-slate-800">
|
v-if="download.status === 'downloading'"
|
||||||
<div class="h-full rounded-full bg-brand" :style="{ width: `${download.progress}%` }"></div>
|
class="mt-2 h-1.5 rounded-full bg-slate-100 dark:bg-slate-800"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="h-full rounded-full bg-brand"
|
||||||
|
:style="{ width: `${download.progress}%` }"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<button v-if="download.status === 'failed'" type="button"
|
<button
|
||||||
|
v-if="download.status === 'failed'"
|
||||||
|
type="button"
|
||||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-rose-200/60 text-rose-500 transition hover:bg-rose-50"
|
class="inline-flex h-9 w-9 items-center justify-center rounded-full border border-rose-200/60 text-rose-500 transition hover:bg-rose-50"
|
||||||
title="重试" @click.stop="retryDownload(download)">
|
title="重试"
|
||||||
|
@click.stop="retryDownload(download)"
|
||||||
|
>
|
||||||
<i class="fas fa-redo"></i>
|
<i class="fas fa-redo"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -71,28 +127,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed } from 'vue';
|
import { ref, computed } from "vue";
|
||||||
import type { DownloadItem } from '../global/typedefinition';
|
import type { DownloadItem } from "../global/typedefinition";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
downloads: DownloadItem[];
|
downloads: DownloadItem[];
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'pause', download: DownloadItem): void;
|
(e: "pause", download: DownloadItem): void;
|
||||||
(e: 'resume', download: DownloadItem): void;
|
(e: "resume", download: DownloadItem): void;
|
||||||
(e: 'cancel', download: DownloadItem): void;
|
(e: "cancel", download: DownloadItem): void;
|
||||||
(e: 'retry', download: DownloadItem): void;
|
(e: "retry", download: DownloadItem): void;
|
||||||
(e: 'clear-completed'): void;
|
(e: "clear-completed"): void;
|
||||||
(e: 'show-detail', download: DownloadItem): void;
|
(e: "show-detail", download: DownloadItem): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
|
|
||||||
const isExpanded = ref(false);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
const activeDownloads = computed(() => {
|
const activeDownloads = computed(() => {
|
||||||
return props.downloads.filter(d =>
|
return props.downloads.filter(
|
||||||
d.status === 'downloading' || d.status === 'installing'
|
(d) => d.status === "downloading" || d.status === "installing",
|
||||||
).length;
|
).length;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -101,14 +156,14 @@ const toggleExpand = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const retryDownload = (download: DownloadItem) => {
|
const retryDownload = (download: DownloadItem) => {
|
||||||
emit('retry', download);
|
emit("retry", download);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearCompleted = () => {
|
const clearCompleted = () => {
|
||||||
emit('clear-completed');
|
emit("clear-completed");
|
||||||
};
|
};
|
||||||
|
|
||||||
const showDownloadDetail = (download: DownloadItem) => {
|
const showDownloadDetail = (download: DownloadItem) => {
|
||||||
emit('show-detail', download);
|
emit("show-detail", download);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,67 +1,108 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 scale-95"
|
<Transition
|
||||||
enter-to-class="opacity-100 scale-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
enter-from-class="opacity-0 scale-95"
|
||||||
<div v-if="show"
|
enter-to-class="opacity-100 scale-100"
|
||||||
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10">
|
leave-active-class="duration-150 ease-in"
|
||||||
<div class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
leave-from-class="opacity-100 scale-100"
|
||||||
|
leave-to-class="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth scrollbar-thumb-slate-200 dark:scrollbar-thumb-slate-700 rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
>
|
||||||
<div class="flex items-start justify-between">
|
<div class="flex items-start justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">已安装应用</p>
|
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">来自本机 APM 安装列表</p>
|
已安装应用
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
来自本机 APM 安装列表
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<button type="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 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
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"
|
||||||
:disabled="loading" @click="$emit('refresh')">
|
:disabled="loading"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
刷新
|
刷新
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
||||||
@click="$emit('close')" aria-label="关闭">
|
@click="$emit('close')"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 space-y-4">
|
<div class="mt-6 space-y-4">
|
||||||
<div v-if="loading"
|
<div
|
||||||
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400">
|
v-if="loading"
|
||||||
|
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||||
|
>
|
||||||
正在读取已安装应用…
|
正在读取已安装应用…
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error"
|
<div
|
||||||
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-4 py-6 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10">
|
v-else-if="error"
|
||||||
|
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-4 py-6 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10"
|
||||||
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="apps.length === 0"
|
<div
|
||||||
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">
|
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>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<div v-for="app in apps" :key="app.pkgname"
|
<div
|
||||||
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">
|
v-for="app in apps"
|
||||||
|
: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"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-base font-semibold text-slate-900 dark:text-white">{{ app.pkgname }}</p>
|
<p
|
||||||
<span v-if="app.flags && app.flags.includes('automatic')"
|
class="text-base font-semibold text-slate-900 dark:text-white"
|
||||||
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">
|
>
|
||||||
|
{{ app.pkgname }}
|
||||||
|
</p>
|
||||||
|
<span
|
||||||
|
v-if="app.flags && app.flags.includes('automatic')"
|
||||||
|
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"
|
||||||
|
>
|
||||||
依赖项
|
依赖项
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400">
|
<div
|
||||||
|
class="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-500 dark:text-slate-400"
|
||||||
|
>
|
||||||
<span>{{ app.version }}</span>
|
<span>{{ app.version }}</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{{ app.arch }}</span>
|
<span>{{ app.arch }}</span>
|
||||||
<template v-if="app.flags && !app.flags.includes('automatic')">
|
<template
|
||||||
|
v-if="app.flags && !app.flags.includes('automatic')"
|
||||||
|
>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{{ app.flags }}</span>
|
<span>{{ app.flags }}</span>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-600 transition hover:bg-rose-50 disabled:opacity-50"
|
class="inline-flex items-center gap-2 rounded-2xl border border-rose-300/60 px-4 py-2 text-sm font-semibold text-rose-600 transition hover:bg-rose-50 disabled:opacity-50"
|
||||||
:disabled="app.currentStatus === 'not-installed'" @click="$emit('uninstall', app)">
|
:disabled="app.currentStatus === 'not-installed'"
|
||||||
|
@click="$emit('uninstall', app)"
|
||||||
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
卸载
|
卸载
|
||||||
</button>
|
</button>
|
||||||
@@ -74,7 +115,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { App } from '../global/typedefinition';
|
import { App } from "../global/typedefinition";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -84,9 +125,8 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
defineEmits<{
|
defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'refresh'): void;
|
(e: "refresh"): void;
|
||||||
(e: 'uninstall', app: App): void;
|
(e: "uninstall", app: App): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +1,62 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0"
|
<Transition
|
||||||
enter-to-class="opacity-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100" leave-to-class="opacity-0">
|
enter-from-class="opacity-0"
|
||||||
<div v-if="show"
|
enter-to-class="opacity-100"
|
||||||
|
leave-active-class="duration-150 ease-in"
|
||||||
|
leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
class="fixed inset-0 z-[60] flex items-center justify-center bg-slate-900/80 px-4 py-10"
|
class="fixed inset-0 z-[60] flex items-center justify-center bg-slate-900/80 px-4 py-10"
|
||||||
@click.self="closePreview">
|
@click.self="closePreview"
|
||||||
|
>
|
||||||
<div class="relative w-full max-w-5xl">
|
<div class="relative w-full max-w-5xl">
|
||||||
<img :src="currentScreenshot" alt="应用截图预览"
|
<img
|
||||||
class="max-h-[80vh] w-full rounded-3xl border border-slate-200/40 bg-black/40 object-contain shadow-2xl dark:border-slate-700" />
|
:src="currentScreenshot"
|
||||||
<div class="absolute inset-x-0 top-4 flex items-center justify-between px-6">
|
alt="应用截图预览"
|
||||||
|
class="max-h-[80vh] w-full rounded-3xl border border-slate-200/40 bg-black/40 object-contain shadow-2xl dark:border-slate-700"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="absolute inset-x-0 top-4 flex items-center justify-between px-6"
|
||||||
|
>
|
||||||
<div class="flex gap-3">
|
<div class="flex gap-3">
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
@click="prevScreen" :disabled="currentScreenIndex === 0" aria-label="上一张">
|
@click="prevScreen"
|
||||||
|
:disabled="currentScreenIndex === 0"
|
||||||
|
aria-label="上一张"
|
||||||
|
>
|
||||||
<i class="fas fa-chevron-left"></i>
|
<i class="fas fa-chevron-left"></i>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
@click="nextScreen" :disabled="currentScreenIndex === screenshots.length - 1" aria-label="下一张">
|
@click="nextScreen"
|
||||||
|
:disabled="currentScreenIndex === screenshots.length - 1"
|
||||||
|
aria-label="下一张"
|
||||||
|
>
|
||||||
<i class="fas fa-chevron-right"></i>
|
<i class="fas fa-chevron-right"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white"
|
class="inline-flex h-11 w-11 items-center justify-center rounded-full bg-white/80 text-slate-700 shadow-lg transition hover:bg-white"
|
||||||
@click="closePreview" aria-label="关闭">
|
@click="closePreview"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute inset-x-0 bottom-6 flex items-center justify-center">
|
<div
|
||||||
<span class="rounded-full bg-black/60 px-4 py-1 text-sm font-medium text-white">{{ previewCounterText }}</span>
|
class="absolute inset-x-0 bottom-6 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="rounded-full bg-black/60 px-4 py-1 text-sm font-medium text-white"
|
||||||
|
>{{ previewCounterText }}</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +64,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from "vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -45,13 +73,13 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'prev'): void;
|
(e: "prev"): void;
|
||||||
(e: 'next'): void;
|
(e: "next"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const currentScreenshot = computed(() => {
|
const currentScreenshot = computed(() => {
|
||||||
return props.screenshots[props.currentScreenIndex] || '';
|
return props.screenshots[props.currentScreenIndex] || "";
|
||||||
});
|
});
|
||||||
|
|
||||||
const previewCounterText = computed(() => {
|
const previewCounterText = computed(() => {
|
||||||
@@ -59,14 +87,14 @@ const previewCounterText = computed(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const closePreview = () => {
|
const closePreview = () => {
|
||||||
emit('close');
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const prevScreen = () => {
|
const prevScreen = () => {
|
||||||
emit('prev');
|
emit("prev");
|
||||||
};
|
};
|
||||||
|
|
||||||
const nextScreen = () => {
|
const nextScreen = () => {
|
||||||
emit('next');
|
emit("next");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,13 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="flex items-center justify-between rounded-2xl border border-slate-200/80 bg-white/70 px-4 py-3 text-sm font-medium text-slate-600 shadow-sm transition hover:border-brand/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-300"
|
class="flex items-center justify-between rounded-2xl border border-slate-200/80 bg-white/70 px-4 py-3 text-sm font-medium text-slate-600 shadow-sm transition hover:border-brand/40 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40 dark:border-slate-800/70 dark:bg-slate-900/60 dark:text-slate-300"
|
||||||
:aria-pressed="isDark" @click="toggle">
|
:aria-pressed="isDark"
|
||||||
|
@click="toggle"
|
||||||
|
>
|
||||||
<span class="flex items-center gap-2">
|
<span class="flex items-center gap-2">
|
||||||
<i class="fas" :class="isDark ? 'fa-moon text-amber-200' : 'fa-sun text-amber-400'"></i>
|
<i
|
||||||
<span>{{ isDark ? '深色主题' : '浅色主题' }}</span>
|
class="fas"
|
||||||
|
:class="isDark ? 'fa-moon text-amber-200' : 'fa-sun text-amber-400'"
|
||||||
|
></i>
|
||||||
|
<span>{{ isDark ? "深色主题" : "浅色主题" }}</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700">
|
<span
|
||||||
<span :class="['inline-block h-4 w-4 rounded-full bg-white shadow transition', isDark ? 'translate-x-6' : 'translate-x-1']"></span>
|
class="relative inline-flex h-6 w-12 items-center rounded-full bg-slate-300/80 transition dark:bg-slate-700"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'inline-block h-4 w-4 rounded-full bg-white shadow transition',
|
||||||
|
isDark ? 'translate-x-6' : 'translate-x-1',
|
||||||
|
]"
|
||||||
|
></span>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
@@ -18,10 +31,10 @@ defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'toggle'): void;
|
(e: "toggle"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const toggle = () => {
|
const toggle = () => {
|
||||||
emit('toggle');
|
emit("toggle");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-wrap gap-3">
|
<div class="flex flex-wrap gap-3">
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand/40"
|
||||||
@click="handleUpdate" title="启动 apm-update-tool">
|
@click="handleUpdate"
|
||||||
|
title="启动 apm-update-tool"
|
||||||
|
>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
<span>软件更新</span>
|
<span>软件更新</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-slate-900/90 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-slate-900/40 transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/40 dark:bg-white/90 dark:text-slate-900"
|
class="inline-flex items-center gap-2 rounded-2xl bg-slate-900/90 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-slate-900/40 transition hover:-translate-y-0.5 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-900/40 dark:bg-white/90 dark:text-slate-900"
|
||||||
@click="handleList" title="启动 apm-installer --list">
|
@click="handleList"
|
||||||
|
title="启动 apm-installer --list"
|
||||||
|
>
|
||||||
<i class="fas fa-download"></i>
|
<i class="fas fa-download"></i>
|
||||||
<span>应用管理</span>
|
<span>应用管理</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -17,15 +23,15 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update'): void;
|
(e: "update"): void;
|
||||||
(e: 'list'): void;
|
(e: "list"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
emit('update');
|
emit("update");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleList = () => {
|
const handleList = () => {
|
||||||
emit('list');
|
emit("list");
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,64 +1,100 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 scale-95"
|
<Transition
|
||||||
enter-to-class="opacity-100 scale-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
enter-from-class="opacity-0 scale-95"
|
||||||
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/70 p-4"
|
enter-to-class="opacity-100 scale-100"
|
||||||
@click.self="handleClose">
|
leave-active-class="duration-150 ease-in"
|
||||||
<div class="relative w-full max-w-lg overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
leave-from-class="opacity-100 scale-100"
|
||||||
|
leave-to-class="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/70 p-4"
|
||||||
|
@click.self="handleClose"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="relative w-full max-w-lg overflow-hidden rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
>
|
||||||
<div class="mb-6 flex items-center gap-4">
|
<div class="mb-6 flex items-center gap-4">
|
||||||
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-rose-100 to-rose-50 shadow-inner dark:from-rose-900/30 dark:to-rose-800/20">
|
<div
|
||||||
|
class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-rose-100 to-rose-50 shadow-inner dark:from-rose-900/30 dark:to-rose-800/20"
|
||||||
|
>
|
||||||
<i class="fas fa-trash-alt text-2xl text-rose-500"></i>
|
<i class="fas fa-trash-alt text-2xl text-rose-500"></i>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-slate-900 dark:text-white">卸载应用</h3>
|
<h3 class="text-xl font-bold text-slate-900 dark:text-white">
|
||||||
|
卸载应用
|
||||||
|
</h3>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
您确定要卸载 <span class="font-semibold text-slate-700 dark:text-slate-200">{{ appName }}</span> 吗?
|
您确定要卸载
|
||||||
|
<span class="font-semibold text-slate-700 dark:text-slate-200">{{
|
||||||
|
appName
|
||||||
|
}}</span>
|
||||||
|
吗?
|
||||||
</p>
|
</p>
|
||||||
<p class="text-xs text-slate-400 mt-1">{{ appPkg }}</p>
|
<p class="text-xs text-slate-400 mt-1">{{ appPkg }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Terminal Output -->
|
<!-- Terminal Output -->
|
||||||
<div v-if="uninstalling || completed"
|
<div
|
||||||
class="mb-6 max-h-48 overflow-y-auto rounded-xl border border-slate-200/50 bg-slate-900 p-3 font-mono text-xs text-slate-300 shadow-inner scrollbar-muted dark:border-slate-700">
|
v-if="uninstalling || completed"
|
||||||
<div v-for="(line, index) in logs" :key="index" class="whitespace-pre-wrap break-all">{{ line }}</div>
|
class="mb-6 max-h-48 overflow-y-auto rounded-xl border border-slate-200/50 bg-slate-900 p-3 font-mono text-xs text-slate-300 shadow-inner scrollbar-muted dark:border-slate-700"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(line, index) in logs"
|
||||||
|
:key="index"
|
||||||
|
class="whitespace-pre-wrap break-all"
|
||||||
|
>
|
||||||
|
{{ line }}
|
||||||
|
</div>
|
||||||
<div ref="logEnd"></div>
|
<div ref="logEnd"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="error" class="mb-4 rounded-xl border border-rose-200/50 bg-rose-50 p-3 text-sm text-rose-600 dark:border-rose-900/30 dark:bg-rose-900/20 dark:text-rose-400">
|
<div
|
||||||
|
v-if="error"
|
||||||
|
class="mb-4 rounded-xl border border-rose-200/50 bg-rose-50 p-3 text-sm text-rose-600 dark:border-rose-900/30 dark:bg-rose-900/20 dark:text-rose-400"
|
||||||
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center justify-end gap-3">
|
<div class="flex items-center justify-end gap-3">
|
||||||
<button v-if="!uninstalling" type="button"
|
<button
|
||||||
|
v-if="!uninstalling"
|
||||||
|
type="button"
|
||||||
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-600 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300 dark:hover:bg-slate-700"
|
||||||
@click="handleClose">
|
@click="handleClose"
|
||||||
|
>
|
||||||
取消
|
取消
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="!uninstalling && !completed" type="button"
|
<button
|
||||||
|
v-if="!uninstalling && !completed"
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-xl bg-rose-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 transition hover:bg-rose-600 hover:-translate-y-0.5"
|
class="inline-flex items-center gap-2 rounded-xl bg-rose-500 px-4 py-2 text-sm font-semibold text-white shadow-lg shadow-rose-500/30 transition hover:bg-rose-600 hover:-translate-y-0.5"
|
||||||
@click="confirmUninstall">
|
@click="confirmUninstall"
|
||||||
|
>
|
||||||
<i class="fas fa-trash"></i>
|
<i class="fas fa-trash"></i>
|
||||||
确认卸载
|
确认卸载
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button v-if="completed" type="button"
|
<button
|
||||||
|
v-if="completed"
|
||||||
|
type="button"
|
||||||
class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600"
|
class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white transition hover:bg-slate-800 dark:bg-slate-700 dark:hover:bg-slate-600"
|
||||||
@click="handleFinish">
|
@click="handleFinish"
|
||||||
|
>
|
||||||
完成
|
完成
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Transition>
|
</Transition>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, nextTick, onUnmounted } from 'vue';
|
import { computed, ref, watch, nextTick, onUnmounted } from "vue";
|
||||||
import type { App } from '../global/typedefinition';
|
import type { App } from "../global/typedefinition";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -66,55 +102,55 @@ const props = defineProps<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'success'): void;
|
(e: "success"): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const uninstalling = ref(false);
|
const uninstalling = ref(false);
|
||||||
const completed = ref(false);
|
const completed = ref(false);
|
||||||
const logs = ref<string[]>([]);
|
const logs = ref<string[]>([]);
|
||||||
const error = ref('');
|
const error = ref("");
|
||||||
const logEnd = ref<HTMLElement | null>(null);
|
const logEnd = ref<HTMLElement | null>(null);
|
||||||
|
|
||||||
const appName = computed(() => props.app?.name || '未知应用');
|
const appName = computed(() => props.app?.name || "未知应用");
|
||||||
const appPkg = computed(() => props.app?.pkgname || '');
|
const appPkg = computed(() => props.app?.pkgname || "");
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
if (uninstalling.value && !completed.value) return; // Prevent closing while uninstalling
|
if (uninstalling.value && !completed.value) return; // Prevent closing while uninstalling
|
||||||
reset();
|
reset();
|
||||||
emit('close');
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleFinish = () => {
|
const handleFinish = () => {
|
||||||
reset();
|
reset();
|
||||||
emit('success'); // Parent should refresh list
|
emit("success"); // Parent should refresh list
|
||||||
emit('close');
|
emit("close");
|
||||||
};
|
};
|
||||||
|
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
uninstalling.value = false;
|
uninstalling.value = false;
|
||||||
completed.value = false;
|
completed.value = false;
|
||||||
logs.value = [];
|
logs.value = [];
|
||||||
error.value = '';
|
error.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const confirmUninstall = () => {
|
const confirmUninstall = () => {
|
||||||
if (!appPkg.value) {
|
if (!appPkg.value) {
|
||||||
error.value = '无效的包名';
|
error.value = "无效的包名";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
uninstalling.value = true;
|
uninstalling.value = true;
|
||||||
logs.value = ['正在请求卸载: ' + appPkg.value + '...'];
|
logs.value = ["正在请求卸载: " + appPkg.value + "..."];
|
||||||
|
|
||||||
window.ipcRenderer.send('remove-installed', appPkg.value);
|
window.ipcRenderer.send("remove-installed", appPkg.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onProgress = (_event: any, chunk: string) => {
|
const onProgress = (_event: any, chunk: string) => {
|
||||||
if (!uninstalling.value) return;
|
if (!uninstalling.value) return;
|
||||||
// Split by newline but handle chunks correctly?
|
// Split by newline but handle chunks correctly?
|
||||||
// For simplicity, just appending lines if chunk contains newlines, or appending to last line?
|
// For simplicity, just appending lines if chunk contains newlines, or appending to last line?
|
||||||
// Let's just push lines. The backend output might come in partial chunks.
|
// Let's just push lines. The backend output might come in partial chunks.
|
||||||
// A simple way is just to push the chunk and let CSS whitespace-pre-wrap handle it.
|
// A simple way is just to push the chunk and let CSS whitespace-pre-wrap handle it.
|
||||||
@@ -124,17 +160,23 @@ const onProgress = (_event: any, chunk: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const onComplete = (_event: any, result: { success: boolean; message: any }) => {
|
const onComplete = (
|
||||||
|
_event: any,
|
||||||
|
result: { success: boolean; message: any },
|
||||||
|
) => {
|
||||||
if (!uninstalling.value) return; // Ignore if not current session
|
if (!uninstalling.value) return; // Ignore if not current session
|
||||||
|
|
||||||
const msgObj = typeof result.message === 'string' ? JSON.parse(result.message) : result.message;
|
const msgObj =
|
||||||
|
typeof result.message === "string"
|
||||||
|
? JSON.parse(result.message)
|
||||||
|
: result.message;
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
logs.value.push('\n[完成] ' + (msgObj.message || '卸载成功'));
|
logs.value.push("\n[完成] " + (msgObj.message || "卸载成功"));
|
||||||
completed.value = true;
|
completed.value = true;
|
||||||
} else {
|
} else {
|
||||||
logs.value.push('\n[错误] ' + (msgObj.message || '卸载失败'));
|
logs.value.push("\n[错误] " + (msgObj.message || "卸载失败"));
|
||||||
error.value = msgObj.message || '卸载失败';
|
error.value = msgObj.message || "卸载失败";
|
||||||
// Allow trying again or closing?
|
// Allow trying again or closing?
|
||||||
// We stay in "uninstalling" state visually or switch to completed=true but with error?
|
// We stay in "uninstalling" state visually or switch to completed=true but with error?
|
||||||
// Let's set completed=true so user can click "Finish" (Close).
|
// Let's set completed=true so user can click "Finish" (Close).
|
||||||
@@ -146,25 +188,27 @@ const onComplete = (_event: any, result: { success: boolean; message: any }) =>
|
|||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
if (logEnd.value) {
|
if (logEnd.value) {
|
||||||
logEnd.value.scrollIntoView({ behavior: 'smooth' });
|
logEnd.value.scrollIntoView({ behavior: "smooth" });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
watch(() => props.show, (val) => {
|
watch(
|
||||||
if (val) {
|
() => props.show,
|
||||||
// specific setup if needed
|
(val) => {
|
||||||
window.ipcRenderer.on('remove-progress', onProgress);
|
if (val) {
|
||||||
window.ipcRenderer.on('remove-complete', onComplete);
|
// specific setup if needed
|
||||||
} else {
|
window.ipcRenderer.on("remove-progress", onProgress);
|
||||||
window.ipcRenderer.off('remove-progress', onProgress);
|
window.ipcRenderer.on("remove-complete", onComplete);
|
||||||
window.ipcRenderer.off('remove-complete', onComplete);
|
} else {
|
||||||
}
|
window.ipcRenderer.off("remove-progress", onProgress);
|
||||||
});
|
window.ipcRenderer.off("remove-complete", onComplete);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
window.ipcRenderer.off('remove-progress', onProgress);
|
window.ipcRenderer.off("remove-progress", onProgress);
|
||||||
window.ipcRenderer.off('remove-complete', onComplete);
|
window.ipcRenderer.off("remove-complete", onComplete);
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,74 +1,118 @@
|
|||||||
<template>
|
<template>
|
||||||
<Transition enter-active-class="duration-200 ease-out" enter-from-class="opacity-0 scale-95"
|
<Transition
|
||||||
enter-to-class="opacity-100 scale-100" leave-active-class="duration-150 ease-in"
|
enter-active-class="duration-200 ease-out"
|
||||||
leave-from-class="opacity-100 scale-100" leave-to-class="opacity-0 scale-95">
|
enter-from-class="opacity-0 scale-95"
|
||||||
<div v-if="show"
|
enter-to-class="opacity-100 scale-100"
|
||||||
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10">
|
leave-active-class="duration-150 ease-in"
|
||||||
<div class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900">
|
leave-from-class="opacity-100 scale-100"
|
||||||
|
leave-to-class="opacity-0 scale-95"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-if="show"
|
||||||
|
class="fixed inset-0 z-50 flex items-start justify-center bg-slate-900/70 px-4 py-10"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="w-full max-w-4xl max-h-[85vh] overflow-y-auto scrollbar-nowidth rounded-3xl border border-white/10 bg-white/95 p-6 shadow-2xl dark:border-slate-800 dark:bg-slate-900"
|
||||||
|
>
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<p class="text-2xl font-semibold text-slate-900 dark:text-white">软件更新</p>
|
<p class="text-2xl font-semibold text-slate-900 dark:text-white">
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">可更新的 APM 应用</p>
|
软件更新
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
可更新的 APM 应用
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-wrap gap-2">
|
<div class="flex flex-wrap gap-2">
|
||||||
<button type="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 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
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"
|
||||||
:disabled="loading" @click="$emit('refresh')">
|
:disabled="loading"
|
||||||
|
@click="$emit('refresh')"
|
||||||
|
>
|
||||||
<i class="fas fa-sync-alt"></i>
|
<i class="fas fa-sync-alt"></i>
|
||||||
刷新
|
刷新
|
||||||
</button>
|
</button>
|
||||||
<button type="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 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
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"
|
||||||
:disabled="loading || apps.length === 0" @click="$emit('toggle-all')">
|
:disabled="loading || apps.length === 0"
|
||||||
|
@click="$emit('toggle-all')"
|
||||||
|
>
|
||||||
<i class="fas fa-check-square"></i>
|
<i class="fas fa-check-square"></i>
|
||||||
全选/全不选
|
全选/全不选
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40"
|
class="inline-flex items-center gap-2 rounded-2xl bg-gradient-to-r from-brand to-brand-dark px-4 py-2 text-sm font-semibold text-white shadow-lg disabled:opacity-40"
|
||||||
:disabled="loading || !hasSelected" @click="$emit('upgrade-selected')">
|
:disabled="loading || !hasSelected"
|
||||||
|
@click="$emit('upgrade-selected')"
|
||||||
|
>
|
||||||
<i class="fas fa-upload"></i>
|
<i class="fas fa-upload"></i>
|
||||||
更新选中
|
更新选中
|
||||||
</button>
|
</button>
|
||||||
<button type="button"
|
<button
|
||||||
|
type="button"
|
||||||
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
class="inline-flex h-10 w-10 items-center justify-center rounded-full border border-slate-200/70 text-slate-500 transition hover:text-slate-900 dark:border-slate-700"
|
||||||
@click="$emit('close')" aria-label="关闭">
|
@click="$emit('close')"
|
||||||
|
aria-label="关闭"
|
||||||
|
>
|
||||||
<i class="fas fa-xmark"></i>
|
<i class="fas fa-xmark"></i>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 space-y-4">
|
<div class="mt-6 space-y-4">
|
||||||
<div v-if="loading"
|
<div
|
||||||
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400">
|
v-if="loading"
|
||||||
|
class="rounded-2xl border border-dashed border-slate-200/80 px-4 py-10 text-center text-slate-500 dark:border-slate-800/80 dark:text-slate-400"
|
||||||
|
>
|
||||||
正在检查可更新应用…
|
正在检查可更新应用…
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="error"
|
<div
|
||||||
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-4 py-6 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10">
|
v-else-if="error"
|
||||||
|
class="rounded-2xl border border-rose-200/70 bg-rose-50/60 px-4 py-6 text-center text-sm text-rose-600 dark:border-rose-500/40 dark:bg-rose-500/10"
|
||||||
|
>
|
||||||
{{ error }}
|
{{ error }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if="apps.length === 0"
|
<div
|
||||||
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">
|
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>
|
||||||
<div v-else class="space-y-3">
|
<div v-else class="space-y-3">
|
||||||
<label v-for="app in apps" :key="app.pkgname"
|
<label
|
||||||
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:gap-4">
|
v-for="app in apps"
|
||||||
|
: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:gap-4"
|
||||||
|
>
|
||||||
<div class="flex items-start gap-3">
|
<div class="flex items-start gap-3">
|
||||||
<input type="checkbox" class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
<input
|
||||||
v-model="app.selected" :disabled="app.upgrading" />
|
type="checkbox"
|
||||||
|
class="mt-1 h-4 w-4 rounded border-slate-300 accent-brand focus:ring-brand"
|
||||||
|
v-model="app.selected"
|
||||||
|
:disabled="app.upgrading"
|
||||||
|
/>
|
||||||
<div>
|
<div>
|
||||||
<p class="font-semibold text-slate-900 dark:text-white">{{ app.pkgname }}</p>
|
<p class="font-semibold text-slate-900 dark:text-white">
|
||||||
|
{{ app.pkgname }}
|
||||||
|
</p>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
当前 {{ app.currentVersion || '-' }} · 更新至 {{ app.newVersion || '-' }}
|
当前 {{ app.currentVersion || "-" }} · 更新至
|
||||||
|
{{ app.newVersion || "-" }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 sm:ml-auto">
|
<div class="flex items-center gap-2 sm:ml-auto">
|
||||||
<button type="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 disabled:opacity-40 dark:border-slate-700 dark:text-slate-200"
|
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"
|
||||||
:disabled="app.upgrading" @click.prevent="$emit('upgrade-one', app)">
|
:disabled="app.upgrading"
|
||||||
|
@click.prevent="$emit('upgrade-one', app)"
|
||||||
|
>
|
||||||
<i class="fas fa-arrow-up"></i>
|
<i class="fas fa-arrow-up"></i>
|
||||||
{{ app.upgrading ? '更新中…' : '更新' }}
|
{{ app.upgrading ? "更新中…" : "更新" }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
@@ -80,7 +124,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { UpdateAppItem } from '../global/typedefinition';
|
import type { UpdateAppItem } from "../global/typedefinition";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
show: boolean;
|
show: boolean;
|
||||||
@@ -92,11 +136,10 @@ defineProps<{
|
|||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'close'): void;
|
(e: "close"): void;
|
||||||
(e: 'refresh'): void;
|
(e: "refresh"): void;
|
||||||
(e: 'toggle-all'): void;
|
(e: "toggle-all"): void;
|
||||||
(e: 'upgrade-selected'): void;
|
(e: "upgrade-selected"): void;
|
||||||
(e: 'upgrade-one', app: UpdateAppItem): void;
|
(e: "upgrade-one", app: UpdateAppItem): void;
|
||||||
}>();
|
}>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
import { computed,ComputedRef,ref,unref,watch } from "vue";
|
import { computed, ComputedRef, ref, unref, watch } from "vue";
|
||||||
import type { DownloadItem,DownloadItemStatus } from "./typedefinition";
|
import type { DownloadItem, DownloadItemStatus } from "./typedefinition";
|
||||||
|
|
||||||
export const downloads = ref<DownloadItem[]>([]);
|
export const downloads = ref<DownloadItem[]>([]);
|
||||||
|
|
||||||
export function removeDownloadItem(pkgname:string) {
|
export function removeDownloadItem(pkgname: string) {
|
||||||
const list = downloads.value;
|
const list = downloads.value;
|
||||||
for (let i = list.length - 1; i >= 0; i -= 1) {
|
for (let i = list.length - 1; i >= 0; i -= 1) {
|
||||||
if (list[i].pkgname === pkgname) {
|
if (list[i].pkgname === pkgname) {
|
||||||
list.splice(i,1);
|
list.splice(i, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function watchDownloadsChange (cb: () => void) {
|
export function watchDownloadsChange(cb: () => void) {
|
||||||
const statusById = new Map<number,DownloadItemStatus>();
|
const statusById = new Map<number, DownloadItemStatus>();
|
||||||
|
|
||||||
for (const item of downloads.value) {
|
for (const item of downloads.value) {
|
||||||
statusById.set(item.id,item.status);
|
statusById.set(item.id, item.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
@@ -27,7 +27,7 @@ export function watchDownloadsChange (cb: () => void) {
|
|||||||
if (item.status === "completed" && prevStatus !== "completed") {
|
if (item.status === "completed" && prevStatus !== "completed") {
|
||||||
cb();
|
cb();
|
||||||
}
|
}
|
||||||
statusById.set(item.id,item.status);
|
statusById.set(item.id, item.status);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusById.size > list.length) {
|
if (statusById.size > list.length) {
|
||||||
@@ -42,7 +42,7 @@ export function watchDownloadsChange (cb: () => void) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useDownloadItemStatus (
|
export function useDownloadItemStatus(
|
||||||
pkgname?: ComputedRef<string | undefined>,
|
pkgname?: ComputedRef<string | undefined>,
|
||||||
) {
|
) {
|
||||||
const status: ComputedRef<DownloadItemStatus | undefined> = computed(() => {
|
const status: ComputedRef<DownloadItemStatus | undefined> = computed(() => {
|
||||||
@@ -63,7 +63,7 @@ export function useDownloadItemStatus (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useInstallFeedback (pkgname?: ComputedRef<string | undefined>) {
|
export function useInstallFeedback(pkgname?: ComputedRef<string | undefined>) {
|
||||||
const installFeedback = computed(() => {
|
const installFeedback = computed(() => {
|
||||||
const name = unref(pkgname);
|
const name = unref(pkgname);
|
||||||
if (!name) return false;
|
if (!name) return false;
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { App } from "./typedefinition";
|
import type { App } from "./typedefinition";
|
||||||
|
|
||||||
export const APM_STORE_BASE_URL: string = import.meta.env.VITE_APM_STORE_BASE_URL || '';
|
export const APM_STORE_BASE_URL: string =
|
||||||
|
import.meta.env.VITE_APM_STORE_BASE_URL || "";
|
||||||
|
|
||||||
// 下面的变量用于存储当前应用的信息,其实用在多个组件中
|
// 下面的变量用于存储当前应用的信息,其实用在多个组件中
|
||||||
export const currentApp = ref<App | null>(null);
|
export const currentApp = ref<App | null>(null);
|
||||||
|
|||||||
@@ -1,40 +1,46 @@
|
|||||||
export interface InstallLog {
|
export interface InstallLog {
|
||||||
id: number;
|
id: number;
|
||||||
success: boolean;
|
success: boolean;
|
||||||
time: number;
|
time: number;
|
||||||
exitCode: number | null;
|
exitCode: number | null;
|
||||||
message: string;
|
message: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DownloadResult extends InstallLog {
|
export interface DownloadResult extends InstallLog {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
exitCode: number | null;
|
exitCode: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type DownloadItemStatus = 'downloading' | 'installing' | 'paused' | 'completed' | 'failed' | 'queued'; // 可根据实际状态扩展
|
export type DownloadItemStatus =
|
||||||
|
| "downloading"
|
||||||
|
| "installing"
|
||||||
|
| "paused"
|
||||||
|
| "completed"
|
||||||
|
| "failed"
|
||||||
|
| "queued"; // 可根据实际状态扩展
|
||||||
|
|
||||||
export interface DownloadItem {
|
export interface DownloadItem {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
version: string;
|
version: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
status: DownloadItemStatus;
|
status: DownloadItemStatus;
|
||||||
progress: number; // 0 ~ 100 的百分比,或 0 ~ 1 的小数(建议统一)
|
progress: number; // 0 ~ 100 的百分比,或 0 ~ 1 的小数(建议统一)
|
||||||
downloadedSize: number; // 已下载字节数
|
downloadedSize: number; // 已下载字节数
|
||||||
totalSize: number; // 总字节数(可能为 0 初始时)
|
totalSize: number; // 总字节数(可能为 0 初始时)
|
||||||
speed: number; // 当前下载速度,单位如 B/s
|
speed: number; // 当前下载速度,单位如 B/s
|
||||||
timeRemaining: number; // 剩余时间(秒),0 表示未知
|
timeRemaining: number; // 剩余时间(秒),0 表示未知
|
||||||
startTime: number; // Date.now() 返回的时间戳(毫秒)
|
startTime: number; // Date.now() 返回的时间戳(毫秒)
|
||||||
endTime?: number; // 下载完成时间戳(毫秒),可选
|
endTime?: number; // 下载完成时间戳(毫秒),可选
|
||||||
logs: Array<{
|
logs: Array<{
|
||||||
time: number; // 日志时间戳
|
time: number; // 日志时间戳
|
||||||
message: string; // 日志消息
|
message: string; // 日志消息
|
||||||
}>;
|
}>;
|
||||||
source: string; // 例如 'APM Store'
|
source: string; // 例如 'APM Store'
|
||||||
retry: boolean; // 当前是否为重试下载
|
retry: boolean; // 当前是否为重试下载
|
||||||
upgradeOnly?: boolean; // 是否为仅升级任务
|
upgradeOnly?: boolean; // 是否为仅升级任务
|
||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -54,68 +60,67 @@ export interface DownloadItem {
|
|||||||
"icons": "https://cdn.d.store.deepinos.org.cn/store/development/code/icon.png"
|
"icons": "https://cdn.d.store.deepinos.org.cn/store/development/code/icon.png"
|
||||||
*/
|
*/
|
||||||
export interface AppJson {
|
export interface AppJson {
|
||||||
// 原始数据
|
// 原始数据
|
||||||
Name: string;
|
Name: string;
|
||||||
Version: string;
|
Version: string;
|
||||||
Filename: string;
|
Filename: string;
|
||||||
Torrent_address: string;
|
Torrent_address: string;
|
||||||
Pkgname: string;
|
Pkgname: string;
|
||||||
Author: string;
|
Author: string;
|
||||||
Contributor: string;
|
Contributor: string;
|
||||||
Website: string;
|
Website: string;
|
||||||
Update: string;
|
Update: string;
|
||||||
Size: string;
|
Size: string;
|
||||||
More: string;
|
More: string;
|
||||||
Tags: string;
|
Tags: string;
|
||||||
img_urls: string; // 注意:部分 json 里可能是字符串形式的数组
|
img_urls: string; // 注意:部分 json 里可能是字符串形式的数组
|
||||||
icons: string;
|
icons: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface App {
|
export interface App {
|
||||||
name: string;
|
name: string;
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
version: string;
|
version: string;
|
||||||
filename: string;
|
filename: string;
|
||||||
torrent_address: string;
|
torrent_address: string;
|
||||||
author: string;
|
author: string;
|
||||||
contributor: string;
|
contributor: string;
|
||||||
website: string;
|
website: string;
|
||||||
update: string;
|
update: string;
|
||||||
size: string;
|
size: string;
|
||||||
more: string;
|
more: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
img_urls: string[];
|
img_urls: string[];
|
||||||
icons: string;
|
icons: string;
|
||||||
category: string; // Frontend added
|
category: string; // Frontend added
|
||||||
installed?: boolean; // Frontend state
|
installed?: boolean; // Frontend state
|
||||||
flags?: string; // Tags in apm packages manager, e.g. "automatic" for dependencies
|
flags?: string; // Tags in apm packages manager, e.g. "automatic" for dependencies
|
||||||
arch?: string; // Architecture, e.g. "amd64", "arm64"
|
arch?: string; // Architecture, e.g. "amd64", "arm64"
|
||||||
currentStatus: 'not-installed' | 'installed'; // Current installation status
|
currentStatus: "not-installed" | "installed"; // Current installation status
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UpdateAppItem {
|
export interface UpdateAppItem {
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
currentVersion?: string;
|
currentVersion?: string;
|
||||||
newVersion?: string;
|
newVersion?: string;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
upgrading?: boolean;
|
upgrading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**************Below are type from main process ********************/
|
/**************Below are type from main process ********************/
|
||||||
export interface InstalledAppInfo {
|
export interface InstalledAppInfo {
|
||||||
pkgname: string;
|
pkgname: string;
|
||||||
version: string;
|
version: string;
|
||||||
arch: string;
|
arch: string;
|
||||||
flags: string;
|
flags: string;
|
||||||
raw: string;
|
raw: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ipcSender传递的信息
|
* ipcSender传递的信息
|
||||||
*/
|
*/
|
||||||
export type ChannelPayload = {
|
export type ChannelPayload = {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
message: string;
|
||||||
[k: string]: unknown;
|
[k: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|||||||
14
src/main.ts
14
src/main.ts
@@ -1,15 +1,15 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from "vue";
|
||||||
import App from './App.vue'
|
import App from "./App.vue";
|
||||||
|
|
||||||
import './3rdparty/fontawesome-free-6.7.2/css/all.min.css'
|
import "./3rdparty/fontawesome-free-6.7.2/css/all.min.css";
|
||||||
import './assets/css/appstyle.css'
|
import "./assets/css/appstyle.css";
|
||||||
|
|
||||||
// import './demos/ipc'
|
// import './demos/ipc'
|
||||||
// If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process.
|
// If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process.
|
||||||
// import './demos/node'
|
// import './demos/node'
|
||||||
|
|
||||||
createApp(App)
|
createApp(App)
|
||||||
.mount('#app')
|
.mount("#app")
|
||||||
.$nextTick(() => {
|
.$nextTick(() => {
|
||||||
postMessage({ payload: 'removeLoading' }, '*')
|
postMessage({ payload: "removeLoading" }, "*");
|
||||||
})
|
});
|
||||||
|
|||||||
@@ -1,21 +1,27 @@
|
|||||||
// window.ipcRenderer.on('main-process-message', (_event, ...args) => {
|
// window.ipcRenderer.on('main-process-message', (_event, ...args) => {
|
||||||
// console.log('[Receive Main-process message]:', ...args)
|
// console.log('[Receive Main-process message]:', ...args)
|
||||||
// })
|
// })
|
||||||
import pino from 'pino';
|
import pino from "pino";
|
||||||
|
|
||||||
import { currentApp, currentAppIsInstalled } from "../global/storeConfig";
|
import { currentApp, currentAppIsInstalled } from "../global/storeConfig";
|
||||||
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
import { APM_STORE_BASE_URL } from "../global/storeConfig";
|
||||||
import { downloads } from "../global/downloadStatus";
|
import { downloads } from "../global/downloadStatus";
|
||||||
|
|
||||||
import { InstallLog, DownloadItem, DownloadResult, App, DownloadItemStatus } from '../global/typedefinition';
|
import {
|
||||||
|
InstallLog,
|
||||||
|
DownloadItem,
|
||||||
|
DownloadResult,
|
||||||
|
App,
|
||||||
|
DownloadItemStatus,
|
||||||
|
} from "../global/typedefinition";
|
||||||
|
|
||||||
let downloadIdCounter = 0;
|
let downloadIdCounter = 0;
|
||||||
const logger = pino({ name: 'processInstall.ts' });
|
const logger = pino({ name: "processInstall.ts" });
|
||||||
|
|
||||||
export const handleInstall = () => {
|
export const handleInstall = () => {
|
||||||
if (!currentApp.value?.pkgname) return;
|
if (!currentApp.value?.pkgname) return;
|
||||||
|
|
||||||
if (downloads.value.find(d => d.pkgname === currentApp.value?.pkgname)) {
|
if (downloads.value.find((d) => d.pkgname === currentApp.value?.pkgname)) {
|
||||||
logger.info(`任务已存在,忽略重复添加: ${currentApp.value.pkgname}`);
|
logger.info(`任务已存在,忽略重复添加: ${currentApp.value.pkgname}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -28,24 +34,22 @@ export const handleInstall = () => {
|
|||||||
pkgname: currentApp.value.pkgname,
|
pkgname: currentApp.value.pkgname,
|
||||||
version: currentApp.value.version,
|
version: currentApp.value.version,
|
||||||
icon: `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${currentApp.value.category}/${currentApp.value.pkgname}/icon.png`,
|
icon: `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${currentApp.value.category}/${currentApp.value.pkgname}/icon.png`,
|
||||||
status: 'queued',
|
status: "queued",
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloadedSize: 0,
|
downloadedSize: 0,
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
timeRemaining: 0,
|
timeRemaining: 0,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
logs: [
|
logs: [{ time: Date.now(), message: "开始下载..." }],
|
||||||
{ time: Date.now(), message: '开始下载...' }
|
source: "APM Store",
|
||||||
],
|
retry: false,
|
||||||
source: 'APM Store',
|
|
||||||
retry: false
|
|
||||||
};
|
};
|
||||||
|
|
||||||
downloads.value.push(download);
|
downloads.value.push(download);
|
||||||
|
|
||||||
// Send to main process to start download
|
// Send to main process to start download
|
||||||
window.ipcRenderer.send('queue-install', JSON.stringify(download));
|
window.ipcRenderer.send("queue-install", JSON.stringify(download));
|
||||||
|
|
||||||
// const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
// const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
||||||
// openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
// openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
||||||
@@ -55,15 +59,15 @@ export const handleInstall = () => {
|
|||||||
|
|
||||||
export const handleRetry = (download_: DownloadItem) => {
|
export const handleRetry = (download_: DownloadItem) => {
|
||||||
if (!download_?.pkgname) return;
|
if (!download_?.pkgname) return;
|
||||||
download_.retry = true;
|
download_.retry = true;
|
||||||
// Send to main process to start download
|
// Send to main process to start download
|
||||||
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
window.ipcRenderer.send("queue-install", JSON.stringify(download_));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleUpgrade = (app: App) => {
|
export const handleUpgrade = (app: App) => {
|
||||||
if (!app.pkgname) return;
|
if (!app.pkgname) return;
|
||||||
|
|
||||||
if (downloads.value.find(d => d.pkgname === app.pkgname)) {
|
if (downloads.value.find((d) => d.pkgname === app.pkgname)) {
|
||||||
logger.info(`任务已存在,忽略重复添加: ${app.pkgname}`);
|
logger.info(`任务已存在,忽略重复添加: ${app.pkgname}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -75,58 +79,57 @@ export const handleUpgrade = (app: App) => {
|
|||||||
pkgname: app.pkgname,
|
pkgname: app.pkgname,
|
||||||
version: app.version,
|
version: app.version,
|
||||||
icon: `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${app.category}/${app.pkgname}/icon.png`,
|
icon: `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${app.category}/${app.pkgname}/icon.png`,
|
||||||
status: 'queued',
|
status: "queued",
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloadedSize: 0,
|
downloadedSize: 0,
|
||||||
totalSize: 0,
|
totalSize: 0,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
timeRemaining: 0,
|
timeRemaining: 0,
|
||||||
startTime: Date.now(),
|
startTime: Date.now(),
|
||||||
logs: [
|
logs: [{ time: Date.now(), message: "开始更新..." }],
|
||||||
{ time: Date.now(), message: '开始更新...' }
|
source: "APM Update",
|
||||||
],
|
|
||||||
source: 'APM Update',
|
|
||||||
retry: false,
|
retry: false,
|
||||||
upgradeOnly: true
|
upgradeOnly: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
downloads.value.push(download);
|
downloads.value.push(download);
|
||||||
window.ipcRenderer.send('queue-install', JSON.stringify(download));
|
window.ipcRenderer.send("queue-install", JSON.stringify(download));
|
||||||
};
|
};
|
||||||
|
|
||||||
export const handleRemove = () => {
|
export const handleRemove = () => {
|
||||||
if (!currentApp.value?.pkgname) return;
|
if (!currentApp.value?.pkgname) return;
|
||||||
window.ipcRenderer.send('remove-installed', currentApp.value.pkgname);
|
window.ipcRenderer.send("remove-installed", currentApp.value.pkgname);
|
||||||
}
|
};
|
||||||
|
|
||||||
window.ipcRenderer.on('remove-complete', (_event, log: DownloadResult) => {
|
window.ipcRenderer.on("remove-complete", (_event, log: DownloadResult) => {
|
||||||
if (log.success) {
|
if (log.success) {
|
||||||
currentAppIsInstalled.value = false;
|
currentAppIsInstalled.value = false;
|
||||||
} else {
|
} else {
|
||||||
currentAppIsInstalled.value = true;
|
currentAppIsInstalled.value = true;
|
||||||
console.error('卸载失败:', log.message);
|
console.error("卸载失败:", log.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
window.ipcRenderer.on("install-status", (_event, log: InstallLog) => {
|
||||||
const downloadObj = downloads.value.find(d => d.id === log.id);
|
const downloadObj = downloads.value.find((d) => d.id === log.id);
|
||||||
if(downloadObj) downloadObj.status = log.message as DownloadItemStatus;
|
if (downloadObj) downloadObj.status = log.message as DownloadItemStatus;
|
||||||
});
|
});
|
||||||
window.ipcRenderer.on('install-log', (_event, log: InstallLog) => {
|
window.ipcRenderer.on("install-log", (_event, log: InstallLog) => {
|
||||||
const downloadObj = downloads.value.find(d => d.id === log.id);
|
const downloadObj = downloads.value.find((d) => d.id === log.id);
|
||||||
if(downloadObj) downloadObj.logs.push({
|
if (downloadObj)
|
||||||
time: log.time,
|
downloadObj.logs.push({
|
||||||
message: log.message
|
time: log.time,
|
||||||
|
message: log.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
window.ipcRenderer.on('install-complete', (_event, log: DownloadResult) => {
|
window.ipcRenderer.on("install-complete", (_event, log: DownloadResult) => {
|
||||||
const downloadObj = downloads.value.find(d => d.id === log.id);
|
const downloadObj = downloads.value.find((d) => d.id === log.id);
|
||||||
if (downloadObj) {
|
if (downloadObj) {
|
||||||
if (log.success) {
|
if (log.success) {
|
||||||
downloadObj.status = 'completed';
|
downloadObj.status = "completed";
|
||||||
} else {
|
} else {
|
||||||
downloadObj.status = 'failed';
|
downloadObj.status = "failed";
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
12
src/vite-env.d.ts
vendored
12
src/vite-env.d.ts
vendored
@@ -1,14 +1,14 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare module '*.vue' {
|
declare module "*.vue" {
|
||||||
import type { DefineComponent } from 'vue'
|
import type { DefineComponent } from "vue";
|
||||||
const component: DefineComponent<{}, {}, any>
|
const component: DefineComponent<{}, {}, any>;
|
||||||
export default component
|
export default component;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Window {
|
interface Window {
|
||||||
// expose in the `electron/preload/index.ts`
|
// expose in the `electron/preload/index.ts`
|
||||||
ipcRenderer: import('electron').IpcRenderer
|
ipcRenderer: import("electron").IpcRenderer;
|
||||||
apm_store: any
|
apm_store: any;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user