mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat: 添加已安装应用和可更新应用的管理功能,支持卸载和升级操作
This commit is contained in:
@@ -37,10 +37,87 @@ const checkSuperUserCommand = async (): Promise<string> => {
|
||||
return superUserCmd;
|
||||
}
|
||||
|
||||
const runCommandCapture = async (execCommand: string, execParams: string[], envOverride?: NodeJS.ProcessEnv) => {
|
||||
return await new Promise<{ code: number; stdout: string; stderr: string }>((resolve) => {
|
||||
const child = spawn(execCommand, execParams, {
|
||||
shell: true,
|
||||
env: { ...process.env, ...(envOverride || {}) }
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
child.stdout?.on('data', (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
resolve({ code: -1, stdout, stderr: err.message });
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
resolve({ code: typeof code === 'number' ? code : -1, stdout, stderr });
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const parseInstalledList = (output: string) => {
|
||||
const apps: Array<{ pkgname: string; version: string; arch: string; flags: string; raw: string }> = [];
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
if (trimmed.startsWith('Listing')) continue;
|
||||
if (trimmed.startsWith('[INFO]')) continue;
|
||||
|
||||
const match = trimmed.match(/^(\S+)\/\S+,\S+\s+(\S+)\s+(\S+)\s+\[(.+)\]$/);
|
||||
if (!match) continue;
|
||||
apps.push({
|
||||
pkgname: match[1],
|
||||
version: match[2],
|
||||
arch: match[3],
|
||||
flags: match[4],
|
||||
raw: trimmed
|
||||
});
|
||||
}
|
||||
return apps;
|
||||
};
|
||||
|
||||
const parseUpgradableList = (output: string) => {
|
||||
const apps: Array<{ pkgname: string; newVersion: string; currentVersion: string; raw: string }> = [];
|
||||
const lines = output.split('\n');
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
if (trimmed.startsWith('Listing')) continue;
|
||||
if (trimmed.startsWith('[INFO]')) continue;
|
||||
if (trimmed.includes('=') && !trimmed.includes('/')) continue;
|
||||
|
||||
if (!trimmed.includes('/')) continue;
|
||||
|
||||
const tokens = trimmed.split(/\s+/);
|
||||
if (tokens.length < 2) continue;
|
||||
const pkgToken = tokens[0];
|
||||
const pkgname = pkgToken.split('/')[0];
|
||||
const newVersion = tokens[1] || '';
|
||||
const currentMatch = trimmed.match(/\[(?:upgradable from|from):\s*([^\]\s]+)\]/i);
|
||||
const currentToken = tokens[5] || '';
|
||||
const currentVersion = currentMatch?.[1] || currentToken.replace('[', '').replace(']', '');
|
||||
|
||||
if (!pkgname) continue;
|
||||
apps.push({ pkgname, newVersion, currentVersion, raw: trimmed });
|
||||
}
|
||||
return apps;
|
||||
};
|
||||
|
||||
// Listen for download requests from renderer process
|
||||
ipcMain.on('queue-install', async (event, download_json) => {
|
||||
const download = JSON.parse(download_json);
|
||||
const { id, pkgname } = download || {};
|
||||
const { id, pkgname, upgradeOnly } = download || {};
|
||||
|
||||
if (!id || !pkgname) {
|
||||
logger.warn('passed arguments missing id or pkgname');
|
||||
@@ -251,3 +328,71 @@ ipcMain.on('remove-installed', async (_event, pkgname: string) => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('list-upgradable', async () => {
|
||||
const listCommand = 'source /opt/apm-store/transhell.sh; load_transhell_debug; amber-pm-debug aptss list --upgradable';
|
||||
const { code, stdout, stderr } = await runCommandCapture(
|
||||
'/bin/bash',
|
||||
['-lc', listCommand],
|
||||
{ LANGUAGE: 'en_US' }
|
||||
);
|
||||
if (code !== 0) {
|
||||
logger.error(`list-upgradable failed: ${stderr || stdout}`);
|
||||
return {
|
||||
success: false,
|
||||
message: stderr || stdout || `list-upgradable failed with code ${code}`,
|
||||
apps: []
|
||||
};
|
||||
}
|
||||
|
||||
const apps = parseUpgradableList(stdout);
|
||||
return { success: true, apps };
|
||||
});
|
||||
|
||||
ipcMain.handle('list-installed', async () => {
|
||||
const superUserCmd = await checkSuperUserCommand();
|
||||
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||||
const execParams = superUserCmd.length > 0
|
||||
? ['/usr/bin/apm', 'list', '--installed']
|
||||
: ['list', '--installed'];
|
||||
|
||||
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||||
if (code !== 0) {
|
||||
logger.error(`list-installed failed: ${stderr || stdout}`);
|
||||
return {
|
||||
success: false,
|
||||
message: stderr || stdout || `list-installed failed with code ${code}`,
|
||||
apps: []
|
||||
};
|
||||
}
|
||||
|
||||
const apps = parseInstalledList(stdout);
|
||||
return { success: true, apps };
|
||||
});
|
||||
|
||||
ipcMain.handle('uninstall-installed', async (_event, pkgname: string) => {
|
||||
if (!pkgname) {
|
||||
logger.warn('uninstall-installed missing pkgname');
|
||||
return { success: false, message: 'missing pkgname' };
|
||||
}
|
||||
|
||||
const superUserCmd = await checkSuperUserCommand();
|
||||
const execCommand = superUserCmd.length > 0 ? superUserCmd : '/usr/bin/apm';
|
||||
const execParams = superUserCmd.length > 0
|
||||
? ['/usr/bin/apm', 'remove', '-y', pkgname]
|
||||
: ['remove', '-y', pkgname];
|
||||
|
||||
const { code, stdout, stderr } = await runCommandCapture(execCommand, execParams);
|
||||
const success = code === 0;
|
||||
|
||||
if (success) {
|
||||
logger.info(`卸载完成: ${pkgname}`);
|
||||
} else {
|
||||
logger.error(`卸载失败: ${pkgname} ${stderr || stdout}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
message: success ? '卸载完成' : (stderr || stdout || `卸载失败,退出码 ${code}`)
|
||||
};
|
||||
});
|
||||
133
src/App.vue
133
src/App.vue
@@ -24,6 +24,15 @@
|
||||
<DownloadDetail :show="showDownloadDetailModal" :download="currentDownload" @close="closeDownloadDetail"
|
||||
@pause="pauseDownload" @resume="resumeDownload" @cancel="cancelDownload" @retry="retryDownload"
|
||||
@open-app="openDownloadedApp" />
|
||||
|
||||
<InstalledAppsModal :show="showInstalledModal" :apps="installedApps" :loading="installedLoading"
|
||||
:error="installedError" @close="closeInstalledModal" @refresh="refreshInstalledApps"
|
||||
@uninstall="uninstallInstalledApp" />
|
||||
|
||||
<UpdateAppsModal :show="showUpdateModal" :apps="upgradableApps" :loading="updateLoading"
|
||||
:error="updateError" :has-selected="hasSelectedUpgrades" @close="closeUpdateModal"
|
||||
@refresh="refreshUpgradableApps" @toggle-all="toggleAllUpgrades"
|
||||
@upgrade-selected="upgradeSelectedApps" @upgrade-one="upgradeSingleApp" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -38,9 +47,11 @@ import AppDetailModal from './components/AppDetailModal.vue';
|
||||
import ScreenPreview from './components/ScreenPreview.vue';
|
||||
import DownloadQueue from './components/DownloadQueue.vue';
|
||||
import DownloadDetail from './components/DownloadDetail.vue';
|
||||
import InstalledAppsModal from './components/InstalledAppsModal.vue';
|
||||
import UpdateAppsModal from './components/UpdateAppsModal.vue';
|
||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp, currentAppIsInstalled } from './global/storeConfig';
|
||||
import { downloads } from './global/downloadStatus';
|
||||
import { handleInstall, handleRetry, handleRemove } from './modeuls/processInstall';
|
||||
import { handleInstall, handleRetry, handleRemove, handleUpgrade } from './modeuls/processInstall';
|
||||
|
||||
const logger = pino();
|
||||
|
||||
@@ -63,6 +74,14 @@ const screenshots = ref([]);
|
||||
const loading = ref(true);
|
||||
const showDownloadDetailModal = ref(false);
|
||||
const currentDownload = ref(null);
|
||||
const showInstalledModal = ref(false);
|
||||
const installedApps = ref([]);
|
||||
const installedLoading = ref(false);
|
||||
const installedError = ref('');
|
||||
const showUpdateModal = ref(false);
|
||||
const upgradableApps = ref([]);
|
||||
const updateLoading = ref(false);
|
||||
const updateError = ref('');
|
||||
|
||||
// 计算属性
|
||||
const filteredApps = computed(() => {
|
||||
@@ -96,6 +115,10 @@ const categoryCounts = computed(() => {
|
||||
return counts;
|
||||
});
|
||||
|
||||
const hasSelectedUpgrades = computed(() => {
|
||||
return upgradableApps.value.some(app => app.selected);
|
||||
});
|
||||
|
||||
// 方法
|
||||
const initTheme = () => {
|
||||
const savedTheme = localStorage.getItem('theme');
|
||||
@@ -183,17 +206,115 @@ const nextScreen = () => {
|
||||
};
|
||||
|
||||
const handleUpdate = () => {
|
||||
openApmStoreUrl('apmstore://action?cmd=update', {
|
||||
fallbackText: 'apm-update-tool'
|
||||
});
|
||||
openUpdateModal();
|
||||
};
|
||||
|
||||
const handleList = () => {
|
||||
openApmStoreUrl('apmstore://action?cmd=list', {
|
||||
fallbackText: '/usr/bin/apm-installer --list'
|
||||
openInstalledModal();
|
||||
};
|
||||
|
||||
const openUpdateModal = () => {
|
||||
showUpdateModal.value = true;
|
||||
refreshUpgradableApps();
|
||||
};
|
||||
|
||||
const closeUpdateModal = () => {
|
||||
showUpdateModal.value = false;
|
||||
};
|
||||
|
||||
const refreshUpgradableApps = async () => {
|
||||
updateLoading.value = true;
|
||||
updateError.value = '';
|
||||
try {
|
||||
const result = await window.ipcRenderer.invoke('list-upgradable');
|
||||
if (!result?.success) {
|
||||
upgradableApps.value = [];
|
||||
updateError.value = result?.message || '检查更新失败';
|
||||
return;
|
||||
}
|
||||
upgradableApps.value = (result.apps || []).map(app => ({
|
||||
...app,
|
||||
selected: false,
|
||||
upgrading: false
|
||||
}));
|
||||
} catch (error) {
|
||||
upgradableApps.value = [];
|
||||
updateError.value = error?.message || '检查更新失败';
|
||||
} finally {
|
||||
updateLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAllUpgrades = () => {
|
||||
const shouldSelectAll = !hasSelectedUpgrades.value || upgradableApps.value.some(app => !app.selected);
|
||||
upgradableApps.value = upgradableApps.value.map(app => ({
|
||||
...app,
|
||||
selected: shouldSelectAll ? true : false
|
||||
}));
|
||||
};
|
||||
|
||||
const upgradeSingleApp = (app) => {
|
||||
if (!app?.pkgname) return;
|
||||
handleUpgrade(app.pkgname, app.newVersion || '');
|
||||
};
|
||||
|
||||
const upgradeSelectedApps = () => {
|
||||
const selectedApps = upgradableApps.value.filter(app => app.selected);
|
||||
selectedApps.forEach(app => {
|
||||
upgradeSingleApp(app);
|
||||
});
|
||||
};
|
||||
|
||||
const openInstalledModal = () => {
|
||||
showInstalledModal.value = true;
|
||||
refreshInstalledApps();
|
||||
};
|
||||
|
||||
const closeInstalledModal = () => {
|
||||
showInstalledModal.value = false;
|
||||
};
|
||||
|
||||
const refreshInstalledApps = async () => {
|
||||
installedLoading.value = true;
|
||||
installedError.value = '';
|
||||
try {
|
||||
const result = await window.ipcRenderer.invoke('list-installed');
|
||||
if (!result?.success) {
|
||||
installedApps.value = [];
|
||||
installedError.value = result?.message || '读取已安装应用失败';
|
||||
return;
|
||||
}
|
||||
installedApps.value = (result.apps || []).map((app) => ({
|
||||
...app,
|
||||
removing: false
|
||||
}));
|
||||
} catch (error) {
|
||||
installedApps.value = [];
|
||||
installedError.value = error?.message || '读取已安装应用失败';
|
||||
} finally {
|
||||
installedLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const uninstallInstalledApp = async (app) => {
|
||||
if (!app?.pkgname) return;
|
||||
const target = installedApps.value.find(item => item.pkgname === app.pkgname);
|
||||
if (target) target.removing = true;
|
||||
try {
|
||||
const result = await window.ipcRenderer.invoke('uninstall-installed', app.pkgname);
|
||||
if (!result?.success) {
|
||||
installedError.value = result?.message || '卸载失败';
|
||||
} else {
|
||||
installedApps.value = installedApps.value.filter(item => item.pkgname !== app.pkgname);
|
||||
}
|
||||
} catch (error) {
|
||||
installedError.value = error?.message || '卸载失败';
|
||||
} finally {
|
||||
const restore = installedApps.value.find(item => item.pkgname === app.pkgname);
|
||||
if (restore) restore.removing = false;
|
||||
}
|
||||
};
|
||||
|
||||
const openApmStoreUrl = (url, { fallbackText } = {}) => {
|
||||
try {
|
||||
window.location.href = url;
|
||||
|
||||
148
src/components/InstalledAppsModal.vue
Normal file
148
src/components/InstalledAppsModal.vue
Normal file
@@ -0,0 +1,148 @@
|
||||
<template>
|
||||
<div class="modal-backdrop" :style="{ display: show ? 'flex' : 'none' }" role="dialog" aria-hidden="false">
|
||||
<div class="modal installed-modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title-section">
|
||||
<div class="modal-title">已安装应用</div>
|
||||
<div class="modal-subtitle">来自本机 APM 安装列表</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="action-btn" :disabled="loading" @click="$emit('refresh')">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button class="close-modal" @click="$emit('close')" aria-label="关闭">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="installed-content">
|
||||
<div v-if="loading" class="installed-empty">正在读取已安装应用…</div>
|
||||
<div v-else-if="error" class="installed-empty error">{{ error }}</div>
|
||||
<div v-else-if="apps.length === 0" class="installed-empty">暂无已安装应用</div>
|
||||
<div v-else class="installed-list">
|
||||
<div v-for="app in apps" :key="app.pkgname" class="installed-item">
|
||||
<div class="installed-info">
|
||||
<div class="installed-name">{{ app.pkgname }}</div>
|
||||
<div class="installed-meta">
|
||||
<span>{{ app.version }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>{{ app.arch }}</span>
|
||||
<span v-if="app.flags" class="dot">·</span>
|
||||
<span v-if="app.flags">{{ app.flags }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="installed-actions">
|
||||
<button class="action-btn secondary danger" :disabled="app.removing" @click="$emit('uninstall', app)">
|
||||
<i class="fas fa-trash"></i>
|
||||
{{ app.removing ? '卸载中…' : '卸载' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
apps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['close', 'refresh', 'uninstall']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.installed-modal {
|
||||
width: min(900px, calc(100% - 40px));
|
||||
}
|
||||
|
||||
.installed-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.installed-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
background: var(--glass);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.installed-empty.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.installed-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.installed-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 16px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--glass);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.installed-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.installed-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.installed-meta {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.installed-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.action-btn.danger {
|
||||
color: #ef4444;
|
||||
border-color: rgba(239, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.action-btn.danger:hover {
|
||||
color: #dc2626;
|
||||
}
|
||||
</style>
|
||||
154
src/components/UpdateAppsModal.vue
Normal file
154
src/components/UpdateAppsModal.vue
Normal file
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="modal-backdrop" :style="{ display: show ? 'flex' : 'none' }" role="dialog" aria-hidden="false">
|
||||
<div class="modal update-modal">
|
||||
<div class="modal-header">
|
||||
<div class="modal-title-section">
|
||||
<div class="modal-title">软件更新</div>
|
||||
<div class="modal-subtitle">可更新的 APM 应用</div>
|
||||
</div>
|
||||
<div class="modal-actions">
|
||||
<button class="action-btn" :disabled="loading" @click="$emit('refresh')">
|
||||
<i class="fas fa-sync-alt"></i> 刷新
|
||||
</button>
|
||||
<button class="action-btn secondary" :disabled="loading || apps.length === 0" @click="$emit('toggle-all')">
|
||||
<i class="fas fa-check-square"></i> 全选/全不选
|
||||
</button>
|
||||
<button class="apm-btn" :disabled="loading || !hasSelected" @click="$emit('upgrade-selected')">
|
||||
<i class="fas fa-upload"></i> 更新选中
|
||||
</button>
|
||||
<button class="close-modal" @click="$emit('close')" aria-label="关闭">×</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="update-content">
|
||||
<div v-if="loading" class="update-empty">正在检查可更新应用…</div>
|
||||
<div v-else-if="error" class="update-empty error">{{ error }}</div>
|
||||
<div v-else-if="apps.length === 0" class="update-empty">暂无可更新应用</div>
|
||||
<div v-else class="update-list">
|
||||
<label v-for="app in apps" :key="app.pkgname" class="update-item">
|
||||
<input type="checkbox" v-model="app.selected" :disabled="app.upgrading" />
|
||||
<div class="update-info">
|
||||
<div class="update-name">{{ app.pkgname }}</div>
|
||||
<div class="update-meta">
|
||||
<span>当前 {{ app.currentVersion || '-' }}</span>
|
||||
<span class="dot">·</span>
|
||||
<span>更新至 {{ app.newVersion || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="update-actions">
|
||||
<button class="action-btn secondary" :disabled="app.upgrading" @click.prevent="$emit('upgrade-one', app)">
|
||||
<i class="fas fa-arrow-up"></i>
|
||||
{{ app.upgrading ? '更新中…' : '更新' }}
|
||||
</button>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits } from 'vue';
|
||||
|
||||
defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
apps: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
error: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
hasSelected: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
});
|
||||
|
||||
defineEmits(['close', 'refresh', 'toggle-all', 'upgrade-selected', 'upgrade-one']);
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.update-modal {
|
||||
width: min(920px, calc(100% - 40px));
|
||||
}
|
||||
|
||||
.update-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.update-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
background: var(--glass);
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.update-empty.error {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.update-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.update-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 14px 16px;
|
||||
border-radius: var(--radius);
|
||||
background: var(--glass);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.update-item input[type="checkbox"] {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
accent-color: var(--accent);
|
||||
}
|
||||
|
||||
.update-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.update-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.update-meta {
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.dot {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.update-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
@@ -30,4 +30,5 @@ export interface DownloadItem {
|
||||
}>;
|
||||
source: string; // 例如 'APM Store'
|
||||
retry: boolean; // 当前是否为重试下载
|
||||
upgradeOnly?: boolean; // 是否为仅升级任务
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import { downloads } from "../global/downloadStatus";
|
||||
import { InstallLog, DownloadItem, DownloadResult } from '../global/typedefinition';
|
||||
|
||||
let downloadIdCounter = 0;
|
||||
const fallbackIcon = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"%3E%3Crect fill="%23f0f0f0" width="100" height="100"/%3E%3Ctext x="50" y="50" text-anchor="middle" dy=".3em" fill="%23999" font-size="14"%3EAPM%3C/text%3E%3C/svg%3E';
|
||||
|
||||
export const handleInstall = () => {
|
||||
if (!currentApp.value?.Pkgname) return;
|
||||
@@ -53,6 +54,35 @@ export const handleRetry = (download_: DownloadItem) => {
|
||||
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
||||
};
|
||||
|
||||
export const handleUpgrade = (pkgname: string, newVersion = '') => {
|
||||
if (!pkgname) return;
|
||||
|
||||
downloadIdCounter += 1;
|
||||
const download: DownloadItem = {
|
||||
id: downloadIdCounter,
|
||||
name: pkgname,
|
||||
pkgname: pkgname,
|
||||
version: newVersion,
|
||||
icon: fallbackIcon,
|
||||
status: 'queued',
|
||||
progress: 0,
|
||||
downloadedSize: 0,
|
||||
totalSize: 0,
|
||||
speed: 0,
|
||||
timeRemaining: 0,
|
||||
startTime: Date.now(),
|
||||
logs: [
|
||||
{ time: Date.now(), message: '开始更新...' }
|
||||
],
|
||||
source: 'APM Update',
|
||||
retry: false,
|
||||
upgradeOnly: true
|
||||
};
|
||||
|
||||
downloads.value.push(download);
|
||||
window.ipcRenderer.send('queue-install', JSON.stringify(download));
|
||||
};
|
||||
|
||||
export const handleRemove = () => {
|
||||
if (!currentApp.value?.Pkgname) return;
|
||||
window.ipcRenderer.send('remove-installed', currentApp.value.Pkgname);
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: #1a1a1a;
|
||||
padding: 2px 4px;
|
||||
margin: 0 4px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
code {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user