feat: 优化更新提示对话框,支持文件路径复制和后台下载功能,优化安装流程

feat: #100
This commit is contained in:
alger
2025-03-31 23:01:03 +08:00
parent 7e826311fe
commit 23b2340169
4 changed files with 250 additions and 46 deletions

View File

@@ -39,7 +39,18 @@ export default {
downloadFailed: 'Download failed, please try again or download manually',
startFailed: 'Start download failed, please try again or download manually',
noDownloadUrl:
'No suitable installation package found for the current system, please download manually'
'No suitable installation package found for the current system, please download manually',
installConfirmTitle: 'Install Update',
installConfirmContent: 'Do you want to close the application and install the update?',
manualInstallTip:
'If the installer does not open automatically after closing the application, please find the file in your download folder and open it manually.',
yesInstall: 'Install Now',
noThanks: 'Later',
fileLocation: 'File Location',
copy: 'Copy Path',
copySuccess: 'Path copied to clipboard',
copyFailed: 'Copy failed',
backgroundDownload: 'Background Download'
},
coffee: {
title: 'Buy me a coffee',

View File

@@ -38,7 +38,17 @@ export default {
nowUpdate: '立即更新',
downloadFailed: '下载失败,请重试或手动下载',
startFailed: '启动下载失败,请重试或手动下载',
noDownloadUrl: '未找到适合当前系统的安装包,请手动下载'
noDownloadUrl: '未找到适合当前系统的安装包,请手动下载',
installConfirmTitle: '安装更新',
installConfirmContent: '是否关闭应用并安装更新?',
manualInstallTip: '如果关闭应用后没有正常弹出安装程序,请至下载文件夹查找文件并手动打开。',
yesInstall: '立即安装',
noThanks: '稍后安装',
fileLocation: '文件位置',
copy: '复制路径',
copySuccess: '路径已复制到剪贴板',
copyFailed: '复制失败',
backgroundDownload: '后台下载'
},
coffee: {
title: '请我喝咖啡',

View File

@@ -1,5 +1,5 @@
import axios from 'axios';
import { exec } from 'child_process';
import { spawn } from 'child_process';
import { app, BrowserWindow, ipcMain } from 'electron';
import * as fs from 'fs';
import * as path from 'path';
@@ -53,38 +53,49 @@ export function setupUpdateHandlers(_mainWindow: BrowserWindow) {
const { platform } = process;
// 关闭当前应用
app.quit();
// 根据不同平台执行安装
if (platform === 'win32') {
exec(`"${filePath}"`, (error) => {
if (error) {
console.error('Error starting installer:', error);
}
});
} else if (platform === 'darwin') {
// 挂载 DMG 文件
exec(`open "${filePath}"`, (error) => {
if (error) {
console.error('Error opening DMG:', error);
}
});
} else if (platform === 'linux') {
const ext = path.extname(filePath);
if (ext === '.AppImage') {
exec(`chmod +x "${filePath}" && "${filePath}"`, (error) => {
if (error) {
console.error('Error running AppImage:', error);
}
// 先启动安装程序,再退出应用
try {
if (platform === 'win32') {
// 使用spawn替代exec并使用detached选项确保子进程独立运行
const child = spawn(filePath, [], {
detached: true,
stdio: 'ignore'
});
} else if (ext === '.deb') {
exec(`pkexec dpkg -i "${filePath}"`, (error) => {
if (error) {
console.error('Error installing deb package:', error);
}
child.unref();
} else if (platform === 'darwin') {
// 挂载 DMG 文件
const child = spawn('open', [filePath], {
detached: true,
stdio: 'ignore'
});
child.unref();
} else if (platform === 'linux') {
const ext = path.extname(filePath);
if (ext === '.AppImage') {
// 先添加执行权限
fs.chmodSync(filePath, '755');
const child = spawn(filePath, [], {
detached: true,
stdio: 'ignore'
});
child.unref();
} else if (ext === '.deb') {
const child = spawn('pkexec', ['dpkg', '-i', filePath], {
detached: true,
stdio: 'ignore'
});
child.unref();
}
}
// 给安装程序一点时间启动
setTimeout(() => {
app.quit();
}, 500);
} catch (error) {
console.error('启动安装程序失败:', error);
// 尽管出错,仍然尝试退出应用
app.quit();
}
});
}

View File

@@ -35,23 +35,22 @@
</div>
</div>
<div class="modal-actions" :class="{ 'mt-6': !downloading }">
<n-button
class="cancel-btn"
:disabled="downloading"
:loading="downloading"
@click="closeModal"
>
<n-button class="cancel-btn" :disabled="downloading" @click="closeModal">
{{ t('comp.update.cancel') }}
</n-button>
<n-button
v-if="!downloading"
type="primary"
class="update-btn"
:loading="downloading"
:disabled="downloading"
@click="handleUpdate"
>
{{ downloadBtnText }}
</n-button>
<!-- 后台下载 -->
<n-button v-else class="update-btn" type="primary" @click="closeModal">
{{ t('comp.update.backgroundDownload') }}
</n-button>
</div>
<div v-if="!downloading" class="modal-desc mt-4 text-center">
<p class="text-xs text-gray-400">
@@ -71,7 +70,7 @@
<script setup lang="ts">
import { marked } from 'marked';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { computed, h, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useSettingsStore } from '@/store/modules/settings';
@@ -80,6 +79,8 @@ import { checkUpdate, getProxyNodes, UpdateResult } from '@/utils/update';
import config from '../../../../package.json';
const { t } = useI18n();
const dialog = useDialog();
const message = useMessage();
// 配置 marked
marked.setOptions({
@@ -136,6 +137,11 @@ const downloadBtnText = computed(() => {
return t('comp.update.nowUpdate');
});
// 下载完成后的文件路径
const downloadedFilePath = ref('');
// 防止对话框重复弹出
const isDialogShown = ref(false);
// 处理下载状态更新
const handleDownloadProgress = (_event: any, progress: number, status: string) => {
downloadProgress.value = progress;
@@ -145,16 +151,73 @@ const handleDownloadProgress = (_event: any, progress: number, status: string) =
// 处理下载完成
const handleDownloadComplete = (_event: any, success: boolean, filePath: string) => {
downloading.value = false;
if (success) {
window.electron.ipcRenderer.send('install-update', filePath);
} else {
window.$message.error(t('comp.update.downloadFailed'));
closeModal();
if (success && !isDialogShown.value) {
downloadedFilePath.value = filePath;
isDialogShown.value = true;
// 复制文件路径到剪贴板
const copyFilePath = () => {
navigator.clipboard
.writeText(filePath)
.then(() => {
message.success(t('comp.update.copySuccess'));
})
.catch(() => {
message.error(t('comp.update.copyFailed'));
});
};
// 使用naive-ui的对话框询问用户是否安装
const dialogRef = dialog.create({
title: t('comp.update.installConfirmTitle'),
content: () =>
h('div', { class: 'update-dialog-content' }, [
h('p', { class: 'content-text' }, t('comp.update.installConfirmContent')),
h('div', { class: 'divider' }),
h('p', { class: 'manual-tip' }, t('comp.update.manualInstallTip')),
h('div', { class: 'file-path-container' }, [
h('div', { class: 'file-path-box' }, [
h('p', { class: 'file-path-label' }, t('comp.update.fileLocation')),
h('div', { class: 'file-path-value' }, filePath)
]),
h(
'div',
{
class: 'copy-btn',
onClick: copyFilePath
},
[h('i', { class: 'ri-file-copy-line' }), h('span', t('comp.update.copy'))]
)
])
]),
positiveText: t('comp.update.yesInstall'),
negativeText: t('comp.update.noThanks'),
onPositiveClick: () => {
window.electron.ipcRenderer.send('install-update', filePath);
},
onNegativeClick: () => {
closeModal();
// 关闭当前窗口
dialogRef.destroy();
},
onClose: () => {
isDialogShown.value = false;
}
});
} else if (!success) {
message.error(t('comp.update.downloadFailed'));
}
};
// 监听下载事件
onMounted(() => {
checkForUpdates();
// 确保事件监听器只注册一次
window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);
window.electron.ipcRenderer.on('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.on('download-complete', handleDownloadComplete);
});
@@ -163,6 +226,7 @@ onMounted(() => {
onUnmounted(() => {
window.electron.ipcRenderer.removeListener('download-progress', handleDownloadProgress);
window.electron.ipcRenderer.removeListener('download-complete', handleDownloadComplete);
isDialogShown.value = false;
});
const handleUpdate = async () => {
@@ -215,6 +279,7 @@ const handleUpdate = async () => {
try {
downloading.value = true;
downloadStatus.value = t('comp.update.prepareDownload');
isDialogShown.value = false;
// 获取代理节点列表
const proxyHosts = await getProxyNodes();
@@ -224,11 +289,11 @@ const handleUpdate = async () => {
window.electron.ipcRenderer.send('start-download', proxyDownloadUrl);
} catch (error) {
downloading.value = false;
window.$message.error(t('comp.update.startFailed'));
message.error(t('comp.update.startFailed'));
console.error('下载失败:', error);
}
} else {
window.$message.error(t('comp.update.noDownloadUrl'));
message.error(t('comp.update.noDownloadUrl'));
window.open('https://github.com/algerkong/AlgerMusicPlayer/releases/latest', '_blank');
}
};
@@ -355,3 +420,110 @@ const handleUpdate = async () => {
}
}
</style>
<style lang="scss">
/* 对话框内容样式 */
.update-dialog-content {
display: flex;
flex-direction: column;
gap: 12px;
.content-text {
font-size: 16px;
font-weight: 500;
}
.divider {
width: 100%;
height: 1px;
background-color: #e5e7eb;
margin: 4px 0;
}
.manual-tip {
font-size: 14px;
color: #6b7280;
}
.file-path-container {
display: flex;
align-items: center;
gap: 12px;
margin-top: 8px;
.file-path-box {
flex: 1;
.file-path-label {
font-size: 12px;
color: #6b7280;
margin-bottom: 4px;
}
.file-path-value {
padding: 8px;
border-radius: 4px;
background-color: #f3f4f6;
font-size: 12px;
font-family: monospace;
color: #1f2937;
word-break: break-all;
}
}
.copy-btn {
display: flex;
align-items: center;
gap: 4px;
padding: 8px 12px;
border-radius: 4px;
background-color: #e5e7eb;
color: #4b5563;
font-size: 12px;
cursor: pointer;
transition: background-color 0.2s;
&:hover {
background-color: #d1d5db;
}
i {
font-size: 14px;
}
}
}
}
/* 深色模式样式 */
.dark .update-dialog-content {
.divider {
background-color: #374151;
}
.manual-tip {
color: #9ca3af;
}
.file-path-container {
.file-path-box {
.file-path-label {
color: #9ca3af;
}
.file-path-value {
background-color: #1f2937;
color: #d1d5db;
}
}
.copy-btn {
background-color: #374151;
color: #d1d5db;
&:hover {
background-color: #4b5563;
}
}
}
}
</style>