mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-06 17:03:56 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8a2bb921b | ||
|
|
ca5e7b98ac | ||
|
|
5a01d95e0f | ||
|
|
37217cd6fb | ||
|
|
61f5cc954c | ||
|
|
7278153ec9 | ||
|
|
2f93ff9727 | ||
|
|
f585be0dcd | ||
|
|
a8ea7bec34 | ||
|
|
b06966c0a7 | ||
|
|
e496449c31 | ||
|
|
0a908a31f6 | ||
|
|
68195a94f6 |
@@ -29,6 +29,9 @@ install:
|
||||
mkdir -p $(DESTDIR)/usr/share/icons/
|
||||
mkdir -p $(DESTDIR)/usr/lib/
|
||||
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/extras/* $(DESTDIR)/opt/spark-store/extras/
|
||||
cp -rv tool/* $(DESTDIR)/opt/durapps/spark-store/bin/
|
||||
@@ -39,5 +42,9 @@ install:
|
||||
cp -rv pkg/usr/share/applications/ $(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/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 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
|
||||
|
||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
||||
spark-store (5.2.0) UNRELEASED; urgency=medium
|
||||
spark-store (5.2.1-0) UNRELEASED; urgency=medium
|
||||
|
||||
* 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 {
|
||||
const hexChars = "0123456789abcdef";
|
||||
let uuid = "";
|
||||
@@ -660,43 +641,113 @@ export function registerSubmitterHandlers(
|
||||
}
|
||||
});
|
||||
|
||||
const FALLBACK_CATEGORIES = [
|
||||
"chat",
|
||||
"development",
|
||||
"games",
|
||||
"image_graphics",
|
||||
"music",
|
||||
"network",
|
||||
"office",
|
||||
"others",
|
||||
"reading",
|
||||
"themes",
|
||||
"tools",
|
||||
"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, useMirror = false) => {
|
||||
async (_event, pkgname: string, baseUrl?: string) => {
|
||||
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",
|
||||
"development",
|
||||
"games",
|
||||
"image_graphics",
|
||||
"music",
|
||||
"network",
|
||||
"office",
|
||||
"others",
|
||||
"reading",
|
||||
"themes",
|
||||
"tools",
|
||||
"video",
|
||||
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[] = [];
|
||||
|
||||
// 为每个架构目录获取各自的分类列表(不同目录可能有不同分类,如 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(
|
||||
"[Submitter] ============== SEARCH HISTORY APP START ==============",
|
||||
);
|
||||
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",
|
||||
);
|
||||
|
||||
const allPromises: Promise<void>[] = [];
|
||||
|
||||
for (const arch of storeArchs) {
|
||||
const categories = archCategoriesMap.get(arch) || FALLBACK_CATEGORIES;
|
||||
for (const category of categories) {
|
||||
const url = `${baseUrl}/${arch}/${category}/${pkgname}/app.json`;
|
||||
const url = `${resolvedBaseUrl}/${arch}/${category}/${pkgname}/app.json`;
|
||||
logger.info(
|
||||
{ arch, category, url },
|
||||
"[Submitter] Starting search request",
|
||||
@@ -735,7 +786,7 @@ export function registerSubmitterHandlers(
|
||||
"[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 || [];
|
||||
|
||||
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({
|
||||
id: json.id || json.Id || 0,
|
||||
name: json.name || json.Name || "",
|
||||
@@ -802,11 +832,24 @@ export function registerSubmitterHandlers(
|
||||
"[Submitter] Added to results",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
logger.warn(
|
||||
{
|
||||
arch,
|
||||
category,
|
||||
url,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
},
|
||||
"[Submitter] Non-ok response",
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
const errMsg =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
logger.warn(
|
||||
{ arch, category, error },
|
||||
{ arch, category, url, error: errMsg },
|
||||
"[Submitter] Request failed or exception caught",
|
||||
);
|
||||
});
|
||||
@@ -1083,7 +1126,33 @@ export function registerSubmitterHandlers(
|
||||
);
|
||||
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(
|
||||
{ iconPath },
|
||||
"[Submitter] Icon is a remote URL, downloading first",
|
||||
@@ -1140,7 +1209,8 @@ export function registerSubmitterHandlers(
|
||||
|
||||
if (
|
||||
iconPath.startsWith("http://") ||
|
||||
iconPath.startsWith("https://")
|
||||
iconPath.startsWith("https://") ||
|
||||
iconPath.startsWith("data:")
|
||||
) {
|
||||
fs.unlinkSync(iconFilePath);
|
||||
fs.rmdirSync(path.dirname(iconFilePath));
|
||||
@@ -1203,6 +1273,35 @@ export function registerSubmitterHandlers(
|
||||
);
|
||||
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)) {
|
||||
@@ -1241,7 +1340,8 @@ export function registerSubmitterHandlers(
|
||||
|
||||
if (
|
||||
screenshot.startsWith("http://") ||
|
||||
screenshot.startsWith("https://")
|
||||
screenshot.startsWith("https://") ||
|
||||
screenshot.startsWith("data:")
|
||||
) {
|
||||
fs.unlinkSync(screenshotFilePath);
|
||||
fs.rmdirSync(path.dirname(screenshotFilePath));
|
||||
@@ -1294,11 +1394,23 @@ export function registerSubmitterHandlers(
|
||||
logger.info(
|
||||
"[Submitter] ============== STEP 4: SUBMIT APPLICATION ==============",
|
||||
);
|
||||
sendUploadProgress("submit", 0, "正在提交投稿信息...");
|
||||
|
||||
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 categoryId = getCategoryIdByName(categoryName);
|
||||
const categoryId = Number(dataObj.categoryId) || 0;
|
||||
logger.info(
|
||||
{ categoryName, categoryId },
|
||||
"[Submitter] Category name and ID",
|
||||
@@ -1313,55 +1425,80 @@ export function registerSubmitterHandlers(
|
||||
: [];
|
||||
logger.info({ tagsString, tagsArray }, "[Submitter] Tags conversion");
|
||||
|
||||
const submitData = {
|
||||
application_name: String(dataObj.pkgname || ""),
|
||||
const remark =
|
||||
((dataObj.remark as string) || "") +
|
||||
" - (来自于投稿器_v" +
|
||||
getAppVersion() +
|
||||
")";
|
||||
|
||||
const submitData: Record<string, unknown> = {
|
||||
application_name: debPkgName,
|
||||
application_name_zh: String(dataObj.name || ""),
|
||||
contributor: String(dataObj.contributor || ""),
|
||||
icons: iconUrl,
|
||||
size: debFileStat.size,
|
||||
file_name: path.basename(debFilePath).replace(/\s+/g, "_plus_"),
|
||||
file_name: formFileName,
|
||||
website: String(dataObj.website || ""),
|
||||
version: String(dataObj.version || ""),
|
||||
version: pkgVersion,
|
||||
more: String(dataObj.description || ""),
|
||||
type_id: categoryId,
|
||||
author: String(dataObj.author || ""),
|
||||
remark:
|
||||
((dataObj.remark as string) || "") +
|
||||
" - (来自于投稿器_v" +
|
||||
getAppVersion() +
|
||||
")",
|
||||
remark,
|
||||
img_urls: screenshotUrls,
|
||||
deb_url: debUrl,
|
||||
mail: String(dataObj.mail || dataObj.contributor || ""),
|
||||
tags: tagsArray,
|
||||
architecture: debArch,
|
||||
};
|
||||
|
||||
logger.info(
|
||||
"[Submitter] ============== VALIDATING SUBMISSION DATA ==============",
|
||||
);
|
||||
const requiredFields = [
|
||||
"application_name",
|
||||
"application_name_zh",
|
||||
"contributor",
|
||||
"icons",
|
||||
"size",
|
||||
"file_name",
|
||||
"version",
|
||||
"type_id",
|
||||
"author",
|
||||
"deb_url",
|
||||
// 对齐老 Qt 投稿器 isReadySubmit() 的完整校验
|
||||
const checks: Array<{ field: keyof typeof submitData; label: string }> = [
|
||||
{ field: "application_name", label: "包名" },
|
||||
{ field: "application_name_zh", label: "应用名称" },
|
||||
{ field: "contributor", label: "贡献者" },
|
||||
{ field: "icons", label: "图标URL" },
|
||||
{ field: "size", label: "文件大小" },
|
||||
{ field: "file_name", label: "文件名" },
|
||||
{ field: "website", label: "官网地址" },
|
||||
{ field: "version", label: "版本号" },
|
||||
{ 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(
|
||||
(field) => !submitData[field as keyof typeof submitData],
|
||||
);
|
||||
const missingFields: string[] = [];
|
||||
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) {
|
||||
logger.error({ missingFields }, "[Submitter] Missing required fields");
|
||||
return {
|
||||
success: false,
|
||||
message: `缺少必填字段: ${missingFields.join(", ")}`,
|
||||
};
|
||||
}
|
||||
|
||||
logger.info(
|
||||
"[Submitter] ============== PREPARING SUBMISSION REQUEST ==============",
|
||||
);
|
||||
logger.info({ submitData }, "[Submitter] Final submission data");
|
||||
logger.info(
|
||||
`[Submitter] ============== FULL SUBMIT JSON ==============\n${JSON.stringify(submitData, null, 2)}`,
|
||||
);
|
||||
|
||||
const submitterApiUrl =
|
||||
"https://upload.deepinos.org.cn/api/index/upload_application";
|
||||
@@ -1471,6 +1608,22 @@ export function registerSubmitterHandlers(
|
||||
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(
|
||||
"[Submitter] ============== SUBMISSION SUCCESSFUL ==============",
|
||||
);
|
||||
@@ -1479,6 +1632,7 @@ export function registerSubmitterHandlers(
|
||||
{ duration: requestDuration },
|
||||
"[Submitter] Total duration (ms)",
|
||||
);
|
||||
sendUploadProgress("submit", 100, "投稿信息提交完成");
|
||||
|
||||
return { success: true, data: result };
|
||||
} catch (err) {
|
||||
|
||||
+37
-23
@@ -256,8 +256,22 @@ const requestApplicationExit = (): void => {
|
||||
app.quit();
|
||||
};
|
||||
|
||||
const showAndFocusMainWindow = (): void => {
|
||||
if (!win || win.isDestroyed()) {
|
||||
createWindow();
|
||||
return;
|
||||
}
|
||||
|
||||
if (win.isMinimized()) {
|
||||
win.restore();
|
||||
}
|
||||
win.show();
|
||||
win.setSkipTaskbar(false);
|
||||
win.focus();
|
||||
};
|
||||
|
||||
async function createWindow() {
|
||||
win = new BrowserWindow({
|
||||
const mainWindow = new BrowserWindow({
|
||||
title: "星火应用商店",
|
||||
width: 1366,
|
||||
height: 768,
|
||||
@@ -274,30 +288,40 @@ async function createWindow() {
|
||||
// contextIsolation: false,
|
||||
},
|
||||
});
|
||||
win = mainWindow;
|
||||
|
||||
if (VITE_DEV_SERVER_URL) {
|
||||
// #298
|
||||
win.loadURL(VITE_DEV_SERVER_URL);
|
||||
mainWindow.loadURL(VITE_DEV_SERVER_URL);
|
||||
// Open devTool if the app is not packaged
|
||||
win.webContents.openDevTools({ mode: "detach" });
|
||||
mainWindow.webContents.openDevTools({ mode: "detach" });
|
||||
} else {
|
||||
win.loadFile(indexHtml);
|
||||
mainWindow.loadFile(indexHtml);
|
||||
}
|
||||
|
||||
// Test actively push message to the Electron-Renderer
|
||||
win.webContents.on("did-finish-load", () => {
|
||||
win?.webContents.send("main-process-message", new Date().toLocaleString());
|
||||
mainWindow.webContents.on("did-finish-load", () => {
|
||||
mainWindow.webContents.send(
|
||||
"main-process-message",
|
||||
new Date().toLocaleString(),
|
||||
);
|
||||
logger.info("Renderer process is ready.");
|
||||
});
|
||||
|
||||
// Make all links open with the browser, not with the application
|
||||
win.webContents.setWindowOpenHandler(({ url }) => {
|
||||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||||
if (url.startsWith("https:")) shell.openExternal(url);
|
||||
return { action: "deny" };
|
||||
});
|
||||
// win.webContents.on('will-navigate', (event, url) => { }) #344
|
||||
|
||||
win.on("close", (event) => {
|
||||
mainWindow.on("closed", () => {
|
||||
if (win === mainWindow) {
|
||||
win = null;
|
||||
}
|
||||
});
|
||||
|
||||
mainWindow.on("close", (event) => {
|
||||
if (allowAppExit) {
|
||||
return;
|
||||
}
|
||||
@@ -476,20 +500,11 @@ app.on("window-all-closed", () => {
|
||||
});
|
||||
|
||||
app.on("second-instance", () => {
|
||||
if (win) {
|
||||
// Focus on the main window if the user tried to open another
|
||||
if (win.isMinimized()) win.restore();
|
||||
win.focus();
|
||||
}
|
||||
showAndFocusMainWindow();
|
||||
});
|
||||
|
||||
app.on("activate", () => {
|
||||
const allWindows = BrowserWindow.getAllWindows();
|
||||
if (allWindows.length) {
|
||||
allWindows[0].focus();
|
||||
} else {
|
||||
createWindow();
|
||||
}
|
||||
showAndFocusMainWindow();
|
||||
});
|
||||
|
||||
app.on("will-quit", () => {
|
||||
@@ -542,7 +557,7 @@ app.whenReady().then(() => {
|
||||
{
|
||||
label: "显示主界面",
|
||||
click: () => {
|
||||
win.show();
|
||||
showAndFocusMainWindow();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -557,12 +572,11 @@ app.whenReady().then(() => {
|
||||
// 双击触发
|
||||
tray.on("click", () => {
|
||||
// 双击通知区图标实现应用的显示或隐藏
|
||||
if (win.isVisible()) {
|
||||
if (win && !win.isDestroyed() && win.isVisible()) {
|
||||
win.hide();
|
||||
win.setSkipTaskbar(true);
|
||||
} else {
|
||||
win.show();
|
||||
win.setSkipTaskbar(false);
|
||||
showAndFocusMainWindow();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "spark-store",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1-0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "spark-store",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1-0",
|
||||
"license": "GPL-3.0",
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.1.18",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "spark-store",
|
||||
"version": "5.2.0",
|
||||
"version": "5.2.1-0",
|
||||
"main": "dist-electron/main/index.js",
|
||||
"description": "Client for Spark App Store",
|
||||
"author": "elysia-best <elysia-best@simplelinux.cn.eu.org>",
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
Package: *
|
||||
Pin: origin *.deepinos.org.cn
|
||||
Pin-Priority: 400
|
||||
@@ -1 +0,0 @@
|
||||
deb [by-hash=force] https://d.store.deepinos.org.cn /
|
||||
@@ -1,11 +0,0 @@
|
||||
[Unit]
|
||||
Description=Timer for Spark Update Notifier
|
||||
|
||||
[Timer]
|
||||
# 开机后第一次执行
|
||||
OnBootSec=1min
|
||||
# 每天执行一次
|
||||
OnUnitActiveSec=1d
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,9 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
echo "From:sparkstorefeedback@163.com
|
||||
To:sparkstorefeedback@163.com
|
||||
Subject: spark-store_3.0.2: $(lsb_release -a | grep "Description" | sed -e "s#\t#@#" | cut -d "@" -f 2)
|
||||
|
||||
$(uname -a)" | tee /tmp/spark-store-install/feedback.txt > /dev/null
|
||||
|
||||
curl -s --url "smtp://smtp.163.com" --mail-from "${MAIL_FEEDBACK}" --mail-rcpt "${MAIL_FEEDBACK}" --upload-file /tmp/spark-store-install/feedback.txt --user "${MAIL_FEEDBACK}:${M}AIL_AUTH"
|
||||
@@ -1,62 +0,0 @@
|
||||
build
|
||||
.vscode
|
||||
.cache
|
||||
CMakeLists.txt.user
|
||||
CMakeLists.txt.user.*
|
||||
obj-x86_64-linux-gnu
|
||||
# C++ objects and libs
|
||||
*.slo
|
||||
*.lo
|
||||
*.o
|
||||
*.a
|
||||
*.la
|
||||
*.lai
|
||||
*.so
|
||||
*.dll
|
||||
*.dylib
|
||||
|
||||
# Qt-es
|
||||
object_script.*.Release
|
||||
object_script.*.Debug
|
||||
*_plugin_import.cpp
|
||||
/.qmake.cache
|
||||
/.qmake.stash
|
||||
*.pro.user
|
||||
*.pro.user.*
|
||||
*.qbs.user
|
||||
*.qbs.user.*
|
||||
*.moc
|
||||
moc_*.cpp
|
||||
moc_*.h
|
||||
qrc_*.cpp
|
||||
ui_*.h
|
||||
*.qmlc
|
||||
*.jsc
|
||||
Makefile*
|
||||
*build-*
|
||||
|
||||
# Qt unit tests
|
||||
target_wrapper.*
|
||||
|
||||
# Qt qm files
|
||||
translations/*.qm
|
||||
|
||||
# QtCreator
|
||||
*.autosave
|
||||
|
||||
# QtCreator Qml
|
||||
*.qmlproject.user
|
||||
*.qmlproject.user.*
|
||||
|
||||
# QtCreator CMake
|
||||
CMakeLists.txt.user*
|
||||
build
|
||||
|
||||
# Debian dpkg-buildpackage
|
||||
debian/*.debhelper*
|
||||
debian/files
|
||||
debian/*.substvars
|
||||
debian/spark-update-tool
|
||||
|
||||
.vscode/*
|
||||
src/spark-update-tool
|
||||
@@ -1,99 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(spark-update-tool VERSION 0.1 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTORCC ON)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets Network Concurrent)
|
||||
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Network Concurrent)
|
||||
|
||||
set(PROJECT_SOURCES
|
||||
src/main.cpp
|
||||
src/mainwindow.cpp
|
||||
src/mainwindow.h
|
||||
src/mainwindow.ui
|
||||
src/aptssupdater.h
|
||||
src/aptssupdater.cpp
|
||||
src/icons.qrc
|
||||
src/appdelegate.h
|
||||
src/appdelegate.cpp
|
||||
src/applistmodel.h
|
||||
src/applistmodel.cpp
|
||||
src/downloadmanager.h
|
||||
src/downloadmanager.cpp
|
||||
src/ignoreconfig.h
|
||||
src/ignoreconfig.cpp
|
||||
)
|
||||
|
||||
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
|
||||
qt_add_executable(spark-update-tool
|
||||
MANUAL_FINALIZATION
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
# Define target properties for Android with Qt 6 as:
|
||||
# set_property(TARGET spark-update-tool APPEND PROPERTY QT_ANDROID_PACKAGE_SOURCE_DIR
|
||||
# ${CMAKE_CURRENT_SOURCE_DIR}/android)
|
||||
# For more information, see https://doc.qt.io/qt-6/qt-add-executable.html#target-creation
|
||||
else()
|
||||
if(ANDROID)
|
||||
add_library(spark-update-tool SHARED
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
# Define properties for Android with Qt 5 after find_package() calls as:
|
||||
# set(ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/android")
|
||||
else()
|
||||
add_executable(spark-update-tool
|
||||
${PROJECT_SOURCES}
|
||||
)
|
||||
endif()
|
||||
endif()
|
||||
|
||||
target_link_libraries(spark-update-tool PRIVATE Qt${QT_VERSION_MAJOR}::Widgets Qt${QT_VERSION_MAJOR}::Network Qt${QT_VERSION_MAJOR}::Concurrent)
|
||||
|
||||
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
|
||||
# If you are developing for iOS or macOS you should consider setting an
|
||||
# explicit, fixed bundle identifier manually though.
|
||||
if(${QT_VERSION} VERSION_LESS 6.1.0)
|
||||
set(BUNDLE_ID_OPTION MACOSX_BUNDLE_GUI_IDENTIFIER com.example.spark-update-tool)
|
||||
endif()
|
||||
set_target_properties(spark-update-tool PROPERTIES
|
||||
${BUNDLE_ID_OPTION}
|
||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
||||
MACOSX_BUNDLE TRUE
|
||||
WIN32_EXECUTABLE TRUE
|
||||
)
|
||||
|
||||
# 安装配置
|
||||
# 对于Linux,安装到/usr/bin目录
|
||||
if(UNIX AND NOT APPLE)
|
||||
# 直接指定安装路径为 /usr/bin
|
||||
install(TARGETS spark-update-tool
|
||||
RUNTIME DESTINATION /usr/bin
|
||||
)
|
||||
|
||||
# 可选:安装桌面文件和应用图标
|
||||
# install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/spark-update-tool.desktop
|
||||
# DESTINATION /usr/share/applications
|
||||
# )
|
||||
# install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/icons/spark-update-tool.png
|
||||
# DESTINATION /usr/share/icons/hicolor/256x256/apps
|
||||
# )
|
||||
else()
|
||||
# 对于其他系统,使用GNU标准安装路径
|
||||
include(GNUInstallDirs)
|
||||
install(TARGETS spark-update-tool
|
||||
BUNDLE DESTINATION .
|
||||
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif()
|
||||
|
||||
if(QT_VERSION_MAJOR EQUAL 6)
|
||||
qt_finalize_executable(spark-update-tool)
|
||||
endif()
|
||||
@@ -1,674 +0,0 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
@@ -1,15 +0,0 @@
|
||||
### Spark Update Tool
|
||||
#### Introduction
|
||||
|
||||
Welcome to Spark Software Updater. Use this tool to update applications on your Linux system.
|
||||
This version is specifically designed for Linux distributions with Qt6 support.
|
||||
Please run under root privileges (recommended: use `sudo`).
|
||||
#### Currently Supported Linux Distributions
|
||||
- [x] GXDE OS
|
||||
- [x] Ubuntu
|
||||
- [x] deepin
|
||||
- [ ] Kylin
|
||||
|
||||
|
||||
#### Contact & Feedback
|
||||
momen@momen.world
|
||||
@@ -1,25 +0,0 @@
|
||||
### 星火软件更新器
|
||||
#### 简介
|
||||
|
||||
欢迎使用星火软件更新器,您可以使用此更新器更新位于您 Linux 计算机的程序。
|
||||
此版本专为有qt6的Linux发行版所使用。
|
||||
请在root环境下运行。
|
||||
#### 当前支持的 Linux 发行版
|
||||
- [x] GXDE OS
|
||||
- [x] Ubuntu
|
||||
- [x] deepin
|
||||
- [ ] Kylin
|
||||
|
||||
#### 功能清单
|
||||
|
||||
| 功能模块 | 描述 |
|
||||
|------------------|--------------------------------------|
|
||||
| 应用名识别 | 基于 `ss-do-upgrade.sh` 部分代码实现 |
|
||||
| 应用包大小识别 | 通过 dpkg 获取包大小信息 |
|
||||
| 获取应用图标 | 利用 QDesktopServices 实现 |
|
||||
| 支持 ACE 兼容环境| |
|
||||
| 多线程下载 | 基于 aptss 方案 |
|
||||
|
||||
如您已安装星火应用商店,则会附带本程序。
|
||||
#### 联系与反馈
|
||||
momen@momen.world
|
||||
@@ -1,27 +0,0 @@
|
||||
spark-update-tool (1.0.4) unstable; urgency=low
|
||||
|
||||
* 修复点击更新全部按钮后,会更新被忽略应用的问题。
|
||||
|
||||
spark-update-tool (1.0.3) unstable; urgency=low
|
||||
|
||||
* 修复默认图标加载失败的问题
|
||||
* 修复更新器在安装阶段强制关闭窗口后再次更新无法安装软件包的问题。
|
||||
|
||||
-- momen <vmomenv@gmail.com> Fri, 17 Oct 2025 00:00:00 +0000
|
||||
spark-update-tool (1.0.2) unstable; urgency=low
|
||||
|
||||
* 添加复选框,选择多个包更新
|
||||
* 修复缩放问题
|
||||
* 添加忽略应用功能
|
||||
|
||||
-- momen <vmomenv@gmail.com> Mon, 29 Sep 2025 00:00:00 +0000
|
||||
spark-update-tool (1.0.1) unstable; urgency=low
|
||||
|
||||
* 修复窗口调整大小时的错误
|
||||
|
||||
-- momen <vmomenv@gmail.com> Wed, 18 Jun 2025 00:00:00 +0000
|
||||
spark-update-tool (1.0.0) unstable; urgency=low
|
||||
|
||||
* Initial release.
|
||||
|
||||
-- momen <vmomenv@gmail.com> Wed, 18 Jun 2025 00:00:00 +0000
|
||||
@@ -1 +0,0 @@
|
||||
13
|
||||
@@ -1,13 +0,0 @@
|
||||
Source: spark-update-tool
|
||||
Section: utils
|
||||
Priority: optional
|
||||
Maintainer: momen <vmomenv@gmail.com>
|
||||
Build-Depends: debhelper (>= 9)
|
||||
Standards-Version: 3.9.6
|
||||
Homepage: https://gitee.com/spark-store-project/Spark-Update-Tool
|
||||
|
||||
Package: spark-update-tool
|
||||
Architecture: any
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}
|
||||
Description: Spark Update Tool
|
||||
星火应用商店更新组件。This package provides the Spark Update Tool. It includes features for checking for updates, downloading, and applying them seamlessly.
|
||||
@@ -1,7 +0,0 @@
|
||||
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
|
||||
Upstream-Name: spark-update-tool
|
||||
Source: https://gitee.com/spark-store-project/Spark-Update-Tool
|
||||
|
||||
Files: *
|
||||
Copyright: 2025, momen <vmomenv@gmail.com>
|
||||
License: GPL-3.0+
|
||||
@@ -1,3 +0,0 @@
|
||||
build/spark-update-tool /usr/bin/
|
||||
debian/spark-update-tool.desktop /usr/share/applications
|
||||
resources/128*128/spark-update-tool.png /usr/share/icons/hicolor/128x128/apps
|
||||
@@ -1,11 +0,0 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
case "$1" in
|
||||
purge)
|
||||
|
||||
rm -rf /usr/share/spark-update-tool
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
@@ -1,27 +0,0 @@
|
||||
#!/usr/bin/make -f
|
||||
|
||||
# 声明兼容性级别
|
||||
export DH_VERBOSE=1
|
||||
|
||||
%:
|
||||
dh $@ --buildsystem=cmake
|
||||
|
||||
# 确保使用CMake进行配置
|
||||
override_dh_auto_configure:
|
||||
dh_auto_configure -- -DCMAKE_INSTALL_PREFIX=/usr
|
||||
|
||||
# 确保使用CMake进行构建
|
||||
override_dh_auto_build:
|
||||
dh_auto_build
|
||||
|
||||
# 确保使用CMake进行安装
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
||||
|
||||
# 确保使用CMake进行清理
|
||||
override_dh_auto_clean:
|
||||
dh_auto_clean
|
||||
|
||||
# 确保使用CMake进行依赖解析
|
||||
override_dh_shlibdeps:
|
||||
dh_shlibdeps --dpkg-shlibdeps-params=--ignore-missing-info
|
||||
@@ -1 +0,0 @@
|
||||
3.0 (native)
|
||||
@@ -1,9 +0,0 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Name=Spark Update Tool
|
||||
Comment=A Qt-based application for managing and updating software
|
||||
Exec=spark-update-tool
|
||||
Icon=spark-update-tool
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility;
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.9 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 98 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 86 KiB |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="none" version="1.1" width="12" height="16" viewBox="0 0 12 16"><defs><clipPath id="master_svg0_1_074"><rect x="0.75" y="2" width="10.5" height="12" rx="0"/></clipPath></defs><g><rect x="0" y="0" width="12" height="16" rx="0" fill="#000000" fill-opacity="0" style="mix-blend-mode:passthrough"/><g clip-path="url(#master_svg0_1_074)"><g transform="matrix(1,0,0,-1,0,24.015625)"><g><path d="M4.71094,12.2187505Q4.94531,12.0078125,5.25,12.0078125Q5.55469,12.0078125,5.78906,12.2187505L10.2891,16.7187525Q10.5,16.9531225,10.5,17.2578125Q10.5,17.5625025,10.2891,17.7968725Q10.0547,18.0078125,9.75,18.0078125Q9.44531,18.0078125,9.21094,17.7968725L5.25,13.8125025L1.28906,17.7968725Q1.05469,18.0078125,0.75,18.0078125Q0.445312,18.0078125,0.210938,17.7968725Q0,17.5625025,0,17.2578125Q0,16.9531225,0.210938,16.7187525L4.71094,12.2187505Z" fill="#4B5563" fill-opacity="1" style="mix-blend-mode:passthrough"/></g></g></g></g></svg>
|
||||
|
Before Width: | Height: | Size: 1009 B |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 14 KiB |
Binary file not shown.
@@ -1,54 +0,0 @@
|
||||
QT += core gui widgets network concurrent
|
||||
TARGET = spark-update-tool
|
||||
TEMPLATE = app
|
||||
|
||||
# Set C++ standard to C++17
|
||||
CONFIG += c++17
|
||||
|
||||
# Enable auto features (uic, moc, rcc)
|
||||
CONFIG += qt warn_on release
|
||||
|
||||
# Version info
|
||||
VERSION = 0.1.0
|
||||
DEFINES += APP_VERSION=\\\"$$VERSION\\\"
|
||||
|
||||
# Source files
|
||||
SOURCES += \
|
||||
src/main.cpp \
|
||||
src/mainwindow.cpp \
|
||||
src/aptssupdater.cpp \
|
||||
src/appdelegate.cpp \
|
||||
src/applistmodel.cpp \
|
||||
src/downloadmanager.cpp \
|
||||
src/ignoreconfig.cpp
|
||||
|
||||
HEADERS += \
|
||||
src/mainwindow.h \
|
||||
src/aptssupdater.h \
|
||||
src/appdelegate.h \
|
||||
src/applistmodel.h \
|
||||
src/downloadmanager.h \
|
||||
src/ignoreconfig.h
|
||||
|
||||
FORMS += \
|
||||
src/mainwindow.ui
|
||||
|
||||
RESOURCES += \
|
||||
src/icons.qrc
|
||||
|
||||
# Linux-specific settings
|
||||
unix:!macx {
|
||||
# 安装到 /usr/bin 目录
|
||||
target.path = /usr/bin
|
||||
INSTALLS += target
|
||||
|
||||
# 如果需要安装其他文件(如桌面文件、图标等),可以添加
|
||||
# desktop.path = /usr/share/applications
|
||||
# desktop.files = spark-update-tool.desktop
|
||||
# INSTALLS += desktop
|
||||
|
||||
# Additional Linux specific configurations if needed
|
||||
QMAKE_CXXFLAGS += -Wall -Wextra
|
||||
}
|
||||
|
||||
# Remove Windows and macOS specific sections since we're focusing on Linux
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,75 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStyledItemDelegate>
|
||||
#include <QHash>
|
||||
#include <QQueue>
|
||||
#include <QProcess>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
#include <QSet>
|
||||
|
||||
#include "downloadmanager.h"
|
||||
|
||||
struct DownloadInfo {
|
||||
int progress = 0;
|
||||
bool isDownloading = false;
|
||||
bool isInstalled = false;
|
||||
bool isInstalling = false;
|
||||
};
|
||||
|
||||
class AppDelegate : public QStyledItemDelegate {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AppDelegate(QObject *parent = nullptr);
|
||||
~AppDelegate();
|
||||
|
||||
void setModel(QAbstractItemModel *model);
|
||||
|
||||
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override;
|
||||
bool editorEvent(QEvent *event, QAbstractItemModel *model,
|
||||
const QStyleOptionViewItem &option, const QModelIndex &index) override;
|
||||
void startDownloadForAll();
|
||||
void startDownloadForSelected();
|
||||
|
||||
// 复选框相关方法
|
||||
void setSelectedPackages(const QSet<QString> &selected);
|
||||
QSet<QString> getSelectedPackages() const;
|
||||
void clearSelection();
|
||||
|
||||
// 获取下载状态信息
|
||||
const QHash<QString, DownloadInfo>& getDownloads() const;
|
||||
|
||||
|
||||
signals:
|
||||
void updateDisplay(const QString &packageName);
|
||||
void updateFinished(bool success); //传递是否完成更新
|
||||
void ignoreApp(const QString &packageName, const QString &version); // 新增:忽略应用信号
|
||||
void unignoreApp(const QString &packageName, const QString &version); // 新增:取消忽略应用信号
|
||||
|
||||
private slots:
|
||||
void updateSpinner(); // 新增槽函数
|
||||
|
||||
private:
|
||||
DownloadManager *m_downloadManager;
|
||||
QHash<QString, DownloadInfo> m_downloads;
|
||||
QAbstractItemModel *m_model = nullptr;
|
||||
|
||||
// 复选框相关成员变量
|
||||
QSet<QString> m_selectedPackages;
|
||||
|
||||
// 迁移包集合(用户确认要迁移的包)
|
||||
QSet<QString> m_migrationPackages;
|
||||
|
||||
QQueue<QString> m_installQueue;
|
||||
bool m_isInstalling = false;
|
||||
QProcess *m_installProcess = nullptr;
|
||||
QString m_installingPackage;
|
||||
QElapsedTimer m_spinnerTimer;
|
||||
|
||||
QTimer m_spinnerUpdateTimer; // 新增定时器
|
||||
int m_spinnerAngle = 0; // 新增角度变量
|
||||
|
||||
void enqueueInstall(const QString &packageName);
|
||||
void startNextInstall();
|
||||
};
|
||||
@@ -1,86 +0,0 @@
|
||||
#include "applistmodel.h"
|
||||
|
||||
AppListModel::AppListModel(QObject *parent) : QAbstractListModel(parent) {}
|
||||
|
||||
int AppListModel::rowCount(const QModelIndex &parent) const
|
||||
{
|
||||
if (parent.isValid())
|
||||
return 0;
|
||||
return m_data.size();
|
||||
}
|
||||
|
||||
QVariant AppListModel::data(const QModelIndex &index, int role) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_data.size())
|
||||
return QVariant();
|
||||
|
||||
const QVariantMap &map = m_data.at(index.row()); // 直接访问 QVariantMap
|
||||
switch (role) {
|
||||
case Qt::DisplayRole:
|
||||
return map.value("name");
|
||||
case Qt::UserRole + 1: // 包名
|
||||
return map.value("package");
|
||||
case Qt::UserRole + 2: // 当前版本
|
||||
return map.value("current_version");
|
||||
case Qt::UserRole + 3: // 新版本
|
||||
return map.value("new_version");
|
||||
case Qt::UserRole + 4: // 图标路径
|
||||
return map.value("icon");
|
||||
case Qt::UserRole + 5: // 文件大小
|
||||
return map.value("size");
|
||||
case Qt::UserRole + 6: // 描述
|
||||
return map.value("description");
|
||||
case Qt::UserRole + 7: // 下载 URL
|
||||
return map.value("download_url"); // 返回下载 URL
|
||||
case Qt::UserRole + 8: // 忽略状态
|
||||
return map.value("ignored");
|
||||
case Qt::UserRole + 9: // 包来源
|
||||
return map.value("source");
|
||||
case Qt::UserRole + 10: // 是否为迁移项
|
||||
return map.value("is_migration");
|
||||
case Qt::UserRole + 11: // 迁移源
|
||||
return map.value("migration_source");
|
||||
case Qt::UserRole + 12: // 迁移目标
|
||||
return map.value("migration_target");
|
||||
default:
|
||||
return QVariant();
|
||||
}
|
||||
}
|
||||
|
||||
void AppListModel::setUpdateData(const QJsonArray &updateInfo)
|
||||
{
|
||||
beginResetModel();
|
||||
m_data.clear(); // 清空 QList<QVariantMap>
|
||||
|
||||
for (const auto &item : updateInfo) {
|
||||
QJsonObject obj = item.toObject();
|
||||
QVariantMap map;
|
||||
map["package"] = obj["package"].toString();
|
||||
map["name"] = obj["name"].toString();
|
||||
map["current_version"] = obj["current_version"].toString();
|
||||
map["new_version"] = obj["new_version"].toString();
|
||||
map["icon"] = obj["icon"].toString();
|
||||
map["size"] = obj["size"].toString();
|
||||
map["download_url"] = obj["download_url"].toString(); // 确保设置下载 URL
|
||||
map["ignored"] = obj["ignored"].toBool(); // 设置忽略状态
|
||||
map["source"] = obj["source"].toString(); // 设置包来源
|
||||
map["is_migration"] = obj["is_migration"].toBool(); // 设置是否为迁移项
|
||||
map["migration_source"] = obj["migration_source"].toString(); // 设置迁移源
|
||||
map["migration_target"] = obj["migration_target"].toString(); // 设置迁移目标
|
||||
m_data.append(map); // 添加到 QList<QVariantMap>
|
||||
|
||||
qDebug() << "设置到模型的包名:" << map["package"].toString() << "忽略状态:" << map["ignored"].toBool() << "来源:" << map["source"].toString();
|
||||
qDebug() << "设置到模型的下载 URL:" << map["download_url"].toString(); // 检查设置的数据
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
}
|
||||
|
||||
bool AppListModel::isAppIgnored(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid() || index.row() >= m_data.size())
|
||||
return false;
|
||||
|
||||
const QVariantMap &map = m_data.at(index.row());
|
||||
return map.value("ignored").toBool();
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#ifndef APPLISTMODEL_H
|
||||
#define APPLISTMODEL_H
|
||||
|
||||
#include <QAbstractListModel>
|
||||
#include <QJsonArray>
|
||||
// 添加 QJsonObject 头文件
|
||||
#include <QJsonObject>
|
||||
#include <QDebug>
|
||||
class AppListModel : public QAbstractListModel
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit AppListModel(QObject *parent = nullptr);
|
||||
|
||||
// 重写方法
|
||||
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
|
||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||
|
||||
// 设置更新数据
|
||||
void setUpdateData(const QJsonArray &data);
|
||||
|
||||
// 获取忽略状态
|
||||
bool isAppIgnored(const QModelIndex &index) const;
|
||||
|
||||
private:
|
||||
QList<QVariantMap> m_data; // 修改类型为 QList<QVariantMap>
|
||||
};
|
||||
|
||||
#endif // APPLISTMODEL_H
|
||||
@@ -1,709 +0,0 @@
|
||||
#include "aptssupdater.h"
|
||||
#include <QProcess>
|
||||
#include <QTextStream>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QDir>
|
||||
#include <qdebug.h>
|
||||
|
||||
aptssUpdater::aptssUpdater(QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
packageName = getUpdateablePackages();
|
||||
apmPackageName = getApmUpdateablePackages();
|
||||
}
|
||||
|
||||
QStringList aptssUpdater::getUpdateablePackages()
|
||||
{
|
||||
QStringList packageDetails;
|
||||
|
||||
// 检查aptss命令是否存在
|
||||
QProcess checkProcess;
|
||||
checkProcess.start("which", QStringList() << "aptss");
|
||||
if (!checkProcess.waitForFinished(5000) || checkProcess.exitCode() != 0) {
|
||||
qDebug() << "aptss命令不存在,跳过Spark更新检查";
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QProcess process;
|
||||
QString command = R"(env LANGUAGE=en_US /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf list --upgradable -o Dir::Etc::sourcelist="/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list" -o Dir::Etc::sourceparts="/dev/null" -o APT::Get::List-Cleanup="0" | awk 'NR>1')";
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished(30000)) { // 30秒超时
|
||||
qWarning() << "Process failed to finish within 30 seconds.";
|
||||
process.kill();
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QString output = process.readAllStandardOutput();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
|
||||
#else
|
||||
QStringList lines = output.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
|
||||
// 创建临时文件
|
||||
QTemporaryFile tempFile;
|
||||
tempFile.setAutoRemove(false);
|
||||
if (tempFile.open()) {
|
||||
QTextStream stream(&tempFile);
|
||||
|
||||
for (const QString &line : lines) {
|
||||
QRegularExpression regex(R"(([\w\-\+\.]+)/\S+\s+([^\s]+)\s+\S+\s+\[upgradable from: ([^\]]+)\])");
|
||||
QRegularExpressionMatch match = regex.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString name = match.captured(1);
|
||||
QString newVersion = match.captured(2);
|
||||
QString oldVersion = match.captured(3);
|
||||
|
||||
// 检查版本是否相同,相同则跳过
|
||||
if (newVersion == oldVersion) {
|
||||
qDebug() << "跳过版本相同的包:" << name << "(" << oldVersion << "→" << newVersion << ")";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 写入内存列表
|
||||
packageDetails << QString("%1: %2 → %3").arg(name, oldVersion, newVersion);
|
||||
|
||||
// 写入临时文件(原始数据)
|
||||
stream << name << "|" << oldVersion << "|" << newVersion << "\n";
|
||||
}
|
||||
}
|
||||
tempFile.close();
|
||||
m_tempFilePath = tempFile.fileName();
|
||||
qDebug()<< "临时文件路径:" << m_tempFilePath;
|
||||
|
||||
} else {
|
||||
qWarning() << "无法创建临时文件";
|
||||
}
|
||||
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
|
||||
QStringList aptssUpdater::getPackageSizes()
|
||||
{
|
||||
QStringList packageDetails;
|
||||
|
||||
// 获取可更新包名列表
|
||||
QStringList updateablePackages;
|
||||
for (const QString &pkgInfo : packageName) {
|
||||
updateablePackages << pkgInfo.section(":", 0, 0).trimmed();
|
||||
}
|
||||
|
||||
foreach (const QString &packageName, updateablePackages) {
|
||||
QProcess process; // 在循环内部创建新的QProcess实例
|
||||
|
||||
// 构建新命令(包含包名参数)
|
||||
QString command = QString("/usr/bin/apt download %1 --print-uris -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf "
|
||||
"-o Dir::Etc::sourcelist=\"/opt/durapps/spark-store/bin/apt-fast-conf/sources.list.d/aptss.list\" "
|
||||
"-o Dir::Etc::sourceparts=\"/dev/null\"").arg(packageName);
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished(30000)) { // 30秒超时
|
||||
qWarning() << "获取包信息失败:" << packageName << "(超时)";
|
||||
process.kill();
|
||||
continue;
|
||||
}
|
||||
|
||||
QString output = process.readAllStandardOutput();
|
||||
// 使用正则匹配所有信息
|
||||
// 调整正则表达式匹配分组
|
||||
QRegularExpression regex(R"('([^']+)'\s+(\S+)\s+(\d+)\s+SHA512:([^\s]+))"); // 分组1:URL 分组2:文件名 分组3:大小 分组4:SHA512
|
||||
QRegularExpressionMatch match = regex.match(output);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString url = match.captured(1);
|
||||
QString fileName = match.captured(2);
|
||||
QString size = match.captured(3);
|
||||
QString sha512 = match.captured(4);
|
||||
|
||||
// 调整字段顺序:包名 | 大小 | URL | SHA512
|
||||
packageDetails << QString("%1: %2|%3|%4").arg(packageName, size, url, sha512);
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "完整包信息:" << packageDetails;
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
QStringList aptssUpdater::getDesktopAppNames()
|
||||
{
|
||||
QStringList appNames;
|
||||
|
||||
// 获取当前系统语言环境
|
||||
QString lang = QLocale().name().replace("_", "-");
|
||||
|
||||
// 遍历所有可更新包(复用已有的临时文件数据)
|
||||
QStringList packages = packageName;
|
||||
|
||||
foreach (const QString &package, packages) {
|
||||
QProcess dpkgProcess; // 在循环内部创建新的QProcess实例
|
||||
|
||||
QString packageName = package.split(":")[0];
|
||||
QString finalName = packageName; // 默认使用包名
|
||||
|
||||
// 获取包文件列表
|
||||
dpkgProcess.start("dpkg", QStringList() << "-L" << packageName);
|
||||
if (!dpkgProcess.waitForFinished(30000)) { // 30秒超时
|
||||
qWarning() << "获取包文件列表失败:" << packageName << "(超时)";
|
||||
dpkgProcess.kill();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 修复:添加这行代码来获取进程输出
|
||||
QString output = dpkgProcess.readAllStandardOutput();
|
||||
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList files = output.split('\n', Qt::SkipEmptyParts);
|
||||
#else
|
||||
QStringList files = output.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
// 先检查常规应用目录
|
||||
QStringList regularDesktopFiles = files.filter("/usr/share/applications/");
|
||||
QString regularAppName;
|
||||
if (!regularDesktopFiles.isEmpty()) {
|
||||
checkDesktopFiles(regularDesktopFiles, regularAppName, lang, packageName);
|
||||
}
|
||||
|
||||
// 如果常规目录没有找到,再检查特殊目录
|
||||
if (regularAppName.isEmpty()) {
|
||||
QStringList specialDesktopFiles = files.filter(QRegularExpression(QString("/opt/apps/%1/entries/applications").arg(packageName)));
|
||||
QString specialAppName;
|
||||
if (!specialDesktopFiles.isEmpty()) {
|
||||
checkDesktopFiles(specialDesktopFiles, specialAppName, lang, packageName);
|
||||
if (!specialAppName.isEmpty()) {
|
||||
finalName = specialAppName;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
finalName = regularAppName;
|
||||
}
|
||||
|
||||
// 输出格式为[软件名|包名]
|
||||
appNames << QString("[%1|%2]").arg(finalName, packageName);
|
||||
}
|
||||
qDebug()<< "应用名称列表:" << appNames;
|
||||
return appNames;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
bool aptssUpdater::checkDesktopFiles(const QStringList &desktopFiles, QString &appName, const QString &lang, const QString &packageName)
|
||||
{
|
||||
QString lastValidName;
|
||||
QRegularExpression noDisplayRe("^NoDisplay=(true|True)");
|
||||
QRegularExpression nameRe("^Name\\[?" + lang + "?\\]?=(.*)");
|
||||
QRegularExpression nameOrigRe("^Name=(.*)");
|
||||
|
||||
foreach (const QString &filePath, desktopFiles) {
|
||||
if (!filePath.endsWith(".desktop")) continue;
|
||||
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) continue;
|
||||
|
||||
bool skip = false;
|
||||
QString currentName;
|
||||
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
|
||||
// 检查NoDisplay属性
|
||||
if (line.startsWith("NoDisplay=")) {
|
||||
if (noDisplayRe.match(line).hasMatch()) {
|
||||
skip = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 优先匹配本地化名称
|
||||
if (currentName.isEmpty()) {
|
||||
QRegularExpressionMatch match = nameRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
currentName = match.captured(1);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 匹配原始名称
|
||||
match = nameOrigRe.match(line);
|
||||
if (match.hasMatch()) {
|
||||
currentName = match.captured(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!skip && !currentName.isEmpty()) {
|
||||
lastValidName = currentName;
|
||||
}
|
||||
}
|
||||
|
||||
// 处理最终的有效名称
|
||||
if (!lastValidName.isEmpty()) {
|
||||
appName = lastValidName; // 直接赋值而不是使用<<
|
||||
return true;
|
||||
}
|
||||
|
||||
// 回退到包名
|
||||
appName = packageName;
|
||||
return false;
|
||||
}
|
||||
|
||||
QStringList aptssUpdater::getPackageIcons()
|
||||
{
|
||||
QStringList packageIcons;
|
||||
|
||||
// 遍历所有可更新包
|
||||
QStringList packages = packageName;
|
||||
|
||||
foreach (const QString &package, packages) {
|
||||
QProcess dpkgProcess; // 在循环内部创建新的QProcess实例
|
||||
|
||||
QString packageName = package.split(":")[0];
|
||||
QString iconPath = ":/resources/default_icon.png"; // 默认图标
|
||||
|
||||
// 获取包文件列表
|
||||
dpkgProcess.start("dpkg", QStringList() << "-L" << packageName);
|
||||
if (!dpkgProcess.waitForFinished(30000)) { // 30秒超时
|
||||
qWarning() << "获取包文件列表失败:" << packageName << "(超时)";
|
||||
dpkgProcess.kill();
|
||||
packageIcons << QString("%1: %2").arg(packageName, iconPath);
|
||||
continue;
|
||||
}
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList files = QString(dpkgProcess.readAllStandardOutput()).split('\n', Qt::SkipEmptyParts);
|
||||
#else
|
||||
QStringList files = QString(dpkgProcess.readAllStandardOutput()).split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
|
||||
// 查找.desktop文件
|
||||
QStringList desktopFiles = files.filter(QRegularExpression("/(usr/share|opt/apps)/.*\\.desktop$"));
|
||||
|
||||
// 从.desktop文件中提取图标
|
||||
foreach (const QString &desktopFile, desktopFiles) {
|
||||
QFile file(desktopFile);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
if (line.startsWith("Icon=")) {
|
||||
QString iconName = line.mid(5).trimmed();
|
||||
|
||||
// 处理相对图标名(如Icon=vscode)
|
||||
if (!iconName.contains('/')) {
|
||||
// 查找标准图标路径
|
||||
QStringList iconPaths = {
|
||||
QString("/usr/share/pixmaps/%1.png").arg(iconName),
|
||||
QString("/usr/share/icons/hicolor/48x48/apps/%1.png").arg(iconName),
|
||||
QString("/usr/share/icons/hicolor/scalable/apps/%1.svg").arg(iconName),
|
||||
QString("/opt/apps/%1/entries/icons/hicolor/48x48/apps/%2.png").arg(packageName, iconName)
|
||||
};
|
||||
|
||||
foreach (const QString &path, iconPaths) {
|
||||
if (QFile::exists(path)) {
|
||||
iconPath = path;
|
||||
qDebug() << "找到图标文件:" << path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 已经是绝对路径
|
||||
if (QFile::exists(iconName)) {
|
||||
iconPath = iconName;
|
||||
qDebug() << "使用绝对路径图标文件:" << iconName;
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
|
||||
// 如果.desktop中没有找到图标,尝试直接查找包中的图标文件
|
||||
if (iconPath == ":/resources/default_icon.png") {
|
||||
qDebug() << "未在.desktop文件中找到图标,尝试直接查找包中的图标文件";
|
||||
QStringList iconFiles = files.filter(QRegularExpression("/(usr/share/pixmaps|usr/share/icons|opt/apps/.*/entries/icons)/.*\\.(png|svg)$"));
|
||||
if (!iconFiles.isEmpty()) {
|
||||
iconPath = iconFiles.first();
|
||||
qDebug() << "从包中找到图标文件:" << iconPath;
|
||||
} else {
|
||||
qDebug() << "未在包中找到图标文件,使用默认图标";
|
||||
}
|
||||
}
|
||||
|
||||
qDebug() << "包名:" << packageName << "图标路径:" << iconPath;
|
||||
packageIcons << QString("%1: %2").arg(packageName, iconPath);
|
||||
}
|
||||
|
||||
return packageIcons;
|
||||
}
|
||||
|
||||
|
||||
QJsonArray aptssUpdater::getUpdateInfoAsJson()
|
||||
{
|
||||
QJsonArray jsonArray;
|
||||
|
||||
// 获取所有需要的信息
|
||||
QStringList sizes = getPackageSizes();
|
||||
QStringList names = getDesktopAppNames();
|
||||
QStringList icons = getPackageIcons();
|
||||
|
||||
// 创建包名到各种信息的映射
|
||||
QHash<QString, QHash<QString, QString>> packageInfo;
|
||||
|
||||
// 解析包版本信息
|
||||
for (const QString &pkg : packageName) {
|
||||
QStringList parts = pkg.split(": ");
|
||||
if (parts.size() >= 2) {
|
||||
QString packageName = parts[0];
|
||||
QStringList versions = parts[1].split(" → ");
|
||||
if (versions.size() == 2) {
|
||||
packageInfo[packageName]["current_version"] = versions[0];
|
||||
packageInfo[packageName]["new_version"] = versions[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析包详细信息(新增部分)
|
||||
for (const QString &sizeInfo : sizes) {
|
||||
QStringList parts = sizeInfo.split(": ");
|
||||
if (parts.size() == 2) {
|
||||
QString packageName = parts[0];
|
||||
QStringList details = parts[1].split("|");
|
||||
if (details.size() == 3) { // 现在包含大小|URL|SHA512
|
||||
packageInfo[packageName]["size"] = details[0];
|
||||
packageInfo[packageName]["url"] = details[1];
|
||||
packageInfo[packageName]["sha512"] = details[2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析应用名称信息
|
||||
for (const QString &nameInfo : names) {
|
||||
if (nameInfo.startsWith("[") && nameInfo.endsWith("]")) {
|
||||
QString content = nameInfo.mid(1, nameInfo.length() - 2);
|
||||
QStringList parts = content.split("|");
|
||||
if (parts.size() == 2) {
|
||||
QString displayName = parts[0];
|
||||
QString packageName = parts[1];
|
||||
packageInfo[packageName]["display_name"] = displayName;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析图标信息
|
||||
for (const QString &iconInfo : icons) {
|
||||
QStringList parts = iconInfo.split(": ");
|
||||
if (parts.size() == 2) {
|
||||
QString packageName = parts[0];
|
||||
packageInfo[packageName]["icon"] = parts[1].trimmed();
|
||||
}
|
||||
}
|
||||
|
||||
// 构建JSON数组
|
||||
for (const QString &packageName : packageInfo.keys()) {
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["package"] = packageName;
|
||||
|
||||
// 使用显示名称(如果有),否则使用包名
|
||||
if (packageInfo[packageName].contains("display_name")) {
|
||||
jsonObj["name"] = packageInfo[packageName]["display_name"];
|
||||
} else {
|
||||
jsonObj["name"] = packageName;
|
||||
}
|
||||
|
||||
jsonObj["current_version"] = packageInfo[packageName]["current_version"];
|
||||
jsonObj["new_version"] = packageInfo[packageName]["new_version"];
|
||||
jsonObj["icon"] = packageInfo[packageName]["icon"];
|
||||
jsonObj["ignored"] = false; // 默认不忽略
|
||||
|
||||
// 如果有大小信息也加入
|
||||
if (packageInfo[packageName].contains("size")) {
|
||||
jsonObj["size"] = packageInfo[packageName]["size"];
|
||||
}
|
||||
|
||||
// 在构建JSON对象时添加新字段(在jsonObj中添加):
|
||||
if (packageInfo[packageName].contains("url")) {
|
||||
jsonObj["download_url"] = packageInfo[packageName]["url"];
|
||||
qDebug() << "生成的下载 URL:" << packageInfo[packageName]["url"]; // 检查生成的 URL
|
||||
} else {
|
||||
qWarning() << "未找到下载 URL,包名:" << packageName;
|
||||
jsonObj["download_url"] = ""; // 设置为空字符串以避免崩溃
|
||||
}
|
||||
jsonObj["sha512"] = packageInfo[packageName]["sha512"];
|
||||
jsonArray.append(jsonObj);
|
||||
}
|
||||
qDebug()<<jsonArray;
|
||||
return jsonArray;
|
||||
}
|
||||
|
||||
QStringList aptssUpdater::getApmUpdateablePackages()
|
||||
{
|
||||
QStringList packageDetails;
|
||||
|
||||
// 检查apm命令是否存在
|
||||
QProcess checkProcess;
|
||||
checkProcess.start("which", QStringList() << "apm");
|
||||
if (!checkProcess.waitForFinished(5000) || checkProcess.exitCode() != 0) {
|
||||
qDebug() << "apm命令不存在,跳过APM更新检查";
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QProcess process;
|
||||
QString command = R"(env LANGUAGE=en_US /usr/bin/apm list --upgradable | awk 'NR>1')";
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished(30000)) { // 30秒超时
|
||||
qWarning() << "APM process failed to finish within 30 seconds.";
|
||||
process.kill();
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QString output = process.readAllStandardOutput();
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0)
|
||||
QStringList lines = output.split('\n', Qt::SkipEmptyParts);
|
||||
#else
|
||||
QStringList lines = output.split('\n', QString::SkipEmptyParts);
|
||||
#endif
|
||||
|
||||
for (const QString &line : lines) {
|
||||
QRegularExpression regex(R"(([\w\-\+\.]+)/\S+\s+([^\s]+)\s+\S+\s+\[upgradable from: ([^\]]+)\])");
|
||||
QRegularExpressionMatch match = regex.match(line);
|
||||
if (match.hasMatch()) {
|
||||
QString name = match.captured(1);
|
||||
QString newVersion = match.captured(2);
|
||||
QString oldVersion = match.captured(3);
|
||||
|
||||
// 检查版本是否相同,相同则跳过
|
||||
if (newVersion == oldVersion) {
|
||||
qDebug() << "跳过版本相同的APM包:" << name << "(" << oldVersion << "→" << newVersion << ")";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 写入内存列表
|
||||
packageDetails << QString("%1: %2 → %3").arg(name, oldVersion, newVersion);
|
||||
}
|
||||
}
|
||||
|
||||
return packageDetails;
|
||||
}
|
||||
|
||||
QJsonArray aptssUpdater::getApmUpdateInfoAsJson()
|
||||
{
|
||||
QJsonArray jsonArray;
|
||||
|
||||
// 解析APM包版本信息
|
||||
QHash<QString, QHash<QString, QString>> packageInfo;
|
||||
for (const QString &pkg : apmPackageName) {
|
||||
QStringList parts = pkg.split(": ");
|
||||
if (parts.size() >= 2) {
|
||||
QString packageName = parts[0];
|
||||
QStringList versions = parts[1].split(" → ");
|
||||
if (versions.size() == 2) {
|
||||
packageInfo[packageName]["current_version"] = versions[0];
|
||||
packageInfo[packageName]["new_version"] = versions[1];
|
||||
packageInfo[packageName]["source"] = "apm";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 构建JSON数组
|
||||
for (const QString &packageName : packageInfo.keys()) {
|
||||
QJsonObject jsonObj;
|
||||
jsonObj["package"] = packageName;
|
||||
|
||||
// 从APM桌面文件中解析应用名称和图标
|
||||
QString displayName = packageName; // 默认使用包名
|
||||
QString iconPath = ":/resources/default_icon.png"; // 默认图标
|
||||
|
||||
// APM应用的desktop文件路径
|
||||
QString apmDesktopPath = QString("/var/lib/apm/apm/files/ace-env/var/lib/apm/%1/entries/applications").arg(packageName);
|
||||
QDir desktopDir(apmDesktopPath);
|
||||
if (desktopDir.exists()) {
|
||||
// 查找desktop文件
|
||||
QStringList desktopFiles = desktopDir.entryList(QStringList() << "*.desktop", QDir::Files);
|
||||
if (!desktopFiles.isEmpty()) {
|
||||
QString desktopFile = desktopDir.absoluteFilePath(desktopFiles.first());
|
||||
QFile file(desktopFile);
|
||||
if (file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
if (line.startsWith("Name=")) {
|
||||
displayName = line.mid(5).trimmed();
|
||||
} else if (line.startsWith("Icon=")) {
|
||||
QString iconName = line.mid(5).trimmed();
|
||||
// 处理图标路径
|
||||
if (!iconName.contains('/')) {
|
||||
// 查找APM包中的图标
|
||||
QString apmIconPath = QString("/var/lib/apm/apm/files/ace-env/var/lib/apm/%1/entries/icons/hicolor/48x48/apps/%2.png").arg(packageName, iconName);
|
||||
if (QFile::exists(apmIconPath)) {
|
||||
iconPath = apmIconPath;
|
||||
}
|
||||
} else {
|
||||
// 已经是绝对路径
|
||||
if (QFile::exists(iconName)) {
|
||||
iconPath = iconName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取APM包大小和下载信息
|
||||
QString size = "0";
|
||||
QString url = "";
|
||||
QString sha512 = "";
|
||||
|
||||
QProcess process;
|
||||
QString command = QString("amber-pm-debug /usr/bin/apt -c /opt/durapps/spark-store/bin/apt-fast-conf/aptss-apt.conf download %1 --print-uris").arg(packageName);
|
||||
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (process.waitForFinished(30000)) { // 30秒超时
|
||||
QString output = process.readAllStandardOutput();
|
||||
// 解析输出格式:'URL' 文件名 大小 SHA512:哈希值
|
||||
QRegularExpression regex(R"('([^']+)'\s+\S+\s+(\d+)\s+SHA512:([^\s]+))");
|
||||
QRegularExpressionMatch match = regex.match(output);
|
||||
|
||||
if (match.hasMatch()) {
|
||||
url = match.captured(1);
|
||||
size = match.captured(2);
|
||||
sha512 = match.captured(3);
|
||||
}
|
||||
}
|
||||
|
||||
jsonObj["name"] = displayName;
|
||||
jsonObj["current_version"] = packageInfo[packageName]["current_version"];
|
||||
jsonObj["new_version"] = packageInfo[packageName]["new_version"];
|
||||
jsonObj["icon"] = iconPath;
|
||||
jsonObj["ignored"] = false; // 默认不忽略
|
||||
jsonObj["source"] = "apm";
|
||||
jsonObj["size"] = size;
|
||||
jsonObj["download_url"] = url;
|
||||
jsonObj["sha512"] = sha512;
|
||||
jsonArray.append(jsonObj);
|
||||
}
|
||||
qDebug()<<"APM更新信息:"<<jsonArray;
|
||||
return jsonArray;
|
||||
}
|
||||
|
||||
QJsonArray aptssUpdater::mergeUpdateInfo()
|
||||
{
|
||||
QJsonArray aptssInfo = getUpdateInfoAsJson();
|
||||
QJsonArray apmInfo = getApmUpdateInfoAsJson();
|
||||
|
||||
// 创建包名到更新信息的映射
|
||||
QHash<QString, QJsonObject> aptssMap;
|
||||
for (const QJsonValue &value : aptssInfo) {
|
||||
QJsonObject obj = value.toObject();
|
||||
QString packageName = obj["package"].toString();
|
||||
obj["source"] = "aptss";
|
||||
aptssMap[packageName] = obj;
|
||||
}
|
||||
|
||||
QHash<QString, QJsonObject> apmMap;
|
||||
for (const QJsonValue &value : apmInfo) {
|
||||
QJsonObject obj = value.toObject();
|
||||
QString packageName = obj["package"].toString();
|
||||
obj["source"] = "apm";
|
||||
apmMap[packageName] = obj;
|
||||
}
|
||||
|
||||
QJsonArray mergedArray;
|
||||
|
||||
// 处理只在aptss中存在的包
|
||||
for (const QString &packageName : aptssMap.keys()) {
|
||||
if (!apmMap.contains(packageName)) {
|
||||
mergedArray.append(aptssMap[packageName]);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理只在apm中存在的包
|
||||
for (const QString &packageName : apmMap.keys()) {
|
||||
if (!aptssMap.contains(packageName)) {
|
||||
mergedArray.append(apmMap[packageName]);
|
||||
}
|
||||
}
|
||||
|
||||
// 处理在两者中都存在的包
|
||||
for (const QString &packageName : aptssMap.keys()) {
|
||||
if (apmMap.contains(packageName)) {
|
||||
QJsonObject aptssObj = aptssMap[packageName];
|
||||
QJsonObject apmObj = apmMap[packageName];
|
||||
|
||||
// 比较版本
|
||||
QString aptssVersion = aptssObj["new_version"].toString();
|
||||
QString apmVersion = apmObj["new_version"].toString();
|
||||
|
||||
// 检查包在两个源中的安装状态
|
||||
bool installedInAptss = isPackageInstalledInAptss(packageName);
|
||||
bool installedInApm = isPackageInstalledInApm(packageName);
|
||||
|
||||
// 判断是否为迁移场景:
|
||||
// 1. 只在 aptss 中安装了包(不在 apm 中安装)
|
||||
// 2. APM 版本更新
|
||||
if (installedInAptss && !installedInApm && apmVersion > aptssVersion) {
|
||||
// 迁移场景:Spark -> APM
|
||||
QJsonObject migrationObj = apmObj;
|
||||
migrationObj["is_migration"] = true;
|
||||
migrationObj["migration_source"] = "aptss";
|
||||
migrationObj["migration_target"] = "apm";
|
||||
migrationObj["aptss_version"] = aptssVersion;
|
||||
mergedArray.append(migrationObj);
|
||||
|
||||
// 同时保留aptss的更新项(如果aptss也有更新)
|
||||
mergedArray.append(aptssObj);
|
||||
} else {
|
||||
// 非迁移场景(共存场景):同时展示两个来源的更新
|
||||
// 包括以下情况:
|
||||
// 1. 同时在 aptss 和 apm 中都安装了包
|
||||
// 2. 只在 apm 中安装了包
|
||||
// 3. APM 版本不更新
|
||||
mergedArray.append(aptssObj);
|
||||
mergedArray.append(apmObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
qDebug()<<"合并后的更新信息:"<<mergedArray;
|
||||
return mergedArray;
|
||||
}
|
||||
|
||||
bool aptssUpdater::isPackageInstalledInAptss(const QString &packageName)
|
||||
{
|
||||
QProcess process;
|
||||
QString command = QString("dpkg -s '%1' 2>/dev/null | grep -q 'Status: install ok installed'").arg(packageName);
|
||||
process.start("bash", QStringList() << "-c" << command);
|
||||
if (!process.waitForFinished(5000)) {
|
||||
process.kill();
|
||||
return false;
|
||||
}
|
||||
return process.exitCode() == 0;
|
||||
}
|
||||
|
||||
bool aptssUpdater::isPackageInstalledInApm(const QString &packageName)
|
||||
{
|
||||
QProcess process;
|
||||
process.start("apm", QStringList() << "list" << "--installed");
|
||||
if (!process.waitForFinished(30000)) {
|
||||
process.kill();
|
||||
return false;
|
||||
}
|
||||
QString output = process.readAllStandardOutput();
|
||||
// 解析格式: pkgname/repo,section version arch [flags]
|
||||
// 或: pkgname/repo version arch [flags]
|
||||
QRegularExpression regex(QString("^%1/\\S+(?:,\\S+)?\\s+\\S+\\s+\\S+\\s+\\[").arg(QRegularExpression::escape(packageName)));
|
||||
return regex.match(output).hasMatch();
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
#ifndef APTSSUPDATER_H
|
||||
#define APTSSUPDATER_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <QStringList>
|
||||
#include <QTemporaryFile>
|
||||
#include <QLocale>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
class aptssUpdater : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit aptssUpdater(QWidget *parent = nullptr);
|
||||
|
||||
QStringList getUpdateablePackages(); // 查询可更新包列表及更新内容
|
||||
QStringList getPackageSizes(); // 获取每个包的大小
|
||||
QStringList getDesktopAppNames(); // 获取桌面应用名称列表
|
||||
QStringList getPackageIcons(); // 获取包图标列表
|
||||
QJsonArray getUpdateInfoAsJson(); // 获取更新信息的 JSON 格式
|
||||
QString m_tempFilePath;
|
||||
|
||||
// APM 相关方法
|
||||
QStringList getApmUpdateablePackages(); // 查询 APM 可更新包列表及更新内容
|
||||
QJsonArray getApmUpdateInfoAsJson(); // 获取 APM 更新信息的 JSON 格式
|
||||
QJsonArray mergeUpdateInfo(); // 合并 APTSS 和 APM 的更新信息
|
||||
|
||||
signals:
|
||||
private:
|
||||
bool checkDesktopFiles(const QStringList &desktopFiles, QString &appName, const QString &lang, const QString &packageName);
|
||||
QStringList packageName;
|
||||
QStringList apmPackageName; // APM 包列表
|
||||
|
||||
// 检查包安装状态的方法
|
||||
bool isPackageInstalledInAptss(const QString &packageName); // 检查包是否在 aptss 中已安装
|
||||
bool isPackageInstalledInApm(const QString &packageName); // 检查包是否在 apm 中已安装
|
||||
};
|
||||
|
||||
#endif // APTSSUPDATER_H
|
||||
@@ -1,134 +0,0 @@
|
||||
#include "downloadmanager.h"
|
||||
#include <QFileInfo>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QRegularExpression>
|
||||
#include <QDebug>
|
||||
|
||||
DownloadManager::DownloadManager(QObject *parent) : QObject(parent)
|
||||
{
|
||||
cleanupTempFiles();
|
||||
}
|
||||
|
||||
DownloadManager::~DownloadManager()
|
||||
{
|
||||
// 终止并清理所有正在运行的下载进程
|
||||
for (auto it = m_processes.begin(); it != m_processes.end(); ) {
|
||||
QProcess *process = it.value();
|
||||
if (process->state() != QProcess::NotRunning) {
|
||||
process->kill(); // 立即终止进程
|
||||
process->waitForFinished(3000); // 最多等待3秒
|
||||
}
|
||||
process->deleteLater();
|
||||
it = m_processes.erase(it);
|
||||
}
|
||||
}
|
||||
|
||||
void DownloadManager::startDownload(const QString &packageName, const QString &url, const QString &outputPath)
|
||||
{
|
||||
if (m_processes.contains(packageName)) {
|
||||
qWarning() << packageName << " is already downloading.";
|
||||
return;
|
||||
}
|
||||
|
||||
QString metalinkUrl = url + ".metalink";
|
||||
QFileInfo fileInfo(outputPath);
|
||||
|
||||
QStringList arguments = {
|
||||
"--enable-rpc=false",
|
||||
"--console-log-level=warn",
|
||||
"--async-dns=false",
|
||||
"--summary-interval=1",
|
||||
"--allow-overwrite=true",
|
||||
"--connect-timeout=30",
|
||||
"--max-tries=3",
|
||||
"--dir=" + fileInfo.absolutePath(),
|
||||
"--out=" + fileInfo.fileName(),
|
||||
metalinkUrl
|
||||
};
|
||||
|
||||
QProcess *process = new QProcess(this);
|
||||
m_processes.insert(packageName, process);
|
||||
|
||||
// 新增:准备日志文件
|
||||
QString logPath = QString("/tmp/%1_download.log").arg(packageName);
|
||||
QFile *logFile = new QFile(logPath, process);
|
||||
if (logFile->open(QIODevice::Append | QIODevice::Text)) {
|
||||
// 设置权限为777
|
||||
QFile::setPermissions(logPath, QFile::ReadOwner | QFile::WriteOwner | QFile::ExeOwner |
|
||||
QFile::ReadGroup | QFile::WriteGroup | QFile::ExeGroup |
|
||||
QFile::ReadOther | QFile::WriteOther | QFile::ExeOther);
|
||||
connect(process, &QProcess::readyReadStandardOutput, this, [this, packageName, process, logFile]() {
|
||||
while (process->canReadLine()) {
|
||||
QString line = QString::fromUtf8(process->readLine()).trimmed();
|
||||
// 写入日志
|
||||
logFile->write(line.toUtf8() + '\n');
|
||||
logFile->flush();
|
||||
QRegularExpression regex(R"(\((\d+)%\))");
|
||||
QRegularExpressionMatch match = regex.match(line);
|
||||
if (match.hasMatch()) {
|
||||
int progress = match.captured(1).toInt();
|
||||
emit downloadProgress(packageName, progress);
|
||||
}
|
||||
}
|
||||
});
|
||||
connect(process, &QProcess::readyReadStandardError, this, [process, logFile]() {
|
||||
QByteArray err = process->readAllStandardError();
|
||||
logFile->write(err);
|
||||
logFile->flush();
|
||||
});
|
||||
}
|
||||
|
||||
connect(process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
|
||||
this, [this, packageName, outputPath, logFile](int exitCode, QProcess::ExitStatus status) {
|
||||
bool success = (exitCode == 0 && status == QProcess::NormalExit);
|
||||
if (!success) {
|
||||
qWarning() << "Download failed for" << packageName << "exit code:" << exitCode;
|
||||
}
|
||||
|
||||
removeAria2Files(outputPath); // 清理残留 .aria2
|
||||
emit downloadFinished(packageName, success);
|
||||
|
||||
if (logFile) logFile->close();
|
||||
|
||||
QProcess *proc = m_processes.take(packageName);
|
||||
if (proc) proc->deleteLater();
|
||||
});
|
||||
|
||||
process->start("aria2c", arguments);
|
||||
}
|
||||
|
||||
void DownloadManager::cancelDownload(const QString &packageName)
|
||||
{
|
||||
if (!m_processes.contains(packageName)) return;
|
||||
|
||||
QProcess *process = m_processes.take(packageName);
|
||||
if (process) {
|
||||
process->kill(); // 立即终止
|
||||
process->waitForFinished(3000); // 最多等待3秒
|
||||
process->deleteLater();
|
||||
}
|
||||
|
||||
emit downloadFinished(packageName, false); // 显式通知取消
|
||||
|
||||
}
|
||||
|
||||
void DownloadManager::removeAria2Files(const QString &filePath)
|
||||
{
|
||||
QString ariaFile = filePath + ".aria2";
|
||||
QFile::remove(ariaFile);
|
||||
}
|
||||
|
||||
bool DownloadManager::isDownloading(const QString &packageName) const
|
||||
{
|
||||
return m_processes.contains(packageName);
|
||||
}
|
||||
|
||||
void DownloadManager::cleanupTempFiles()
|
||||
{
|
||||
QDir tempDir(QDir::tempPath());
|
||||
QStringList leftovers = tempDir.entryList(QStringList() << "*.aria2", QDir::Files);
|
||||
for (const QString &f : leftovers) {
|
||||
tempDir.remove(f);
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
#ifndef DOWNLOADMANAGER_H
|
||||
#define DOWNLOADMANAGER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QMap>
|
||||
#include <QProcess>
|
||||
|
||||
class DownloadManager : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit DownloadManager(QObject *parent = nullptr);
|
||||
~DownloadManager();
|
||||
void startDownload(const QString &packageName, const QString &url, const QString &outputPath);
|
||||
void cancelDownload(const QString &packageName);
|
||||
bool isDownloading(const QString &packageName) const;
|
||||
|
||||
signals:
|
||||
void downloadProgress(const QString &packageName, int progress);
|
||||
void downloadFinished(const QString &packageName, bool success);
|
||||
|
||||
private:
|
||||
void cleanupTempFiles();
|
||||
void removeAria2Files(const QString &filePath);
|
||||
|
||||
QMap<QString, QProcess*> m_processes;
|
||||
};
|
||||
|
||||
#endif // DOWNLOADMANAGER_H
|
||||
@@ -1,9 +0,0 @@
|
||||
<RCC>
|
||||
<qresource prefix="/">
|
||||
<file>../resources/down_arrow.svg</file>
|
||||
<file>../resources/default_icon.svg</file>
|
||||
<file>../resources/spark-update-tool.svg</file>
|
||||
<file>../resources/128*128/spark-update-tool.png</file>
|
||||
<file>../resources/default_icon.png</file>
|
||||
</qresource>
|
||||
</RCC>
|
||||
@@ -1,119 +0,0 @@
|
||||
#include "ignoreconfig.h"
|
||||
#include <QStandardPaths>
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QDebug>
|
||||
|
||||
IgnoreConfig::IgnoreConfig(QObject *parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
QString configDir;
|
||||
QByteArray sudoUserHomeEnv = qgetenv("SUDO_USER_HOME");
|
||||
|
||||
if (!sudoUserHomeEnv.isEmpty()) {
|
||||
configDir = QString::fromLocal8Bit(sudoUserHomeEnv) + "/.config";
|
||||
} else {
|
||||
configDir = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation);
|
||||
}
|
||||
|
||||
QDir dir(configDir);
|
||||
if (!dir.exists()) {
|
||||
dir.mkpath(".");
|
||||
}
|
||||
m_configFilePath = dir.filePath("spark-store/ignored_apps.conf");
|
||||
|
||||
// 确保目录存在
|
||||
QFileInfo fileInfo(m_configFilePath);
|
||||
QDir configDirPath = fileInfo.dir();
|
||||
if (!configDirPath.exists()) {
|
||||
configDirPath.mkpath(".");
|
||||
}
|
||||
|
||||
// 加载现有配置
|
||||
loadConfig();
|
||||
|
||||
// 输出忽略列表到 qDebug
|
||||
printIgnoredApps();
|
||||
}
|
||||
|
||||
void IgnoreConfig::addIgnoredApp(const QString &packageName, const QString &version)
|
||||
{
|
||||
m_ignoredApps.insert(qMakePair(packageName, version));
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
void IgnoreConfig::removeIgnoredApp(const QString &packageName, const QString &version)
|
||||
{
|
||||
m_ignoredApps.remove(qMakePair(packageName, version));
|
||||
saveConfig();
|
||||
}
|
||||
|
||||
bool IgnoreConfig::isAppIgnored(const QString &packageName, const QString &version) const
|
||||
{
|
||||
return m_ignoredApps.contains(qMakePair(packageName, version));
|
||||
}
|
||||
|
||||
QSet<QPair<QString, QString>> IgnoreConfig::getIgnoredApps() const
|
||||
{
|
||||
return m_ignoredApps;
|
||||
}
|
||||
|
||||
void IgnoreConfig::printIgnoredApps() const
|
||||
{
|
||||
qDebug() << "=== 忽略的应用列表 ===";
|
||||
qDebug() << "配置文件路径:" << m_configFilePath;
|
||||
|
||||
if (m_ignoredApps.isEmpty()) {
|
||||
qDebug() << "没有忽略的应用";
|
||||
} else {
|
||||
for (const auto &app : m_ignoredApps) {
|
||||
qDebug() << "忽略的应用:" << app.first << "版本:" << app.second;
|
||||
}
|
||||
}
|
||||
qDebug() << "====================";
|
||||
}
|
||||
|
||||
bool IgnoreConfig::saveConfig()
|
||||
{
|
||||
QFile file(m_configFilePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) {
|
||||
qDebug() << "无法打开配置文件进行写入:" << m_configFilePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
QTextStream out(&file);
|
||||
for (const auto &app : m_ignoredApps) {
|
||||
out << app.first << "|" << app.second << "\n";
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
|
||||
bool IgnoreConfig::loadConfig()
|
||||
{
|
||||
QFile file(m_configFilePath);
|
||||
if (!file.exists()) {
|
||||
// 配置文件不存在,这是正常的,返回true表示没有错误
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
qDebug() << "无法打开配置文件进行读取:" << m_configFilePath;
|
||||
return false;
|
||||
}
|
||||
|
||||
m_ignoredApps.clear();
|
||||
QTextStream in(&file);
|
||||
while (!in.atEnd()) {
|
||||
QString line = in.readLine().trimmed();
|
||||
if (!line.isEmpty()) {
|
||||
QStringList parts = line.split('|');
|
||||
if (parts.size() == 2) {
|
||||
m_ignoredApps.insert(qMakePair(parts[0], parts[1]));
|
||||
}
|
||||
}
|
||||
}
|
||||
file.close();
|
||||
return true;
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
#ifndef IGNORECONFIG_H
|
||||
#define IGNORECONFIG_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QSet>
|
||||
#include <QString>
|
||||
#include <QPair>
|
||||
|
||||
class IgnoreConfig : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit IgnoreConfig(QObject *parent = nullptr);
|
||||
|
||||
// 添加忽略的应用(包名和版本号)
|
||||
void addIgnoredApp(const QString &packageName, const QString &version);
|
||||
|
||||
// 移除忽略的应用
|
||||
void removeIgnoredApp(const QString &packageName, const QString &version);
|
||||
|
||||
// 检查应用是否被忽略
|
||||
bool isAppIgnored(const QString &packageName, const QString &version) const;
|
||||
|
||||
// 获取所有被忽略的应用
|
||||
QSet<QPair<QString, QString>> getIgnoredApps() const;
|
||||
|
||||
// 输出所有被忽略的应用到 qDebug
|
||||
void printIgnoredApps() const;
|
||||
|
||||
// 保存配置到文件
|
||||
bool saveConfig();
|
||||
|
||||
// 从文件加载配置
|
||||
bool loadConfig();
|
||||
|
||||
private:
|
||||
QSet<QPair<QString, QString>> m_ignoredApps;
|
||||
QString m_configFilePath;
|
||||
};
|
||||
|
||||
#endif // IGNORECONFIG_H
|
||||
@@ -1,88 +0,0 @@
|
||||
#include "mainwindow.h"
|
||||
#include <QApplication>
|
||||
#include <QProcess>
|
||||
#include <QMessageBox>
|
||||
#include <unistd.h> // for geteuid
|
||||
#include <cstdlib> // for getenv
|
||||
#include <QDebug> // For debugging output
|
||||
|
||||
bool isRoot() {
|
||||
return geteuid() == 0;
|
||||
}
|
||||
|
||||
bool elevateToRoot() {
|
||||
QString program = QCoreApplication::applicationFilePath();
|
||||
qDebug() << "Current application path:" << program;
|
||||
|
||||
QByteArray display = qgetenv("DISPLAY");
|
||||
QByteArray xauthority = qgetenv("XAUTHORITY");
|
||||
QByteArray home = qgetenv("HOME"); // 获取原始用户的 HOME 目境变量
|
||||
|
||||
QStringList args;
|
||||
args << "env"
|
||||
<< "DISPLAY=" + display
|
||||
<< "XAUTHORITY=" + xauthority
|
||||
<< "SUDO_USER_HOME=" + home // 传递原始用户的 HOME 路径
|
||||
<< program;
|
||||
|
||||
QProcess process;
|
||||
process.setProgram("pkexec");
|
||||
process.setArguments(args);
|
||||
|
||||
qDebug() << "Attempting to elevate using pkexec with arguments:" << args;
|
||||
|
||||
process.start();
|
||||
if (!process.waitForStarted(5000)) {
|
||||
qDebug() << "Failed to start pkexec.";
|
||||
return false;
|
||||
}
|
||||
|
||||
// 阻塞等待提权进程退出(比如主程序窗口关闭)
|
||||
if (!process.waitForFinished(-1)) { // 等待直到新进程退出
|
||||
qDebug() << "pkexec process waitForFinished failed.";
|
||||
return false;
|
||||
}
|
||||
|
||||
int exitCode = process.exitCode();
|
||||
QProcess::ExitStatus exitStatus = process.exitStatus();
|
||||
|
||||
qDebug() << "pkexec exit code:" << exitCode;
|
||||
qDebug() << "pkexec exit status:" << exitStatus;
|
||||
qDebug() << "pkexec stderr:" << process.readAllStandardError();
|
||||
|
||||
return (exitStatus == QProcess::NormalExit && exitCode == 0);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
// 必须在 QGuiApplication 实例创建之前调用
|
||||
// QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
|
||||
|
||||
QApplication a(argc, argv);
|
||||
a.setWindowIcon(QIcon(":/resources/128*128/spark-update-tool.png"));
|
||||
if (!isRoot()) {
|
||||
qDebug() << "Not running as root. Attempting to elevate...";
|
||||
if (!elevateToRoot()) {
|
||||
qDebug() << "Elevation failed or pkexec command was not executed successfully.";
|
||||
QMessageBox::critical(nullptr,
|
||||
"权限不足",
|
||||
"提权失败!\n\n您的系统可能不支持 `pkexec` 或 `polkit` 配置不正确,"
|
||||
"或者您取消了授权。\n\n请尝试使用 `sudo` 命令运行此程序:"
|
||||
"\n\n在终端中输入:\n`sudo " + QCoreApplication::applicationName() + "`");
|
||||
return 0; // 提权失败,退出程序
|
||||
} else {
|
||||
// 如果 elevateToRoot 返回 true,说明 pkexec 命令本身执行成功
|
||||
// 但这并不意味着原始程序以 root 权限启动了
|
||||
// 因为 elevateToRoot 启动的是一个新的进程,当前进程应该退出
|
||||
// 否则会并行运行两个程序实例
|
||||
qDebug() << "pkexec command executed successfully (new process likely started). Exiting current process.";
|
||||
return 0; // 当前非root进程退出
|
||||
}
|
||||
} else {
|
||||
qDebug() << "Running as root.";
|
||||
}
|
||||
|
||||
MainWindow w;
|
||||
w.show();
|
||||
return a.exec();
|
||||
}
|
||||
@@ -1,488 +0,0 @@
|
||||
#include "mainwindow.h"
|
||||
#include "./ui_mainwindow.h"
|
||||
#include <QProcess>
|
||||
#include <QMessageBox>
|
||||
#include <QProgressDialog>
|
||||
#include <QtConcurrent> // 新增
|
||||
#include <QFutureWatcher> // 新增
|
||||
#include <QIcon>
|
||||
#include <qicon.h>
|
||||
#include <unistd.h> // for geteuid
|
||||
MainWindow::MainWindow(QWidget *parent)
|
||||
: QMainWindow(parent)
|
||||
, ui(new Ui::MainWindow)
|
||||
, m_model(new AppListModel(this))
|
||||
, m_delegate(new AppDelegate(this))
|
||||
, m_ignoreConfig(new IgnoreConfig(this))
|
||||
{
|
||||
QIcon icon(":/resources/128*128/spark-update-tool.png");
|
||||
setWindowIcon(icon);
|
||||
QProgressDialog *progressDialog = new QProgressDialog("正在与服务器通信,获取更新信息中...", QString(), 0, 0, this);
|
||||
progressDialog->setWindowModality(Qt::ApplicationModal);
|
||||
progressDialog->setCancelButton(nullptr);
|
||||
progressDialog->setWindowTitle("请稍候");
|
||||
progressDialog->setMinimumDuration(0);
|
||||
progressDialog->setWindowFlags(progressDialog->windowFlags() & ~Qt::WindowCloseButtonHint); // 禁用关闭按钮
|
||||
progressDialog->show();
|
||||
//异步执行runAptssUpgrade
|
||||
QFutureWatcher<void> *watcher = new QFutureWatcher<void>(this);
|
||||
connect(watcher, &QFutureWatcher<void>::finished, this, [=]() {
|
||||
progressDialog->close();
|
||||
progressDialog->deleteLater();
|
||||
watcher->deleteLater();
|
||||
ui->setupUi(this);
|
||||
QIcon icon(":/resources/128*128/spark-update-tool.png");
|
||||
setWindowIcon(icon);
|
||||
// 创建 QListView 并设置父控件为 ui->appWidget
|
||||
listView = new QListView(ui->appWidget);
|
||||
listView->setModel(m_model);
|
||||
listView->setItemDelegate(m_delegate);
|
||||
|
||||
// 新增:确保 delegate 拥有 model 指针
|
||||
m_delegate->setModel(m_model);
|
||||
|
||||
// 设置 QListView 填充 ui->appWidget
|
||||
QVBoxLayout *layout = new QVBoxLayout(ui->appWidget);
|
||||
layout->addWidget(listView);
|
||||
layout->setContentsMargins(0, 0, 0, 0);
|
||||
connect(m_delegate, &AppDelegate::updateDisplay, this, [=](const QString &packageName) {
|
||||
for (int i = 0; i < m_model->rowCount(); ++i) {
|
||||
QModelIndex index = m_model->index(i);
|
||||
if (index.data(Qt::UserRole + 1).toString() == packageName) {
|
||||
m_model->dataChanged(index, index); // 刷新该行
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 连接应用委托的信号
|
||||
connect(m_delegate, &AppDelegate::ignoreApp, this, &MainWindow::onIgnoreApp);
|
||||
connect(m_delegate, &AppDelegate::unignoreApp, this, &MainWindow::onUnignoreApp);
|
||||
|
||||
// 新增:点击“更新全部”按钮批量下载
|
||||
connect(ui->updatePushButton, &QPushButton::clicked, this, [=](){
|
||||
qDebug()<<"更新按钮被点击";
|
||||
if (m_delegate->getSelectedPackages().isEmpty()) {
|
||||
// 没有选中任何应用,更新全部
|
||||
m_delegate->startDownloadForAll();
|
||||
} else {
|
||||
// 有选中应用,更新选中
|
||||
m_delegate->startDownloadForSelected();
|
||||
m_delegate->clearSelection();
|
||||
updateButtonText();
|
||||
}
|
||||
});
|
||||
|
||||
// 新增:监听选择变化
|
||||
connect(m_delegate, &AppDelegate::updateDisplay, this, &MainWindow::handleSelectionChanged);
|
||||
|
||||
checkUpdates();
|
||||
// 新增:监听搜索框文本变化
|
||||
connect(ui->searchPlainTextEdit, &QPlainTextEdit::textChanged, this, [=]() {
|
||||
QString keyword = ui->searchPlainTextEdit->toPlainText();
|
||||
filterAppsByKeyword(keyword);
|
||||
});
|
||||
initStyle();
|
||||
|
||||
// 确保搜索框内容为空,placeholder 能显示
|
||||
ui->searchPlainTextEdit->clear();
|
||||
});
|
||||
|
||||
// 启动异步任务
|
||||
watcher->setFuture(QtConcurrent::run([this](){
|
||||
runAptssUpgrade();
|
||||
}));
|
||||
QScreen *screen = QGuiApplication::screenAt(QCursor::pos());
|
||||
if (!screen) screen = QGuiApplication::primaryScreen();
|
||||
QRect screenGeometry = screen->geometry();
|
||||
int x = screenGeometry.x() + (screenGeometry.width() - this->width()) / 2;
|
||||
int y = screenGeometry.y() + (screenGeometry.height() - this->height()) / 2;
|
||||
this->move(x, y);
|
||||
}
|
||||
//初始化控件样式
|
||||
void MainWindow::initStyle()
|
||||
{
|
||||
//设置窗口标题
|
||||
this->setWindowTitle("软件更新中心");
|
||||
|
||||
//查询框样式
|
||||
ui->searchPlainTextEdit->setStyleSheet(R"(
|
||||
QPlainTextEdit {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
padding-top: 8px;
|
||||
padding-bottom: 8px;
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
color: #9CA3AF;
|
||||
}
|
||||
QPlainTextEdit[placeholderText]:empty {
|
||||
color: #9CA3AF;
|
||||
}
|
||||
)");
|
||||
|
||||
ui->searchPlainTextEdit->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
ui->searchPlainTextEdit->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
|
||||
|
||||
//筛选框样式
|
||||
ui->FilterComboBox->setStyleSheet(R"(
|
||||
QComboBox {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
border-radius: 4px;
|
||||
color: #4B5563;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
QComboBox::drop-down {
|
||||
border: none;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
QComboBox::down-arrow {
|
||||
image: url(:/resources/down_arrow.svg);
|
||||
width: 12px;
|
||||
height: 16px;
|
||||
}
|
||||
QComboBox QAbstractItemView {
|
||||
background-color: #FFFFFF;
|
||||
border: 1px solid #E5E7EB;
|
||||
color: #4B5563;
|
||||
selection-background-color: #F3F4F6;
|
||||
selection-color: #111827;
|
||||
}
|
||||
)");
|
||||
|
||||
//更新软件按钮样式
|
||||
ui->updatePushButton->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #2563EB;
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 14px;
|
||||
padding: 6px 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
QPushButton:hover {
|
||||
background-color: #1D4ED8; /* 深一点的 hover 效果,可选 */
|
||||
}
|
||||
|
||||
QPushButton:pressed {
|
||||
background-color: #1E40AF; /* 按下效果,可选 */
|
||||
}
|
||||
|
||||
QPushButton:disabled {
|
||||
background-color: #A5B4FC;
|
||||
color: #F9FAFB;
|
||||
}
|
||||
)");
|
||||
|
||||
//设置背景填充颜色
|
||||
ui->backgroundWidget->setStyleSheet(R"(
|
||||
QWidget {
|
||||
background-color: #FFFFFF;
|
||||
border-radius: 12px;
|
||||
}
|
||||
)");
|
||||
|
||||
//设置主背景颜色
|
||||
this->setStyleSheet("background-color: #F8FAFC;");
|
||||
|
||||
// 添加滚动条样式
|
||||
this->setStyleSheet(R"(
|
||||
QScrollBar:vertical {
|
||||
background: #F3F4F6;
|
||||
width: 8px;
|
||||
margin: 0px;
|
||||
}
|
||||
QScrollBar::handle:vertical {
|
||||
background: #D1D5DB;
|
||||
border-radius: 4px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QScrollBar::handle:vertical:hover {
|
||||
background: #9CA3AF;
|
||||
}
|
||||
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
|
||||
background: none;
|
||||
height: 0px;
|
||||
}
|
||||
|
||||
QScrollBar:horizontal {
|
||||
background: #F3F4F6;
|
||||
height: 8px;
|
||||
margin: 0px;
|
||||
}
|
||||
QScrollBar::handle:horizontal {
|
||||
background: #D1D5DB;
|
||||
border-radius: 4px;
|
||||
min-width: 30px;
|
||||
}
|
||||
QScrollBar::handle:horizontal:hover {
|
||||
background: #9CA3AF;
|
||||
}
|
||||
)");
|
||||
}
|
||||
void MainWindow::checkUpdates()
|
||||
{
|
||||
aptssUpdater updater;
|
||||
QJsonArray updateInfo = updater.mergeUpdateInfo();
|
||||
|
||||
// 分离正常应用和忽略应用
|
||||
QJsonArray normalApps;
|
||||
QJsonArray ignoredApps;
|
||||
|
||||
for (const auto &item : updateInfo) {
|
||||
QJsonObject obj = item.toObject();
|
||||
QString packageName = obj["package"].toString();
|
||||
QString newVersion = obj["new_version"].toString();
|
||||
|
||||
// 检查应用是否被忽略
|
||||
if (m_ignoreConfig->isAppIgnored(packageName, newVersion)) {
|
||||
// 标记为忽略状态
|
||||
obj["ignored"] = true;
|
||||
ignoredApps.append(obj);
|
||||
} else {
|
||||
obj["ignored"] = false;
|
||||
normalApps.append(obj);
|
||||
}
|
||||
}
|
||||
|
||||
// 合并数组:正常应用在前,忽略应用在后
|
||||
QJsonArray finalApps;
|
||||
for (const auto &item : normalApps) {
|
||||
finalApps.append(item);
|
||||
}
|
||||
for (const auto &item : ignoredApps) {
|
||||
finalApps.append(item);
|
||||
}
|
||||
|
||||
m_allApps = finalApps; // 保存所有应用数据
|
||||
m_model->setUpdateData(finalApps);
|
||||
|
||||
for (const auto &item : finalApps) {
|
||||
QJsonObject obj = item.toObject();
|
||||
qDebug() << "模型设置的包名:" << obj["package"].toString() << "忽略状态:" << obj["ignored"].toBool() << "来源:" << obj["source"].toString();
|
||||
qDebug() << "模型设置的下载 URL:" << obj["download_url"].toString(); // 检查模型数据
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:根据关键字过滤应用
|
||||
void MainWindow::filterAppsByKeyword(const QString &keyword)
|
||||
{
|
||||
if (keyword.trimmed().isEmpty()) {
|
||||
m_model->setUpdateData(m_allApps);
|
||||
return;
|
||||
}
|
||||
|
||||
// 分离正常应用和忽略应用
|
||||
QJsonArray normalApps;
|
||||
QJsonArray ignoredApps;
|
||||
|
||||
for (const auto &item : m_allApps) {
|
||||
QJsonObject obj = item.toObject();
|
||||
// 可根据需要匹配更多字段
|
||||
QString name = obj.value("name").toString();
|
||||
QString package = obj.value("package").toString();
|
||||
if (name.contains(keyword, Qt::CaseInsensitive) ||
|
||||
package.contains(keyword, Qt::CaseInsensitive)) {
|
||||
|
||||
// 检查是否为忽略状态
|
||||
if (obj.value("ignored").toBool()) {
|
||||
ignoredApps.append(item);
|
||||
} else {
|
||||
normalApps.append(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 合并数组:正常应用在前,忽略应用在后
|
||||
QJsonArray filtered;
|
||||
for (const auto &item : normalApps) {
|
||||
filtered.append(item);
|
||||
}
|
||||
for (const auto &item : ignoredApps) {
|
||||
filtered.append(item);
|
||||
}
|
||||
|
||||
m_model->setUpdateData(filtered);
|
||||
}
|
||||
|
||||
void MainWindow::runAptssUpgrade()
|
||||
{
|
||||
// 检查aptss命令是否存在
|
||||
QProcess checkProcess;
|
||||
checkProcess.start("which", QStringList() << "aptss");
|
||||
if (checkProcess.waitForFinished(5000) && checkProcess.exitCode() == 0) {
|
||||
// aptss存在,执行aptss ssupdate
|
||||
QProcess process;
|
||||
|
||||
// 检查是否已经是root用户,如果是则直接执行命令,否则使用sudo
|
||||
if (geteuid() == 0) {
|
||||
// root用户直接执行
|
||||
process.start("aptss", QStringList() << "ssupdate");
|
||||
} else {
|
||||
// 非root用户使用sudo
|
||||
process.start("sudo", QStringList() << "aptss" << "ssupdate");
|
||||
}
|
||||
|
||||
if (!process.waitForStarted(5000)) {
|
||||
qDebug() << "无法启动 aptss ssupdate";
|
||||
return;
|
||||
}
|
||||
process.write("n\n");
|
||||
process.closeWriteChannel();
|
||||
|
||||
// 设置超时时间,避免无限等待
|
||||
if (!process.waitForFinished(30000)) { // 30秒超时
|
||||
qDebug() << "aptss ssupdate 执行超时";
|
||||
process.kill(); // 强制终止进程
|
||||
return;
|
||||
}
|
||||
|
||||
if (process.exitCode() != 0) {
|
||||
qDebug() << "执行 aptss ssupdate 失败,请检查系统环境或稍后再试。";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "aptss命令不存在,跳过aptss ssupdate";
|
||||
}
|
||||
|
||||
// 检查apm命令是否存在,如果存在则执行apm update
|
||||
QProcess checkApmProcess;
|
||||
checkApmProcess.start("which", QStringList() << "apm");
|
||||
if (checkApmProcess.waitForFinished(5000) && checkApmProcess.exitCode() == 0) {
|
||||
qDebug() << "apm命令存在,执行apm update";
|
||||
QProcess apmProcess;
|
||||
|
||||
// 检查是否已经是root用户,如果是则直接执行命令,否则使用sudo
|
||||
if (geteuid() == 0) {
|
||||
// root用户直接执行
|
||||
apmProcess.start("apm", QStringList() << "update");
|
||||
} else {
|
||||
// 非root用户使用sudo
|
||||
apmProcess.start("sudo", QStringList() << "apm" << "update");
|
||||
}
|
||||
|
||||
if (!apmProcess.waitForStarted(5000)) {
|
||||
qDebug() << "无法启动 apm update";
|
||||
return;
|
||||
}
|
||||
|
||||
// 设置超时时间,避免无限等待
|
||||
if (!apmProcess.waitForFinished(30000)) { // 30秒超时
|
||||
qDebug() << "apm update 执行超时";
|
||||
apmProcess.kill(); // 强制终止进程
|
||||
return;
|
||||
}
|
||||
|
||||
if (apmProcess.exitCode() != 0) {
|
||||
qDebug() << "执行 apm update 失败";
|
||||
}
|
||||
} else {
|
||||
qDebug() << "apm命令不存在,跳过apm update";
|
||||
}
|
||||
}
|
||||
void MainWindow::closeEvent(QCloseEvent *event)
|
||||
{
|
||||
// 检查是否正在进行更新
|
||||
bool isUpdating = false;
|
||||
|
||||
// 通过AppDelegate检查是否有正在下载或安装的应用
|
||||
const QHash<QString, DownloadInfo>& downloads = m_delegate->getDownloads();
|
||||
for (auto it = downloads.constBegin(); it != downloads.constEnd(); ++it) {
|
||||
if (it.value().isDownloading || it.value().isInstalling) {
|
||||
isUpdating = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果正在更新,才显示确认对话框
|
||||
if (isUpdating) {
|
||||
QMessageBox::StandardButton reply = QMessageBox::question(this, "确认关闭", "正在更新,是否确认关闭窗口?", QMessageBox::Yes | QMessageBox::No);
|
||||
|
||||
if (reply == QMessageBox::Yes) {
|
||||
event->accept();
|
||||
} else {
|
||||
event->ignore();
|
||||
}
|
||||
} else {
|
||||
// 如果没有更新,直接关闭窗口
|
||||
event->accept();
|
||||
}
|
||||
}
|
||||
void MainWindow::handleUpdateFinished(bool success)
|
||||
{
|
||||
if (success) {
|
||||
// 更新成功时的处理逻辑
|
||||
QMessageBox::information(this, "更新完成", "软件更新已成功完成!");
|
||||
} else {
|
||||
// 更新失败时的处理逻辑
|
||||
QMessageBox::warning(this, "更新失败", "软件更新过程中出现错误,请稍后再试。");
|
||||
}
|
||||
|
||||
// 刷新应用列表
|
||||
checkUpdates();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
delete ui;
|
||||
}
|
||||
|
||||
// 新增:更新按钮文本
|
||||
void MainWindow::updateButtonText() {
|
||||
int selectedCount = m_delegate->getSelectedPackages().size();
|
||||
if (selectedCount > 0) {
|
||||
ui->updatePushButton->setText(QString("更新选中(%1)").arg(selectedCount));
|
||||
} else {
|
||||
ui->updatePushButton->setText("更新全部");
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:处理选择变化
|
||||
void MainWindow::handleSelectionChanged() {
|
||||
updateButtonText();
|
||||
}
|
||||
|
||||
// 新增:处理忽略应用的槽函数
|
||||
void MainWindow::onIgnoreApp(const QString &packageName, const QString &version) {
|
||||
// 将应用添加到忽略配置中
|
||||
m_ignoreConfig->addIgnoredApp(packageName, version);
|
||||
|
||||
// 更新模型中应用的状态,而不是移除
|
||||
QJsonArray updatedApps;
|
||||
for (const auto &item : m_allApps) {
|
||||
QJsonObject obj = item.toObject();
|
||||
if (obj["package"].toString() == packageName) {
|
||||
obj["ignored"] = true; // 标记为忽略状态
|
||||
}
|
||||
updatedApps.append(obj);
|
||||
}
|
||||
m_allApps = updatedApps;
|
||||
|
||||
// 重新排序:正常应用在前,忽略应用在后
|
||||
checkUpdates();
|
||||
}
|
||||
|
||||
// 新增:处理取消忽略应用的槽函数
|
||||
void MainWindow::onUnignoreApp(const QString &packageName, const QString &version) {
|
||||
// 从忽略配置中移除应用
|
||||
m_ignoreConfig->removeIgnoredApp(packageName, version);
|
||||
|
||||
// 更新模型中应用的状态
|
||||
QJsonArray updatedApps;
|
||||
for (const auto &item : m_allApps) {
|
||||
QJsonObject obj = item.toObject();
|
||||
if (obj["package"].toString() == packageName) {
|
||||
obj["ignored"] = false; // 标记为非忽略状态
|
||||
}
|
||||
updatedApps.append(obj);
|
||||
}
|
||||
m_allApps = updatedApps;
|
||||
|
||||
// 重新排序:正常应用在前,忽略应用在后
|
||||
checkUpdates();
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
#include <QMainWindow>
|
||||
#include "aptssupdater.h"
|
||||
#include "applistmodel.h"
|
||||
#include "appdelegate.h"
|
||||
#include "ignoreconfig.h"
|
||||
#include <QListView>
|
||||
#include <QJsonArray> // 添加头文件
|
||||
#include <QScreen>
|
||||
QT_BEGIN_NAMESPACE
|
||||
namespace Ui {
|
||||
class MainWindow;
|
||||
}
|
||||
QT_END_NAMESPACE
|
||||
|
||||
class MainWindow : public QMainWindow
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
MainWindow(QWidget *parent = nullptr);
|
||||
~MainWindow();
|
||||
|
||||
protected:
|
||||
void closeEvent(QCloseEvent *event) override;
|
||||
|
||||
private:
|
||||
Ui::MainWindow *ui;
|
||||
void checkUpdates();
|
||||
void initStyle();
|
||||
void runAptssUpgrade();
|
||||
AppListModel *m_model;
|
||||
AppDelegate *m_delegate;
|
||||
IgnoreConfig *m_ignoreConfig; // 新增:忽略配置管理
|
||||
QListView *listView; // 声明 QListView 指针
|
||||
QJsonArray m_allApps; // 新增:保存所有应用数据
|
||||
void filterAppsByKeyword(const QString &keyword); // 新增:搜索过滤函数声明
|
||||
void updateButtonText(); // 新增:更新按钮文本
|
||||
|
||||
private slots:
|
||||
void handleUpdateFinished(bool success); // 新增:处理更新完成的槽函数
|
||||
void handleSelectionChanged(); // 新增:处理选择变化的槽函数
|
||||
void onIgnoreApp(const QString &packageName, const QString &version); // 新增:处理忽略应用的槽函数
|
||||
void onUnignoreApp(const QString &packageName, const QString &version); // 新增:处理取消忽略应用
|
||||
};
|
||||
#endif // MAINWINDOW_H
|
||||
@@ -1,256 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>MainWindow</class>
|
||||
<widget class="QMainWindow" name="MainWindow">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>800</width>
|
||||
<height>600</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>MainWindow</string>
|
||||
</property>
|
||||
<widget class="QWidget" name="centralwidget">
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QWidget" name="titleWidget" native="true">
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="font">
|
||||
<font>
|
||||
<family>Noto Sans Vai</family>
|
||||
<pointsize>22</pointsize>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>软件更新中心</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>1245</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QWidget" name="backgroundWidget" native="true">
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_4" native="true">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget_3" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>320</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||
<property name="spacing">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="searchPlainTextEdit">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>200</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="plainText">
|
||||
<string/>
|
||||
</property>
|
||||
<property name="tabStopDistance">
|
||||
<double>81.000000000000000</double>
|
||||
</property>
|
||||
<property name="placeholderText">
|
||||
<string>搜索软件...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="widget" native="true">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>214</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>214</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
<widget class="QComboBox" name="FilterComboBox">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximumSize">
|
||||
<size>
|
||||
<width>102</width>
|
||||
<height>38</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font/>
|
||||
</property>
|
||||
<property name="layoutDirection">
|
||||
<enum>Qt::LayoutDirection::LeftToRight</enum>
|
||||
</property>
|
||||
<property name="sizeAdjustPolicy">
|
||||
<enum>QComboBox::SizeAdjustPolicy::AdjustToContentsOnFirstShow</enum>
|
||||
</property>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>按名称</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
<widget class="QPushButton" name="updatePushButton">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>118</x>
|
||||
<y>0</y>
|
||||
<width>96</width>
|
||||
<height>40</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>更新全部</string>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QWidget" name="appWidget" native="true">
|
||||
<property name="enabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QMenuBar" name="menubar">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>1440</width>
|
||||
<height>23</height>
|
||||
</rect>
|
||||
</property>
|
||||
</widget>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
+25
-25
@@ -45,9 +45,9 @@
|
||||
/>
|
||||
</aside>
|
||||
|
||||
<main class="h-full min-h-0 flex-1">
|
||||
<main class="flex h-full min-h-0 flex-1 flex-col">
|
||||
<div
|
||||
class="sticky top-10 z-30 border-b border-slate-200/70 bg-slate-50 px-4 py-4 lg:px-10 dark:border-slate-800/70 dark:bg-slate-950"
|
||||
class="sticky top-10 z-30 shrink-0 border-b border-slate-200/70 bg-slate-50 px-4 py-4 lg:px-10 dark:border-slate-800/70 dark:bg-slate-950"
|
||||
>
|
||||
<AppHeader
|
||||
:search-query="searchQuery"
|
||||
@@ -67,12 +67,13 @@
|
||||
activeTab !== 'home' &&
|
||||
Object.keys(displayCategories).length > 0
|
||||
"
|
||||
class="shrink-0"
|
||||
:categories="displayCategories"
|
||||
:selected-category="selectedCategory"
|
||||
:category-counts="categoryCounts"
|
||||
@select-category="selectSubCategory"
|
||||
/>
|
||||
<div class="px-4 py-6 lg:px-10">
|
||||
<div class="flex min-h-0 flex-1 flex-col px-4 py-6 lg:px-10">
|
||||
<FavoriteFolderManager
|
||||
v-if="currentView === 'favorites'"
|
||||
:folders="favoriteFolders"
|
||||
@@ -87,9 +88,7 @@
|
||||
@open-detail="openDetail"
|
||||
/>
|
||||
<template v-else-if="activeTab === 'home'">
|
||||
<div
|
||||
class="max-h-[calc(100vh-8rem)] overflow-y-auto pr-2 scrollbar-nowidth"
|
||||
>
|
||||
<div class="h-full overflow-y-auto pr-2 scrollbar-nowidth">
|
||||
<HomeView
|
||||
:links="homeLinks"
|
||||
:loading="homeLoading"
|
||||
@@ -99,14 +98,16 @@
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<AppGrid
|
||||
:apps="filteredApps"
|
||||
:loading="effectiveLoading"
|
||||
:scroll-key="activeTab + '-' + selectedCategory"
|
||||
:store-filter="storeFilter"
|
||||
:show-origin="storeFilter === 'both' && !isHomeListTab"
|
||||
@open-detail="handleAppCardOpenDetail"
|
||||
/>
|
||||
<div class="min-h-0 flex-1">
|
||||
<AppGrid
|
||||
:apps="filteredApps"
|
||||
:loading="effectiveLoading"
|
||||
:scroll-key="activeTab + '-' + selectedCategory"
|
||||
:store-filter="storeFilter"
|
||||
:show-origin="storeFilter === 'both' && !isHomeListTab"
|
||||
@open-detail="handleAppCardOpenDetail"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</main>
|
||||
@@ -2838,16 +2839,11 @@ onMounted(async () => {
|
||||
|
||||
await loadTabCategories();
|
||||
|
||||
// 先启动侧边栏入口预加载,让其请求优先进入网络队列
|
||||
// 避免被「全部应用」的大量并发请求抢占连接池
|
||||
const sidebarPreloadPromise = preloadSidebarTabApps();
|
||||
|
||||
loading.value = true;
|
||||
homeLoading.value = true;
|
||||
|
||||
// 侧边栏入口预加载先启动,全部应用和主页数据随后并行加载
|
||||
Promise.all([
|
||||
sidebarPreloadPromise,
|
||||
// 先加载全部应用,再预加载侧边栏入口
|
||||
await Promise.all([
|
||||
loadHome(),
|
||||
new Promise<void>((resolve) => {
|
||||
loadApps(() => {
|
||||
@@ -2855,12 +2851,16 @@ onMounted(async () => {
|
||||
resolve();
|
||||
});
|
||||
}),
|
||||
]).then(() => {
|
||||
// 所有数据加载完成后的回调(可选)
|
||||
logger.info("所有应用数据加载完成");
|
||||
void maybePromptInstalledSync();
|
||||
]);
|
||||
|
||||
// 全部应用加载完成后再预加载侧边栏入口
|
||||
preloadSidebarTabApps().then(() => {
|
||||
logger.info("侧边栏入口预加载完成");
|
||||
});
|
||||
|
||||
void maybePromptInstalledSync();
|
||||
logger.info("所有应用数据加载完成");
|
||||
|
||||
// 设置键盘导航
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (showPreview.value) {
|
||||
|
||||
@@ -18,10 +18,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 应用数量较少时,使用普通网格(带滚动) -->
|
||||
<div
|
||||
v-else-if="!loading && apps.length <= 50"
|
||||
class="non-virtual-scroller"
|
||||
>
|
||||
<div v-else-if="!loading && apps.length <= 50" class="non-virtual-scroller">
|
||||
<div class="grid gap-4 sm:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4">
|
||||
<AppCard
|
||||
v-for="(app, index) in apps"
|
||||
@@ -176,14 +173,14 @@ const gridRows = computed(() => {
|
||||
|
||||
<style scoped>
|
||||
.scroller {
|
||||
height: calc(100vh - 140px); /* 调整高度 */
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0; /* 移除内边距 */
|
||||
margin: -24px -16px; /* 抵消父容器的 px-4 py-6 */
|
||||
}
|
||||
|
||||
.non-virtual-scroller {
|
||||
height: calc(100vh - 140px);
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white shadow-2xl dark:border-slate-800/70 dark:bg-slate-900 sm:left-auto sm:right-6 sm:w-96"
|
||||
ref="queueRef"
|
||||
class="fixed inset-x-4 bottom-4 z-40 rounded-3xl border border-slate-200/70 bg-white shadow-2xl transition-all duration-200 dark:border-slate-800/70 dark:bg-slate-900 sm:left-auto sm:right-6 sm:w-96"
|
||||
:class="
|
||||
isHidden
|
||||
? 'pointer-events-none translate-y-[calc(100%+1rem)] opacity-0'
|
||||
: 'translate-y-0 opacity-100'
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-between px-5 py-4"
|
||||
@@ -130,7 +136,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
||||
import type { DownloadItem } from "../global/typedefinition";
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -147,6 +153,30 @@ const emit = defineEmits<{
|
||||
}>();
|
||||
|
||||
const isExpanded = ref(false);
|
||||
const isHidden = ref(false);
|
||||
const queueRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const onWheel = (event: WheelEvent) => {
|
||||
if (queueRef.value?.contains(event.target as Node) || event.deltaY === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.deltaY > 0) {
|
||||
isExpanded.value = false;
|
||||
isHidden.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
isHidden.value = false;
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener("wheel", onWheel, { passive: true, capture: true });
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener("wheel", onWheel, { capture: true });
|
||||
});
|
||||
|
||||
const completedDownloads = computed(() => {
|
||||
return props.downloads.filter((d) => d.status === "completed").length;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="h-screen overflow-y-auto bg-slate-50 dark:bg-slate-950 text-slate-900 dark:text-slate-100"
|
||||
>
|
||||
<div
|
||||
class="sticky top-0 z-30 border-b border-slate-200/70 bg-white px-4 py-3 dark:border-slate-800/70 dark:bg-slate-900"
|
||||
class="submitter-titlebar sticky top-0 z-30 border-b border-slate-200/70 bg-white px-4 py-3 dark:border-slate-800/70 dark:bg-slate-900"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
@@ -16,7 +16,7 @@
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
class="submitter-close-button inline-flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
@click="closeWindow"
|
||||
>
|
||||
<i class="fas fa-times"></i>
|
||||
@@ -203,8 +203,8 @@
|
||||
<p class="text-slate-600 dark:text-slate-400">
|
||||
{{ iconPreview ? "点击浏览更换图标" : "点击浏览" }}
|
||||
</p>
|
||||
<p v-if="formData.iconPath" class="mt-2 text-sm text-blue-500">
|
||||
{{ formData.iconPath.split("/").pop() }}
|
||||
<p v-if="iconFileName" class="mt-2 text-sm text-blue-500">
|
||||
{{ iconFileName }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -214,7 +214,14 @@
|
||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||
>截图(最多5张)</label
|
||||
>
|
||||
<div class="grid grid-cols-5 gap-3">
|
||||
<p class="mb-2 text-xs text-slate-500 dark:text-slate-400">
|
||||
可直接 Ctrl+V 粘贴截图
|
||||
</p>
|
||||
<div
|
||||
tabindex="0"
|
||||
class="grid grid-cols-5 gap-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
@paste="handleScreenshotPaste"
|
||||
>
|
||||
<div
|
||||
v-for="(screenshot, index) in formData.screenshots"
|
||||
:key="index"
|
||||
@@ -384,15 +391,156 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 表单校验提示 -->
|
||||
<div
|
||||
v-if="submitSuccess"
|
||||
class="p-4 bg-green-50 border border-green-200 rounded-lg dark:bg-green-900/20 dark:border-green-800"
|
||||
v-if="!isFormValid && hasUserStartedFilling"
|
||||
class="mt-3 p-3 bg-amber-50 border border-amber-200 rounded-lg dark:bg-amber-900/20 dark:border-amber-800"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2 text-green-700 dark:text-green-400"
|
||||
<p
|
||||
class="text-sm font-medium text-amber-700 dark:text-amber-400 mb-1.5"
|
||||
>
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<span>投稿提交成功!我们会尽快审核你的应用。</span>
|
||||
请完善以下信息:
|
||||
</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
|
||||
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"
|
||||
>
|
||||
<div
|
||||
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>
|
||||
|
||||
@@ -559,7 +707,8 @@
|
||||
</template>
|
||||
|
||||
<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 {
|
||||
id: number;
|
||||
@@ -595,12 +744,23 @@ const formData = reactive({
|
||||
description: "",
|
||||
tags: "",
|
||||
category: "",
|
||||
categoryId: 0,
|
||||
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 submitSuccess = ref(false);
|
||||
const submitError = ref("");
|
||||
const showSubmitSuccessModal = ref(false);
|
||||
const isParsingDeb = ref(false);
|
||||
const debParseError = ref("");
|
||||
const isSearchingHistory = ref(false);
|
||||
@@ -608,6 +768,14 @@ const showArchDialog = ref(false);
|
||||
const availableArchs = ref<HistoryArchInfo[]>([]);
|
||||
const currentDebArch = 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 categoriesLoadError = ref("");
|
||||
@@ -634,23 +802,6 @@ interface Category {
|
||||
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 {
|
||||
name: string;
|
||||
value: string;
|
||||
@@ -661,13 +812,55 @@ const tagsList = ref<Tag[]>([]);
|
||||
const selectedTags = ref<Tag[]>([]);
|
||||
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(() => {
|
||||
return (
|
||||
const hasRequiredFields =
|
||||
formData.name.trim() &&
|
||||
formData.pkgname.trim() &&
|
||||
formData.version.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 +904,13 @@ const loadCategoriesList = async (): Promise<void> => {
|
||||
data.data.length > 0
|
||||
) {
|
||||
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,
|
||||
// API 只返回中文 name,value 通过 id 反向映射为英文标识符
|
||||
// 英文标识符与历史记录中的 category 字段一致,用于匹配
|
||||
name: item.name || "",
|
||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
||||
value: item.type || String(item.id),
|
||||
}),
|
||||
);
|
||||
categoriesLoadError.value = "";
|
||||
@@ -726,10 +920,13 @@ const loadCategoriesList = async (): Promise<void> => {
|
||||
);
|
||||
} else if (data.code === 0 && Array.isArray(data)) {
|
||||
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,
|
||||
name: item.name || "",
|
||||
value: categoryIdToValueMap[item.id] || item.name || "",
|
||||
value: item.type || String(item.id),
|
||||
}),
|
||||
);
|
||||
categoriesLoadError.value = "";
|
||||
@@ -853,8 +1050,6 @@ const selectDebFile = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const useMirror = ref(true);
|
||||
|
||||
const searchHistoryApp = async () => {
|
||||
console.log(
|
||||
"[Submitter] ============== SEARCHING HISTORY INFO ==============",
|
||||
@@ -863,7 +1058,6 @@ const searchHistoryApp = async () => {
|
||||
"[Submitter] pkgname is not empty, searching history with:",
|
||||
formData.pkgname,
|
||||
);
|
||||
console.log("[Submitter] Using mirror:", useMirror.value);
|
||||
|
||||
console.log(
|
||||
"[Submitter] Calling IPC: search-history-app with pkgname:",
|
||||
@@ -872,7 +1066,7 @@ const searchHistoryApp = async () => {
|
||||
const historyResult = await window.ipcRenderer.invoke(
|
||||
"search-history-app",
|
||||
formData.pkgname,
|
||||
useMirror.value,
|
||||
APM_STORE_BASE_URL,
|
||||
);
|
||||
console.log(
|
||||
"[Submitter] Received history search response:",
|
||||
@@ -980,6 +1174,9 @@ const searchHistoryApp = async () => {
|
||||
historyResult?.message,
|
||||
);
|
||||
}
|
||||
// 未找到历史记录时清空旧数据,避免上一个应用的残留
|
||||
availableArchs.value = [];
|
||||
showArchDialog.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1162,9 +1359,12 @@ const selectArch = async (arch: HistoryArchInfo) => {
|
||||
console.log("[Submitter] Tags loaded from history:", selectedTags.value);
|
||||
}
|
||||
|
||||
const baseUrl = useMirror.value
|
||||
? `https://mirrors.sdu.edu.cn/spark-store/${arch.store}/${arch.category}/${arch.pkgname}`
|
||||
: `https://spk-json.spark-app.store/${arch.store}/${arch.category}/${arch.pkgname}`;
|
||||
// 开发模式下来自 Vite 代理的路径(如 /local_amd64-store),
|
||||
// 后端 fetch/fs 无法处理,需要解析为实际 CDN 地址
|
||||
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(
|
||||
"[Submitter] Building icon and screenshot URLs with baseUrl:",
|
||||
@@ -1174,6 +1374,7 @@ const selectArch = async (arch: HistoryArchInfo) => {
|
||||
if (arch.icon) {
|
||||
formData.iconPath = `${baseUrl}/icon.png`;
|
||||
iconPreview.value = formData.iconPath;
|
||||
iconFileName.value = "icon.png";
|
||||
console.log("[Submitter] Icon URL:", formData.iconPath);
|
||||
console.log("[Submitter] Icon preview set:", iconPreview.value);
|
||||
}
|
||||
@@ -1198,12 +1399,14 @@ const selectIconFile = () => {
|
||||
|
||||
const handleIconFileSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0] as File & { path?: string };
|
||||
const file = target.files?.[0];
|
||||
if (file) {
|
||||
formData.iconPath = file.path || file.name;
|
||||
iconFileName.value = file.name;
|
||||
const reader = new FileReader();
|
||||
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);
|
||||
}
|
||||
@@ -1211,17 +1414,19 @@ const handleIconFileSelect = (event: Event) => {
|
||||
|
||||
const handleIconDrop = (event: DragEvent) => {
|
||||
event.preventDefault();
|
||||
const file = event.dataTransfer?.files?.[0] as File & { path?: string };
|
||||
const file = event.dataTransfer?.files?.[0];
|
||||
if (
|
||||
file &&
|
||||
(file.name.endsWith(".png") ||
|
||||
file.name.endsWith(".jpg") ||
|
||||
file.name.endsWith(".jpeg"))
|
||||
) {
|
||||
formData.iconPath = file.path || file.name;
|
||||
iconFileName.value = file.name;
|
||||
const reader = new FileReader();
|
||||
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);
|
||||
}
|
||||
@@ -1231,30 +1436,44 @@ const addScreenshot = () => {
|
||||
screenshotInput.value?.click();
|
||||
};
|
||||
|
||||
const importScreenshots = (files: File[]) => {
|
||||
for (const file of files) {
|
||||
if (formData.screenshots.length >= 5) break;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
if (
|
||||
formData.screenshots.length < 5 &&
|
||||
typeof reader.result === "string"
|
||||
) {
|
||||
formData.screenshots.push(reader.result);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleScreenshotSelect = (event: Event) => {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const files = target.files;
|
||||
if (files) {
|
||||
for (let i = 0; i < files.length && formData.screenshots.length < 5; i++) {
|
||||
const file = files[i];
|
||||
if (
|
||||
file.name.endsWith(".png") ||
|
||||
file.name.endsWith(".jpg") ||
|
||||
file.name.endsWith(".jpeg")
|
||||
) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
if (formData.screenshots.length < 5) {
|
||||
formData.screenshots.push(e.target?.result as string);
|
||||
}
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
const files = Array.from(target.files ?? []).filter((file) =>
|
||||
/\.(png|jpe?g)$/i.test(file.name),
|
||||
);
|
||||
importScreenshots(files);
|
||||
target.value = "";
|
||||
};
|
||||
|
||||
const handleScreenshotPaste = (event: ClipboardEvent) => {
|
||||
const files = Array.from(event.clipboardData?.items ?? [])
|
||||
.filter((item) => item.type.startsWith("image/"))
|
||||
.map((item) => item.getAsFile())
|
||||
.filter((file): file is File => file !== null);
|
||||
|
||||
if (files.length > 0) {
|
||||
event.preventDefault();
|
||||
importScreenshots(files);
|
||||
}
|
||||
};
|
||||
|
||||
const removeScreenshot = (index: number) => {
|
||||
formData.screenshots.splice(index, 1);
|
||||
};
|
||||
@@ -1273,6 +1492,7 @@ const resetForm = () => {
|
||||
formData.description = "";
|
||||
formData.tags = "";
|
||||
formData.category = "";
|
||||
formData.categoryId = 0;
|
||||
formData.remark = "";
|
||||
submitSuccess.value = false;
|
||||
submitError.value = "";
|
||||
@@ -1280,6 +1500,28 @@ const resetForm = () => {
|
||||
packageSuccess.value = false;
|
||||
packageError.value = "";
|
||||
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 () => {
|
||||
@@ -1289,6 +1531,45 @@ const submitForm = async () => {
|
||||
submitSuccess.value = false;
|
||||
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 {
|
||||
const submitData = {
|
||||
name: formData.name,
|
||||
@@ -1304,7 +1585,9 @@ const submitForm = async () => {
|
||||
description: formData.description,
|
||||
tags: formData.tags,
|
||||
category: formData.category,
|
||||
categoryId: formData.categoryId,
|
||||
remark: formData.remark,
|
||||
arch: currentDebArch.value,
|
||||
};
|
||||
|
||||
console.log("[Submitter] ============== SUBMIT FORM ==============");
|
||||
@@ -1321,14 +1604,19 @@ const submitForm = async () => {
|
||||
const result = await window.ipcRenderer.invoke("submit-app", submitData);
|
||||
|
||||
if (result?.success) {
|
||||
uploadStages.value.forEach((s) => (s.progress = 100));
|
||||
uploadProgress.value = 100;
|
||||
uploadStage.value = "提交完成";
|
||||
uploadStageMessage.value = "投稿提交成功!";
|
||||
submitSuccess.value = true;
|
||||
resetForm();
|
||||
showSubmitSuccessModal.value = true;
|
||||
} else {
|
||||
submitError.value = result?.message || "提交失败";
|
||||
}
|
||||
} catch (error) {
|
||||
submitError.value = (error as Error)?.message || "提交失败";
|
||||
} finally {
|
||||
window.ipcRenderer.off("submit-upload-progress", progressListener);
|
||||
isSubmitting.value = false;
|
||||
}
|
||||
};
|
||||
@@ -1346,6 +1634,35 @@ const packageApp = async (storeArch: string) => {
|
||||
packageError.value = "";
|
||||
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 {
|
||||
const packageData = {
|
||||
name: formData.name,
|
||||
@@ -1361,6 +1678,7 @@ const packageApp = async (storeArch: string) => {
|
||||
description: formData.description,
|
||||
tags: formData.tags,
|
||||
category: formData.category,
|
||||
categoryId: formData.categoryId,
|
||||
remark: formData.remark,
|
||||
storeArch,
|
||||
};
|
||||
@@ -1374,6 +1692,9 @@ const packageApp = async (storeArch: string) => {
|
||||
const result = await window.ipcRenderer.invoke("package-app", packageData);
|
||||
|
||||
if (result?.success) {
|
||||
uploadStages.value.forEach((s) => (s.progress = 100));
|
||||
uploadProgress.value = 100;
|
||||
uploadStageMessage.value = "打包完成!";
|
||||
packageSuccess.value = true;
|
||||
packageResult.value = result.data;
|
||||
} else {
|
||||
@@ -1382,6 +1703,7 @@ const packageApp = async (storeArch: string) => {
|
||||
} catch (error) {
|
||||
packageError.value = (error as Error)?.message || "打包失败";
|
||||
} finally {
|
||||
window.ipcRenderer.off("package-progress", progressListener);
|
||||
isPackaging.value = false;
|
||||
}
|
||||
};
|
||||
@@ -1412,3 +1734,13 @@ onMounted(async () => {
|
||||
await getGitEmail();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.submitter-titlebar {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.submitter-close-button {
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
HERE=$(dirname $0)
|
||||
|
||||
case `uname -m` in
|
||||
x86_64)
|
||||
sender_appendix="amd64"
|
||||
;;
|
||||
aarch64)
|
||||
sender_appendix="arm64"
|
||||
;;
|
||||
loongarch64)
|
||||
sender_appendix="loong64"
|
||||
;;
|
||||
esac
|
||||
|
||||
${HERE}/sender-d-${sender_appendix} $@
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user