🐛 Fixed bug
This commit is contained in:
@@ -8,9 +8,9 @@ info:
|
|||||||
companyName: "Voidraft" # The name of the company
|
companyName: "Voidraft" # The name of the company
|
||||||
productName: "Voidraft" # The name of the application
|
productName: "Voidraft" # The name of the application
|
||||||
productIdentifier: "landaiqing" # The unique product identifier
|
productIdentifier: "landaiqing" # The unique product identifier
|
||||||
description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design" # The application description
|
description: "Voidraft" # The application description
|
||||||
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
|
copyright: "© 2025 Voidraft. All rights reserved." # Copyright text
|
||||||
comments: "Effortlessly capture and organize fleeting ideas with minimal design" # Comments
|
comments: "Voidraft" # Comments
|
||||||
version: "0.0.1.0" # The application version
|
version: "0.0.1.0" # The application version
|
||||||
|
|
||||||
# Dev mode configuration
|
# Dev mode configuration
|
||||||
|
@@ -6,13 +6,13 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Voidraft</string>
|
<string>Voidraft</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>voidraft</string>
|
<string></string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>landaiqing</string>
|
<string>landaiqing</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
|
<string>Voidraft</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
|
@@ -6,13 +6,13 @@
|
|||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>Voidraft</string>
|
<string>Voidraft</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>voidraft</string>
|
<string></string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>landaiqing</string>
|
<string>landaiqing</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleGetInfoString</key>
|
<key>CFBundleGetInfoString</key>
|
||||||
<string>Effortlessly capture and organize fleeting ideas with minimal design</string>
|
<string>Voidraft</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>0.0.1.0</string>
|
<string>0.0.1.0</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
|
@@ -3,26 +3,26 @@
|
|||||||
#
|
#
|
||||||
# The lines below are called `modelines`. See `:help modeline`
|
# The lines below are called `modelines`. See `:help modeline`
|
||||||
|
|
||||||
name: "voidraft"
|
name: ""
|
||||||
arch: ${GOARCH}
|
arch: ${GOARCH}
|
||||||
platform: "linux"
|
platform: "linux"
|
||||||
version: "0.0.1.0"
|
version: "0.0.1.0"
|
||||||
section: "default"
|
section: "default"
|
||||||
priority: "extra"
|
priority: "extra"
|
||||||
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
|
||||||
description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design"
|
description: "Voidraft"
|
||||||
vendor: "Voidraft"
|
vendor: "Voidraft"
|
||||||
homepage: "https://wails.io"
|
homepage: "https://wails.io"
|
||||||
license: "MIT"
|
license: "MIT"
|
||||||
release: "1"
|
release: "1"
|
||||||
|
|
||||||
contents:
|
contents:
|
||||||
- src: "./bin/voidraft"
|
- src: "./bin/"
|
||||||
dst: "/usr/local/bin/voidraft"
|
dst: "/usr/local/bin/"
|
||||||
- src: "./build/appicon.png"
|
- src: "./build/appicon.png"
|
||||||
dst: "/usr/share/icons/hicolor/128x128/apps/voidraft.png"
|
dst: "/usr/share/icons/hicolor/128x128/apps/.png"
|
||||||
- src: "./build/linux/voidraft.desktop"
|
- src: "./build/linux/.desktop"
|
||||||
dst: "/usr/share/applications/voidraft.desktop"
|
dst: "/usr/share/applications/.desktop"
|
||||||
|
|
||||||
depends:
|
depends:
|
||||||
- gtk3
|
- gtk3
|
||||||
|
@@ -6,10 +6,10 @@
|
|||||||
"0000": {
|
"0000": {
|
||||||
"ProductVersion": "0.0.1.0",
|
"ProductVersion": "0.0.1.0",
|
||||||
"CompanyName": "Voidraft",
|
"CompanyName": "Voidraft",
|
||||||
"FileDescription": "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design",
|
"FileDescription": "Voidraft",
|
||||||
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
|
"LegalCopyright": "© 2025 Voidraft. All rights reserved.",
|
||||||
"ProductName": "Voidraft",
|
"ProductName": "Voidraft",
|
||||||
"Comments": "Effortlessly capture and organize fleeting ideas with minimal design"
|
"Comments": "Voidraft"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -5,7 +5,7 @@
|
|||||||
!include "FileFunc.nsh"
|
!include "FileFunc.nsh"
|
||||||
|
|
||||||
!ifndef INFO_PROJECTNAME
|
!ifndef INFO_PROJECTNAME
|
||||||
!define INFO_PROJECTNAME "voidraft"
|
!define INFO_PROJECTNAME ""
|
||||||
!endif
|
!endif
|
||||||
!ifndef INFO_COMPANYNAME
|
!ifndef INFO_COMPANYNAME
|
||||||
!define INFO_COMPANYNAME "Voidraft"
|
!define INFO_COMPANYNAME "Voidraft"
|
||||||
|
@@ -345,6 +345,11 @@ export class GeneralConfig {
|
|||||||
*/
|
*/
|
||||||
"enableSystemTray": boolean;
|
"enableSystemTray": boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开机启动设置
|
||||||
|
*/
|
||||||
|
"startAtLogin": boolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 全局热键设置
|
* 全局热键设置
|
||||||
* 是否启用全局热键
|
* 是否启用全局热键
|
||||||
@@ -367,6 +372,9 @@ export class GeneralConfig {
|
|||||||
if (!("enableSystemTray" in $$source)) {
|
if (!("enableSystemTray" in $$source)) {
|
||||||
this["enableSystemTray"] = false;
|
this["enableSystemTray"] = false;
|
||||||
}
|
}
|
||||||
|
if (!("startAtLogin" in $$source)) {
|
||||||
|
this["startAtLogin"] = false;
|
||||||
|
}
|
||||||
if (!("enableGlobalHotkey" in $$source)) {
|
if (!("enableGlobalHotkey" in $$source)) {
|
||||||
this["enableGlobalHotkey"] = false;
|
this["enableGlobalHotkey"] = false;
|
||||||
}
|
}
|
||||||
@@ -381,10 +389,10 @@ export class GeneralConfig {
|
|||||||
* Creates a new GeneralConfig instance from a string or object.
|
* Creates a new GeneralConfig instance from a string or object.
|
||||||
*/
|
*/
|
||||||
static createFrom($$source: any = {}): GeneralConfig {
|
static createFrom($$source: any = {}): GeneralConfig {
|
||||||
const $$createField4_0 = $$createType6;
|
const $$createField5_0 = $$createType6;
|
||||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||||
if ("globalHotkey" in $$parsedSource) {
|
if ("globalHotkey" in $$parsedSource) {
|
||||||
$$parsedSource["globalHotkey"] = $$createField4_0($$parsedSource["globalHotkey"]);
|
$$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]);
|
||||||
}
|
}
|
||||||
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
|
return new GeneralConfig($$parsedSource as Partial<GeneralConfig>);
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import * as DocumentService from "./documentservice.js";
|
|||||||
import * as HotkeyService from "./hotkeyservice.js";
|
import * as HotkeyService from "./hotkeyservice.js";
|
||||||
import * as KeyBindingService from "./keybindingservice.js";
|
import * as KeyBindingService from "./keybindingservice.js";
|
||||||
import * as MigrationService from "./migrationservice.js";
|
import * as MigrationService from "./migrationservice.js";
|
||||||
|
import * as StartupService from "./startupservice.js";
|
||||||
import * as SystemService from "./systemservice.js";
|
import * as SystemService from "./systemservice.js";
|
||||||
import * as TrayService from "./trayservice.js";
|
import * as TrayService from "./trayservice.js";
|
||||||
export {
|
export {
|
||||||
@@ -16,6 +17,7 @@ export {
|
|||||||
HotkeyService,
|
HotkeyService,
|
||||||
KeyBindingService,
|
KeyBindingService,
|
||||||
MigrationService,
|
MigrationService,
|
||||||
|
StartupService,
|
||||||
SystemService,
|
SystemService,
|
||||||
TrayService
|
TrayService
|
||||||
};
|
};
|
||||||
|
@@ -0,0 +1,19 @@
|
|||||||
|
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||||
|
// This file is automatically generated. DO NOT EDIT
|
||||||
|
|
||||||
|
/**
|
||||||
|
* StartupService 开机启动服务
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore: Unused imports
|
||||||
|
import {Call as $Call, Create as $Create} from "@wailsio/runtime";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SetEnabled 设置开机启动状态
|
||||||
|
*/
|
||||||
|
export function SetEnabled(enabled: boolean): Promise<void> & { cancel(): void } {
|
||||||
|
let $resultPromise = $Call.ByID(2911601468, enabled) as any;
|
||||||
|
return $resultPromise;
|
||||||
|
}
|
@@ -44,7 +44,9 @@ export default {
|
|||||||
themeChanged: 'Theme setting updated',
|
themeChanged: 'Theme setting updated',
|
||||||
themeChangeFailed: 'Failed to update theme setting',
|
themeChangeFailed: 'Failed to update theme setting',
|
||||||
systemThemeChanged: 'System theme setting updated',
|
systemThemeChanged: 'System theme setting updated',
|
||||||
systemThemeChangeFailed: 'Failed to update system theme setting'
|
systemThemeChangeFailed: 'Failed to update system theme setting',
|
||||||
|
startupSuccess: 'Startup setting updated',
|
||||||
|
startupFailed: 'Failed to update startup setting'
|
||||||
},
|
},
|
||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
@@ -162,6 +164,8 @@ export default {
|
|||||||
showInSystemTray: 'Show in System Tray',
|
showInSystemTray: 'Show in System Tray',
|
||||||
enableSystemTray: 'Enable System Tray',
|
enableSystemTray: 'Enable System Tray',
|
||||||
alwaysOnTop: 'Always on Top',
|
alwaysOnTop: 'Always on Top',
|
||||||
|
startup: 'Startup Settings',
|
||||||
|
startAtLogin: 'Start at Login',
|
||||||
dataStorage: 'Data Storage',
|
dataStorage: 'Data Storage',
|
||||||
dataPath: 'Data Storage Path',
|
dataPath: 'Data Storage Path',
|
||||||
clickToSelectPath: 'Click to select path',
|
clickToSelectPath: 'Click to select path',
|
||||||
|
@@ -44,7 +44,9 @@ export default {
|
|||||||
themeChanged: '主题设置已更新',
|
themeChanged: '主题设置已更新',
|
||||||
themeChangeFailed: '主题设置更新失败',
|
themeChangeFailed: '主题设置更新失败',
|
||||||
systemThemeChanged: '系统主题设置已更新',
|
systemThemeChanged: '系统主题设置已更新',
|
||||||
systemThemeChangeFailed: '系统主题设置更新失败'
|
systemThemeChangeFailed: '系统主题设置更新失败',
|
||||||
|
startupSuccess: '开机启动设置已更新',
|
||||||
|
startupFailed: '开机启动设置失败'
|
||||||
},
|
},
|
||||||
languages: {
|
languages: {
|
||||||
'zh-CN': '简体中文',
|
'zh-CN': '简体中文',
|
||||||
@@ -162,6 +164,8 @@ export default {
|
|||||||
showInSystemTray: '在系统托盘中显示',
|
showInSystemTray: '在系统托盘中显示',
|
||||||
enableSystemTray: '启用系统托盘',
|
enableSystemTray: '启用系统托盘',
|
||||||
alwaysOnTop: '窗口始终置顶',
|
alwaysOnTop: '窗口始终置顶',
|
||||||
|
startup: '启动设置',
|
||||||
|
startAtLogin: '开机自启动',
|
||||||
dataStorage: '数据存储',
|
dataStorage: '数据存储',
|
||||||
dataPath: '数据存储路径',
|
dataPath: '数据存储路径',
|
||||||
clickToSelectPath: '点击选择路径',
|
clickToSelectPath: '点击选择路径',
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import {defineStore} from 'pinia';
|
import {defineStore} from 'pinia';
|
||||||
import {computed, reactive} from 'vue';
|
import {computed, reactive} from 'vue';
|
||||||
import {ConfigService} from '../../bindings/voidraft/internal/services';
|
import {ConfigService, StartupService} from '../../bindings/voidraft/internal/services';
|
||||||
import {
|
import {
|
||||||
AppConfig,
|
AppConfig,
|
||||||
AppearanceConfig,
|
AppearanceConfig,
|
||||||
@@ -50,6 +50,7 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = {
|
|||||||
alwaysOnTop: 'general.alwaysOnTop',
|
alwaysOnTop: 'general.alwaysOnTop',
|
||||||
dataPath: 'general.dataPath',
|
dataPath: 'general.dataPath',
|
||||||
enableSystemTray: 'general.enableSystemTray',
|
enableSystemTray: 'general.enableSystemTray',
|
||||||
|
startAtLogin: 'general.startAtLogin',
|
||||||
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
enableGlobalHotkey: 'general.enableGlobalHotkey',
|
||||||
globalHotkey: 'general.globalHotkey'
|
globalHotkey: 'general.globalHotkey'
|
||||||
} as const;
|
} as const;
|
||||||
@@ -118,6 +119,7 @@ const DEFAULT_CONFIG: AppConfig = {
|
|||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
dataPath: '',
|
dataPath: '',
|
||||||
enableSystemTray: true,
|
enableSystemTray: true,
|
||||||
|
startAtLogin: false,
|
||||||
enableGlobalHotkey: false,
|
enableGlobalHotkey: false,
|
||||||
globalHotkey: {
|
globalHotkey: {
|
||||||
ctrl: false,
|
ctrl: false,
|
||||||
@@ -408,6 +410,15 @@ export const useConfigStore = defineStore('config', () => {
|
|||||||
setGlobalHotkey: (hotkey: any) => safeCall(() => updateGeneralConfig('globalHotkey', hotkey), 'config.saveFailed', 'config.saveSuccess'),
|
setGlobalHotkey: (hotkey: any) => safeCall(() => updateGeneralConfig('globalHotkey', hotkey), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
|
|
||||||
// 系统托盘配置相关方法
|
// 系统托盘配置相关方法
|
||||||
setEnableSystemTray: (value: boolean) => safeCall(() => updateGeneralConfig('enableSystemTray', value), 'config.saveFailed', 'config.saveSuccess')
|
setEnableSystemTray: (value: boolean) => safeCall(() => updateGeneralConfig('enableSystemTray', value), 'config.saveFailed', 'config.saveSuccess'),
|
||||||
|
|
||||||
|
// 开机启动配置相关方法
|
||||||
|
setStartAtLogin: async (value: boolean) => {
|
||||||
|
await safeCall(async () => {
|
||||||
|
// 先调用系统设置
|
||||||
|
await StartupService.SetEnabled(value);
|
||||||
|
state.config.general.startAtLogin = value;
|
||||||
|
}, 'config.startupFailed', 'config.startupSuccess');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
@@ -23,24 +23,57 @@ const migrationProgress = ref<MigrationProgress>(new MigrationProgress({
|
|||||||
let pollingTimer: number | null = null;
|
let pollingTimer: number | null = null;
|
||||||
const isPolling = ref(false);
|
const isPolling = ref(false);
|
||||||
|
|
||||||
|
// 进度条显示控制
|
||||||
|
const showProgress = ref(false);
|
||||||
|
const progressError = ref('');
|
||||||
|
let hideProgressTimer: any = null;
|
||||||
|
|
||||||
// 开始轮询迁移进度
|
// 开始轮询迁移进度
|
||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
if (isPolling.value) return;
|
if (isPolling.value) return;
|
||||||
|
|
||||||
isPolling.value = true;
|
isPolling.value = true;
|
||||||
|
showProgress.value = true;
|
||||||
|
progressError.value = '';
|
||||||
|
|
||||||
|
// 立即重置迁移进度状态,避免从之前的失败状态渐变
|
||||||
|
migrationProgress.value = new MigrationProgress({
|
||||||
|
status: MigrationStatus.MigrationStatusMigrating,
|
||||||
|
progress: 0
|
||||||
|
});
|
||||||
|
|
||||||
pollingTimer = window.setInterval(async () => {
|
pollingTimer = window.setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const progress = await MigrationService.GetProgress();
|
const progress = await MigrationService.GetProgress();
|
||||||
migrationProgress.value = progress;
|
migrationProgress.value = progress;
|
||||||
|
|
||||||
|
const { status, error } = progress;
|
||||||
|
const isCompleted = [MigrationStatus.MigrationStatusCompleted, MigrationStatus.MigrationStatusFailed].includes(status);
|
||||||
|
|
||||||
// 如果迁移完成或失败,停止轮询
|
if (isCompleted) {
|
||||||
if (progress.status === MigrationStatus.MigrationStatusCompleted || progress.status === MigrationStatus.MigrationStatusFailed) {
|
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|
||||||
|
// 设置错误信息(如果是失败状态)
|
||||||
|
progressError.value = (status === MigrationStatus.MigrationStatusFailed) ? (error || 'Migration failed') : '';
|
||||||
|
|
||||||
|
const delay = status === MigrationStatus.MigrationStatusCompleted ? 3000 : 5000;
|
||||||
|
hideProgressTimer = setTimeout(hideProgress, delay);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|
||||||
|
// 使用常量简化错误处理
|
||||||
|
const errorMsg = 'Failed to get migration progress';
|
||||||
|
Object.assign(migrationProgress.value, {
|
||||||
|
status: MigrationStatus.MigrationStatusFailed,
|
||||||
|
progress: 0,
|
||||||
|
error: errorMsg
|
||||||
|
});
|
||||||
|
progressError.value = errorMsg;
|
||||||
|
|
||||||
|
hideProgressTimer = setTimeout(hideProgress, 5000);
|
||||||
}
|
}
|
||||||
}, 500); // 每500ms轮询一次
|
}, 200);
|
||||||
};
|
};
|
||||||
|
|
||||||
// 停止轮询
|
// 停止轮询
|
||||||
@@ -52,121 +85,40 @@ const stopPolling = () => {
|
|||||||
isPolling.value = false;
|
isPolling.value = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
// 迁移消息链
|
// 隐藏进度条
|
||||||
interface MigrationMessage {
|
const hideProgress = () => {
|
||||||
id: number;
|
showProgress.value = false;
|
||||||
content: string;
|
progressError.value = '';
|
||||||
type: 'start' | 'progress' | 'success' | 'error';
|
|
||||||
timestamp: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const migrationMessages = ref<MigrationMessage[]>([]);
|
|
||||||
const showMessages = ref(false);
|
|
||||||
let messageIdCounter = 0;
|
|
||||||
let hideMessagesTimer: any = null;
|
|
||||||
|
|
||||||
// 添加迁移消息
|
|
||||||
const addMigrationMessage = (content: string, type: MigrationMessage['type']) => {
|
|
||||||
const message: MigrationMessage = {
|
|
||||||
id: ++messageIdCounter,
|
|
||||||
content,
|
|
||||||
type,
|
|
||||||
timestamp: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
migrationMessages.value.push(message);
|
// 重置迁移状态,避免下次显示时状态不正确
|
||||||
showMessages.value = true;
|
migrationProgress.value = new MigrationProgress({
|
||||||
};
|
status: MigrationStatus.MigrationStatusCompleted,
|
||||||
|
progress: 0
|
||||||
// 清除所有消息
|
});
|
||||||
const clearMigrationMessages = () => {
|
|
||||||
migrationMessages.value = [];
|
if (hideProgressTimer) {
|
||||||
showMessages.value = false;
|
clearTimeout(hideProgressTimer);
|
||||||
};
|
hideProgressTimer = null;
|
||||||
|
|
||||||
// 监听迁移进度变化
|
|
||||||
watch(() => migrationProgress.value, (progress, oldProgress) => {
|
|
||||||
// 清除之前的隐藏定时器
|
|
||||||
if (hideMessagesTimer) {
|
|
||||||
clearTimeout(hideMessagesTimer);
|
|
||||||
hideMessagesTimer = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (progress.status === MigrationStatus.MigrationStatusMigrating) {
|
|
||||||
// 如果是第一次收到迁移状态,添加开始消息
|
|
||||||
if (migrationMessages.value.length === 0) {
|
|
||||||
addMigrationMessage(t('migration.started'), 'start');
|
|
||||||
}
|
|
||||||
// 如果还没有迁移中消息,添加迁移中消息
|
|
||||||
if (!migrationMessages.value.some(msg => msg.type === 'progress' && msg.content === t('migration.migrating'))) {
|
|
||||||
addMigrationMessage(t('migration.migrating'), 'progress');
|
|
||||||
}
|
|
||||||
} else if (progress.status === MigrationStatus.MigrationStatusCompleted && oldProgress?.status === MigrationStatus.MigrationStatusMigrating) {
|
|
||||||
addMigrationMessage(t('migration.completed'), 'success');
|
|
||||||
|
|
||||||
// 5秒后开始逐个隐藏消息
|
|
||||||
hideMessagesTimer = setTimeout(() => {
|
|
||||||
hideMessagesSequentially();
|
|
||||||
}, 5000);
|
|
||||||
|
|
||||||
} else if (progress.status === MigrationStatus.MigrationStatusFailed && oldProgress?.status === MigrationStatus.MigrationStatusMigrating) {
|
|
||||||
const errorMsg = progress.error || t('migration.failed');
|
|
||||||
addMigrationMessage(errorMsg, 'error');
|
|
||||||
|
|
||||||
// 8秒后开始逐个隐藏消息
|
|
||||||
hideMessagesTimer = setTimeout(() => {
|
|
||||||
hideMessagesSequentially();
|
|
||||||
}, 8000);
|
|
||||||
}
|
|
||||||
}, { deep: true });
|
|
||||||
|
|
||||||
// 逐个隐藏消息
|
|
||||||
const hideMessagesSequentially = () => {
|
|
||||||
const hideNextMessage = () => {
|
|
||||||
if (migrationMessages.value.length > 0) {
|
|
||||||
migrationMessages.value.shift(); // 移除第一条消息
|
|
||||||
|
|
||||||
if (migrationMessages.value.length > 0) {
|
|
||||||
// 如果还有消息,1秒后隐藏下一条
|
|
||||||
setTimeout(hideNextMessage, 1000);
|
|
||||||
} else {
|
|
||||||
// 所有消息都隐藏完了,同时隐藏进度条
|
|
||||||
showMessages.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
hideNextMessage();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 迁移状态
|
// 简化的迁移状态管理
|
||||||
const isMigrating = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusMigrating);
|
const isMigrating = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusMigrating);
|
||||||
const migrationComplete = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusCompleted);
|
|
||||||
const migrationFailed = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusFailed);
|
|
||||||
|
|
||||||
// 进度条样式和宽度
|
// 进度条样式 - 使用 Map 简化条件判断
|
||||||
const progressBarClass = computed(() => {
|
const statusClassMap = new Map([
|
||||||
switch (migrationProgress.value.status) {
|
[MigrationStatus.MigrationStatusMigrating, 'migrating'],
|
||||||
case MigrationStatus.MigrationStatusMigrating:
|
[MigrationStatus.MigrationStatusCompleted, 'success'],
|
||||||
return 'migrating';
|
[MigrationStatus.MigrationStatusFailed, 'error']
|
||||||
case MigrationStatus.MigrationStatusCompleted:
|
]);
|
||||||
return 'success';
|
|
||||||
case MigrationStatus.MigrationStatusFailed:
|
const progressBarClass = computed(() =>
|
||||||
return 'error';
|
showProgress.value ? statusClassMap.get(migrationProgress.value.status) ?? '' : ''
|
||||||
default:
|
);
|
||||||
return '';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const progressBarWidth = computed(() => {
|
const progressBarWidth = computed(() => {
|
||||||
// 只有在显示消息且正在迁移时才显示进度条
|
if (!showProgress.value) return '0%';
|
||||||
if (showMessages.value && isMigrating.value) {
|
return isMigrating.value ? `${migrationProgress.value.progress}%` : '100%';
|
||||||
return migrationProgress.value.progress + '%';
|
|
||||||
} else if (showMessages.value && (migrationComplete.value || migrationFailed.value)) {
|
|
||||||
// 迁移完成或失败时,短暂显示100%,然后随着消息隐藏而隐藏
|
|
||||||
return '100%';
|
|
||||||
}
|
|
||||||
return '0%';
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// 重置确认状态
|
// 重置确认状态
|
||||||
@@ -206,6 +158,12 @@ const enableSystemTray = computed({
|
|||||||
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
set: (value: boolean) => configStore.setEnableSystemTray(value)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 计算属性 - 开机启动
|
||||||
|
const startAtLogin = computed({
|
||||||
|
get: () => configStore.config.general.startAtLogin,
|
||||||
|
set: (value: boolean) => configStore.setStartAtLogin(value)
|
||||||
|
});
|
||||||
|
|
||||||
// 修饰键配置 - 只读计算属性
|
// 修饰键配置 - 只读计算属性
|
||||||
const modifierKeys = computed(() => ({
|
const modifierKeys = computed(() => ({
|
||||||
ctrl: configStore.config.general.globalHotkey.ctrl,
|
ctrl: configStore.config.general.globalHotkey.ctrl,
|
||||||
@@ -253,19 +211,20 @@ const confirmReset = async () => {
|
|||||||
await configStore.resetConfig();
|
await configStore.resetConfig();
|
||||||
};
|
};
|
||||||
|
|
||||||
// 计算热键预览文本
|
// 计算热键预览文本 - 使用现代语法简化
|
||||||
const hotkeyPreview = computed(() => {
|
const hotkeyPreview = computed(() => {
|
||||||
if (!enableGlobalHotkey.value) return '';
|
if (!enableGlobalHotkey.value) return '';
|
||||||
|
|
||||||
const hotkey = configStore.config.general.globalHotkey;
|
const { ctrl, shift, alt, win, key } = configStore.config.general.globalHotkey;
|
||||||
const parts: string[] = [];
|
const modifiers = [
|
||||||
if (hotkey.ctrl) parts.push('Ctrl');
|
ctrl && 'Ctrl',
|
||||||
if (hotkey.shift) parts.push('Shift');
|
shift && 'Shift',
|
||||||
if (hotkey.alt) parts.push('Alt');
|
alt && 'Alt',
|
||||||
if (hotkey.win) parts.push('Win');
|
win && 'Win',
|
||||||
if (hotkey.key) parts.push(hotkey.key);
|
key
|
||||||
|
].filter(Boolean);
|
||||||
|
|
||||||
return parts.join(' + ');
|
return modifiers.join(' + ');
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据路径配置
|
// 数据路径配置
|
||||||
@@ -275,41 +234,54 @@ const currentDataPath = computed(() => configStore.config.general.dataPath);
|
|||||||
const selectDataDirectory = async () => {
|
const selectDataDirectory = async () => {
|
||||||
if (isMigrating.value) return;
|
if (isMigrating.value) return;
|
||||||
|
|
||||||
const selectedPath = await DialogService.SelectDirectory();
|
try {
|
||||||
|
const selectedPath = await DialogService.SelectDirectory();
|
||||||
if (selectedPath && selectedPath.trim() && selectedPath !== currentDataPath.value) {
|
|
||||||
// 清除之前的消息
|
// 检查用户是否取消了选择或路径为空
|
||||||
clearMigrationMessages();
|
if (!selectedPath || !selectedPath.trim() || selectedPath === currentDataPath.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const oldPath = currentDataPath.value;
|
||||||
|
const newPath = selectedPath.trim();
|
||||||
|
|
||||||
|
// 清除之前的进度状态
|
||||||
|
hideProgress();
|
||||||
|
|
||||||
// 开始轮询迁移进度
|
// 开始轮询迁移进度
|
||||||
startPolling();
|
startPolling();
|
||||||
|
|
||||||
// 开始迁移
|
// 开始迁移
|
||||||
try {
|
try {
|
||||||
await safeCall(async () => {
|
await MigrationService.MigrateDirectory(oldPath, newPath);
|
||||||
const oldPath = currentDataPath.value;
|
await configStore.setDataPath(newPath);
|
||||||
const newPath = selectedPath.trim();
|
|
||||||
|
|
||||||
await MigrationService.MigrateDirectory(oldPath, newPath);
|
|
||||||
await configStore.setDataPath(newPath);
|
|
||||||
}, '');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// 发生错误时清除消息并停止轮询
|
|
||||||
clearMigrationMessages();
|
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
|
||||||
|
// 使用解构和默认值简化错误处理
|
||||||
|
const errorMsg = error?.toString() || 'Migration failed';
|
||||||
|
showProgress.value = true;
|
||||||
|
|
||||||
|
Object.assign(migrationProgress.value, {
|
||||||
|
status: MigrationStatus.MigrationStatusFailed,
|
||||||
|
progress: 0,
|
||||||
|
error: errorMsg
|
||||||
|
});
|
||||||
|
progressError.value = errorMsg;
|
||||||
|
|
||||||
|
hideProgressTimer = setTimeout(hideProgress, 5000);
|
||||||
}
|
}
|
||||||
|
} catch (dialogError) {
|
||||||
|
console.error(dialogError);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 清理定时器
|
// 清理定时器
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
hideProgress();
|
||||||
if (resetConfirmTimer) {
|
if (resetConfirmTimer) {
|
||||||
clearTimeout(resetConfirmTimer);
|
clearTimeout(resetConfirmTimer);
|
||||||
}
|
}
|
||||||
if (hideMessagesTimer) {
|
|
||||||
clearTimeout(hideMessagesTimer);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -363,6 +335,12 @@ onUnmounted(() => {
|
|||||||
</SettingItem>
|
</SettingItem>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
|
|
||||||
|
<SettingSection :title="t('settings.startup')">
|
||||||
|
<SettingItem :title="t('settings.startAtLogin')">
|
||||||
|
<ToggleSwitch v-model="startAtLogin"/>
|
||||||
|
</SettingItem>
|
||||||
|
</SettingSection>
|
||||||
|
|
||||||
<SettingSection :title="t('settings.dataStorage')">
|
<SettingSection :title="t('settings.dataStorage')">
|
||||||
<div class="data-path-setting">
|
<div class="data-path-setting">
|
||||||
<div class="setting-header">
|
<div class="setting-header">
|
||||||
@@ -380,27 +358,23 @@ onUnmounted(() => {
|
|||||||
:title="t('settings.clickToSelectPath')"
|
:title="t('settings.clickToSelectPath')"
|
||||||
:disabled="isMigrating"
|
:disabled="isMigrating"
|
||||||
/>
|
/>
|
||||||
|
<!-- 简洁的进度条 -->
|
||||||
<div
|
<div
|
||||||
class="progress-bar"
|
class="progress-bar"
|
||||||
:class="[
|
:class="[
|
||||||
{ 'active': showMessages && (isMigrating || migrationComplete || migrationFailed) },
|
{ 'active': showProgress },
|
||||||
progressBarClass
|
progressBarClass
|
||||||
]"
|
]"
|
||||||
:style="{ width: progressBarWidth }"
|
:style="{ width: progressBarWidth }"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="migration-status-container">
|
|
||||||
<Transition name="fade-slide" mode="out-in">
|
<!-- 错误提示 -->
|
||||||
<div v-if="showMessages" class="migration-messages">
|
<Transition name="error-fade">
|
||||||
<TransitionGroup name="message-list" tag="div">
|
<div v-if="progressError" class="progress-error">
|
||||||
<div v-for="message in migrationMessages" :key="message.id" class="migration-message" :class="message.type">
|
{{ progressError }}
|
||||||
{{ message.content }}
|
</div>
|
||||||
</div>
|
</Transition>
|
||||||
</TransitionGroup>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingSection>
|
</SettingSection>
|
||||||
@@ -612,108 +586,46 @@ onUnmounted(() => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
height: 2px;
|
height: 3px;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 0 0 4px 4px;
|
||||||
transition: all 0.3s ease;
|
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1), opacity 0.3s ease;
|
||||||
width: 0;
|
width: 0;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
&.active {
|
&.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
||||||
&.migrating {
|
&.migrating {
|
||||||
background-color: #3b82f6;
|
background: linear-gradient(90deg, #22c55e, #16a34a);
|
||||||
animation: progress-wave 2s infinite;
|
animation: progress-pulse 2s ease-in-out infinite;
|
||||||
|
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.success {
|
&.success {
|
||||||
background-color: #22c55e;
|
background-color: #22c55e;
|
||||||
|
animation: none;
|
||||||
|
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.error {
|
&.error {
|
||||||
background-color: #ef4444;
|
background-color: #ef4444;
|
||||||
|
animation: none;
|
||||||
|
transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.migration-status-container {
|
.progress-error {
|
||||||
margin-top: 8px;
|
margin-top: 6px;
|
||||||
min-height: 0;
|
font-size: 12px;
|
||||||
overflow: hidden;
|
color: #ef4444;
|
||||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
padding: 0 2px;
|
||||||
|
line-height: 1.4;
|
||||||
.migration-messages {
|
|
||||||
margin-bottom: 6px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.migration-message {
|
|
||||||
font-size: 11px;
|
|
||||||
padding: 2px 0;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
transform: translateY(0);
|
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.3s ease;
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: '';
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.start {
|
|
||||||
color: var(--text-muted);
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: var(--text-muted);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.progress {
|
|
||||||
color: #3b82f6;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #3b82f6;
|
|
||||||
animation: pulse-dot 1.5s infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.success {
|
|
||||||
color: #22c55e;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #22c55e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.error {
|
|
||||||
color: #ef4444;
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
background-color: #ef4444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.v-enter-from, &.v-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateY(-8px);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
&:empty {
|
|
||||||
min-height: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -749,16 +661,15 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 进度条波浪动画
|
// 进度条脉冲动画
|
||||||
@keyframes progress-wave {
|
@keyframes progress-pulse {
|
||||||
0% {
|
0%, 100% {
|
||||||
background-position: 0% 50%;
|
opacity: 0.8;
|
||||||
|
transform: scaleY(1);
|
||||||
}
|
}
|
||||||
50% {
|
50% {
|
||||||
background-position: 100% 50%;
|
opacity: 1;
|
||||||
}
|
transform: scaleY(1.1);
|
||||||
100% {
|
|
||||||
background-position: 0% 50%;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -787,58 +698,22 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Vue Transition 动画
|
// 错误提示动画
|
||||||
.fade-slide-enter-active {
|
.error-fade-enter-active {
|
||||||
transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1);
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-leave-active {
|
.error-fade-leave-active {
|
||||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.6, 1);
|
transition: all 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-enter-from {
|
.error-fade-enter-from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-8px);
|
transform: translateY(-4px);
|
||||||
max-height: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.fade-slide-leave-to {
|
.error-fade-leave-to {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-5px);
|
transform: translateY(-4px);
|
||||||
max-height: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
margin-bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.fade-slide-enter-to,
|
|
||||||
.fade-slide-leave-from {
|
|
||||||
opacity: 1;
|
|
||||||
transform: translateY(0);
|
|
||||||
max-height: 150px;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 消息列表动画
|
|
||||||
.message-list-enter-active {
|
|
||||||
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list-leave-active {
|
|
||||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.6, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list-enter-from {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(-16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list-leave-to {
|
|
||||||
opacity: 0;
|
|
||||||
transform: translateX(16px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-list-move {
|
|
||||||
transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
2
go.mod
2
go.mod
@@ -8,6 +8,7 @@ require (
|
|||||||
github.com/fsnotify/fsnotify v1.9.0
|
github.com/fsnotify/fsnotify v1.9.0
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
github.com/wailsapp/wails/v3 v3.0.0-alpha.9
|
||||||
|
golang.org/x/sys v0.33.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -57,7 +58,6 @@ require (
|
|||||||
golang.org/x/crypto v0.39.0 // indirect
|
golang.org/x/crypto v0.39.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // indirect
|
||||||
golang.org/x/net v0.41.0 // indirect
|
golang.org/x/net v0.41.0 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
|
||||||
golang.org/x/text v0.26.0 // indirect
|
golang.org/x/text v0.26.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
|
@@ -29,8 +29,6 @@ func RegisterTrayEvents(app *application.App, systray *application.SystemTray, m
|
|||||||
trayService.HandleWindowClose()
|
trayService.HandleWindowClose()
|
||||||
})
|
})
|
||||||
|
|
||||||
// 不再拦截窗口最小化事件,让任务栏点击保持正常行为
|
|
||||||
// 最小化到托盘的逻辑由前端标题栏按钮直接处理
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// RegisterTrayMenuEvents 注册系统托盘菜单事件
|
// RegisterTrayMenuEvents 注册系统托盘菜单事件
|
||||||
|
@@ -43,6 +43,7 @@ type GeneralConfig struct {
|
|||||||
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
|
AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶
|
||||||
DataPath string `json:"dataPath"` // 数据存储路径
|
DataPath string `json:"dataPath"` // 数据存储路径
|
||||||
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
|
EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘
|
||||||
|
StartAtLogin bool `json:"startAtLogin"` // 开机启动设置
|
||||||
|
|
||||||
// 全局热键设置
|
// 全局热键设置
|
||||||
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
|
EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键
|
||||||
@@ -115,7 +116,8 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
General: GeneralConfig{
|
General: GeneralConfig{
|
||||||
AlwaysOnTop: false,
|
AlwaysOnTop: false,
|
||||||
DataPath: dataDir,
|
DataPath: dataDir,
|
||||||
EnableSystemTray: true, // 默认启用系统托盘
|
EnableSystemTray: true,
|
||||||
|
StartAtLogin: false,
|
||||||
EnableGlobalHotkey: false,
|
EnableGlobalHotkey: false,
|
||||||
GlobalHotkey: HotkeyCombo{
|
GlobalHotkey: HotkeyCombo{
|
||||||
Ctrl: false,
|
Ctrl: false,
|
||||||
@@ -140,7 +142,7 @@ func NewDefaultAppConfig() *AppConfig {
|
|||||||
},
|
},
|
||||||
Appearance: AppearanceConfig{
|
Appearance: AppearanceConfig{
|
||||||
Language: LangZhCN,
|
Language: LangZhCN,
|
||||||
SystemTheme: SystemThemeDark, // 默认使用深色系统主题
|
SystemTheme: SystemThemeAuto, // 默认使用深色系统主题
|
||||||
},
|
},
|
||||||
Updates: UpdatesConfig{},
|
Updates: UpdatesConfig{},
|
||||||
Metadata: ConfigMetadata{
|
Metadata: ConfigMetadata{
|
||||||
|
@@ -36,6 +36,6 @@ func NewDefaultDocument() *Document {
|
|||||||
LastUpdated: now,
|
LastUpdated: now,
|
||||||
CreatedAt: now,
|
CreatedAt: now,
|
||||||
},
|
},
|
||||||
Content: "// 在此处编写文本...",
|
Content: "\n∞∞∞text-a\n",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -17,6 +17,7 @@ type ServiceManager struct {
|
|||||||
dialogService *DialogService
|
dialogService *DialogService
|
||||||
trayService *TrayService
|
trayService *TrayService
|
||||||
keyBindingService *KeyBindingService
|
keyBindingService *KeyBindingService
|
||||||
|
startupService *StartupService
|
||||||
logger *log.LoggerService
|
logger *log.LoggerService
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,6 +50,9 @@ func NewServiceManager() *ServiceManager {
|
|||||||
// 初始化快捷键服务
|
// 初始化快捷键服务
|
||||||
keyBindingService := NewKeyBindingService(logger)
|
keyBindingService := NewKeyBindingService(logger)
|
||||||
|
|
||||||
|
// 初始化开机启动服务
|
||||||
|
startupService := NewStartupService(configService, logger)
|
||||||
|
|
||||||
// 使用新的配置通知系统设置热键配置变更监听
|
// 使用新的配置通知系统设置热键配置变更监听
|
||||||
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error {
|
||||||
return hotkeyService.UpdateHotkey(enable, hotkey)
|
return hotkeyService.UpdateHotkey(enable, hotkey)
|
||||||
@@ -83,6 +87,7 @@ func NewServiceManager() *ServiceManager {
|
|||||||
dialogService: dialogService,
|
dialogService: dialogService,
|
||||||
trayService: trayService,
|
trayService: trayService,
|
||||||
keyBindingService: keyBindingService,
|
keyBindingService: keyBindingService,
|
||||||
|
startupService: startupService,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -98,6 +103,7 @@ func (sm *ServiceManager) GetServices() []application.Service {
|
|||||||
application.NewService(sm.dialogService),
|
application.NewService(sm.dialogService),
|
||||||
application.NewService(sm.trayService),
|
application.NewService(sm.trayService),
|
||||||
application.NewService(sm.keyBindingService),
|
application.NewService(sm.keyBindingService),
|
||||||
|
application.NewService(sm.startupService),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,3 +136,8 @@ func (sm *ServiceManager) GetTrayService() *TrayService {
|
|||||||
func (sm *ServiceManager) GetKeyBindingService() *KeyBindingService {
|
func (sm *ServiceManager) GetKeyBindingService() *KeyBindingService {
|
||||||
return sm.keyBindingService
|
return sm.keyBindingService
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetStartupService 获取开机启动服务实例
|
||||||
|
func (sm *ServiceManager) GetStartupService() *StartupService {
|
||||||
|
return sm.startupService
|
||||||
|
}
|
||||||
|
89
internal/services/startup_darwin.go
Normal file
89
internal/services/startup_darwin.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/application"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/mac"
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DarwinStartupImpl macOS 平台开机启动实现
|
||||||
|
type DarwinStartupImpl struct {
|
||||||
|
logger *log.LoggerService
|
||||||
|
disabled bool
|
||||||
|
appPath string
|
||||||
|
appName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
|
func newStartupImplementation(logger *log.LoggerService) StartupImplementation {
|
||||||
|
return &DarwinStartupImpl{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化 macOS 实现
|
||||||
|
func (d *DarwinStartupImpl) Initialize() error {
|
||||||
|
if mac.GetBundleID() == "" {
|
||||||
|
d.disabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
exe, _ := os.Executable()
|
||||||
|
binName := filepath.Base(exe)
|
||||||
|
if !strings.HasSuffix(exe, "/Contents/MacOS/"+binName) {
|
||||||
|
d.disabled = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
d.appPath = strings.TrimSuffix(exe, "/Contents/MacOS/"+binName)
|
||||||
|
d.appName = strings.TrimSuffix(filepath.Base(d.appPath), ".app")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled 设置开机启动状态
|
||||||
|
func (d *DarwinStartupImpl) SetEnabled(enabled bool) error {
|
||||||
|
if d.disabled {
|
||||||
|
return fmt.Errorf("app is not properly packaged as .app bundle, cannot set startup")
|
||||||
|
}
|
||||||
|
|
||||||
|
var command string
|
||||||
|
if enabled {
|
||||||
|
command = fmt.Sprintf(
|
||||||
|
`tell application "System Events" to make login item at end with properties {name: "%s",path:"%s", hidden:false}`,
|
||||||
|
d.appName, d.appPath,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
command = fmt.Sprintf(
|
||||||
|
`tell application "System Events" to delete login item "%s"`,
|
||||||
|
d.appName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.Command("osascript", "-e", command)
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(string(output), "not allowed") || strings.Contains(string(output), "permission") {
|
||||||
|
return fmt.Errorf("accessibility permission required: go to System Preferences > Security & Privacy > Privacy > Accessibility")
|
||||||
|
}
|
||||||
|
return fmt.Errorf("failed to set login item: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 简单验证:重新查询登录项
|
||||||
|
if enabled {
|
||||||
|
checkCmd := exec.Command("osascript", "-e", `tell application "System Events" to get the name of every login item`)
|
||||||
|
checkOutput, _ := checkCmd.CombinedOutput()
|
||||||
|
if !strings.Contains(string(checkOutput), d.appName) {
|
||||||
|
return fmt.Errorf("login item verification failed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
110
internal/services/startup_linux.go
Normal file
110
internal/services/startup_linux.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// LinuxStartupImpl Linux 平台开机启动实现
|
||||||
|
type LinuxStartupImpl struct {
|
||||||
|
logger *log.LoggerService
|
||||||
|
autostartDir string
|
||||||
|
execPath string
|
||||||
|
appName string
|
||||||
|
}
|
||||||
|
|
||||||
|
// desktopEntry 桌面条目模板数据
|
||||||
|
type desktopEntry struct {
|
||||||
|
Name string
|
||||||
|
Cmd string
|
||||||
|
Comment string
|
||||||
|
}
|
||||||
|
|
||||||
|
const desktopEntryTemplate = `[Desktop Entry]
|
||||||
|
Name={{.Name}}
|
||||||
|
Comment={{.Comment}}
|
||||||
|
Type=Application
|
||||||
|
Exec={{.Cmd}}
|
||||||
|
Hidden=false
|
||||||
|
NoDisplay=false
|
||||||
|
X-GNOME-Autostart-enabled=true
|
||||||
|
`
|
||||||
|
|
||||||
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
|
func newStartupImplementation(logger *log.LoggerService) StartupImplementation {
|
||||||
|
return &LinuxStartupImpl{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化 Linux 实现
|
||||||
|
func (l *LinuxStartupImpl) Initialize() error {
|
||||||
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
l.autostartDir = filepath.Join(homeDir, ".config", "autostart")
|
||||||
|
|
||||||
|
// 检查是否有桌面环境
|
||||||
|
if os.Getenv("DISPLAY") == "" && os.Getenv("WAYLAND_DISPLAY") == "" {
|
||||||
|
return fmt.Errorf("no desktop environment detected, cannot set startup")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(l.autostartDir, 0755); err != nil {
|
||||||
|
return fmt.Errorf("failed to create autostart directory: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
execPath, _ := os.Executable()
|
||||||
|
l.execPath = execPath
|
||||||
|
l.appName = filepath.Base(execPath)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDesktopFilePath 获取桌面文件路径
|
||||||
|
func (l *LinuxStartupImpl) getDesktopFilePath() string {
|
||||||
|
filename := fmt.Sprintf("%s-autostart.desktop", l.appName)
|
||||||
|
return filepath.Join(l.autostartDir, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled 设置开机启动状态
|
||||||
|
func (l *LinuxStartupImpl) SetEnabled(enabled bool) error {
|
||||||
|
desktopFile := l.getDesktopFilePath()
|
||||||
|
|
||||||
|
if !enabled {
|
||||||
|
os.Remove(desktopFile)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.createDesktopFile(desktopFile); err != nil {
|
||||||
|
return fmt.Errorf("failed to create autostart file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证文件是否创建成功
|
||||||
|
if _, err := os.Stat(desktopFile); err != nil {
|
||||||
|
return fmt.Errorf("autostart file verification failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDesktopFile 创建桌面文件
|
||||||
|
func (l *LinuxStartupImpl) createDesktopFile(filename string) error {
|
||||||
|
file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
tmpl, _ := template.New("desktopEntry").Parse(desktopEntryTemplate)
|
||||||
|
data := desktopEntry{
|
||||||
|
Name: l.appName,
|
||||||
|
Cmd: l.execPath,
|
||||||
|
Comment: fmt.Sprintf("Autostart service for %s", l.appName),
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpl.Execute(file, data)
|
||||||
|
}
|
53
internal/services/startup_service.go
Normal file
53
internal/services/startup_service.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StartupService 开机启动服务
|
||||||
|
type StartupService struct {
|
||||||
|
configService *ConfigService
|
||||||
|
logger *log.LoggerService
|
||||||
|
impl StartupImplementation
|
||||||
|
initError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// StartupImplementation 开机启动实现接口
|
||||||
|
type StartupImplementation interface {
|
||||||
|
SetEnabled(enabled bool) error
|
||||||
|
Initialize() error
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStartupService 创建开机启动服务实例
|
||||||
|
func NewStartupService(configService *ConfigService, logger *log.LoggerService) *StartupService {
|
||||||
|
service := &StartupService{
|
||||||
|
configService: configService,
|
||||||
|
logger: logger,
|
||||||
|
impl: newStartupImplementation(logger),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化平台特定实现
|
||||||
|
service.initError = service.impl.Initialize()
|
||||||
|
|
||||||
|
return service
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled 设置开机启动状态
|
||||||
|
func (s *StartupService) SetEnabled(enabled bool) error {
|
||||||
|
// 检查初始化是否成功
|
||||||
|
if s.initError != nil {
|
||||||
|
return s.initError
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置系统开机启动
|
||||||
|
if err := s.impl.SetEnabled(enabled); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新配置文件
|
||||||
|
if s.configService != nil {
|
||||||
|
s.configService.Set("general.startAtLogin", enabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
151
internal/services/startup_windows.go
Normal file
151
internal/services/startup_windows.go
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package services
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/wailsapp/wails/v3/pkg/services/log"
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
// WindowsStartupImpl Windows 平台开机启动实现
|
||||||
|
type WindowsStartupImpl struct {
|
||||||
|
logger *log.LoggerService
|
||||||
|
registryKey string
|
||||||
|
execPath string
|
||||||
|
workingDir string
|
||||||
|
batchFile string
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStartupImplementation 创建平台特定的开机启动实现
|
||||||
|
func newStartupImplementation(logger *log.LoggerService) StartupImplementation {
|
||||||
|
return &WindowsStartupImpl{
|
||||||
|
logger: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize 初始化 Windows 实现
|
||||||
|
func (w *WindowsStartupImpl) Initialize() error {
|
||||||
|
exePath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get executable path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取绝对路径并规范化
|
||||||
|
absPath, err := filepath.Abs(exePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get absolute path: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为Windows标准路径格式
|
||||||
|
w.execPath = filepath.ToSlash(absPath)
|
||||||
|
w.execPath = strings.ReplaceAll(w.execPath, "/", "\\")
|
||||||
|
|
||||||
|
// 获取工作目录(可执行文件所在目录)
|
||||||
|
w.workingDir = filepath.Dir(w.execPath)
|
||||||
|
|
||||||
|
// 使用文件名作为注册表键名
|
||||||
|
w.registryKey = strings.TrimSuffix(filepath.Base(w.execPath), filepath.Ext(w.execPath))
|
||||||
|
|
||||||
|
// 批处理文件路径(放在临时目录)
|
||||||
|
tempDir := os.TempDir()
|
||||||
|
w.batchFile = filepath.Join(tempDir, w.registryKey+"_startup.bat")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// openRegistryKey 打开注册表键
|
||||||
|
func (w *WindowsStartupImpl) openRegistryKey() (registry.Key, error) {
|
||||||
|
key, err := registry.OpenKey(
|
||||||
|
registry.CURRENT_USER,
|
||||||
|
`Software\Microsoft\Windows\CurrentVersion\Run`,
|
||||||
|
registry.ALL_ACCESS,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("failed to open registry key: %w", err)
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createBatchFile 创建批处理文件
|
||||||
|
func (w *WindowsStartupImpl) createBatchFile() error {
|
||||||
|
// 批处理文件内容
|
||||||
|
batchContent := fmt.Sprintf(`@echo off
|
||||||
|
cd /d "%s"
|
||||||
|
start "" "%s"
|
||||||
|
`, w.workingDir, w.execPath)
|
||||||
|
|
||||||
|
// 写入批处理文件
|
||||||
|
if err := os.WriteFile(w.batchFile, []byte(batchContent), 0644); err != nil {
|
||||||
|
return fmt.Errorf("failed to create batch file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteBatchFile 删除批处理文件
|
||||||
|
func (w *WindowsStartupImpl) deleteBatchFile() {
|
||||||
|
if _, err := os.Stat(w.batchFile); err == nil {
|
||||||
|
os.Remove(w.batchFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildStartupCommand 构建启动命令
|
||||||
|
func (w *WindowsStartupImpl) buildStartupCommand() (string, error) {
|
||||||
|
// 尝试直接使用可执行文件路径
|
||||||
|
execPath := w.execPath
|
||||||
|
if strings.Contains(execPath, " ") {
|
||||||
|
execPath = `"` + execPath + `"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 首先尝试直接路径,如果有问题再使用批处理文件
|
||||||
|
return execPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled 设置开机启动状态
|
||||||
|
func (w *WindowsStartupImpl) SetEnabled(enabled bool) error {
|
||||||
|
key, err := w.openRegistryKey()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to access registry: %w", err)
|
||||||
|
}
|
||||||
|
defer key.Close()
|
||||||
|
|
||||||
|
if enabled {
|
||||||
|
startupCmd, err := w.buildStartupCommand()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build startup command: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.logger.Info("Setting Windows startup", "command", startupCmd)
|
||||||
|
|
||||||
|
if err := key.SetStringValue(w.registryKey, startupCmd); err != nil {
|
||||||
|
return fmt.Errorf("failed to set startup entry: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证设置是否成功
|
||||||
|
if value, _, err := key.GetStringValue(w.registryKey); err != nil {
|
||||||
|
return fmt.Errorf("startup entry verification failed: %w", err)
|
||||||
|
} else if value != startupCmd {
|
||||||
|
w.logger.Error("Startup command verification mismatch", "expected", startupCmd, "actual", value)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.logger.Info("Windows startup enabled successfully")
|
||||||
|
} else {
|
||||||
|
// 删除批处理文件(如果存在)
|
||||||
|
w.deleteBatchFile()
|
||||||
|
|
||||||
|
if err := key.DeleteValue(w.registryKey); err != nil {
|
||||||
|
// 如果键不存在,这不是错误
|
||||||
|
if err != registry.ErrNotExist {
|
||||||
|
return fmt.Errorf("failed to remove startup entry: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.logger.Info("Windows startup disabled successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
Reference in New Issue
Block a user