mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
update:添加apm与普通商店双支持
This commit is contained in:
@@ -21,6 +21,7 @@ type InstallTask = {
|
|||||||
downloadDir?: string;
|
downloadDir?: string;
|
||||||
metalinkUrl?: string;
|
metalinkUrl?: string;
|
||||||
filename?: string;
|
filename?: string;
|
||||||
|
origin: "spark" | "apm";
|
||||||
};
|
};
|
||||||
|
|
||||||
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
const SHELL_CALLER_PATH = "/opt/spark-store/extras/shell-caller.sh";
|
||||||
@@ -137,24 +138,24 @@ 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 = typeof download_json === "string" ? JSON.parse(download_json) : download_json;
|
||||||
const { id, pkgname, metalinkUrl, filename, upgradeOnly } = download || {};
|
const { id, pkgname, metalinkUrl, filename, upgradeOnly, origin } = 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}, 来源: ${origin}`);
|
||||||
|
|
||||||
// 避免重复添加同一任务,但允许重试下载
|
// 避免重复添加同一任务,但允许重试下载
|
||||||
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(),
|
||||||
@@ -165,42 +166,36 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const webContents = event.sender;
|
const webContents = event.sender;
|
||||||
|
|
||||||
// 开始组装安装命令
|
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
let execCommand = "";
|
let execCommand = "";
|
||||||
const execParams = [];
|
const execParams = [];
|
||||||
const downloadDir = `/tmp/spark-store/download/${pkgname}`;
|
const downloadDir = `/tmp/spark-store/download/${pkgname}`;
|
||||||
|
|
||||||
// 升级操作:使用 spark-update-tool
|
if (origin === "spark") {
|
||||||
|
// Spark Store logic
|
||||||
if (upgradeOnly) {
|
if (upgradeOnly) {
|
||||||
execCommand = "pkexec";
|
execCommand = "pkexec";
|
||||||
execParams.push("spark-update-tool", pkgname);
|
execParams.push("spark-update-tool", pkgname);
|
||||||
logger.info(`升级模式: 使用 spark-update-tool 升级 ${pkgname}`);
|
} else {
|
||||||
} else if (superUserCmd.length > 0) {
|
execCommand = superUserCmd || SHELL_CALLER_PATH;
|
||||||
execCommand = superUserCmd;
|
if (superUserCmd) execParams.push(SHELL_CALLER_PATH);
|
||||||
execParams.push(SHELL_CALLER_PATH);
|
|
||||||
|
|
||||||
if (metalinkUrl && filename) {
|
if (metalinkUrl && filename) {
|
||||||
execParams.push(
|
execParams.push("ssinstall", `${downloadDir}/${filename}`, "--delete-after-install");
|
||||||
"ssinstall",
|
|
||||||
`${downloadDir}/${filename}`,
|
|
||||||
"--delete-after-install",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
execParams.push("aptss", "install", "-y", pkgname);
|
execParams.push("aptss", "install", "-y", pkgname);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
execCommand = SHELL_CALLER_PATH;
|
// APM Store logic
|
||||||
|
execCommand = "apm"; // apm handles its own sudo if needed or we use pkexec wrap if required
|
||||||
|
// Actually, usually apm is called directly and it might prompt.
|
||||||
|
// Let's stick to the pattern of using SHELL_CALLER_PATH if possible or follow apm-app-store demo.
|
||||||
|
|
||||||
if (metalinkUrl && filename) {
|
if (metalinkUrl && filename) {
|
||||||
execParams.push(
|
execParams.push("ssaudit", `${downloadDir}/${filename}`);
|
||||||
"ssinstall",
|
|
||||||
`${downloadDir}/${filename}`,
|
|
||||||
"--delete-after-install",
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
execParams.push("aptss", "install", "-y", pkgname);
|
execParams.push("install", "-y", pkgname);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,6 +210,7 @@ ipcMain.on("queue-install", async (event, download_json) => {
|
|||||||
downloadDir,
|
downloadDir,
|
||||||
metalinkUrl,
|
metalinkUrl,
|
||||||
filename,
|
filename,
|
||||||
|
origin: origin || "apm",
|
||||||
};
|
};
|
||||||
tasks.set(id, task);
|
tasks.set(id, task);
|
||||||
if (idle) processNextInQueue();
|
if (idle) processNextInQueue();
|
||||||
@@ -463,31 +459,34 @@ 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, payload) => {
|
||||||
const webContents = _event.sender;
|
const webContents = _event.sender;
|
||||||
|
const pkgname = typeof payload === "string" ? payload : payload.pkgname;
|
||||||
|
const origin = typeof payload === "string" ? "spark" : payload.origin;
|
||||||
|
|
||||||
if (!pkgname) {
|
if (!pkgname) {
|
||||||
logger.warn("remove-installed missing pkgname");
|
logger.warn("remove-installed missing pkgname");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logger.info(`卸载已安装应用: ${pkgname}`);
|
logger.info(`卸载已安装应用: ${pkgname} (来源: ${origin})`);
|
||||||
|
|
||||||
const superUserCmd = await checkSuperUserCommand();
|
|
||||||
let execCommand = "";
|
let execCommand = "";
|
||||||
const execParams = [];
|
const execParams = [];
|
||||||
if (superUserCmd.length > 0) {
|
|
||||||
execCommand = superUserCmd;
|
if (origin === "spark") {
|
||||||
execParams.push(SHELL_CALLER_PATH);
|
const superUserCmd = await checkSuperUserCommand();
|
||||||
|
execCommand = superUserCmd || SHELL_CALLER_PATH;
|
||||||
|
if (superUserCmd) execParams.push(SHELL_CALLER_PATH);
|
||||||
|
execParams.push("aptss", "remove", pkgname);
|
||||||
} else {
|
} else {
|
||||||
execCommand = SHELL_CALLER_PATH;
|
execCommand = "apm";
|
||||||
|
execParams.push("remove", pkgname);
|
||||||
}
|
}
|
||||||
const child = spawn(
|
|
||||||
execCommand,
|
const child = spawn(execCommand, execParams, {
|
||||||
[...execParams, "aptss", "remove", pkgname],
|
|
||||||
{
|
|
||||||
shell: false,
|
shell: false,
|
||||||
env: process.env,
|
env: process.env,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
let output = "";
|
let output = "";
|
||||||
|
|
||||||
child.stdout.on("data", (data) => {
|
child.stdout.on("data", (data) => {
|
||||||
|
|||||||
11917
package-lock.json
generated
Normal file
11917
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
107
src/App.vue
107
src/App.vue
@@ -152,6 +152,7 @@ import {
|
|||||||
APM_STORE_BASE_URL,
|
APM_STORE_BASE_URL,
|
||||||
currentApp,
|
currentApp,
|
||||||
currentAppIsInstalled,
|
currentAppIsInstalled,
|
||||||
|
currentStoreMode,
|
||||||
} from "./global/storeConfig";
|
} from "./global/storeConfig";
|
||||||
import {
|
import {
|
||||||
downloads,
|
downloads,
|
||||||
@@ -377,8 +378,13 @@ const checkAppInstalled = (app: App) => {
|
|||||||
|
|
||||||
const loadScreenshots = (app: App) => {
|
const loadScreenshots = (app: App) => {
|
||||||
screenshots.value = [];
|
screenshots.value = [];
|
||||||
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
app.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
const screenshotUrl = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${app.category}/${app.pkgname}/screen_${i}.png`;
|
const screenshotUrl = `${APM_STORE_BASE_URL}/${finalArch}/${app.category}/${app.pkgname}/screen_${i}.png`;
|
||||||
screenshots.value.push(screenshotUrl);
|
screenshots.value.push(screenshotUrl);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -411,33 +417,45 @@ const loadHome = async () => {
|
|||||||
homeLinks.value = [];
|
homeLinks.value = [];
|
||||||
homeLists.value = [];
|
homeLists.value = [];
|
||||||
try {
|
try {
|
||||||
const base = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/home`;
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const modes: Array<"spark" | "apm"> = [];
|
||||||
|
if (currentStoreMode.value === "hybrid") modes.push("spark", "apm");
|
||||||
|
else modes.push(currentStoreMode.value as "spark" | "apm");
|
||||||
|
|
||||||
|
for (const mode of modes) {
|
||||||
|
const finalArch =
|
||||||
|
mode === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
const base = `${APM_STORE_BASE_URL}/${finalArch}/home`;
|
||||||
|
|
||||||
// homelinks.json
|
// homelinks.json
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${base}/homelinks.json`);
|
const res = await fetch(cacheBuster(`${base}/homelinks.json`));
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
homeLinks.value = await res.json();
|
const links = await res.json();
|
||||||
|
const taggedLinks = links.map((l: any) => ({ ...l, origin: mode }));
|
||||||
|
homeLinks.value.push(...taggedLinks);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore single file failures
|
console.warn(`Failed to load ${mode} homelinks.json`, e);
|
||||||
console.warn("Failed to load homelinks.json", e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// homelist.json
|
// homelist.json
|
||||||
try {
|
try {
|
||||||
const res2 = await fetch(`${base}/homelist.json`);
|
const res2 = await fetch(cacheBuster(`${base}/homelist.json`));
|
||||||
if (res2.ok) {
|
if (res2.ok) {
|
||||||
const lists = await res2.json();
|
const lists = await res2.json();
|
||||||
for (const item of lists) {
|
for (const item of lists) {
|
||||||
if (item.type === "appList" && item.jsonUrl) {
|
if (item.type === "appList" && item.jsonUrl) {
|
||||||
try {
|
try {
|
||||||
const url = `${APM_STORE_BASE_URL}/${window.apm_store.arch}${item.jsonUrl}`;
|
const url = `${APM_STORE_BASE_URL}/${finalArch}${item.jsonUrl}`;
|
||||||
const r = await fetch(url);
|
const r = await fetch(cacheBuster(url));
|
||||||
if (r.ok) {
|
if (r.ok) {
|
||||||
const appsJson = await r.json();
|
const appsJson = await r.json();
|
||||||
const rawApps = appsJson || [];
|
const rawApps = appsJson || [];
|
||||||
const apps = await Promise.all(
|
const apps = await Promise.all(
|
||||||
rawApps.map(async (a: Record<string, unknown>) => {
|
rawApps.map(async (a: any) => {
|
||||||
const baseApp = {
|
const baseApp = {
|
||||||
name: a.Name || a.name || a.Pkgname || a.PkgName || "",
|
name: a.Name || a.name || a.Pkgname || a.PkgName || "",
|
||||||
pkgname: a.Pkgname || a.pkgname || "",
|
pkgname: a.Pkgname || a.pkgname || "",
|
||||||
@@ -445,15 +463,14 @@ const loadHome = async () => {
|
|||||||
more: a.More || a.more || "",
|
more: a.More || a.more || "",
|
||||||
version: a.Version || "",
|
version: a.Version || "",
|
||||||
filename: a.Filename || a.filename || "",
|
filename: a.Filename || a.filename || "",
|
||||||
|
origin: mode as "spark" | "apm",
|
||||||
};
|
};
|
||||||
|
|
||||||
// 根据官网的要求,读取Category和Pkgname,拼接出 源地址/架构/Category/Pkgname/app.json来获取对应的真实json
|
|
||||||
try {
|
try {
|
||||||
const realAppUrl = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${baseApp.category}/${baseApp.pkgname}/app.json`;
|
const realAppUrl = `${APM_STORE_BASE_URL}/${finalArch}/${baseApp.category}/${baseApp.pkgname}/app.json`;
|
||||||
const realRes = await fetch(realAppUrl);
|
const realRes = await fetch(cacheBuster(realAppUrl));
|
||||||
if (realRes.ok) {
|
if (realRes.ok) {
|
||||||
const realApp = await realRes.json();
|
const realApp = await realRes.json();
|
||||||
// 用真实json的filename字段和More字段来增补和覆盖当前的json
|
|
||||||
if (realApp.Filename)
|
if (realApp.Filename)
|
||||||
baseApp.filename = realApp.Filename;
|
baseApp.filename = realApp.Filename;
|
||||||
if (realApp.More) baseApp.more = realApp.More;
|
if (realApp.More) baseApp.more = realApp.More;
|
||||||
@@ -468,7 +485,10 @@ const loadHome = async () => {
|
|||||||
return baseApp;
|
return baseApp;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
homeLists.value.push({ title: item.name || "推荐", apps });
|
homeLists.value.push({
|
||||||
|
title: `${item.name || "推荐"} (${mode === "spark" ? "星火" : "APM"})`,
|
||||||
|
apps,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load home list", item, e);
|
console.warn("Failed to load home list", item, e);
|
||||||
@@ -477,7 +497,8 @@ const loadHome = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Failed to load homelist.json", e);
|
console.warn(`Failed to load ${mode} homelist.json`, e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
homeError.value = (error as Error)?.message || "加载首页失败";
|
homeError.value = (error as Error)?.message || "加载首页失败";
|
||||||
@@ -598,6 +619,7 @@ const upgradeSingleApp = (app: UpdateAppItem) => {
|
|||||||
size: "",
|
size: "",
|
||||||
img_urls: [],
|
img_urls: [],
|
||||||
icons: "",
|
icons: "",
|
||||||
|
origin: "apm", // Default to APM if unknown, or try to guess
|
||||||
currentStatus: "installed",
|
currentStatus: "installed",
|
||||||
};
|
};
|
||||||
handleUpgrade(minimalApp);
|
handleUpgrade(minimalApp);
|
||||||
@@ -656,6 +678,7 @@ const refreshInstalledApps = async () => {
|
|||||||
size: "",
|
size: "",
|
||||||
img_urls: [],
|
img_urls: [],
|
||||||
icons: "",
|
icons: "",
|
||||||
|
origin: app.origin || (app.arch?.includes("apm") ? "apm" : "spark"),
|
||||||
currentStatus: "installed",
|
currentStatus: "installed",
|
||||||
arch: app.arch,
|
arch: app.arch,
|
||||||
flags: app.flags,
|
flags: app.flags,
|
||||||
@@ -800,12 +823,36 @@ const openDownloadedApp = (pkgname: string) => {
|
|||||||
|
|
||||||
const loadCategories = async () => {
|
const loadCategories = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
cacheBuster(`/${window.apm_store.arch}/categories.json`),
|
const modes: Array<"spark" | "apm"> = [];
|
||||||
);
|
if (currentStoreMode.value === "hybrid") modes.push("spark", "apm");
|
||||||
categories.value = response.data;
|
else modes.push(currentStoreMode.value as "spark" | "apm");
|
||||||
|
|
||||||
|
const categoryData: Record<string, { zh: string; origin: string }> = {};
|
||||||
|
|
||||||
|
for (const mode of modes) {
|
||||||
|
const finalArch =
|
||||||
|
mode === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
const path = mode === "spark" ? "/store/categories.json" : `/${finalArch}/categories.json`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axiosInstance.get(cacheBuster(path));
|
||||||
|
const data = response.data;
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
categoryData[key] = {
|
||||||
|
zh: data[key].zh || data[key],
|
||||||
|
origin: mode,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`读取 ${mode} categories.json 失败: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
categories.value = categoryData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`读取 categories.json 失败: ${error}`);
|
logger.error(`读取 categories 失败: ${error}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -815,14 +862,27 @@ const loadApps = async (onFirstBatch?: () => void) => {
|
|||||||
|
|
||||||
const categoriesList = Object.keys(categories.value || {});
|
const categoriesList = Object.keys(categories.value || {});
|
||||||
let firstBatchCallDone = false;
|
let firstBatchCallDone = false;
|
||||||
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
|
||||||
// 并发加载所有分类,每个分类自带重试机制
|
// 并发加载所有分类,每个分类自带重试机制
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
categoriesList.map(async (category) => {
|
categoriesList.map(async (category) => {
|
||||||
try {
|
try {
|
||||||
logger.info(`加载分类: ${category}`);
|
const catInfo = categories.value[category];
|
||||||
|
const mode = catInfo.origin;
|
||||||
|
const finalArch =
|
||||||
|
mode === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
|
||||||
|
const path =
|
||||||
|
mode === "spark"
|
||||||
|
? `/store/${category}/applist.json`
|
||||||
|
: `/${finalArch}/${category}/applist.json`;
|
||||||
|
|
||||||
|
logger.info(`加载分类: ${category} (来源: ${mode})`);
|
||||||
const categoryApps = await fetchWithRetry<AppJson[]>(
|
const categoryApps = await fetchWithRetry<AppJson[]>(
|
||||||
cacheBuster(`/${window.apm_store.arch}/${category}/applist.json`),
|
cacheBuster(path),
|
||||||
);
|
);
|
||||||
|
|
||||||
const normalizedApps = (categoryApps || []).map((appJson) => ({
|
const normalizedApps = (categoryApps || []).map((appJson) => ({
|
||||||
@@ -844,6 +904,7 @@ const loadApps = async (onFirstBatch?: () => void) => {
|
|||||||
: appJson.img_urls,
|
: appJson.img_urls,
|
||||||
icons: appJson.icons,
|
icons: appJson.icons,
|
||||||
category: category,
|
category: category,
|
||||||
|
origin: mode as "spark" | "apm",
|
||||||
currentStatus: "not-installed" as const,
|
currentStatus: "not-installed" as const,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,23 @@
|
|||||||
/>
|
/>
|
||||||
</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="flex items-center gap-2">
|
||||||
<div
|
<div
|
||||||
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
class="truncate text-base font-semibold text-slate-900 dark:text-white"
|
||||||
>
|
>
|
||||||
{{ app.name || "" }}
|
{{ app.name || "" }}
|
||||||
</div>
|
</div>
|
||||||
|
<span
|
||||||
|
:class="[
|
||||||
|
'rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm',
|
||||||
|
app.origin === 'spark'
|
||||||
|
? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ app.origin === "spark" ? "Spark" : "APM" }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400">
|
<div class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
{{ app.pkgname || "" }} · {{ app.version || "" }}
|
||||||
</div>
|
</div>
|
||||||
@@ -52,7 +64,12 @@ const loadedIcon = ref(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const iconPath = computed(() => {
|
const iconPath = computed(() => {
|
||||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
props.app.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
return `${APM_STORE_BASE_URL}/${finalArch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const description = computed(() => {
|
const description = computed(() => {
|
||||||
|
|||||||
@@ -36,7 +36,17 @@
|
|||||||
<p class="text-2xl font-bold text-slate-900 dark:text-white">
|
<p class="text-2xl font-bold text-slate-900 dark:text-white">
|
||||||
{{ app?.name || "" }}
|
{{ app?.name || "" }}
|
||||||
</p>
|
</p>
|
||||||
<!-- Close button for mobile layout could be considered here if needed, but for now sticking to desktop layout logic mainly -->
|
<span
|
||||||
|
v-if="app"
|
||||||
|
:class="[
|
||||||
|
'rounded-md px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider shadow-sm',
|
||||||
|
app.origin === 'spark'
|
||||||
|
? 'bg-orange-100 text-orange-600 dark:bg-orange-900/30 dark:text-orange-400'
|
||||||
|
: 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
{{ app.origin === "spark" ? "Spark" : "APM" }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="text-sm text-slate-500 dark:text-slate-400">
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
{{ app?.pkgname || "" }} · {{ app?.version || "" }}
|
{{ app?.pkgname || "" }} · {{ app?.version || "" }}
|
||||||
@@ -269,7 +279,12 @@ const installBtnText = computed(() => {
|
|||||||
});
|
});
|
||||||
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`;
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
props.app.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
return `${APM_STORE_BASE_URL}/${finalArch}/${props.app.category}/${props.app.pkgname}/icon.png`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const downloadCount = ref<string>("");
|
const downloadCount = ref<string>("");
|
||||||
@@ -281,7 +296,12 @@ watch(
|
|||||||
if (newApp) {
|
if (newApp) {
|
||||||
downloadCount.value = "";
|
downloadCount.value = "";
|
||||||
try {
|
try {
|
||||||
const url = `${APM_STORE_BASE_URL}/${window.apm_store.arch}/${newApp.category}/${newApp.pkgname}/download-times.txt`;
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
newApp.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
const url = `${APM_STORE_BASE_URL}/${finalArch}/${newApp.category}/${newApp.pkgname}/download-times.txt`;
|
||||||
const resp = await axios.get(url, { responseType: "text" });
|
const resp = await axios.get(url, { responseType: "text" });
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
downloadCount.value = String(resp.data).trim();
|
downloadCount.value = String(resp.data).trim();
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
<ThemeToggle :theme-mode="themeMode" @toggle="toggleTheme" />
|
||||||
|
<StoreModeSwitcher />
|
||||||
|
|
||||||
<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
|
<button
|
||||||
@@ -88,6 +89,7 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import ThemeToggle from "./ThemeToggle.vue";
|
import ThemeToggle from "./ThemeToggle.vue";
|
||||||
|
import StoreModeSwitcher from "./StoreModeSwitcher.vue";
|
||||||
import amberLogo from "../assets/imgs/spark-store.svg";
|
import amberLogo from "../assets/imgs/spark-store.svg";
|
||||||
|
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
:title="link.more"
|
:title="link.more"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="computedImgUrl(link.imgUrl)"
|
:src="computedImgUrl(link)"
|
||||||
class="h-20 w-full object-contain"
|
class="h-20 w-full object-contain"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
@@ -62,10 +62,14 @@ defineEmits<{
|
|||||||
(e: "open-detail", app: Record<string, unknown>): void;
|
(e: "open-detail", app: Record<string, unknown>): void;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const computedImgUrl = (imgUrl: string) => {
|
const computedImgUrl = (link: Record<string, any>) => {
|
||||||
if (!imgUrl) return "";
|
if (!link.imgUrl) return "";
|
||||||
// imgUrl is like /home/links/bbs.png -> join with base
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
return `${APM_STORE_BASE_URL}/${window.apm_store.arch}${imgUrl}`;
|
const finalArch =
|
||||||
|
link.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
return `${APM_STORE_BASE_URL}/${finalArch}${link.imgUrl}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onLinkClick = (link: Record<string, unknown>) => {
|
const onLinkClick = (link: Record<string, unknown>) => {
|
||||||
|
|||||||
38
src/components/StoreModeSwitcher.vue
Normal file
38
src/components/StoreModeSwitcher.vue
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-2 p-4 rounded-2xl bg-slate-50 dark:bg-slate-800/50 border border-slate-200/70 dark:border-slate-700/70">
|
||||||
|
<span class="text-xs font-semibold uppercase tracking-wider text-slate-500 dark:text-slate-400 px-1">商店模式</span>
|
||||||
|
<div class="grid grid-cols-3 gap-1 p-1 bg-slate-200/50 dark:bg-slate-900/50 rounded-xl">
|
||||||
|
<button
|
||||||
|
v-for="mode in modes"
|
||||||
|
:key="mode.id"
|
||||||
|
type="button"
|
||||||
|
class="flex flex-col items-center justify-center py-2 px-1 rounded-lg text-[10px] font-medium transition-all duration-200"
|
||||||
|
:class="currentStoreMode === mode.id
|
||||||
|
? 'bg-white dark:bg-slate-700 text-brand shadow-sm scale-105 z-10'
|
||||||
|
: 'text-slate-500 dark:text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'"
|
||||||
|
@click="setMode(mode.id as StoreMode)"
|
||||||
|
>
|
||||||
|
<i :class="mode.icon" class="mb-1 text-xs"></i>
|
||||||
|
{{ mode.label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { currentStoreMode } from "../global/storeConfig";
|
||||||
|
import type { StoreMode } from "../global/typedefinition";
|
||||||
|
|
||||||
|
const modes = [
|
||||||
|
{ id: "spark", label: "星火", icon: "fas fa-fire" },
|
||||||
|
{ id: "apm", label: "APM", icon: "fas fa-box-open" },
|
||||||
|
{ id: "hybrid", label: "混合", icon: "fas fa-layer-group" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const setMode = (mode: StoreMode) => {
|
||||||
|
currentStoreMode.value = mode;
|
||||||
|
localStorage.setItem("store_mode", mode);
|
||||||
|
// Reload page to re-fetch data based on new mode
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ref } from "vue";
|
import { ref } from "vue";
|
||||||
import type { App } from "./typedefinition";
|
import type { App, StoreMode } from "./typedefinition";
|
||||||
|
|
||||||
export const APM_STORE_BASE_URL: string =
|
export const APM_STORE_BASE_URL: string =
|
||||||
import.meta.env.VITE_APM_STORE_BASE_URL || "";
|
import.meta.env.VITE_APM_STORE_BASE_URL || "";
|
||||||
@@ -10,3 +10,14 @@ export const APM_STORE_STATS_BASE_URL: string =
|
|||||||
// 下面的变量用于存储当前应用的信息,其实用在多个组件中
|
// 下面的变量用于存储当前应用的信息,其实用在多个组件中
|
||||||
export const currentApp = ref<App | null>(null);
|
export const currentApp = ref<App | null>(null);
|
||||||
export const currentAppIsInstalled = ref(false);
|
export const currentAppIsInstalled = ref(false);
|
||||||
|
|
||||||
|
const initialMode = (localStorage.getItem("store_mode") as StoreMode) || "hybrid";
|
||||||
|
export const currentStoreMode = ref<StoreMode>(initialMode);
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
apm_store: {
|
||||||
|
arch: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ export type DownloadItemStatus =
|
|||||||
| "failed"
|
| "failed"
|
||||||
| "queued"; // 可根据实际状态扩展
|
| "queued"; // 可根据实际状态扩展
|
||||||
|
|
||||||
|
export type StoreMode = "spark" | "apm" | "hybrid";
|
||||||
|
|
||||||
export interface DownloadItem {
|
export interface DownloadItem {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -42,6 +44,7 @@ export interface DownloadItem {
|
|||||||
message: string; // 日志消息
|
message: string; // 日志消息
|
||||||
}>;
|
}>;
|
||||||
source: string; // 例如 'APM Store'
|
source: string; // 例如 'APM Store'
|
||||||
|
origin: "spark" | "apm"; // 数据来源
|
||||||
retry: boolean; // 当前是否为重试下载
|
retry: boolean; // 当前是否为重试下载
|
||||||
upgradeOnly?: boolean; // 是否为仅升级任务
|
upgradeOnly?: boolean; // 是否为仅升级任务
|
||||||
error?: string;
|
error?: string;
|
||||||
@@ -99,6 +102,7 @@ export interface App {
|
|||||||
img_urls: string[];
|
img_urls: string[];
|
||||||
icons: string;
|
icons: string;
|
||||||
category: string; // Frontend added
|
category: string; // Frontend added
|
||||||
|
origin: "spark" | "apm"; // 数据来源
|
||||||
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"
|
||||||
|
|||||||
@@ -33,12 +33,19 @@ export const handleInstall = () => {
|
|||||||
|
|
||||||
downloadIdCounter += 1;
|
downloadIdCounter += 1;
|
||||||
// 创建下载任务
|
// 创建下载任务
|
||||||
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
currentApp.value.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
|
||||||
const download: DownloadItem = {
|
const download: DownloadItem = {
|
||||||
id: downloadIdCounter,
|
id: downloadIdCounter,
|
||||||
name: currentApp.value.name,
|
name: currentApp.value.name,
|
||||||
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}/${finalArch}/${currentApp.value.category}/${currentApp.value.pkgname}/icon.png`,
|
||||||
|
origin: currentApp.value.origin,
|
||||||
status: "queued",
|
status: "queued",
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloadedSize: 0,
|
downloadedSize: 0,
|
||||||
@@ -97,12 +104,18 @@ export const handleUpgrade = (app: App) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
downloadIdCounter += 1;
|
downloadIdCounter += 1;
|
||||||
|
const arch = window.apm_store.arch || "amd64-apm";
|
||||||
|
const finalArch =
|
||||||
|
app.origin === "spark"
|
||||||
|
? arch.replace("-apm", "-store")
|
||||||
|
: arch.replace("-store", "-apm");
|
||||||
|
|
||||||
const download: DownloadItem = {
|
const download: DownloadItem = {
|
||||||
id: downloadIdCounter,
|
id: downloadIdCounter,
|
||||||
name: app.name,
|
name: app.name,
|
||||||
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}/${finalArch}/${app.category}/${app.pkgname}/icon.png`,
|
||||||
status: "queued",
|
status: "queued",
|
||||||
progress: 0,
|
progress: 0,
|
||||||
downloadedSize: 0,
|
downloadedSize: 0,
|
||||||
@@ -114,6 +127,7 @@ export const handleUpgrade = (app: App) => {
|
|||||||
source: "APM Update",
|
source: "APM Update",
|
||||||
retry: false,
|
retry: false,
|
||||||
upgradeOnly: true,
|
upgradeOnly: true,
|
||||||
|
origin: app.origin,
|
||||||
};
|
};
|
||||||
|
|
||||||
downloads.value.push(download);
|
downloads.value.push(download);
|
||||||
@@ -122,7 +136,10 @@ export const handleUpgrade = (app: App) => {
|
|||||||
|
|
||||||
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", {
|
||||||
|
pkgname: currentApp.value.pkgname,
|
||||||
|
origin: currentApp.value.origin,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
window.ipcRenderer.on("remove-complete", (_event, log: DownloadResult) => {
|
window.ipcRenderer.on("remove-complete", (_event, log: DownloadResult) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user