mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat(install): 实现安装管理器,支持安装、检查已安装状态和初步卸载功能
This commit is contained in:
@@ -6,7 +6,7 @@ import pino from 'pino';
|
|||||||
|
|
||||||
const logger = pino({ 'name': 'download-manager' });
|
const logger = pino({ 'name': 'download-manager' });
|
||||||
|
|
||||||
type DownloadTask = {
|
type InstallTask = {
|
||||||
id: number;
|
id: number;
|
||||||
execCommand: string;
|
execCommand: string;
|
||||||
execParams: string[];
|
execParams: string[];
|
||||||
@@ -14,7 +14,7 @@ type DownloadTask = {
|
|||||||
webContents: WebContents | null;
|
webContents: WebContents | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const tasks = new Map<number, DownloadTask>();
|
const tasks = new Map<number, InstallTask>();
|
||||||
|
|
||||||
let idle = true; // Indicates if the installation manager is idle
|
let idle = true; // Indicates if the installation manager is idle
|
||||||
|
|
||||||
@@ -82,7 +82,7 @@ ipcMain.on('queue-install', async (event, download_json) => {
|
|||||||
}
|
}
|
||||||
execParams.push('install', '-y', pkgname);
|
execParams.push('install', '-y', pkgname);
|
||||||
|
|
||||||
const task: DownloadTask = {
|
const task: InstallTask = {
|
||||||
id,
|
id,
|
||||||
execCommand,
|
execCommand,
|
||||||
execParams,
|
execParams,
|
||||||
@@ -167,3 +167,37 @@ function processNextInQueue(index: number) {
|
|||||||
processNextInQueue(0);
|
processNextInQueue(0);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ipcMain.handle('check-installed', async (_event, pkgname: string) => {
|
||||||
|
if (!pkgname) {
|
||||||
|
logger.warn('check-installed missing pkgname');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let isInstalled = false;
|
||||||
|
|
||||||
|
logger.info(`检查应用是否已安装: ${pkgname}`);
|
||||||
|
|
||||||
|
let child = spawn('/usr/bin/apm', ['list', '--installed', pkgname], {
|
||||||
|
shell: true,
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
output += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
child.on('close', (code) => {
|
||||||
|
if (code === 0 && output.includes(pkgname)) {
|
||||||
|
isInstalled = true;
|
||||||
|
logger.info(`应用已安装: ${pkgname}`);
|
||||||
|
} else {
|
||||||
|
logger.info(`应用未安装: ${pkgname}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return isInstalled;
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@ if (!app.requestSingleInstanceLock()) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
import './handle-url-scheme.js'
|
import './handle-url-scheme.js'
|
||||||
import './backend/download-manager.js'
|
import './backend/install-manager.js'
|
||||||
|
|
||||||
const require = createRequire(import.meta.url)
|
const require = createRequire(import.meta.url)
|
||||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
|
|||||||
17
src/App.vue
17
src/App.vue
@@ -11,8 +11,8 @@
|
|||||||
<AppGrid :apps="filteredApps" :loading="loading" @open-detail="openDetail" />
|
<AppGrid :apps="filteredApps" :loading="loading" @open-detail="openDetail" />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<AppDetailModal :show="showModal" :app="currentApp" :screenshots="screenshots" @close="closeDetail"
|
<AppDetailModal :show="showModal" :app="currentApp" :screenshots="screenshots" :isinstalled="currentAppIsInstalled" @close="closeDetail"
|
||||||
@install="handleInstall" @open-preview="openScreenPreview" />
|
@install="handleInstall" @remove="handleRemove" @open-preview="openScreenPreview" />
|
||||||
|
|
||||||
<ScreenPreview :show="showPreview" :screenshots="screenshots" :current-screen-index="currentScreenIndex"
|
<ScreenPreview :show="showPreview" :screenshots="screenshots" :current-screen-index="currentScreenIndex"
|
||||||
@close="closeScreenPreview" @prev="prevScreen" @next="nextScreen" />
|
@close="closeScreenPreview" @prev="prevScreen" @next="nextScreen" />
|
||||||
@@ -40,7 +40,7 @@ import DownloadQueue from './components/DownloadQueue.vue';
|
|||||||
import DownloadDetail from './components/DownloadDetail.vue';
|
import DownloadDetail from './components/DownloadDetail.vue';
|
||||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp } from './global/storeConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp } from './global/storeConfig';
|
||||||
import { downloads } from './global/downloadStatus';
|
import { downloads } from './global/downloadStatus';
|
||||||
import { handleInstall, handleRetry } from './modeuls/processInstall';
|
import { handleInstall, handleRetry, handleRemove } from './modeuls/processInstall';
|
||||||
|
|
||||||
const logger = pino();
|
const logger = pino();
|
||||||
|
|
||||||
@@ -60,6 +60,7 @@ const showModal = ref(false);
|
|||||||
const showPreview = ref(false);
|
const showPreview = ref(false);
|
||||||
const currentScreenIndex = ref(0);
|
const currentScreenIndex = ref(0);
|
||||||
const screenshots = ref([]);
|
const screenshots = ref([]);
|
||||||
|
const currentAppIsInstalled = ref(false);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const showDownloadDetailModal = ref(false);
|
const showDownloadDetailModal = ref(false);
|
||||||
const currentDownload = ref(null);
|
const currentDownload = ref(null);
|
||||||
@@ -127,6 +128,10 @@ const openDetail = (app) => {
|
|||||||
loadScreenshots(app);
|
loadScreenshots(app);
|
||||||
showModal.value = true;
|
showModal.value = true;
|
||||||
|
|
||||||
|
// 检测本地是否已经安装了该应用
|
||||||
|
currentAppIsInstalled.value = false;
|
||||||
|
checkAppInstalled(app);
|
||||||
|
|
||||||
// 确保模态框显示后滚动到顶部
|
// 确保模态框显示后滚动到顶部
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
const modal = document.querySelector('.modal');
|
const modal = document.querySelector('.modal');
|
||||||
@@ -134,6 +139,12 @@ const openDetail = (app) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const checkAppInstalled = (app) => {
|
||||||
|
window.ipcRenderer.invoke('check-installed', app.Pkgname).then((isInstalled) => {
|
||||||
|
currentAppIsInstalled.value = isInstalled;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadScreenshots = (app) => {
|
const loadScreenshots = (app) => {
|
||||||
screenshots.value = [];
|
screenshots.value = [];
|
||||||
for (let i = 1; i <= 5; i++) {
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
|||||||
@@ -15,9 +15,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-actions">
|
<div class="modal-actions">
|
||||||
<button class="install-btn" @click="handleInstall">
|
<button v-if="!isinstalled" class="install-btn" @click="handleInstall">
|
||||||
<i class="fas fa-download"></i> 安装
|
<i class="fas fa-download"></i> 安装
|
||||||
</button>
|
</button>
|
||||||
|
<button v-else class="install-btn remove" @click="handelRemove">
|
||||||
|
<i class="fas fa-trash"></i> 卸载
|
||||||
|
</button>
|
||||||
<button class="close-modal" @click="closeModal" aria-label="关闭">×</button>
|
<button class="close-modal" @click="closeModal" aria-label="关闭">×</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -95,10 +98,14 @@ const props = defineProps({
|
|||||||
screenshots: {
|
screenshots: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true
|
required: true
|
||||||
|
},
|
||||||
|
isinstalled: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'install', 'open-preview']);
|
const emit = defineEmits(['close', 'install', 'remove', 'open-preview']);
|
||||||
|
|
||||||
const iconPath = computed(() => {
|
const iconPath = computed(() => {
|
||||||
return props.app ? `${APM_STORE_BASE_URL}/${APM_STORE_ARCHITECTURE}/${props.app._category}/${props.app.Pkgname}/icon.png` : '';
|
return props.app ? `${APM_STORE_BASE_URL}/${APM_STORE_ARCHITECTURE}/${props.app._category}/${props.app.Pkgname}/icon.png` : '';
|
||||||
@@ -112,6 +119,10 @@ const handleInstall = () => {
|
|||||||
emit('install');
|
emit('install');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handelRemove = () => {
|
||||||
|
emit('remove');
|
||||||
|
};
|
||||||
|
|
||||||
const openPreview = (index) => {
|
const openPreview = (index) => {
|
||||||
emit('open-preview', index);
|
emit('open-preview', index);
|
||||||
};
|
};
|
||||||
@@ -119,4 +130,9 @@ const openPreview = (index) => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* 该组件样式已在全局样式中定义 */
|
/* 该组件样式已在全局样式中定义 */
|
||||||
|
.install-btn.remove {
|
||||||
|
border-color: rgb(231, 76, 60);
|
||||||
|
box-shadow: 0 4px 12px rgb(231, 76, 60);
|
||||||
|
background: linear-gradient(90deg, rgb(231, 76, 60), rgb(231, 76, 60));
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ export const handleRetry = (download_: DownloadItem) => {
|
|||||||
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
window.ipcRenderer.send('queue-install', JSON.stringify(download_));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleRemove = (download_: DownloadItem) => {
|
||||||
|
if (!currentApp.value?.Pkgname) return;
|
||||||
|
console.log('请求卸载: ', currentApp.value.Pkgname);
|
||||||
|
}
|
||||||
|
|
||||||
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
||||||
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
||||||
downloadObj.status = log.message;
|
downloadObj.status = log.message;
|
||||||
|
|||||||
Reference in New Issue
Block a user