mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-07 04:23:56 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f585be0dcd | ||
|
|
a8ea7bec34 | ||
|
|
b06966c0a7 | ||
|
|
e496449c31 | ||
|
|
0a908a31f6 | ||
|
|
68195a94f6 |
@@ -29,6 +29,9 @@ install:
|
|||||||
mkdir -p $(DESTDIR)/usr/share/icons/
|
mkdir -p $(DESTDIR)/usr/share/icons/
|
||||||
mkdir -p $(DESTDIR)/usr/lib/
|
mkdir -p $(DESTDIR)/usr/lib/
|
||||||
mkdir -p $(DESTDIR)/usr/bin/
|
mkdir -p $(DESTDIR)/usr/bin/
|
||||||
|
mkdir -p $(DESTDIR)/etc/apt/
|
||||||
|
mkdir -p $(DESTDIR)/lib/systemd/
|
||||||
|
mkdir -p $(DESTDIR)/tmp/
|
||||||
cp -rv release/*/linux*-unpacked/* $(DESTDIR)/opt/spark-store/bin/
|
cp -rv release/*/linux*-unpacked/* $(DESTDIR)/opt/spark-store/bin/
|
||||||
cp -rv release/*/linux*-unpacked/extras/* $(DESTDIR)/opt/spark-store/extras/
|
cp -rv release/*/linux*-unpacked/extras/* $(DESTDIR)/opt/spark-store/extras/
|
||||||
cp -rv tool/* $(DESTDIR)/opt/durapps/spark-store/bin/
|
cp -rv tool/* $(DESTDIR)/opt/durapps/spark-store/bin/
|
||||||
@@ -39,5 +42,11 @@ install:
|
|||||||
cp -rv pkg/usr/share/applications/ $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/applications/ $(DESTDIR)/usr/share/
|
||||||
cp -rv pkg/usr/share/polkit-1 $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/polkit-1 $(DESTDIR)/usr/share/
|
||||||
cp -rv pkg/usr/share/aptss $(DESTDIR)/usr/share/
|
cp -rv pkg/usr/share/aptss $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/ssinstall/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/ssinstall-local/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/dsg/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/usr/share/bash-completion/ $(DESTDIR)/usr/share/
|
||||||
|
cp -rv pkg/etc/apt/ $(DESTDIR)/etc/
|
||||||
|
cp -rv pkg/tmp/spark-store-install/ $(DESTDIR)/tmp/
|
||||||
cp -rv tool/spark-store.asc $(DESTDIR)/opt/durapps/spark-store/bin/
|
cp -rv tool/spark-store.asc $(DESTDIR)/opt/durapps/spark-store/bin/
|
||||||
ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
|
ln -s ../../../spark-store/extras/spark-store $(DESTDIR)/opt/durapps/spark-store/bin/spark-store
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
spark-store (5.2.0) UNRELEASED; urgency=medium
|
spark-store (5.2.1) UNRELEASED; urgency=medium
|
||||||
|
|
||||||
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
|
|||||||
@@ -45,25 +45,6 @@ interface OssUploadMetadata {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const categoryNameToIdMap: Record<string, number> = {
|
|
||||||
network: 3,
|
|
||||||
chat: 9,
|
|
||||||
music: 2,
|
|
||||||
video: 12,
|
|
||||||
image_graphics: 6,
|
|
||||||
games: 1,
|
|
||||||
office: 4,
|
|
||||||
reading: 8,
|
|
||||||
development: 7,
|
|
||||||
tools: 11,
|
|
||||||
themes: 10,
|
|
||||||
others: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCategoryIdByName(categoryName: string): number {
|
|
||||||
return categoryNameToIdMap[categoryName];
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateUUID(): string {
|
function generateUUID(): string {
|
||||||
const hexChars = "0123456789abcdef";
|
const hexChars = "0123456789abcdef";
|
||||||
let uuid = "";
|
let uuid = "";
|
||||||
@@ -660,15 +641,7 @@ export function registerSubmitterHandlers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(
|
const FALLBACK_CATEGORIES = [
|
||||||
"search-history-app",
|
|
||||||
async (_event, pkgname: string, useMirror = false) => {
|
|
||||||
try {
|
|
||||||
const baseUrl = useMirror
|
|
||||||
? "https://mirrors.sdu.edu.cn/spark-store"
|
|
||||||
: "https://spk-json.spark-app.store";
|
|
||||||
const storeArchs = ["store", "aarch64-store", "loong64-store"];
|
|
||||||
const categories = [
|
|
||||||
"chat",
|
"chat",
|
||||||
"development",
|
"development",
|
||||||
"games",
|
"games",
|
||||||
@@ -682,21 +655,99 @@ export function registerSubmitterHandlers(
|
|||||||
"tools",
|
"tools",
|
||||||
"video",
|
"video",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
async function fetchCategoriesFromCdn(
|
||||||
|
baseUrl: string,
|
||||||
|
arch: string,
|
||||||
|
): Promise<string[]> {
|
||||||
|
const url = `${baseUrl}/${arch}/categories.json`;
|
||||||
|
logger.info({ url }, "[Submitter] Fetching categories from CDN");
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: { "User-Agent": getUserAgent() },
|
||||||
|
});
|
||||||
|
if (!response.ok) {
|
||||||
|
logger.warn(
|
||||||
|
{ status: response.status, arch },
|
||||||
|
"[Submitter] Failed to fetch categories.json, using fallback",
|
||||||
|
);
|
||||||
|
return FALLBACK_CATEGORIES;
|
||||||
|
}
|
||||||
|
const json = await response.json();
|
||||||
|
if (json && typeof json === "object" && !Array.isArray(json)) {
|
||||||
|
const keys = Object.keys(json);
|
||||||
|
logger.info(
|
||||||
|
{ arch, categories: keys, count: keys.length },
|
||||||
|
"[Submitter] Categories loaded from CDN",
|
||||||
|
);
|
||||||
|
return keys.length > 0 ? keys : FALLBACK_CATEGORIES;
|
||||||
|
}
|
||||||
|
logger.warn(
|
||||||
|
{ arch },
|
||||||
|
"[Submitter] Unexpected categories.json format, using fallback",
|
||||||
|
);
|
||||||
|
return FALLBACK_CATEGORIES;
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ err, arch },
|
||||||
|
"[Submitter] Error fetching categories.json, using fallback",
|
||||||
|
);
|
||||||
|
return FALLBACK_CATEGORIES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.handle(
|
||||||
|
"search-history-app",
|
||||||
|
async (_event, pkgname: string, baseUrl?: string) => {
|
||||||
|
try {
|
||||||
|
let resolvedBaseUrl = baseUrl || "https://erotica.spark-app.store";
|
||||||
|
// 开发模式下来自 Vite 代理的路径(如 /local_amd64-store),
|
||||||
|
// 主进程的 fetch() 无法解析相对 URL,需要转为实际地址
|
||||||
|
if (resolvedBaseUrl.startsWith("/")) {
|
||||||
|
logger.info(
|
||||||
|
{ devPath: resolvedBaseUrl },
|
||||||
|
"[Submitter] Dev proxy path detected, resolving to production URL",
|
||||||
|
);
|
||||||
|
resolvedBaseUrl = "https://erotica.spark-app.store";
|
||||||
|
}
|
||||||
|
const storeArchs = [
|
||||||
|
"amd64-store",
|
||||||
|
"arm64-store",
|
||||||
|
"loong64-store",
|
||||||
|
"amd64-apm",
|
||||||
|
"arm64-apm",
|
||||||
|
"loong64-apm",
|
||||||
|
];
|
||||||
const results: HistoryAppInfo[] = [];
|
const results: HistoryAppInfo[] = [];
|
||||||
|
|
||||||
|
// 为每个架构目录获取各自的分类列表(不同目录可能有不同分类,如 apm-extensions 只在 *-apm 下有)
|
||||||
|
const archCategoriesMap = new Map<string, string[]>();
|
||||||
|
for (const arch of storeArchs) {
|
||||||
|
const cats = await fetchCategoriesFromCdn(resolvedBaseUrl, arch);
|
||||||
|
archCategoriesMap.set(arch, cats);
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== SEARCH HISTORY APP START ==============",
|
"[Submitter] ============== SEARCH HISTORY APP START ==============",
|
||||||
);
|
);
|
||||||
logger.info(
|
logger.info(
|
||||||
{ pkgname, useMirror, baseUrl, storeArchs, categories },
|
{
|
||||||
|
pkgname,
|
||||||
|
baseUrl: resolvedBaseUrl,
|
||||||
|
storeArchs,
|
||||||
|
archCategoryCounts: Array.from(archCategoriesMap.entries()).map(
|
||||||
|
([a, c]) => ({ arch: a, categoryCount: c.length }),
|
||||||
|
),
|
||||||
|
},
|
||||||
"[Submitter] Search parameters",
|
"[Submitter] Search parameters",
|
||||||
);
|
);
|
||||||
|
|
||||||
const allPromises: Promise<void>[] = [];
|
const allPromises: Promise<void>[] = [];
|
||||||
|
|
||||||
for (const arch of storeArchs) {
|
for (const arch of storeArchs) {
|
||||||
|
const categories = archCategoriesMap.get(arch) || FALLBACK_CATEGORIES;
|
||||||
for (const category of categories) {
|
for (const category of categories) {
|
||||||
const url = `${baseUrl}/${arch}/${category}/${pkgname}/app.json`;
|
const url = `${resolvedBaseUrl}/${arch}/${category}/${pkgname}/app.json`;
|
||||||
logger.info(
|
logger.info(
|
||||||
{ arch, category, url },
|
{ arch, category, url },
|
||||||
"[Submitter] Starting search request",
|
"[Submitter] Starting search request",
|
||||||
@@ -735,7 +786,7 @@ export function registerSubmitterHandlers(
|
|||||||
"[Submitter] Found matching item",
|
"[Submitter] Found matching item",
|
||||||
);
|
);
|
||||||
|
|
||||||
let iconUrl = json.icons || json.icon || "";
|
const iconUrl = json.icons || json.icon || "";
|
||||||
let imgs = json.imgUrls || json.imgs || json.img_urls || [];
|
let imgs = json.imgUrls || json.imgs || json.img_urls || [];
|
||||||
|
|
||||||
if (typeof imgs === "string") {
|
if (typeof imgs === "string") {
|
||||||
@@ -760,27 +811,6 @@ export function registerSubmitterHandlers(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iconUrl && typeof iconUrl === "string") {
|
|
||||||
if (useMirror) {
|
|
||||||
iconUrl = iconUrl.replace(
|
|
||||||
"spk-json.spark-app.store",
|
|
||||||
"mirrors.sdu.edu.cn/spark-store",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Array.isArray(imgs)) {
|
|
||||||
imgs = imgs.map((img: string) => {
|
|
||||||
if (useMirror && typeof img === "string") {
|
|
||||||
return img.replace(
|
|
||||||
"spk-json.spark-app.store",
|
|
||||||
"mirrors.sdu.edu.cn/spark-store",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return img;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
results.push({
|
results.push({
|
||||||
id: json.id || json.Id || 0,
|
id: json.id || json.Id || 0,
|
||||||
name: json.name || json.Name || "",
|
name: json.name || json.Name || "",
|
||||||
@@ -802,11 +832,24 @@ export function registerSubmitterHandlers(
|
|||||||
"[Submitter] Added to results",
|
"[Submitter] Added to results",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
{
|
||||||
|
arch,
|
||||||
|
category,
|
||||||
|
url,
|
||||||
|
status: response.status,
|
||||||
|
statusText: response.statusText,
|
||||||
|
},
|
||||||
|
"[Submitter] Non-ok response",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
const errMsg =
|
||||||
|
error instanceof Error ? error.message : String(error);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
{ arch, category, error },
|
{ arch, category, url, error: errMsg },
|
||||||
"[Submitter] Request failed or exception caught",
|
"[Submitter] Request failed or exception caught",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -1083,7 +1126,33 @@ export function registerSubmitterHandlers(
|
|||||||
);
|
);
|
||||||
let iconFilePath = iconPath;
|
let iconFilePath = iconPath;
|
||||||
|
|
||||||
if (iconPath.startsWith("http://") || iconPath.startsWith("https://")) {
|
if (iconPath.startsWith("data:")) {
|
||||||
|
logger.info("[Submitter] Icon is a Base64 data URL, decoding");
|
||||||
|
const base64Data = iconPath.split(",")[1];
|
||||||
|
if (!base64Data) {
|
||||||
|
return { success: false, message: "图标数据格式错误" };
|
||||||
|
}
|
||||||
|
const tempDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "spark-store-submitter-"),
|
||||||
|
);
|
||||||
|
iconFilePath = path.join(tempDir, "icon.png");
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(iconFilePath, Buffer.from(base64Data, "base64"));
|
||||||
|
logger.info(
|
||||||
|
{ iconFilePath },
|
||||||
|
"[Submitter] Icon decoded from data URL",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error({ err }, "[Submitter] Failed to decode icon data URL");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `图标解码失败: ${(err as Error).message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
iconPath.startsWith("http://") ||
|
||||||
|
iconPath.startsWith("https://")
|
||||||
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ iconPath },
|
{ iconPath },
|
||||||
"[Submitter] Icon is a remote URL, downloading first",
|
"[Submitter] Icon is a remote URL, downloading first",
|
||||||
@@ -1140,7 +1209,8 @@ export function registerSubmitterHandlers(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
iconPath.startsWith("http://") ||
|
iconPath.startsWith("http://") ||
|
||||||
iconPath.startsWith("https://")
|
iconPath.startsWith("https://") ||
|
||||||
|
iconPath.startsWith("data:")
|
||||||
) {
|
) {
|
||||||
fs.unlinkSync(iconFilePath);
|
fs.unlinkSync(iconFilePath);
|
||||||
fs.rmdirSync(path.dirname(iconFilePath));
|
fs.rmdirSync(path.dirname(iconFilePath));
|
||||||
@@ -1203,6 +1273,35 @@ export function registerSubmitterHandlers(
|
|||||||
);
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} else if (screenshot.startsWith("data:")) {
|
||||||
|
logger.info(
|
||||||
|
"[Submitter] Screenshot is a Base64 data URL, decoding",
|
||||||
|
);
|
||||||
|
const base64Data = screenshot.split(",")[1];
|
||||||
|
if (!base64Data) {
|
||||||
|
logger.warn("[Submitter] Invalid screenshot data URL, skipping");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const tempDir = fs.mkdtempSync(
|
||||||
|
path.join(os.tmpdir(), "spark-store-submitter-"),
|
||||||
|
);
|
||||||
|
screenshotFilePath = path.join(tempDir, `screen_${i + 1}.png`);
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(
|
||||||
|
screenshotFilePath,
|
||||||
|
Buffer.from(base64Data, "base64"),
|
||||||
|
);
|
||||||
|
logger.info(
|
||||||
|
{ screenshotFilePath },
|
||||||
|
"[Submitter] Screenshot decoded from data URL",
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
logger.error(
|
||||||
|
{ err },
|
||||||
|
"[Submitter] Failed to decode screenshot data URL",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs.existsSync(screenshotFilePath)) {
|
if (fs.existsSync(screenshotFilePath)) {
|
||||||
@@ -1241,7 +1340,8 @@ export function registerSubmitterHandlers(
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
screenshot.startsWith("http://") ||
|
screenshot.startsWith("http://") ||
|
||||||
screenshot.startsWith("https://")
|
screenshot.startsWith("https://") ||
|
||||||
|
screenshot.startsWith("data:")
|
||||||
) {
|
) {
|
||||||
fs.unlinkSync(screenshotFilePath);
|
fs.unlinkSync(screenshotFilePath);
|
||||||
fs.rmdirSync(path.dirname(screenshotFilePath));
|
fs.rmdirSync(path.dirname(screenshotFilePath));
|
||||||
@@ -1294,11 +1394,23 @@ export function registerSubmitterHandlers(
|
|||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== STEP 4: SUBMIT APPLICATION ==============",
|
"[Submitter] ============== STEP 4: SUBMIT APPLICATION ==============",
|
||||||
);
|
);
|
||||||
|
sendUploadProgress("submit", 0, "正在提交投稿信息...");
|
||||||
|
|
||||||
const debFileStat = fs.statSync(debFilePath);
|
const debFileStat = fs.statSync(debFilePath);
|
||||||
|
|
||||||
|
// 参考老 Qt 投稿器:根据 deb 元数据构造 file_name: {pkgname}_{version}_{arch}.deb
|
||||||
|
// arch 由前端解析 deb 时获取并传入,无需再次调用 dpkg-deb
|
||||||
|
const debArch = String(dataObj.arch || "amd64");
|
||||||
|
const pkgVersion = String(dataObj.version || "0.0.0");
|
||||||
|
const debPkgName = String(dataObj.pkgname || "unknown");
|
||||||
|
const formFileName = `${debPkgName}_${pkgVersion}_${debArch}.deb`;
|
||||||
|
logger.info(
|
||||||
|
{ formFileName, debArch },
|
||||||
|
"[Submitter] Constructed file_name",
|
||||||
|
);
|
||||||
|
|
||||||
const categoryName = String(dataObj.category || "");
|
const categoryName = String(dataObj.category || "");
|
||||||
const categoryId = getCategoryIdByName(categoryName);
|
const categoryId = Number(dataObj.categoryId) || 0;
|
||||||
logger.info(
|
logger.info(
|
||||||
{ categoryName, categoryId },
|
{ categoryName, categoryId },
|
||||||
"[Submitter] Category name and ID",
|
"[Submitter] Category name and ID",
|
||||||
@@ -1313,55 +1425,80 @@ export function registerSubmitterHandlers(
|
|||||||
: [];
|
: [];
|
||||||
logger.info({ tagsString, tagsArray }, "[Submitter] Tags conversion");
|
logger.info({ tagsString, tagsArray }, "[Submitter] Tags conversion");
|
||||||
|
|
||||||
const submitData = {
|
const remark =
|
||||||
application_name: String(dataObj.pkgname || ""),
|
((dataObj.remark as string) || "") +
|
||||||
|
" - (来自于投稿器_v" +
|
||||||
|
getAppVersion() +
|
||||||
|
")";
|
||||||
|
|
||||||
|
const submitData: Record<string, unknown> = {
|
||||||
|
application_name: debPkgName,
|
||||||
application_name_zh: String(dataObj.name || ""),
|
application_name_zh: String(dataObj.name || ""),
|
||||||
contributor: String(dataObj.contributor || ""),
|
contributor: String(dataObj.contributor || ""),
|
||||||
icons: iconUrl,
|
icons: iconUrl,
|
||||||
size: debFileStat.size,
|
size: debFileStat.size,
|
||||||
file_name: path.basename(debFilePath).replace(/\s+/g, "_plus_"),
|
file_name: formFileName,
|
||||||
website: String(dataObj.website || ""),
|
website: String(dataObj.website || ""),
|
||||||
version: String(dataObj.version || ""),
|
version: pkgVersion,
|
||||||
more: String(dataObj.description || ""),
|
more: String(dataObj.description || ""),
|
||||||
type_id: categoryId,
|
type_id: categoryId,
|
||||||
author: String(dataObj.author || ""),
|
author: String(dataObj.author || ""),
|
||||||
remark:
|
remark,
|
||||||
((dataObj.remark as string) || "") +
|
|
||||||
" - (来自于投稿器_v" +
|
|
||||||
getAppVersion() +
|
|
||||||
")",
|
|
||||||
img_urls: screenshotUrls,
|
img_urls: screenshotUrls,
|
||||||
deb_url: debUrl,
|
deb_url: debUrl,
|
||||||
mail: String(dataObj.mail || dataObj.contributor || ""),
|
mail: String(dataObj.mail || dataObj.contributor || ""),
|
||||||
tags: tagsArray,
|
tags: tagsArray,
|
||||||
|
architecture: debArch,
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== VALIDATING SUBMISSION DATA ==============",
|
"[Submitter] ============== VALIDATING SUBMISSION DATA ==============",
|
||||||
);
|
);
|
||||||
const requiredFields = [
|
// 对齐老 Qt 投稿器 isReadySubmit() 的完整校验
|
||||||
"application_name",
|
const checks: Array<{ field: keyof typeof submitData; label: string }> = [
|
||||||
"application_name_zh",
|
{ field: "application_name", label: "包名" },
|
||||||
"contributor",
|
{ field: "application_name_zh", label: "应用名称" },
|
||||||
"icons",
|
{ field: "contributor", label: "贡献者" },
|
||||||
"size",
|
{ field: "icons", label: "图标URL" },
|
||||||
"file_name",
|
{ field: "size", label: "文件大小" },
|
||||||
"version",
|
{ field: "file_name", label: "文件名" },
|
||||||
"type_id",
|
{ field: "website", label: "官网地址" },
|
||||||
"author",
|
{ field: "version", label: "版本号" },
|
||||||
"deb_url",
|
{ field: "more", label: "应用描述" },
|
||||||
|
{ field: "type_id", label: "分类ID" },
|
||||||
|
{ field: "author", label: "作者" },
|
||||||
|
{ field: "remark", label: "测试情况" },
|
||||||
|
{ field: "deb_url", label: "安装包URL" },
|
||||||
|
{ field: "mail", label: "联系邮箱" },
|
||||||
];
|
];
|
||||||
const missingFields = requiredFields.filter(
|
const missingFields: string[] = [];
|
||||||
(field) => !submitData[field as keyof typeof submitData],
|
for (const { field, label } of checks) {
|
||||||
);
|
const val = submitData[field];
|
||||||
|
if (val === undefined || val === null || val === "" || val === 0) {
|
||||||
|
missingFields.push(`${label}(${field})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (screenshotUrls.length === 0) {
|
||||||
|
missingFields.push("截图(img_urls)");
|
||||||
|
}
|
||||||
|
if (tagsArray.length === 0) {
|
||||||
|
missingFields.push("标签(tags)");
|
||||||
|
}
|
||||||
if (missingFields.length > 0) {
|
if (missingFields.length > 0) {
|
||||||
logger.error({ missingFields }, "[Submitter] Missing required fields");
|
logger.error({ missingFields }, "[Submitter] Missing required fields");
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `缺少必填字段: ${missingFields.join(", ")}`,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== PREPARING SUBMISSION REQUEST ==============",
|
"[Submitter] ============== PREPARING SUBMISSION REQUEST ==============",
|
||||||
);
|
);
|
||||||
logger.info({ submitData }, "[Submitter] Final submission data");
|
logger.info({ submitData }, "[Submitter] Final submission data");
|
||||||
|
logger.info(
|
||||||
|
`[Submitter] ============== FULL SUBMIT JSON ==============\n${JSON.stringify(submitData, null, 2)}`,
|
||||||
|
);
|
||||||
|
|
||||||
const submitterApiUrl =
|
const submitterApiUrl =
|
||||||
"https://upload.deepinos.org.cn/api/index/upload_application";
|
"https://upload.deepinos.org.cn/api/index/upload_application";
|
||||||
@@ -1471,6 +1608,22 @@ export function registerSubmitterHandlers(
|
|||||||
result = responseText;
|
result = responseText;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查响应体中是否有业务错误码(部分 API HTTP 200 但业务失败)
|
||||||
|
if (result && typeof result === "object" && !Array.isArray(result)) {
|
||||||
|
const respObj = result as Record<string, unknown>;
|
||||||
|
if (respObj.code !== undefined && respObj.code !== 0) {
|
||||||
|
logger.error(
|
||||||
|
{ respCode: respObj.code, respMsg: respObj.msg },
|
||||||
|
"[Submitter] API returned business error",
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: String(respObj.msg || respObj.message || "提交失败"),
|
||||||
|
apiResponse: result,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"[Submitter] ============== SUBMISSION SUCCESSFUL ==============",
|
"[Submitter] ============== SUBMISSION SUCCESSFUL ==============",
|
||||||
);
|
);
|
||||||
@@ -1479,6 +1632,7 @@ export function registerSubmitterHandlers(
|
|||||||
{ duration: requestDuration },
|
{ duration: requestDuration },
|
||||||
"[Submitter] Total duration (ms)",
|
"[Submitter] Total duration (ms)",
|
||||||
);
|
);
|
||||||
|
sendUploadProgress("submit", 100, "投稿信息提交完成");
|
||||||
|
|
||||||
return { success: true, data: result };
|
return { success: true, data: result };
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "spark-store",
|
"name": "spark-store",
|
||||||
"version": "5.2.0",
|
"version": "5.2.1",
|
||||||
"main": "dist-electron/main/index.js",
|
"main": "dist-electron/main/index.js",
|
||||||
"description": "Client for Spark App Store",
|
"description": "Client for Spark App Store",
|
||||||
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
[Unit]
|
|
||||||
Description=Timer for Spark Update Notifier
|
|
||||||
|
|
||||||
[Timer]
|
|
||||||
# 开机后第一次执行
|
|
||||||
OnBootSec=1min
|
|
||||||
# 每天执行一次
|
|
||||||
OnUnitActiveSec=1d
|
|
||||||
|
|
||||||
[Install]
|
|
||||||
WantedBy=timers.target
|
|
||||||
+10
-11
@@ -2838,16 +2838,11 @@ onMounted(async () => {
|
|||||||
|
|
||||||
await loadTabCategories();
|
await loadTabCategories();
|
||||||
|
|
||||||
// 先启动侧边栏入口预加载,让其请求优先进入网络队列
|
|
||||||
// 避免被「全部应用」的大量并发请求抢占连接池
|
|
||||||
const sidebarPreloadPromise = preloadSidebarTabApps();
|
|
||||||
|
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
homeLoading.value = true;
|
homeLoading.value = true;
|
||||||
|
|
||||||
// 侧边栏入口预加载先启动,全部应用和主页数据随后并行加载
|
// 先加载全部应用,再预加载侧边栏入口
|
||||||
Promise.all([
|
await Promise.all([
|
||||||
sidebarPreloadPromise,
|
|
||||||
loadHome(),
|
loadHome(),
|
||||||
new Promise<void>((resolve) => {
|
new Promise<void>((resolve) => {
|
||||||
loadApps(() => {
|
loadApps(() => {
|
||||||
@@ -2855,12 +2850,16 @@ onMounted(async () => {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
]).then(() => {
|
]);
|
||||||
// 所有数据加载完成后的回调(可选)
|
|
||||||
logger.info("所有应用数据加载完成");
|
// 全部应用加载完成后再预加载侧边栏入口
|
||||||
void maybePromptInstalledSync();
|
preloadSidebarTabApps().then(() => {
|
||||||
|
logger.info("侧边栏入口预加载完成");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
void maybePromptInstalledSync();
|
||||||
|
logger.info("所有应用数据加载完成");
|
||||||
|
|
||||||
// 设置键盘导航
|
// 设置键盘导航
|
||||||
document.addEventListener("keydown", (e) => {
|
document.addEventListener("keydown", (e) => {
|
||||||
if (showPreview.value) {
|
if (showPreview.value) {
|
||||||
|
|||||||
@@ -203,8 +203,8 @@
|
|||||||
<p class="text-slate-600 dark:text-slate-400">
|
<p class="text-slate-600 dark:text-slate-400">
|
||||||
{{ iconPreview ? "点击浏览更换图标" : "点击浏览" }}
|
{{ iconPreview ? "点击浏览更换图标" : "点击浏览" }}
|
||||||
</p>
|
</p>
|
||||||
<p v-if="formData.iconPath" class="mt-2 text-sm text-blue-500">
|
<p v-if="iconFileName" class="mt-2 text-sm text-blue-500">
|
||||||
{{ formData.iconPath.split("/").pop() }}
|
{{ iconFileName }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -384,15 +384,156 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 表单校验提示 -->
|
||||||
<div
|
<div
|
||||||
v-if="submitSuccess"
|
v-if="!isFormValid && hasUserStartedFilling"
|
||||||
class="p-4 bg-green-50 border border-green-200 rounded-lg dark:bg-green-900/20 dark:border-green-800"
|
class="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg dark:bg-amber-900/20 dark:border-amber-800"
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
class="text-sm font-medium text-amber-700 dark:text-amber-400 mb-1.5"
|
||||||
|
>
|
||||||
|
请完善以下信息:
|
||||||
|
</p>
|
||||||
|
<ul class="text-xs text-amber-600 dark:text-amber-500 space-y-0.5">
|
||||||
|
<li v-if="!formData.name.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 应用名称
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.pkgname.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 包名
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.version.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 版本号
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.category.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 分类
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.debFilePath">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 安装包
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.remark.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 测试情况
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.website.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 官网地址
|
||||||
|
</li>
|
||||||
|
<li
|
||||||
|
v-if="
|
||||||
|
formData.website.trim() && !isValidUrl(formData.website.trim())
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 官网地址格式不正确(需以
|
||||||
|
http:// 或 https:// 开头)
|
||||||
|
</li>
|
||||||
|
<li v-if="!formData.tags.trim()">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 标签
|
||||||
|
</li>
|
||||||
|
<li v-if="formData.screenshots.length === 0">
|
||||||
|
<i class="fas fa-times-circle mr-1"></i> 至少上传一张截图
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="isSubmitting || isPackaging"
|
||||||
|
class="p-4 bg-blue-50 border border-blue-200 rounded-lg dark:bg-blue-900/20 dark:border-blue-800"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-3">
|
||||||
|
<i
|
||||||
|
class="fas fa-spinner fa-spin text-blue-500 dark:text-blue-400"
|
||||||
|
></i>
|
||||||
|
<span class="text-blue-700 dark:text-blue-400 font-medium text-sm">
|
||||||
|
{{ isPackaging ? "正在打包..." : uploadStage || "正在提交..." }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div
|
||||||
|
class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2.5"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex items-center gap-2 text-green-700 dark:text-green-400"
|
class="h-2.5 rounded-full transition-all duration-300"
|
||||||
|
:class="isPackaging ? 'bg-emerald-500' : 'bg-blue-500'"
|
||||||
|
:style="{ width: uploadProgress + '%' }"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<p class="text-xs text-slate-500 dark:text-slate-400">
|
||||||
|
{{ uploadStageMessage || "准备中..." }}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
v-if="uploadStages.length > 0 && !isPackaging"
|
||||||
|
class="mt-3 space-y-1.5"
|
||||||
>
|
>
|
||||||
<i class="fas fa-check-circle"></i>
|
<div
|
||||||
<span>投稿提交成功!我们会尽快审核你的应用。</span>
|
v-for="stage in uploadStages"
|
||||||
|
:key="stage.stage"
|
||||||
|
class="flex items-center gap-2 text-xs"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
v-if="stage.progress >= 100"
|
||||||
|
class="fas fa-check-circle text-green-500"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else-if="stage.progress > 0"
|
||||||
|
class="fas fa-spinner fa-spin text-blue-500"
|
||||||
|
></i>
|
||||||
|
<i
|
||||||
|
v-else
|
||||||
|
class="far fa-circle text-slate-300 dark:text-slate-600"
|
||||||
|
></i>
|
||||||
|
<span
|
||||||
|
:class="
|
||||||
|
stage.progress >= 100
|
||||||
|
? 'text-green-600 dark:text-green-400'
|
||||||
|
: stage.progress > 0
|
||||||
|
? 'text-blue-600 dark:text-blue-400'
|
||||||
|
: 'text-slate-400 dark:text-slate-500'
|
||||||
|
"
|
||||||
|
>{{ stage.label }}</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
v-if="stage.progress > 0 && stage.progress < 100"
|
||||||
|
class="text-slate-400"
|
||||||
|
>{{ Math.floor(stage.progress) }}%</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="showSubmitSuccessModal" class="relative">
|
||||||
|
<div
|
||||||
|
class="p-6 bg-white dark:bg-slate-900 rounded-xl border border-slate-200 dark:border-slate-700 shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="flex items-center gap-3 mb-4">
|
||||||
|
<div
|
||||||
|
class="w-10 h-10 rounded-full bg-green-100 dark:bg-green-900/30 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<i class="fas fa-check-circle text-green-500 text-xl"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-slate-900 dark:text-slate-100">
|
||||||
|
投稿提交成功!
|
||||||
|
</h3>
|
||||||
|
<p class="text-sm text-slate-500 dark:text-slate-400">
|
||||||
|
我们会尽快审核你的应用
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg bg-blue-500 text-white font-medium hover:bg-blue-600 transition-colors"
|
||||||
|
@click="closeSubmitSuccessModal"
|
||||||
|
>
|
||||||
|
关闭
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="flex-1 px-4 py-2 rounded-lg border border-slate-200 bg-white text-slate-700 dark:bg-slate-800 dark:border-slate-700 dark:text-slate-300 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
|
||||||
|
@click="resetFormAndClose"
|
||||||
|
>
|
||||||
|
继续投递
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -559,7 +700,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, reactive, computed } from "vue";
|
import { ref, reactive, computed, watch } from "vue";
|
||||||
|
import { APM_STORE_BASE_URL } from "@/global/storeConfig";
|
||||||
|
|
||||||
interface HistoryArchInfo {
|
interface HistoryArchInfo {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -595,12 +737,23 @@ const formData = reactive({
|
|||||||
description: "",
|
description: "",
|
||||||
tags: "",
|
tags: "",
|
||||||
category: "",
|
category: "",
|
||||||
|
categoryId: 0,
|
||||||
remark: "",
|
remark: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 当 category 变化时,从 categoriesList 中同步 categoryId
|
||||||
|
watch(
|
||||||
|
() => formData.category,
|
||||||
|
(val) => {
|
||||||
|
const found = categoriesList.value.find((c) => c.value === val);
|
||||||
|
formData.categoryId = found?.id ?? 0;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const isSubmitting = ref(false);
|
const isSubmitting = ref(false);
|
||||||
const submitSuccess = ref(false);
|
const submitSuccess = ref(false);
|
||||||
const submitError = ref("");
|
const submitError = ref("");
|
||||||
|
const showSubmitSuccessModal = ref(false);
|
||||||
const isParsingDeb = ref(false);
|
const isParsingDeb = ref(false);
|
||||||
const debParseError = ref("");
|
const debParseError = ref("");
|
||||||
const isSearchingHistory = ref(false);
|
const isSearchingHistory = ref(false);
|
||||||
@@ -608,6 +761,14 @@ const showArchDialog = ref(false);
|
|||||||
const availableArchs = ref<HistoryArchInfo[]>([]);
|
const availableArchs = ref<HistoryArchInfo[]>([]);
|
||||||
const currentDebArch = ref("");
|
const currentDebArch = ref("");
|
||||||
const iconPreview = ref("");
|
const iconPreview = ref("");
|
||||||
|
const iconFileName = ref("");
|
||||||
|
|
||||||
|
const uploadProgress = ref(0);
|
||||||
|
const uploadStage = ref("");
|
||||||
|
const uploadStageMessage = ref("");
|
||||||
|
const uploadStages = ref<{ stage: string; label: string; progress: number }[]>(
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
const isLoadingCategories = ref(false);
|
const isLoadingCategories = ref(false);
|
||||||
const categoriesLoadError = ref("");
|
const categoriesLoadError = ref("");
|
||||||
@@ -634,23 +795,6 @@ interface Category {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 服务器分类 id → 英文标识符映射(与后端 submitter.ts 的 categoryNameToIdMap 反向)
|
|
||||||
// 历史记录中 category 字段存储的就是这些英文标识符
|
|
||||||
const categoryIdToValueMap: Record<number, string> = {
|
|
||||||
1: "games",
|
|
||||||
2: "music",
|
|
||||||
3: "network",
|
|
||||||
4: "office",
|
|
||||||
5: "others",
|
|
||||||
6: "image_graphics",
|
|
||||||
7: "development",
|
|
||||||
8: "reading",
|
|
||||||
9: "chat",
|
|
||||||
10: "themes",
|
|
||||||
11: "tools",
|
|
||||||
12: "video",
|
|
||||||
};
|
|
||||||
|
|
||||||
interface Tag {
|
interface Tag {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
@@ -661,13 +805,55 @@ const tagsList = ref<Tag[]>([]);
|
|||||||
const selectedTags = ref<Tag[]>([]);
|
const selectedTags = ref<Tag[]>([]);
|
||||||
const selectedTagValue = ref("");
|
const selectedTagValue = ref("");
|
||||||
|
|
||||||
|
const isValidUrl = (url: string): boolean => {
|
||||||
|
if (!url.trim()) return false;
|
||||||
|
try {
|
||||||
|
const parsed = new URL(url);
|
||||||
|
return parsed.protocol === "http:" || parsed.protocol === "https:";
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isFormValid = computed(() => {
|
const isFormValid = computed(() => {
|
||||||
return (
|
const hasRequiredFields =
|
||||||
formData.name.trim() &&
|
formData.name.trim() &&
|
||||||
formData.pkgname.trim() &&
|
formData.pkgname.trim() &&
|
||||||
formData.version.trim() &&
|
formData.version.trim() &&
|
||||||
formData.category.trim() &&
|
formData.category.trim() &&
|
||||||
formData.debFilePath
|
formData.debFilePath &&
|
||||||
|
formData.remark.trim() &&
|
||||||
|
formData.website.trim() &&
|
||||||
|
formData.tags.trim();
|
||||||
|
|
||||||
|
if (!hasRequiredFields) return false;
|
||||||
|
|
||||||
|
// 官网:必须为有效的 http/https URL
|
||||||
|
if (!isValidUrl(formData.website.trim())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 截图:至少一张,且每张都是有效的 data URL 或 http/https URL
|
||||||
|
if (formData.screenshots.length === 0) return false;
|
||||||
|
for (const screenshot of formData.screenshots) {
|
||||||
|
if (
|
||||||
|
!screenshot.startsWith("data:") &&
|
||||||
|
!screenshot.startsWith("http://") &&
|
||||||
|
!screenshot.startsWith("https://")
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasUserStartedFilling = computed(() => {
|
||||||
|
return (
|
||||||
|
formData.name.trim() ||
|
||||||
|
formData.pkgname.trim() ||
|
||||||
|
formData.debFilePath ||
|
||||||
|
formData.screenshots.length > 0
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -711,12 +897,13 @@ const loadCategoriesList = async (): Promise<void> => {
|
|||||||
data.data.length > 0
|
data.data.length > 0
|
||||||
) {
|
) {
|
||||||
categoriesList.value = data.data.map(
|
categoriesList.value = data.data.map(
|
||||||
(item: { id: number; name: string }, index: number) => ({
|
(
|
||||||
|
item: { id: number; name: string; type?: string },
|
||||||
|
index: number,
|
||||||
|
) => ({
|
||||||
id: typeof item.id === "number" ? item.id : index + 1,
|
id: typeof item.id === "number" ? item.id : index + 1,
|
||||||
// API 只返回中文 name,value 通过 id 反向映射为英文标识符
|
|
||||||
// 英文标识符与历史记录中的 category 字段一致,用于匹配
|
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
value: item.type || String(item.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
categoriesLoadError.value = "";
|
categoriesLoadError.value = "";
|
||||||
@@ -726,10 +913,13 @@ const loadCategoriesList = async (): Promise<void> => {
|
|||||||
);
|
);
|
||||||
} else if (data.code === 0 && Array.isArray(data)) {
|
} else if (data.code === 0 && Array.isArray(data)) {
|
||||||
categoriesList.value = data.map(
|
categoriesList.value = data.map(
|
||||||
(item: { id: number; name: string }, index: number) => ({
|
(
|
||||||
|
item: { id: number; name: string; type?: string },
|
||||||
|
index: number,
|
||||||
|
) => ({
|
||||||
id: typeof item.id === "number" ? item.id : index + 1,
|
id: typeof item.id === "number" ? item.id : index + 1,
|
||||||
name: item.name || "",
|
name: item.name || "",
|
||||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
value: item.type || String(item.id),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
categoriesLoadError.value = "";
|
categoriesLoadError.value = "";
|
||||||
@@ -853,8 +1043,6 @@ const selectDebFile = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const useMirror = ref(true);
|
|
||||||
|
|
||||||
const searchHistoryApp = async () => {
|
const searchHistoryApp = async () => {
|
||||||
console.log(
|
console.log(
|
||||||
"[Submitter] ============== SEARCHING HISTORY INFO ==============",
|
"[Submitter] ============== SEARCHING HISTORY INFO ==============",
|
||||||
@@ -863,7 +1051,6 @@ const searchHistoryApp = async () => {
|
|||||||
"[Submitter] pkgname is not empty, searching history with:",
|
"[Submitter] pkgname is not empty, searching history with:",
|
||||||
formData.pkgname,
|
formData.pkgname,
|
||||||
);
|
);
|
||||||
console.log("[Submitter] Using mirror:", useMirror.value);
|
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[Submitter] Calling IPC: search-history-app with pkgname:",
|
"[Submitter] Calling IPC: search-history-app with pkgname:",
|
||||||
@@ -872,7 +1059,7 @@ const searchHistoryApp = async () => {
|
|||||||
const historyResult = await window.ipcRenderer.invoke(
|
const historyResult = await window.ipcRenderer.invoke(
|
||||||
"search-history-app",
|
"search-history-app",
|
||||||
formData.pkgname,
|
formData.pkgname,
|
||||||
useMirror.value,
|
APM_STORE_BASE_URL,
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"[Submitter] Received history search response:",
|
"[Submitter] Received history search response:",
|
||||||
@@ -980,6 +1167,9 @@ const searchHistoryApp = async () => {
|
|||||||
historyResult?.message,
|
historyResult?.message,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
// 未找到历史记录时清空旧数据,避免上一个应用的残留
|
||||||
|
availableArchs.value = [];
|
||||||
|
showArchDialog.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1162,9 +1352,12 @@ const selectArch = async (arch: HistoryArchInfo) => {
|
|||||||
console.log("[Submitter] Tags loaded from history:", selectedTags.value);
|
console.log("[Submitter] Tags loaded from history:", selectedTags.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = useMirror.value
|
// 开发模式下来自 Vite 代理的路径(如 /local_amd64-store),
|
||||||
? `https://mirrors.sdu.edu.cn/spark-store/${arch.store}/${arch.category}/${arch.pkgname}`
|
// 后端 fetch/fs 无法处理,需要解析为实际 CDN 地址
|
||||||
: `https://spk-json.spark-app.store/${arch.store}/${arch.category}/${arch.pkgname}`;
|
const resolvedBaseUrl = APM_STORE_BASE_URL.startsWith("/")
|
||||||
|
? "https://erotica.spark-app.store"
|
||||||
|
: APM_STORE_BASE_URL;
|
||||||
|
const baseUrl = `${resolvedBaseUrl}/${arch.store}/${arch.category}/${arch.pkgname}`;
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
"[Submitter] Building icon and screenshot URLs with baseUrl:",
|
"[Submitter] Building icon and screenshot URLs with baseUrl:",
|
||||||
@@ -1174,6 +1367,7 @@ const selectArch = async (arch: HistoryArchInfo) => {
|
|||||||
if (arch.icon) {
|
if (arch.icon) {
|
||||||
formData.iconPath = `${baseUrl}/icon.png`;
|
formData.iconPath = `${baseUrl}/icon.png`;
|
||||||
iconPreview.value = formData.iconPath;
|
iconPreview.value = formData.iconPath;
|
||||||
|
iconFileName.value = "icon.png";
|
||||||
console.log("[Submitter] Icon URL:", formData.iconPath);
|
console.log("[Submitter] Icon URL:", formData.iconPath);
|
||||||
console.log("[Submitter] Icon preview set:", iconPreview.value);
|
console.log("[Submitter] Icon preview set:", iconPreview.value);
|
||||||
}
|
}
|
||||||
@@ -1198,12 +1392,14 @@ const selectIconFile = () => {
|
|||||||
|
|
||||||
const handleIconFileSelect = (event: Event) => {
|
const handleIconFileSelect = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files?.[0] as File & { path?: string };
|
const file = target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
formData.iconPath = file.path || file.name;
|
iconFileName.value = file.name;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
iconPreview.value = e.target?.result as string;
|
const dataUrl = e.target?.result as string;
|
||||||
|
formData.iconPath = dataUrl;
|
||||||
|
iconPreview.value = dataUrl;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -1211,17 +1407,19 @@ const handleIconFileSelect = (event: Event) => {
|
|||||||
|
|
||||||
const handleIconDrop = (event: DragEvent) => {
|
const handleIconDrop = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.dataTransfer?.files?.[0] as File & { path?: string };
|
const file = event.dataTransfer?.files?.[0];
|
||||||
if (
|
if (
|
||||||
file &&
|
file &&
|
||||||
(file.name.endsWith(".png") ||
|
(file.name.endsWith(".png") ||
|
||||||
file.name.endsWith(".jpg") ||
|
file.name.endsWith(".jpg") ||
|
||||||
file.name.endsWith(".jpeg"))
|
file.name.endsWith(".jpeg"))
|
||||||
) {
|
) {
|
||||||
formData.iconPath = file.path || file.name;
|
iconFileName.value = file.name;
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
iconPreview.value = e.target?.result as string;
|
const dataUrl = e.target?.result as string;
|
||||||
|
formData.iconPath = dataUrl;
|
||||||
|
iconPreview.value = dataUrl;
|
||||||
};
|
};
|
||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
@@ -1273,6 +1471,7 @@ const resetForm = () => {
|
|||||||
formData.description = "";
|
formData.description = "";
|
||||||
formData.tags = "";
|
formData.tags = "";
|
||||||
formData.category = "";
|
formData.category = "";
|
||||||
|
formData.categoryId = 0;
|
||||||
formData.remark = "";
|
formData.remark = "";
|
||||||
submitSuccess.value = false;
|
submitSuccess.value = false;
|
||||||
submitError.value = "";
|
submitError.value = "";
|
||||||
@@ -1280,6 +1479,28 @@ const resetForm = () => {
|
|||||||
packageSuccess.value = false;
|
packageSuccess.value = false;
|
||||||
packageError.value = "";
|
packageError.value = "";
|
||||||
packageResult.value = null;
|
packageResult.value = null;
|
||||||
|
iconFileName.value = "";
|
||||||
|
iconPreview.value = "";
|
||||||
|
uploadProgress.value = 0;
|
||||||
|
uploadStage.value = "";
|
||||||
|
uploadStageMessage.value = "";
|
||||||
|
uploadStages.value = [];
|
||||||
|
selectedTags.value = [];
|
||||||
|
selectedTagValue.value = "";
|
||||||
|
availableArchs.value = [];
|
||||||
|
showArchDialog.value = false;
|
||||||
|
currentDebArch.value = "";
|
||||||
|
isSearchingHistory.value = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeSubmitSuccessModal = () => {
|
||||||
|
showSubmitSuccessModal.value = false;
|
||||||
|
resetForm();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resetFormAndClose = () => {
|
||||||
|
showSubmitSuccessModal.value = false;
|
||||||
|
resetForm();
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
@@ -1289,6 +1510,45 @@ const submitForm = async () => {
|
|||||||
submitSuccess.value = false;
|
submitSuccess.value = false;
|
||||||
submitError.value = "";
|
submitError.value = "";
|
||||||
|
|
||||||
|
uploadProgress.value = 0;
|
||||||
|
uploadStage.value = "准备提交...";
|
||||||
|
uploadStageMessage.value = "正在准备...";
|
||||||
|
uploadStages.value = [
|
||||||
|
{ stage: "icon", label: "上传图标", progress: 0 },
|
||||||
|
{ stage: "screenshots", label: "上传截图", progress: 0 },
|
||||||
|
{ stage: "deb", label: "上传安装包", progress: 0 },
|
||||||
|
{ stage: "submit", label: "提交信息", progress: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const progressListener = (
|
||||||
|
_event: unknown,
|
||||||
|
data: { step: string; progress: number; message: string },
|
||||||
|
) => {
|
||||||
|
let stageIndex: number;
|
||||||
|
if (data.step.startsWith("screenshot")) {
|
||||||
|
stageIndex = uploadStages.value.findIndex(
|
||||||
|
(s) => s.stage === "screenshots",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
stageIndex = uploadStages.value.findIndex((s) => s.stage === data.step);
|
||||||
|
}
|
||||||
|
if (stageIndex !== -1) {
|
||||||
|
uploadStages.value[stageIndex].progress = Math.max(
|
||||||
|
uploadStages.value[stageIndex].progress,
|
||||||
|
data.progress,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (data.progress >= 100 && !data.step.startsWith("screenshot")) {
|
||||||
|
uploadProgress.value = data.progress;
|
||||||
|
} else {
|
||||||
|
uploadProgress.value = Math.max(uploadProgress.value, data.progress);
|
||||||
|
}
|
||||||
|
uploadStage.value = data.message;
|
||||||
|
uploadStageMessage.value = data.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ipcRenderer.on("submit-upload-progress", progressListener);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const submitData = {
|
const submitData = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
@@ -1304,7 +1564,9 @@ const submitForm = async () => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
tags: formData.tags,
|
tags: formData.tags,
|
||||||
category: formData.category,
|
category: formData.category,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
|
arch: currentDebArch.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
||||||
@@ -1321,14 +1583,19 @@ const submitForm = async () => {
|
|||||||
const result = await window.ipcRenderer.invoke("submit-app", submitData);
|
const result = await window.ipcRenderer.invoke("submit-app", submitData);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
|
uploadStages.value.forEach((s) => (s.progress = 100));
|
||||||
|
uploadProgress.value = 100;
|
||||||
|
uploadStage.value = "提交完成";
|
||||||
|
uploadStageMessage.value = "投稿提交成功!";
|
||||||
submitSuccess.value = true;
|
submitSuccess.value = true;
|
||||||
resetForm();
|
showSubmitSuccessModal.value = true;
|
||||||
} else {
|
} else {
|
||||||
submitError.value = result?.message || "提交失败";
|
submitError.value = result?.message || "提交失败";
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
submitError.value = (error as Error)?.message || "提交失败";
|
submitError.value = (error as Error)?.message || "提交失败";
|
||||||
} finally {
|
} finally {
|
||||||
|
window.ipcRenderer.off("submit-upload-progress", progressListener);
|
||||||
isSubmitting.value = false;
|
isSubmitting.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1346,6 +1613,35 @@ const packageApp = async (storeArch: string) => {
|
|||||||
packageError.value = "";
|
packageError.value = "";
|
||||||
packageResult.value = null;
|
packageResult.value = null;
|
||||||
|
|
||||||
|
uploadProgress.value = 0;
|
||||||
|
uploadStage.value = "准备打包...";
|
||||||
|
uploadStageMessage.value = "正在准备...";
|
||||||
|
uploadStages.value = [
|
||||||
|
{ stage: "init", label: "创建临时目录", progress: 0 },
|
||||||
|
{ stage: "icon", label: "处理图标", progress: 0 },
|
||||||
|
{ stage: "screenshots", label: "处理截图", progress: 0 },
|
||||||
|
{ stage: "deb", label: "复制安装包", progress: 0 },
|
||||||
|
{ stage: "json", label: "生成配置文件", progress: 0 },
|
||||||
|
{ stage: "tar", label: "打包压缩", progress: 0 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const progressListener = (
|
||||||
|
_event: unknown,
|
||||||
|
data: { step: string; progress: number; message: string },
|
||||||
|
) => {
|
||||||
|
const stageIndex = uploadStages.value.findIndex(
|
||||||
|
(s) => s.stage === data.step,
|
||||||
|
);
|
||||||
|
if (stageIndex !== -1) {
|
||||||
|
uploadStages.value[stageIndex].progress = data.progress;
|
||||||
|
}
|
||||||
|
uploadProgress.value = data.progress;
|
||||||
|
uploadStage.value = data.step;
|
||||||
|
uploadStageMessage.value = data.message;
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ipcRenderer.on("package-progress", progressListener);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const packageData = {
|
const packageData = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
@@ -1361,6 +1657,7 @@ const packageApp = async (storeArch: string) => {
|
|||||||
description: formData.description,
|
description: formData.description,
|
||||||
tags: formData.tags,
|
tags: formData.tags,
|
||||||
category: formData.category,
|
category: formData.category,
|
||||||
|
categoryId: formData.categoryId,
|
||||||
remark: formData.remark,
|
remark: formData.remark,
|
||||||
storeArch,
|
storeArch,
|
||||||
};
|
};
|
||||||
@@ -1374,6 +1671,9 @@ const packageApp = async (storeArch: string) => {
|
|||||||
const result = await window.ipcRenderer.invoke("package-app", packageData);
|
const result = await window.ipcRenderer.invoke("package-app", packageData);
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
|
uploadStages.value.forEach((s) => (s.progress = 100));
|
||||||
|
uploadProgress.value = 100;
|
||||||
|
uploadStageMessage.value = "打包完成!";
|
||||||
packageSuccess.value = true;
|
packageSuccess.value = true;
|
||||||
packageResult.value = result.data;
|
packageResult.value = result.data;
|
||||||
} else {
|
} else {
|
||||||
@@ -1382,6 +1682,7 @@ const packageApp = async (storeArch: string) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
packageError.value = (error as Error)?.message || "打包失败";
|
packageError.value = (error as Error)?.message || "打包失败";
|
||||||
} finally {
|
} finally {
|
||||||
|
window.ipcRenderer.off("package-progress", progressListener);
|
||||||
isPackaging.value = false;
|
isPackaging.value = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user