mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-08-06 12:33:56 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c6048295 | ||
|
|
3b7f14862d | ||
|
|
367828f1f4 | ||
|
|
70250acc76 | ||
|
|
3157714776 | ||
|
|
bfd7626b8f | ||
|
|
5d512c4e86 | ||
|
|
9323c368d6 | ||
|
|
1f9971f7aa | ||
|
|
d67f7b3666 | ||
|
|
c2ba273ca2 | ||
|
|
361dad1a64 | ||
|
|
5730c0bfea |
+10
@@ -55,3 +55,13 @@ test-results.json
|
|||||||
|
|
||||||
# VSCode CMake Extension
|
# VSCode CMake Extension
|
||||||
build/*
|
build/*
|
||||||
|
|
||||||
|
# Debian build intermediates
|
||||||
|
debian/*.log
|
||||||
|
debian/*.substvars
|
||||||
|
debian/*.debhelper
|
||||||
|
debian/debhelper-build-stamp
|
||||||
|
debian/files
|
||||||
|
debian/tmp/
|
||||||
|
debian/.debhelper/
|
||||||
|
debian/spark-store/
|
||||||
|
|||||||
Vendored
+1
@@ -30,6 +30,7 @@
|
|||||||
// },
|
// },
|
||||||
"runtimeArgs": [
|
"runtimeArgs": [
|
||||||
"--remote-debugging-port=9229",
|
"--remote-debugging-port=9229",
|
||||||
|
"--no-sandbox",
|
||||||
"."
|
"."
|
||||||
],
|
],
|
||||||
"envFile": "${workspaceFolder}/.vscode/.debug.env",
|
"envFile": "${workspaceFolder}/.vscode/.debug.env",
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
spark-store (5.2.1-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>
|
* Initial release. (Closes: #nnnn) <nnnn is the bug number of your ITP>
|
||||||
|
|
||||||
|
|||||||
@@ -972,6 +972,24 @@ export function registerSubmitterHandlers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle("get-git-name", async () => {
|
||||||
|
try {
|
||||||
|
const { exec } = await import("node:child_process");
|
||||||
|
const util = await import("util");
|
||||||
|
const execAsync = util.promisify(exec);
|
||||||
|
const { stdout } = await execAsync("git config user.name");
|
||||||
|
const name = stdout.trim();
|
||||||
|
logger.info({ name }, "[Submitter] Git name retrieved");
|
||||||
|
return { success: true, data: name || "" };
|
||||||
|
} catch (err) {
|
||||||
|
logger.warn(
|
||||||
|
{ err },
|
||||||
|
"[Submitter] Failed to get git name, not a git repo or git not installed",
|
||||||
|
);
|
||||||
|
return { success: false, data: "" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle("get-tags-list", async () => {
|
ipcMain.handle("get-tags-list", async () => {
|
||||||
try {
|
try {
|
||||||
const apiUrl = "https://upload.deepinos.org.cn/api/index/get_tags_list";
|
const apiUrl = "https://upload.deepinos.org.cn/api/index/get_tags_list";
|
||||||
@@ -1402,8 +1420,9 @@ export function registerSubmitterHandlers(
|
|||||||
// arch 由前端解析 deb 时获取并传入,无需再次调用 dpkg-deb
|
// arch 由前端解析 deb 时获取并传入,无需再次调用 dpkg-deb
|
||||||
const debArch = String(dataObj.arch || "amd64");
|
const debArch = String(dataObj.arch || "amd64");
|
||||||
const pkgVersion = String(dataObj.version || "0.0.0");
|
const pkgVersion = String(dataObj.version || "0.0.0");
|
||||||
const debPkgName = String(dataObj.pkgname || "unknown");
|
const debPkgName = String(dataObj.pkgname || "unknown").toLowerCase();
|
||||||
const formFileName = `${debPkgName}_${pkgVersion}_${debArch}.deb`;
|
const formFileName =
|
||||||
|
`${debPkgName}_${pkgVersion}_${debArch}.deb`.toLowerCase();
|
||||||
logger.info(
|
logger.info(
|
||||||
{ formFileName, debArch },
|
{ formFileName, debArch },
|
||||||
"[Submitter] Constructed file_name",
|
"[Submitter] Constructed file_name",
|
||||||
@@ -1671,7 +1690,7 @@ export function registerSubmitterHandlers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const dataObj = formData as Record<string, unknown>;
|
const dataObj = formData as Record<string, unknown>;
|
||||||
const pkgname = String(dataObj.pkgname || "");
|
const pkgname = String(dataObj.pkgname || "").toLowerCase();
|
||||||
const packageName = String(dataObj.name || "");
|
const packageName = String(dataObj.name || "");
|
||||||
const packageVersion = String(dataObj.version || "");
|
const packageVersion = String(dataObj.version || "");
|
||||||
const packageCategory = String(dataObj.category || "");
|
const packageCategory = String(dataObj.category || "");
|
||||||
@@ -1862,7 +1881,8 @@ export function registerSubmitterHandlers(
|
|||||||
"[Submitter] Deb metadata parsed",
|
"[Submitter] Deb metadata parsed",
|
||||||
);
|
);
|
||||||
|
|
||||||
const packDebFileName = `${debPkgName}_${debVersion}_${debArch}.deb`;
|
const packDebFileName =
|
||||||
|
`${debPkgName}_${debVersion}_${debArch}.deb`.toLowerCase();
|
||||||
logger.info(
|
logger.info(
|
||||||
{
|
{
|
||||||
from: debFilePath,
|
from: debFilePath,
|
||||||
@@ -1913,7 +1933,7 @@ export function registerSubmitterHandlers(
|
|||||||
sendPackageProgress("tar", 85, "正在打包 tar.gz...");
|
sendPackageProgress("tar", 85, "正在打包 tar.gz...");
|
||||||
logger.info("[Submitter] Starting tar...");
|
logger.info("[Submitter] Starting tar...");
|
||||||
|
|
||||||
const tarFileName = `${pkgname}-${storeArch}.tar.gz`;
|
const tarFileName = `${pkgname.toLowerCase()}-${storeArch.toLowerCase()}.tar.gz`;
|
||||||
const tarOutputPath = path.join(baseTempDir, tarFileName);
|
const tarOutputPath = path.join(baseTempDir, tarFileName);
|
||||||
try {
|
try {
|
||||||
await execAsync(
|
await execAsync(
|
||||||
@@ -1938,7 +1958,9 @@ export function registerSubmitterHandlers(
|
|||||||
sendPackageProgress("done", 100, "打包完成!");
|
sendPackageProgress("done", 100, "打包完成!");
|
||||||
|
|
||||||
logger.info({ baseTempDir }, "[Submitter] Opening folder");
|
logger.info({ baseTempDir }, "[Submitter] Opening folder");
|
||||||
await shell.openPath(baseTempDir);
|
shell.openPath(baseTempDir).catch((err) => {
|
||||||
|
logger.warn({ err }, "[Submitter] Failed to open folder");
|
||||||
|
});
|
||||||
|
|
||||||
const duration = Date.now() - startTime;
|
const duration = Date.now() - startTime;
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { ipcRenderer, contextBridge, type IpcRendererEvent } from "electron";
|
import {
|
||||||
|
ipcRenderer,
|
||||||
|
contextBridge,
|
||||||
|
webUtils,
|
||||||
|
type IpcRendererEvent,
|
||||||
|
} from "electron";
|
||||||
|
|
||||||
type StoreFilter = "spark" | "apm" | "both";
|
type StoreFilter = "spark" | "apm" | "both";
|
||||||
|
|
||||||
@@ -97,6 +102,10 @@ contextBridge.exposeInMainWorld("apm_store", {
|
|||||||
})(),
|
})(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
contextBridge.exposeInMainWorld("electronUtils", {
|
||||||
|
getPathForFile: (file: File): string => webUtils.getPathForFile(file),
|
||||||
|
});
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("windowControls", {
|
contextBridge.exposeInMainWorld("windowControls", {
|
||||||
minimize: () => ipcRenderer.send("window-control-minimize"),
|
minimize: () => ipcRenderer.send("window-control-minimize"),
|
||||||
toggleMaximize: () => ipcRenderer.send("window-control-toggle-maximize"),
|
toggleMaximize: () => ipcRenderer.send("window-control-toggle-maximize"),
|
||||||
|
|||||||
+11
-1
@@ -29,6 +29,16 @@ if grep -q "ID=aosc" /etc/os-release; then
|
|||||||
ARGS="$ARGS --no-spark"
|
ARGS="$ARGS --no-spark"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# 检查龙GPU,添加 --disable-gpu
|
||||||
|
ARCH=$(uname -m)
|
||||||
|
if [ "$ARCH" = "loongarch64" ] || [ "$ARCH" = "loong64" ]; then
|
||||||
|
is_loonggpu=$(lspci -s $(basename $(readlink $(grep -l connected /sys/class/drm/card*/*/status 2>/dev/null | head -1 | grep -o 'card[0-9]*' | xargs -I{} echo /sys/class/drm/{}/device))) | grep -qi loongson && echo "Found" || echo "NotFound")
|
||||||
|
if [ "$is_loonggpu" = "Found" ]; then
|
||||||
|
echo "检测到龙GPU"
|
||||||
|
ARGS="$ARGS --disable-gpu"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# 注意:已移除原先针对 arm64 + wayland 添加 --disable-gpu 的逻辑,
|
# 注意:已移除原先针对 arm64 + wayland 添加 --disable-gpu 的逻辑,
|
||||||
# 现在 arm64 设备无论是否使用 wayland 均不再添加此参数。
|
# 现在 arm64 设备无论是否使用 wayland 均不再添加此参数。
|
||||||
|
|
||||||
@@ -48,4 +58,4 @@ if [ $exit_code -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
exit $exit_code
|
exit $exit_code
|
||||||
@@ -3,7 +3,7 @@ Description=Spark Store update notifier
|
|||||||
After=apt-daily.service network.target network-online.target systemd-networkd.service NetworkManager.service connman.service
|
After=apt-daily.service network.target network-online.target systemd-networkd.service NetworkManager.service connman.service
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=simple
|
||||||
RemainAfterExit=yes
|
RemainAfterExit=yes
|
||||||
ExecStart=/opt/durapps/spark-store/bin/update-upgrade/ss-update-notifier.sh
|
ExecStart=/opt/durapps/spark-store/bin/update-upgrade/ss-update-notifier.sh
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
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="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
|
<div
|
||||||
class="flex items-center justify-between px-5 py-4"
|
class="flex items-center justify-between px-5 py-4"
|
||||||
@@ -136,7 +130,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, onUnmounted, ref } from "vue";
|
import { computed, ref } from "vue";
|
||||||
import type { DownloadItem } from "../global/typedefinition";
|
import type { DownloadItem } from "../global/typedefinition";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
@@ -153,30 +147,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const isExpanded = ref(false);
|
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(() => {
|
const completedDownloads = computed(() => {
|
||||||
return props.downloads.filter((d) => d.status === "completed").length;
|
return props.downloads.filter((d) => d.status === "completed").length;
|
||||||
|
|||||||
@@ -14,13 +14,23 @@
|
|||||||
</div>
|
</div>
|
||||||
<h1 class="text-lg font-semibold">投稿应用</h1>
|
<h1 class="text-lg font-semibold">投稿应用</h1>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<div class="flex items-center gap-2">
|
||||||
type="button"
|
<button
|
||||||
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"
|
type="button"
|
||||||
@click="closeWindow"
|
class="submitter-close-button inline-flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium text-blue-600 hover:bg-blue-50 dark:text-blue-400 dark:hover:bg-slate-800"
|
||||||
>
|
@click="openSubmissionQueue"
|
||||||
<i class="fas fa-times"></i>
|
>
|
||||||
</button>
|
<i class="fas fa-list"></i>
|
||||||
|
查看当前投稿队列
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
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>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,6 +145,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||||
|
>官网地址</label
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
v-model="formData.website"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://example.com"
|
||||||
|
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||||
@@ -161,19 +184,6 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
|
||||||
>官网地址</label
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
v-model="formData.website"
|
|
||||||
type="url"
|
|
||||||
placeholder="https://example.com"
|
|
||||||
class="w-full px-4 py-2.5 rounded-lg border border-slate-200 bg-white dark:bg-slate-800 dark:border-slate-700 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label
|
<label
|
||||||
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-2"
|
||||||
@@ -215,12 +225,16 @@
|
|||||||
>截图(最多5张)</label
|
>截图(最多5张)</label
|
||||||
>
|
>
|
||||||
<p class="mb-2 text-xs text-slate-500 dark:text-slate-400">
|
<p class="mb-2 text-xs text-slate-500 dark:text-slate-400">
|
||||||
可直接 Ctrl+V 粘贴截图
|
可点击添加、拖放或直接 Ctrl+V 粘贴 PNG 截图
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="grid grid-cols-5 gap-3 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
class="grid grid-cols-5 gap-3 rounded-lg border-2 border-dashed border-transparent p-2 transition-colors hover:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
@paste="handleScreenshotPaste"
|
@paste="handleScreenshotPaste"
|
||||||
|
@drop="handleScreenshotDrop"
|
||||||
|
@dragover.prevent
|
||||||
|
@dragenter.prevent
|
||||||
|
@dragleave.prevent
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="(screenshot, index) in formData.screenshots"
|
v-for="(screenshot, index) in formData.screenshots"
|
||||||
@@ -247,11 +261,17 @@
|
|||||||
<input
|
<input
|
||||||
ref="screenshotInput"
|
ref="screenshotInput"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".png,.jpg,.jpeg"
|
accept=".png"
|
||||||
multiple
|
multiple
|
||||||
class="hidden"
|
class="hidden"
|
||||||
@change="handleScreenshotSelect"
|
@change="handleScreenshotSelect"
|
||||||
/>
|
/>
|
||||||
|
<p
|
||||||
|
v-if="mediaError"
|
||||||
|
class="mt-2 text-sm text-red-600 dark:text-red-400"
|
||||||
|
>
|
||||||
|
{{ mediaError }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -536,7 +556,7 @@
|
|||||||
<button
|
<button
|
||||||
type="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"
|
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"
|
@click="continueSubmission"
|
||||||
>
|
>
|
||||||
继续投递
|
继续投递
|
||||||
</button>
|
</button>
|
||||||
@@ -620,11 +640,25 @@
|
|||||||
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-emerald-500 transition-colors text-left"
|
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-emerald-500 transition-colors text-left"
|
||||||
@click="selectPackArch(arch)"
|
@click="selectPackArch(arch)"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-slate-900 dark:text-slate-100">
|
<div class="flex items-center gap-2">
|
||||||
{{ getArchDisplayName(arch.store) }}
|
<span
|
||||||
|
class="rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
|
:class="
|
||||||
|
isSparkHistoryStore(arch.store)
|
||||||
|
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300'
|
||||||
|
: 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
{{ isSparkHistoryStore(arch.store) ? "Spark" : "APM" }}
|
||||||
|
</span>
|
||||||
|
<span class="font-medium text-slate-900 dark:text-slate-100">
|
||||||
|
{{ getArchDisplayName(arch.store) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||||
输出: {{ formData.pkgname }}-{{ arch.store }}.tar.gz
|
输出: {{ formData.pkgname.toLowerCase() }}-{{
|
||||||
|
arch.store
|
||||||
|
}}.tar.gz
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -676,14 +710,23 @@
|
|||||||
|
|
||||||
<div class="space-y-3 mb-6">
|
<div class="space-y-3 mb-6">
|
||||||
<button
|
<button
|
||||||
v-for="arch in availableArchs"
|
v-for="arch in sortedArchs"
|
||||||
:key="arch.store"
|
:key="arch.store"
|
||||||
type="button"
|
type="button"
|
||||||
class="w-full p-4 rounded-lg border-2 border-slate-200 dark:border-slate-700 hover:border-blue-500 transition-colors text-left"
|
class="w-full p-4 rounded-lg border-2 transition-colors text-left"
|
||||||
|
:class="archOriginBorderClass(arch.store)"
|
||||||
@click="selectArch(arch)"
|
@click="selectArch(arch)"
|
||||||
>
|
>
|
||||||
<div class="font-medium text-slate-900 dark:text-slate-100">
|
<div class="flex items-center gap-2">
|
||||||
{{ getArchDisplayName(arch.store) }}
|
<span class="font-medium text-slate-900 dark:text-slate-100">
|
||||||
|
{{ getArchDisplayName(arch.store) }}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium"
|
||||||
|
:class="getOriginBadgeClass(arch.store)"
|
||||||
|
>
|
||||||
|
{{ getOriginLabel(arch.store) }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
<div class="text-sm text-slate-500 dark:text-slate-400 mt-1">
|
||||||
版本: {{ arch.version }} | 分类: {{ arch.category }}
|
版本: {{ arch.version }} | 分类: {{ arch.category }}
|
||||||
@@ -767,6 +810,7 @@ const isSearchingHistory = ref(false);
|
|||||||
const showArchDialog = ref(false);
|
const showArchDialog = ref(false);
|
||||||
const availableArchs = ref<HistoryArchInfo[]>([]);
|
const availableArchs = ref<HistoryArchInfo[]>([]);
|
||||||
const currentDebArch = ref("");
|
const currentDebArch = ref("");
|
||||||
|
const mediaError = ref("");
|
||||||
const iconPreview = ref("");
|
const iconPreview = ref("");
|
||||||
const iconFileName = ref("");
|
const iconFileName = ref("");
|
||||||
|
|
||||||
@@ -864,13 +908,70 @@ const hasUserStartedFilling = computed(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const getArchDisplayName = (store: string): string => {
|
const getHistoryArch = (
|
||||||
const archMap: Record<string, string> = {
|
store: string,
|
||||||
store: "AMD64 (x86_64)",
|
): "amd64" | "arm64" | "loong64" | "other" => {
|
||||||
"aarch64-store": "ARM64 (aarch64)",
|
if (store === "store" || store.startsWith("amd64-")) return "amd64";
|
||||||
"loong64-store": "LoongArch64",
|
if (store === "aarch64-store" || store.startsWith("arm64-")) return "arm64";
|
||||||
|
if (store === "loong64-store" || store.startsWith("loong64-"))
|
||||||
|
return "loong64";
|
||||||
|
return "other";
|
||||||
|
};
|
||||||
|
|
||||||
|
const isSparkHistoryStore = (store: string): boolean => {
|
||||||
|
// 兼容新格式(amd64-store / arm64-store / loong64-store / amd64-apm ...)
|
||||||
|
// 与旧格式(store / aarch64-store / loong64-store)
|
||||||
|
return store === "store" || store.endsWith("-store");
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortHistoryArchs = (archs: HistoryArchInfo[]): HistoryArchInfo[] => {
|
||||||
|
const archOrder: Record<ReturnType<typeof getHistoryArch>, number> = {
|
||||||
|
amd64: 0,
|
||||||
|
arm64: 1,
|
||||||
|
loong64: 2,
|
||||||
|
other: 3,
|
||||||
};
|
};
|
||||||
return archMap[store] || store;
|
|
||||||
|
return [...archs].sort((a, b) => {
|
||||||
|
const sourceOrder =
|
||||||
|
Number(isSparkHistoryStore(b.store)) -
|
||||||
|
Number(isSparkHistoryStore(a.store));
|
||||||
|
if (sourceOrder !== 0) return sourceOrder;
|
||||||
|
return (
|
||||||
|
archOrder[getHistoryArch(a.store)] - archOrder[getHistoryArch(b.store)]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getArchDisplayName = (store: string): string => {
|
||||||
|
const arch = getHistoryArch(store);
|
||||||
|
const archMap: Record<typeof arch, string> = {
|
||||||
|
amd64: "AMD64 (x86_64)",
|
||||||
|
arm64: "ARM64 (aarch64)",
|
||||||
|
loong64: "LoongArch64",
|
||||||
|
other: store,
|
||||||
|
};
|
||||||
|
return archMap[arch];
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedArchs = computed(() => sortHistoryArchs(availableArchs.value));
|
||||||
|
|
||||||
|
const getOriginLabel = (store: string): string => {
|
||||||
|
return isSparkHistoryStore(store) ? "Spark" : "APM";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOriginBadgeClass = (store: string): string => {
|
||||||
|
if (isSparkHistoryStore(store)) {
|
||||||
|
return "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400";
|
||||||
|
}
|
||||||
|
return "bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400";
|
||||||
|
};
|
||||||
|
|
||||||
|
const archOriginBorderClass = (store: string): string => {
|
||||||
|
if (isSparkHistoryStore(store)) {
|
||||||
|
return "border-slate-200 dark:border-slate-700 hover:border-blue-500";
|
||||||
|
}
|
||||||
|
return "border-slate-200 dark:border-slate-700 hover:border-amber-400";
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCategoriesPromise = ref<Promise<void> | null>(null);
|
const loadCategoriesPromise = ref<Promise<void> | null>(null);
|
||||||
@@ -1042,6 +1143,14 @@ const updateFormTags = () => {
|
|||||||
formData.tags = selectedTags.value.map((t) => t.value).join(";");
|
formData.tags = selectedTags.value.map((t) => t.value).join(";");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const openSubmissionQueue = (): void => {
|
||||||
|
window.open(
|
||||||
|
"https://upload.spark-app.store/",
|
||||||
|
"_blank",
|
||||||
|
"noopener,noreferrer",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const selectDebFile = async () => {
|
const selectDebFile = async () => {
|
||||||
const result = await window.ipcRenderer.invoke("select-deb-file");
|
const result = await window.ipcRenderer.invoke("select-deb-file");
|
||||||
if (result?.success && result.filePath) {
|
if (result?.success && result.filePath) {
|
||||||
@@ -1091,14 +1200,8 @@ const searchHistoryApp = async () => {
|
|||||||
console.log("[Submitter] availableArchs before:", availableArchs.value);
|
console.log("[Submitter] availableArchs before:", availableArchs.value);
|
||||||
console.log("[Submitter] showArchDialog before:", showArchDialog.value);
|
console.log("[Submitter] showArchDialog before:", showArchDialog.value);
|
||||||
|
|
||||||
// 按 amd64 → arm64 → loong64 顺序排序
|
availableArchs.value = sortHistoryArchs(
|
||||||
const archOrder: Record<string, number> = {
|
historyResult.data as HistoryArchInfo[],
|
||||||
store: 0,
|
|
||||||
"aarch64-store": 1,
|
|
||||||
"loong64-store": 2,
|
|
||||||
};
|
|
||||||
availableArchs.value = [...historyResult.data].sort(
|
|
||||||
(a, b) => (archOrder[a.store] ?? 99) - (archOrder[b.store] ?? 99),
|
|
||||||
);
|
);
|
||||||
console.log("[Submitter] availableArchs after:", availableArchs.value);
|
console.log("[Submitter] availableArchs after:", availableArchs.value);
|
||||||
console.log(
|
console.log(
|
||||||
@@ -1112,7 +1215,7 @@ const searchHistoryApp = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 从第一条历史记录预填名称和分类
|
// 从第一条历史记录预填名称和分类
|
||||||
const firstArch = historyResult.data[0];
|
const firstArch = availableArchs.value[0];
|
||||||
if (firstArch) {
|
if (firstArch) {
|
||||||
formData.name = firstArch.name || formData.name;
|
formData.name = firstArch.name || formData.name;
|
||||||
formData.category = firstArch.category || formData.category;
|
formData.category = firstArch.category || formData.category;
|
||||||
@@ -1183,6 +1286,15 @@ const searchHistoryApp = async () => {
|
|||||||
const parseDebFileAndSearchHistory = async (debPath: string) => {
|
const parseDebFileAndSearchHistory = async (debPath: string) => {
|
||||||
isParsingDeb.value = true;
|
isParsingDeb.value = true;
|
||||||
debParseError.value = "";
|
debParseError.value = "";
|
||||||
|
availableArchs.value = [];
|
||||||
|
showArchDialog.value = false;
|
||||||
|
formData.pkgname = "";
|
||||||
|
formData.version = "";
|
||||||
|
formData.author = "";
|
||||||
|
formData.contributor = "";
|
||||||
|
formData.website = "";
|
||||||
|
formData.description = "";
|
||||||
|
currentDebArch.value = "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
console.log(
|
console.log(
|
||||||
@@ -1294,13 +1406,20 @@ const handleDrop = async (event: DragEvent) => {
|
|||||||
if (file.name.endsWith(".deb")) {
|
if (file.name.endsWith(".deb")) {
|
||||||
console.log("[Submitter] File is a deb package");
|
console.log("[Submitter] File is a deb package");
|
||||||
|
|
||||||
const textUriList = event.dataTransfer?.getData("text/uri-list");
|
// 在 contextIsolation 环境下,File.path 不可用
|
||||||
console.log("[Submitter] text/uri-list:", textUriList);
|
// 使用 Electron 的 webUtils.getPathForFile() 获取真实文件系统路径
|
||||||
|
let filePath: string;
|
||||||
const textPlain = event.dataTransfer?.getData("text/plain");
|
try {
|
||||||
console.log("[Submitter] text/plain:", textPlain);
|
filePath = window.electronUtils.getPathForFile(file);
|
||||||
|
console.log("[Submitter] File path from electronUtils:", filePath);
|
||||||
const filePath = file.path || textUriList || textPlain;
|
} catch {
|
||||||
|
console.warn(
|
||||||
|
"[Submitter] electronUtils.getPathForFile failed, trying fallback",
|
||||||
|
);
|
||||||
|
const textUriList = event.dataTransfer?.getData("text/uri-list");
|
||||||
|
const textPlain = event.dataTransfer?.getData("text/plain");
|
||||||
|
filePath = textUriList || textPlain || "";
|
||||||
|
}
|
||||||
console.log("[Submitter] Final filePath:", filePath);
|
console.log("[Submitter] Final filePath:", filePath);
|
||||||
|
|
||||||
if (filePath) {
|
if (filePath) {
|
||||||
@@ -1397,38 +1516,46 @@ const selectIconFile = () => {
|
|||||||
iconFileInput.value?.click();
|
iconFileInput.value?.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const isPngFile = (file: File): boolean => {
|
||||||
|
return (
|
||||||
|
file.type === "image/png" || (file.type === "" && /\.png$/i.test(file.name))
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const readIconFile = (file: File): void => {
|
||||||
|
iconFileName.value = file.name;
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = (e) => {
|
||||||
|
const dataUrl = e.target?.result as string;
|
||||||
|
formData.iconPath = dataUrl;
|
||||||
|
iconPreview.value = dataUrl;
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
};
|
||||||
|
|
||||||
const handleIconFileSelect = (event: Event) => {
|
const handleIconFileSelect = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const file = target.files?.[0];
|
const file = target.files?.[0];
|
||||||
if (file) {
|
if (file) {
|
||||||
iconFileName.value = file.name;
|
if (isPngFile(file)) {
|
||||||
const reader = new FileReader();
|
mediaError.value = "";
|
||||||
reader.onload = (e) => {
|
readIconFile(file);
|
||||||
const dataUrl = e.target?.result as string;
|
} else {
|
||||||
formData.iconPath = dataUrl;
|
mediaError.value = "应用图标仅支持 PNG 格式。";
|
||||||
iconPreview.value = dataUrl;
|
}
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
|
target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleIconDrop = (event: DragEvent) => {
|
const handleIconDrop = (event: DragEvent) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const file = event.dataTransfer?.files?.[0];
|
const file = event.dataTransfer?.files?.[0];
|
||||||
if (
|
if (!file) return;
|
||||||
file &&
|
if (isPngFile(file)) {
|
||||||
(file.name.endsWith(".png") ||
|
mediaError.value = "";
|
||||||
file.name.endsWith(".jpg") ||
|
readIconFile(file);
|
||||||
file.name.endsWith(".jpeg"))
|
} else {
|
||||||
) {
|
mediaError.value = "应用图标仅支持 PNG 格式。";
|
||||||
iconFileName.value = file.name;
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = (e) => {
|
|
||||||
const dataUrl = e.target?.result as string;
|
|
||||||
formData.iconPath = dataUrl;
|
|
||||||
iconPreview.value = dataUrl;
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1437,7 +1564,14 @@ const addScreenshot = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const importScreenshots = (files: File[]) => {
|
const importScreenshots = (files: File[]) => {
|
||||||
for (const file of files) {
|
const pngFiles = files.filter(isPngFile);
|
||||||
|
if (pngFiles.length !== files.length) {
|
||||||
|
mediaError.value = "截图仅支持 PNG 格式。";
|
||||||
|
} else if (pngFiles.length > 0) {
|
||||||
|
mediaError.value = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const file of pngFiles) {
|
||||||
if (formData.screenshots.length >= 5) break;
|
if (formData.screenshots.length >= 5) break;
|
||||||
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -1455,16 +1589,18 @@ const importScreenshots = (files: File[]) => {
|
|||||||
|
|
||||||
const handleScreenshotSelect = (event: Event) => {
|
const handleScreenshotSelect = (event: Event) => {
|
||||||
const target = event.target as HTMLInputElement;
|
const target = event.target as HTMLInputElement;
|
||||||
const files = Array.from(target.files ?? []).filter((file) =>
|
importScreenshots(Array.from(target.files ?? []));
|
||||||
/\.(png|jpe?g)$/i.test(file.name),
|
|
||||||
);
|
|
||||||
importScreenshots(files);
|
|
||||||
target.value = "";
|
target.value = "";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleScreenshotDrop = (event: DragEvent) => {
|
||||||
|
event.preventDefault();
|
||||||
|
importScreenshots(Array.from(event.dataTransfer?.files ?? []));
|
||||||
|
};
|
||||||
|
|
||||||
const handleScreenshotPaste = (event: ClipboardEvent) => {
|
const handleScreenshotPaste = (event: ClipboardEvent) => {
|
||||||
const files = Array.from(event.clipboardData?.items ?? [])
|
const files = Array.from(event.clipboardData?.items ?? [])
|
||||||
.filter((item) => item.type.startsWith("image/"))
|
.filter((item) => item.kind === "file")
|
||||||
.map((item) => item.getAsFile())
|
.map((item) => item.getAsFile())
|
||||||
.filter((file): file is File => file !== null);
|
.filter((file): file is File => file !== null);
|
||||||
|
|
||||||
@@ -1497,6 +1633,7 @@ const resetForm = () => {
|
|||||||
submitSuccess.value = false;
|
submitSuccess.value = false;
|
||||||
submitError.value = "";
|
submitError.value = "";
|
||||||
debParseError.value = "";
|
debParseError.value = "";
|
||||||
|
mediaError.value = "";
|
||||||
packageSuccess.value = false;
|
packageSuccess.value = false;
|
||||||
packageError.value = "";
|
packageError.value = "";
|
||||||
packageResult.value = null;
|
packageResult.value = null;
|
||||||
@@ -1514,14 +1651,29 @@ const resetForm = () => {
|
|||||||
isSearchingHistory.value = false;
|
isSearchingHistory.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeSubmitSuccessModal = () => {
|
const clearSubmitTransientState = (): void => {
|
||||||
showSubmitSuccessModal.value = false;
|
showSubmitSuccessModal.value = false;
|
||||||
resetForm();
|
submitSuccess.value = false;
|
||||||
|
isSubmitting.value = false;
|
||||||
|
uploadProgress.value = 0;
|
||||||
|
uploadStage.value = "";
|
||||||
|
uploadStageMessage.value = "";
|
||||||
|
uploadStages.value = [];
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetFormAndClose = () => {
|
const closeSubmitSuccessModal = () => {
|
||||||
showSubmitSuccessModal.value = false;
|
clearSubmitTransientState();
|
||||||
resetForm();
|
};
|
||||||
|
|
||||||
|
const continueSubmission = () => {
|
||||||
|
const shouldClear = window.confirm(
|
||||||
|
"是否清空当前表单内容后继续投递?选择“取消”将保留已填写内容。",
|
||||||
|
);
|
||||||
|
if (shouldClear) {
|
||||||
|
resetForm();
|
||||||
|
} else {
|
||||||
|
clearSubmitTransientState();
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitForm = async () => {
|
const submitForm = async () => {
|
||||||
@@ -1714,15 +1866,43 @@ const closeWindow = () => {
|
|||||||
|
|
||||||
import { onMounted, nextTick } from "vue";
|
import { onMounted, nextTick } from "vue";
|
||||||
|
|
||||||
const getGitEmail = async () => {
|
const getGitInfo = async () => {
|
||||||
try {
|
try {
|
||||||
const result = await window.ipcRenderer.invoke("get-git-email");
|
const [nameResult, emailResult] = await Promise.all([
|
||||||
if (result?.success && result.data) {
|
window.ipcRenderer.invoke("get-git-name"),
|
||||||
formData.mail = result.data;
|
window.ipcRenderer.invoke("get-git-email"),
|
||||||
console.log("[Submitter] Git email auto-filled:", result.data);
|
]);
|
||||||
|
|
||||||
|
let gitName = "";
|
||||||
|
let gitEmail = "";
|
||||||
|
|
||||||
|
if (nameResult?.success && nameResult.data) {
|
||||||
|
gitName = nameResult.data;
|
||||||
|
console.log("[Submitter] Git name auto-filled:", gitName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emailResult?.success && emailResult.data) {
|
||||||
|
gitEmail = emailResult.data;
|
||||||
|
console.log("[Submitter] Git email auto-filled:", gitEmail);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 git name 和 email 合并填入 contributor 字段,格式: Name <email>
|
||||||
|
if (gitName || gitEmail) {
|
||||||
|
if (gitName && gitEmail) {
|
||||||
|
formData.contributor = `${gitName} <${gitEmail}>`;
|
||||||
|
} else if (gitName) {
|
||||||
|
formData.contributor = gitName;
|
||||||
|
} else if (gitEmail) {
|
||||||
|
formData.contributor = gitEmail;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单独填入邮箱
|
||||||
|
if (gitEmail) {
|
||||||
|
formData.mail = gitEmail;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("[Submitter] Failed to get git email:", err);
|
console.warn("[Submitter] Failed to get git info:", err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1730,8 +1910,8 @@ onMounted(async () => {
|
|||||||
console.log("[Submitter] Component mounted, loading categories and tags");
|
console.log("[Submitter] Component mounted, loading categories and tags");
|
||||||
// 先等待分类列表加载完成,避免后续竞态
|
// 先等待分类列表加载完成,避免后续竞态
|
||||||
await Promise.all([loadCategoriesList(), loadTagsList()]);
|
await Promise.all([loadCategoriesList(), loadTagsList()]);
|
||||||
// 尝试从 git 配置读取邮箱
|
// 尝试从 git 配置读取 name 和 email,填入 contributor 和 mail
|
||||||
await getGitEmail();
|
await getGitInfo();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Vendored
+3
@@ -22,6 +22,9 @@ declare global {
|
|||||||
};
|
};
|
||||||
windowControls: WindowControlBridge;
|
windowControls: WindowControlBridge;
|
||||||
updateCenter: UpdateCenterBridge;
|
updateCenter: UpdateCenterBridge;
|
||||||
|
electronUtils: {
|
||||||
|
getPathForFile: (file: File) => string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user