mirror of
https://gitee.com/spark-store-project/spark-store
synced 2026-04-26 09:20:18 +08:00
feat(install): added basis install process
Now it is able to install apps from the render process and properly display logs on the app detial page.
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -27,4 +27,5 @@ release
|
|||||||
# lockfile
|
# lockfile
|
||||||
package-lock.json
|
package-lock.json
|
||||||
pnpm-lock.yaml
|
pnpm-lock.yaml
|
||||||
yarn.lock
|
yarn.lock
|
||||||
|
.lock
|
||||||
|
|||||||
168
electron/main/backend/download-manager.ts
Normal file
168
electron/main/backend/download-manager.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { ipcMain, WebContents } from 'electron';
|
||||||
|
import { spawn, ChildProcess, exec } from 'node:child_process';
|
||||||
|
import readline from 'node:readline';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import pino from 'pino';
|
||||||
|
|
||||||
|
const logger = pino({ 'name': 'download-manager' });
|
||||||
|
|
||||||
|
type DownloadTask = {
|
||||||
|
id: number;
|
||||||
|
execCommand: string;
|
||||||
|
execParams: string[];
|
||||||
|
process: ChildProcess | null;
|
||||||
|
webContents: WebContents | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const tasks = new Map<number, DownloadTask>();
|
||||||
|
|
||||||
|
let idle = true; // Indicates if the installation manager is idle
|
||||||
|
|
||||||
|
// Listen for download requests from renderer process
|
||||||
|
ipcMain.on('queue-install', async (event, download) => {
|
||||||
|
const { id, pkgname } = download || {};
|
||||||
|
|
||||||
|
if (!id || !pkgname) {
|
||||||
|
logger.warn('passed arguments missing id or pkgname');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`收到下载任务: ${id}, 软件包名称: ${pkgname}`);
|
||||||
|
|
||||||
|
// 避免重复添加同一任务
|
||||||
|
if (tasks.has(id)) {
|
||||||
|
tasks.get(id)?.webContents.send('install-log', {
|
||||||
|
id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: `任务id: ${id} 已在列表中,忽略重复添加`
|
||||||
|
});
|
||||||
|
tasks.get(id)?.webContents.send('install-complete', {
|
||||||
|
id: id,
|
||||||
|
success: false,
|
||||||
|
time: Date.now(),
|
||||||
|
exitCode: -1,
|
||||||
|
message: `{"message":"任务id: ${id} 已在列表中,忽略重复添加","stdout":"","stderr":""}`
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const webContents = event.sender;
|
||||||
|
|
||||||
|
// 开始组装安装命令
|
||||||
|
const execAsync = promisify(exec);
|
||||||
|
let superUserCmd = '';
|
||||||
|
if (process.getuid && process.getuid() !== 0) {
|
||||||
|
const { stdout, stderr } = await execAsync('which pkexec');
|
||||||
|
if (stderr) {
|
||||||
|
logger.error('没有找到 pkexec 命令');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
logger.info(`找到提升权限命令: ${stdout.trim()}`);
|
||||||
|
superUserCmd = stdout.trim();
|
||||||
|
|
||||||
|
if (superUserCmd.length === 0) {
|
||||||
|
logger.error('没有找到提升权限的命令 pkexec, 无法继续安装');
|
||||||
|
webContents.send('install-error', {
|
||||||
|
id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: '无法找到提升权限的命令 pkexec,请手动安装'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let execCommand = '';
|
||||||
|
let execParams = [];
|
||||||
|
if (superUserCmd.length > 0) {
|
||||||
|
execCommand = superUserCmd;
|
||||||
|
execParams.push('/usr/bin/apm');
|
||||||
|
} else {
|
||||||
|
execCommand = '/usr/bin/apm';
|
||||||
|
}
|
||||||
|
execParams.push('install', '-y', pkgname);
|
||||||
|
|
||||||
|
const task: DownloadTask = {
|
||||||
|
id,
|
||||||
|
execCommand,
|
||||||
|
execParams,
|
||||||
|
process: null,
|
||||||
|
webContents
|
||||||
|
};
|
||||||
|
tasks.set(id, task);
|
||||||
|
if (idle) processNextInQueue(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
function processNextInQueue(index: number) {
|
||||||
|
if (!idle) return;
|
||||||
|
|
||||||
|
idle = false;
|
||||||
|
const task = Array.from(tasks.values())[index];
|
||||||
|
const webContents = task.webContents;
|
||||||
|
let stdoutData = '';
|
||||||
|
let stderrData = '';
|
||||||
|
|
||||||
|
webContents.send('install-status', {
|
||||||
|
id: task.id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: 'installing'
|
||||||
|
})
|
||||||
|
webContents.send('install-log', {
|
||||||
|
id: task.id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: `开始执行: ${task.execCommand} ${task.execParams.join(' ')}`
|
||||||
|
});
|
||||||
|
logger.info(`启动安装命令: ${task.execCommand} ${task.execParams.join(' ')}`);
|
||||||
|
|
||||||
|
const child = spawn(task.execCommand, task.execParams, {
|
||||||
|
shell: true,
|
||||||
|
env: process.env
|
||||||
|
});
|
||||||
|
task.process = child;
|
||||||
|
|
||||||
|
// 监听 stdout
|
||||||
|
child.stdout.on('data', (data) => {
|
||||||
|
stdoutData += data.toString();
|
||||||
|
webContents.send('install-log', {
|
||||||
|
id: task.id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: data.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听 stderr
|
||||||
|
child.stderr.on('data', (data) => {
|
||||||
|
stderrData += data.toString();
|
||||||
|
webContents.send('install-log', {
|
||||||
|
id: task.id,
|
||||||
|
time: Date.now(),
|
||||||
|
message: data.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
child.on('close', (code) => {
|
||||||
|
const success = code === 0;
|
||||||
|
// 拼接json消息
|
||||||
|
const messageJSONObj = {
|
||||||
|
message: success ? '安装完成' : `安装失败,退出码 ${code}`,
|
||||||
|
stdout: stdoutData,
|
||||||
|
stderr: stderrData
|
||||||
|
};
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
logger.info(messageJSONObj);
|
||||||
|
} else {
|
||||||
|
logger.error(messageJSONObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
webContents.send('install-complete', {
|
||||||
|
id: task.id,
|
||||||
|
success: success,
|
||||||
|
time: Date.now(),
|
||||||
|
exitCode: code,
|
||||||
|
message: JSON.stringify(messageJSONObj)
|
||||||
|
});
|
||||||
|
tasks.delete(task.id);
|
||||||
|
idle = true;
|
||||||
|
if (tasks.size > 0)
|
||||||
|
processNextInQueue(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { dialog } from 'electron'
|
|
||||||
import { deepLink } from './deeplink';
|
import { deepLink } from './deeplink';
|
||||||
|
|
||||||
deepLink.on("event", (query) => {
|
deepLink.on("event", (query) => {
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { app, BrowserWindow, shell, ipcMain, dialog } from 'electron'
|
import { app, BrowserWindow, shell } from 'electron'
|
||||||
import { createRequire } from 'node:module'
|
import { createRequire } from 'node:module'
|
||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import os from 'node:os'
|
import os from 'node:os'
|
||||||
|
|
||||||
import './handle-url-scheme.js'
|
|
||||||
|
|
||||||
// Assure single instance application
|
// Assure single instance application
|
||||||
if (!app.requestSingleInstanceLock()) {
|
if (!app.requestSingleInstanceLock()) {
|
||||||
app.exit(0);
|
app.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import './handle-url-scheme.js'
|
||||||
|
import './backend/download-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))
|
||||||
@@ -111,18 +111,18 @@ app.on('activate', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// New window example arg: new windows url
|
// New window example arg: new windows url
|
||||||
ipcMain.handle('open-win', (_, arg) => {
|
// ipcMain.handle('open-win', (_, arg) => {
|
||||||
const childWindow = new BrowserWindow({
|
// const childWindow = new BrowserWindow({
|
||||||
webPreferences: {
|
// webPreferences: {
|
||||||
preload,
|
// preload,
|
||||||
nodeIntegration: true,
|
// nodeIntegration: true,
|
||||||
contextIsolation: false,
|
// contextIsolation: false,
|
||||||
},
|
// },
|
||||||
})
|
// })
|
||||||
|
|
||||||
if (VITE_DEV_SERVER_URL) {
|
// if (VITE_DEV_SERVER_URL) {
|
||||||
childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`)
|
// childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`)
|
||||||
} else {
|
// } else {
|
||||||
childWindow.loadFile(indexHtml, { hash: arg })
|
// childWindow.loadFile(indexHtml, { hash: arg })
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite --mode debug",
|
"dev": "vite --mode debug | pino-pretty",
|
||||||
"build": "vue-tsc --noEmit && vite build --mode production && electron-builder",
|
"build": "vue-tsc --noEmit && vite build --mode production && electron-builder",
|
||||||
"preview": "vite preview --mode debug"
|
"preview": "vite preview --mode debug"
|
||||||
},
|
},
|
||||||
@@ -31,6 +31,7 @@
|
|||||||
"electron": "^39.2.7",
|
"electron": "^39.2.7",
|
||||||
"electron-app-universal-protocol-client": "github:witcher112/electron-app-universal-protocol-client",
|
"electron-app-universal-protocol-client": "github:witcher112/electron-app-universal-protocol-client",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
|
"pino-pretty": "^13.1.3",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite": "^5.1.5",
|
"vite": "^5.1.5",
|
||||||
"vite-plugin-electron": "^0.28.4",
|
"vite-plugin-electron": "^0.28.4",
|
||||||
@@ -39,6 +40,7 @@
|
|||||||
"vue-tsc": "^2.0.6"
|
"vue-tsc": "^2.0.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.13.2"
|
"axios": "^1.13.2",
|
||||||
|
"pino": "^10.3.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
52
src/App.vue
52
src/App.vue
@@ -29,6 +29,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, watch, nextTick } from 'vue';
|
||||||
|
import axios from 'axios';
|
||||||
|
import pino from 'pino';
|
||||||
import AppSidebar from './components/AppSidebar.vue';
|
import AppSidebar from './components/AppSidebar.vue';
|
||||||
import AppHeader from './components/AppHeader.vue';
|
import AppHeader from './components/AppHeader.vue';
|
||||||
import AppGrid from './components/AppGrid.vue';
|
import AppGrid from './components/AppGrid.vue';
|
||||||
@@ -36,8 +38,11 @@ import AppDetailModal from './components/AppDetailModal.vue';
|
|||||||
import ScreenPreview from './components/ScreenPreview.vue';
|
import ScreenPreview from './components/ScreenPreview.vue';
|
||||||
import DownloadQueue from './components/DownloadQueue.vue';
|
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 } from './global/StoreConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL, currentApp } from './global/storeConfig';
|
||||||
import axios from 'axios';
|
import { downloads } from './global/downloadStatus';
|
||||||
|
import { handleInstall } from './js/processInstall';
|
||||||
|
|
||||||
|
const logger = pino();
|
||||||
|
|
||||||
// Axios 全局配置
|
// Axios 全局配置
|
||||||
const axiosInstance = axios.create({
|
const axiosInstance = axios.create({
|
||||||
@@ -53,14 +58,11 @@ const activeCategory = ref('all');
|
|||||||
const searchQuery = ref('');
|
const searchQuery = ref('');
|
||||||
const showModal = ref(false);
|
const showModal = ref(false);
|
||||||
const showPreview = ref(false);
|
const showPreview = ref(false);
|
||||||
const currentApp = ref(null);
|
|
||||||
const currentScreenIndex = ref(0);
|
const currentScreenIndex = ref(0);
|
||||||
const screenshots = ref([]);
|
const screenshots = ref([]);
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
const downloads = ref([]);
|
|
||||||
const showDownloadDetailModal = ref(false);
|
const showDownloadDetailModal = ref(false);
|
||||||
const currentDownload = ref(null);
|
const currentDownload = ref(null);
|
||||||
let downloadIdCounter = 0;
|
|
||||||
|
|
||||||
// 计算属性
|
// 计算属性
|
||||||
const filteredApps = computed(() => {
|
const filteredApps = computed(() => {
|
||||||
@@ -170,40 +172,6 @@ const nextScreen = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleInstall = () => {
|
|
||||||
if (!currentApp.value?.Pkgname) return;
|
|
||||||
|
|
||||||
// 创建下载任务
|
|
||||||
const download = {
|
|
||||||
id: ++downloadIdCounter,
|
|
||||||
name: currentApp.value.Name,
|
|
||||||
pkgname: currentApp.value.Pkgname,
|
|
||||||
version: currentApp.value.Version,
|
|
||||||
icon: `${APM_STORE_BASE_URL}/${APM_STORE_ARCHITECTURE}/${currentApp.value._category}/${currentApp.value.Pkgname}/icon.png`,
|
|
||||||
status: 'downloading',
|
|
||||||
progress: 0,
|
|
||||||
downloadedSize: 0,
|
|
||||||
totalSize: 0,
|
|
||||||
speed: 0,
|
|
||||||
timeRemaining: 0,
|
|
||||||
startTime: Date.now(),
|
|
||||||
logs: [
|
|
||||||
{ time: Date.now(), message: '开始下载...' }
|
|
||||||
],
|
|
||||||
source: 'APM Store'
|
|
||||||
};
|
|
||||||
|
|
||||||
downloads.value.push(download);
|
|
||||||
|
|
||||||
// 模拟下载进度(实际应该调用真实的下载 API)
|
|
||||||
simulateDownload(download);
|
|
||||||
|
|
||||||
const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
|
||||||
openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
|
||||||
fallbackText: `/usr/bin/apm-installer --install ${currentApp.value.Pkgname}`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
openApmStoreUrl('apmstore://action?cmd=update', {
|
openApmStoreUrl('apmstore://action?cmd=update', {
|
||||||
fallbackText: 'apm-update-tool'
|
fallbackText: 'apm-update-tool'
|
||||||
@@ -365,6 +333,7 @@ const cancelDownload = (id) => {
|
|||||||
});
|
});
|
||||||
// 延迟删除,让用户看到取消状态
|
// 延迟删除,让用户看到取消状态
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
logger.info(`删除下载: ${download.pkgname}`);
|
||||||
downloads.value.splice(index, 1);
|
downloads.value.splice(index, 1);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
@@ -410,13 +379,14 @@ const loadCategories = async () => {
|
|||||||
const response = await axiosInstance.get(`/${APM_STORE_ARCHITECTURE}/categories.json`);
|
const response = await axiosInstance.get(`/${APM_STORE_ARCHITECTURE}/categories.json`);
|
||||||
categories.value = response.data;
|
categories.value = response.data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('读取 categories.json 失败', error);
|
logger.error('读取 categories.json 失败', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadApps = async () => {
|
const loadApps = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
|
logger.info('开始加载应用数据...');
|
||||||
const promises = Object.keys(categories.value).map(async category => {
|
const promises = Object.keys(categories.value).map(async category => {
|
||||||
try {
|
try {
|
||||||
const response = await axiosInstance.get(`/${APM_STORE_ARCHITECTURE}/${category}/applist.json`);
|
const response = await axiosInstance.get(`/${APM_STORE_ARCHITECTURE}/${category}/applist.json`);
|
||||||
@@ -437,7 +407,7 @@ const loadApps = async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载应用数据失败', error);
|
logger.error('加载应用数据失败', error);
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, defineEmits, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, defineProps, defineEmits, onMounted, onBeforeUnmount, ref, watch } from 'vue';
|
||||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL } from '../global/StoreConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL } from '../global/storeConfig';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
app: {
|
app: {
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, defineProps, defineEmits } from 'vue';
|
import { computed, defineProps, defineEmits } from 'vue';
|
||||||
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL } from '../global/StoreConfig';
|
import { APM_STORE_ARCHITECTURE, APM_STORE_BASE_URL } from '../global/storeConfig';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
show: {
|
show: {
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ const emit = defineEmits([
|
|||||||
'show-detail'
|
'show-detail'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const isExpanded = ref(true);
|
const isExpanded = ref(false);
|
||||||
|
|
||||||
const activeDownloads = computed(() => {
|
const activeDownloads = computed(() => {
|
||||||
return props.downloads.filter(d =>
|
return props.downloads.filter(d =>
|
||||||
@@ -187,6 +187,7 @@ const showDownloadDetail = (download) => {
|
|||||||
|
|
||||||
.queue-header:hover {
|
.queue-header:hover {
|
||||||
background: var(--glass);
|
background: var(--glass);
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.queue-info {
|
.queue-info {
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export const APM_STORE_BASE_URL=import.meta.env.VITE_APM_STORE_BASE_URL;
|
|
||||||
export const APM_STORE_ARCHITECTURE='amd64-apm';
|
|
||||||
4
src/global/downloadStatus.ts
Normal file
4
src/global/downloadStatus.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
import { DownloadItem } from './typedefinition';
|
||||||
|
|
||||||
|
export const downloads = ref(<DownloadItem[]>[]);
|
||||||
5
src/global/storeConfig.ts
Normal file
5
src/global/storeConfig.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
export const APM_STORE_BASE_URL=import.meta.env.VITE_APM_STORE_BASE_URL;
|
||||||
|
export const APM_STORE_ARCHITECTURE='amd64-apm';
|
||||||
|
export const currentApp = ref<any>(null);
|
||||||
32
src/global/typedefinition.ts
Normal file
32
src/global/typedefinition.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
export interface InstallLog {
|
||||||
|
id: number;
|
||||||
|
success: boolean;
|
||||||
|
time: number;
|
||||||
|
exitCode: number | null;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadResult extends InstallLog {
|
||||||
|
success: boolean;
|
||||||
|
exitCode: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DownloadItem {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
pkgname: string;
|
||||||
|
version: string;
|
||||||
|
icon: string;
|
||||||
|
status: 'installing' | 'paused' | 'completed' | 'failed' | 'queued'; // 可根据实际状态扩展
|
||||||
|
progress: number; // 0 ~ 100 的百分比,或 0 ~ 1 的小数(建议统一)
|
||||||
|
downloadedSize: number; // 已下载字节数
|
||||||
|
totalSize: number; // 总字节数(可能为 0 初始时)
|
||||||
|
speed: number; // 当前下载速度,单位如 B/s
|
||||||
|
timeRemaining: number; // 剩余时间(秒),0 表示未知
|
||||||
|
startTime: number; // Date.now() 返回的时间戳(毫秒)
|
||||||
|
logs: Array<{
|
||||||
|
time: number; // 日志时间戳
|
||||||
|
message: string; // 日志消息
|
||||||
|
}>;
|
||||||
|
source: string; // 例如 'APM Store'
|
||||||
|
}
|
||||||
70
src/js/processInstall.ts
Normal file
70
src/js/processInstall.ts
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
// window.ipcRenderer.on('main-process-message', (_event, ...args) => {
|
||||||
|
// console.log('[Receive Main-process message]:', ...args)
|
||||||
|
// })
|
||||||
|
|
||||||
|
import { currentApp } from "../global/storeConfig";
|
||||||
|
import { APM_STORE_BASE_URL, APM_STORE_ARCHITECTURE } from "../global/storeConfig";
|
||||||
|
import { downloads } from "../global/downloadStatus";
|
||||||
|
|
||||||
|
import { InstallLog, DownloadItem, DownloadResult } from '../global/typedefinition';
|
||||||
|
|
||||||
|
let downloadIdCounter = 0;
|
||||||
|
|
||||||
|
export const handleInstall = () => {
|
||||||
|
if (!currentApp.value?.Pkgname) return;
|
||||||
|
|
||||||
|
downloadIdCounter += 1;
|
||||||
|
// 创建下载任务
|
||||||
|
const download: DownloadItem = {
|
||||||
|
id: downloadIdCounter,
|
||||||
|
name: currentApp.value.Name,
|
||||||
|
pkgname: currentApp.value.Pkgname,
|
||||||
|
version: currentApp.value.Version,
|
||||||
|
icon: `${APM_STORE_BASE_URL}/${APM_STORE_ARCHITECTURE}/${currentApp.value._category}/${currentApp.value.Pkgname}/icon.png`,
|
||||||
|
status: 'queued',
|
||||||
|
progress: 0,
|
||||||
|
downloadedSize: 0,
|
||||||
|
totalSize: 0,
|
||||||
|
speed: 0,
|
||||||
|
timeRemaining: 0,
|
||||||
|
startTime: Date.now(),
|
||||||
|
logs: [
|
||||||
|
{ time: Date.now(), message: '开始下载...' }
|
||||||
|
],
|
||||||
|
source: 'APM Store'
|
||||||
|
};
|
||||||
|
|
||||||
|
downloads.value.push(download);
|
||||||
|
|
||||||
|
// 模拟下载进度(实际应该调用真实的下载 API)
|
||||||
|
// simulateDownload(download);
|
||||||
|
|
||||||
|
// Send to main process to start download
|
||||||
|
window.ipcRenderer.send('queue-install', download);
|
||||||
|
|
||||||
|
const encodedPkg = encodeURIComponent(currentApp.value.Pkgname);
|
||||||
|
// openApmStoreUrl(`apmstore://install?pkg=${encodedPkg}`, {
|
||||||
|
// fallbackText: `/usr/bin/apm-installer --install ${currentApp.value.Pkgname}`
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
window.ipcRenderer.on('install-status', (_event, log: InstallLog) => {
|
||||||
|
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
||||||
|
downloadObj.status = log.message;
|
||||||
|
});
|
||||||
|
window.ipcRenderer.on('install-log', (_event, log: InstallLog) => {
|
||||||
|
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
||||||
|
downloadObj.logs.push({
|
||||||
|
time: log.time,
|
||||||
|
message: log.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
window.ipcRenderer.on('install-complete', (_event, log: DownloadResult) => {
|
||||||
|
const downloadObj: any = downloads.value.find(d => d.id === log.id);
|
||||||
|
if (log.success) {
|
||||||
|
downloadObj.status = 'completed';
|
||||||
|
} else {
|
||||||
|
downloadObj.status = 'failed';
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
|
|
||||||
//import './style.css'
|
|
||||||
import './3rdparty/fontawesome-free-6.7.2/css/all.min.css'
|
import './3rdparty/fontawesome-free-6.7.2/css/all.min.css'
|
||||||
import './assets/css/appstyle.css'
|
import './assets/css/appstyle.css'
|
||||||
|
|
||||||
import './demos/ipc'
|
// import './demos/ipc'
|
||||||
// If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process.
|
// If you want use Node.js, the`nodeIntegration` needs to be enabled in the Main process.
|
||||||
// import './demos/node'
|
// import './demos/node'
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user