diff --git a/build/config.yml b/build/config.yml index e1e6071..b56fe6d 100644 --- a/build/config.yml +++ b/build/config.yml @@ -8,9 +8,9 @@ info: companyName: "Voidraft" # The name of the company productName: "Voidraft" # The name of the application 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 - comments: "Effortlessly capture and organize fleeting ideas with minimal design" # Comments + comments: "Voidraft" # Comments version: "0.0.1.0" # The application version # Dev mode configuration diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist index 487c45a..be2cf98 100644 --- a/build/darwin/Info.dev.plist +++ b/build/darwin/Info.dev.plist @@ -6,13 +6,13 @@ CFBundleName Voidraft CFBundleExecutable - voidraft + CFBundleIdentifier landaiqing CFBundleVersion 0.0.1.0 CFBundleGetInfoString - Effortlessly capture and organize fleeting ideas with minimal design + Voidraft CFBundleShortVersionString 0.0.1.0 CFBundleIconFile diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist index 034f38e..3949775 100644 --- a/build/darwin/Info.plist +++ b/build/darwin/Info.plist @@ -6,13 +6,13 @@ CFBundleName Voidraft CFBundleExecutable - voidraft + CFBundleIdentifier landaiqing CFBundleVersion 0.0.1.0 CFBundleGetInfoString - Effortlessly capture and organize fleeting ideas with minimal design + Voidraft CFBundleShortVersionString 0.0.1.0 CFBundleIconFile diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml index fc2e9e1..c4f67ea 100644 --- a/build/linux/nfpm/nfpm.yaml +++ b/build/linux/nfpm/nfpm.yaml @@ -3,26 +3,26 @@ # # The lines below are called `modelines`. See `:help modeline` -name: "voidraft" +name: "" arch: ${GOARCH} platform: "linux" version: "0.0.1.0" section: "default" priority: "extra" maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}> -description: "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design" +description: "Voidraft" vendor: "Voidraft" homepage: "https://wails.io" license: "MIT" release: "1" contents: - - src: "./bin/voidraft" - dst: "/usr/local/bin/voidraft" + - src: "./bin/" + dst: "/usr/local/bin/" - src: "./build/appicon.png" - dst: "/usr/share/icons/hicolor/128x128/apps/voidraft.png" - - src: "./build/linux/voidraft.desktop" - dst: "/usr/share/applications/voidraft.desktop" + dst: "/usr/share/icons/hicolor/128x128/apps/.png" + - src: "./build/linux/.desktop" + dst: "/usr/share/applications/.desktop" depends: - gtk3 diff --git a/build/windows/info.json b/build/windows/info.json index 6865082..7376344 100644 --- a/build/windows/info.json +++ b/build/windows/info.json @@ -6,10 +6,10 @@ "0000": { "ProductVersion": "0.0.1.0", "CompanyName": "Voidraft", - "FileDescription": "Your Inspiration Catcher - Instant thought-capturing tool with minimalist design", + "FileDescription": "Voidraft", "LegalCopyright": "© 2025 Voidraft. All rights reserved.", "ProductName": "Voidraft", - "Comments": "Effortlessly capture and organize fleeting ideas with minimal design" + "Comments": "Voidraft" } } } \ No newline at end of file diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh index 847af20..9cb55db 100644 --- a/build/windows/nsis/wails_tools.nsh +++ b/build/windows/nsis/wails_tools.nsh @@ -5,7 +5,7 @@ !include "FileFunc.nsh" !ifndef INFO_PROJECTNAME - !define INFO_PROJECTNAME "voidraft" + !define INFO_PROJECTNAME "" !endif !ifndef INFO_COMPANYNAME !define INFO_COMPANYNAME "Voidraft" diff --git a/frontend/bindings/voidraft/internal/models/models.ts b/frontend/bindings/voidraft/internal/models/models.ts index 11ea0df..465615c 100644 --- a/frontend/bindings/voidraft/internal/models/models.ts +++ b/frontend/bindings/voidraft/internal/models/models.ts @@ -345,6 +345,11 @@ export class GeneralConfig { */ "enableSystemTray": boolean; + /** + * 开机启动设置 + */ + "startAtLogin": boolean; + /** * 全局热键设置 * 是否启用全局热键 @@ -367,6 +372,9 @@ export class GeneralConfig { if (!("enableSystemTray" in $$source)) { this["enableSystemTray"] = false; } + if (!("startAtLogin" in $$source)) { + this["startAtLogin"] = false; + } if (!("enableGlobalHotkey" in $$source)) { this["enableGlobalHotkey"] = false; } @@ -381,10 +389,10 @@ export class GeneralConfig { * Creates a new GeneralConfig instance from a string or object. */ static createFrom($$source: any = {}): GeneralConfig { - const $$createField4_0 = $$createType6; + const $$createField5_0 = $$createType6; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("globalHotkey" in $$parsedSource) { - $$parsedSource["globalHotkey"] = $$createField4_0($$parsedSource["globalHotkey"]); + $$parsedSource["globalHotkey"] = $$createField5_0($$parsedSource["globalHotkey"]); } return new GeneralConfig($$parsedSource as Partial); } diff --git a/frontend/bindings/voidraft/internal/services/index.ts b/frontend/bindings/voidraft/internal/services/index.ts index 5dfbc99..b9eb333 100644 --- a/frontend/bindings/voidraft/internal/services/index.ts +++ b/frontend/bindings/voidraft/internal/services/index.ts @@ -7,6 +7,7 @@ import * as DocumentService from "./documentservice.js"; import * as HotkeyService from "./hotkeyservice.js"; import * as KeyBindingService from "./keybindingservice.js"; import * as MigrationService from "./migrationservice.js"; +import * as StartupService from "./startupservice.js"; import * as SystemService from "./systemservice.js"; import * as TrayService from "./trayservice.js"; export { @@ -16,6 +17,7 @@ export { HotkeyService, KeyBindingService, MigrationService, + StartupService, SystemService, TrayService }; diff --git a/frontend/bindings/voidraft/internal/services/startupservice.ts b/frontend/bindings/voidraft/internal/services/startupservice.ts new file mode 100644 index 0000000..7156b62 --- /dev/null +++ b/frontend/bindings/voidraft/internal/services/startupservice.ts @@ -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 & { cancel(): void } { + let $resultPromise = $Call.ByID(2911601468, enabled) as any; + return $resultPromise; +} diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 2d43ed8..8a2e64c 100644 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -44,7 +44,9 @@ export default { themeChanged: 'Theme setting updated', themeChangeFailed: 'Failed to update theme setting', 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: { 'zh-CN': '简体中文', @@ -162,6 +164,8 @@ export default { showInSystemTray: 'Show in System Tray', enableSystemTray: 'Enable System Tray', alwaysOnTop: 'Always on Top', + startup: 'Startup Settings', + startAtLogin: 'Start at Login', dataStorage: 'Data Storage', dataPath: 'Data Storage Path', clickToSelectPath: 'Click to select path', diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index 6d0cfd6..64a1629 100644 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -44,7 +44,9 @@ export default { themeChanged: '主题设置已更新', themeChangeFailed: '主题设置更新失败', systemThemeChanged: '系统主题设置已更新', - systemThemeChangeFailed: '系统主题设置更新失败' + systemThemeChangeFailed: '系统主题设置更新失败', + startupSuccess: '开机启动设置已更新', + startupFailed: '开机启动设置失败' }, languages: { 'zh-CN': '简体中文', @@ -162,6 +164,8 @@ export default { showInSystemTray: '在系统托盘中显示', enableSystemTray: '启用系统托盘', alwaysOnTop: '窗口始终置顶', + startup: '启动设置', + startAtLogin: '开机自启动', dataStorage: '数据存储', dataPath: '数据存储路径', clickToSelectPath: '点击选择路径', diff --git a/frontend/src/stores/configStore.ts b/frontend/src/stores/configStore.ts index 5e19e2f..0004b10 100644 --- a/frontend/src/stores/configStore.ts +++ b/frontend/src/stores/configStore.ts @@ -1,6 +1,6 @@ import {defineStore} from 'pinia'; import {computed, reactive} from 'vue'; -import {ConfigService} from '../../bindings/voidraft/internal/services'; +import {ConfigService, StartupService} from '../../bindings/voidraft/internal/services'; import { AppConfig, AppearanceConfig, @@ -50,6 +50,7 @@ const GENERAL_CONFIG_KEY_MAP: GeneralConfigKeyMap = { alwaysOnTop: 'general.alwaysOnTop', dataPath: 'general.dataPath', enableSystemTray: 'general.enableSystemTray', + startAtLogin: 'general.startAtLogin', enableGlobalHotkey: 'general.enableGlobalHotkey', globalHotkey: 'general.globalHotkey' } as const; @@ -118,6 +119,7 @@ const DEFAULT_CONFIG: AppConfig = { alwaysOnTop: false, dataPath: '', enableSystemTray: true, + startAtLogin: false, enableGlobalHotkey: false, globalHotkey: { ctrl: false, @@ -408,6 +410,15 @@ export const useConfigStore = defineStore('config', () => { 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'); + } }; }); \ No newline at end of file diff --git a/frontend/src/views/settings/pages/GeneralPage.vue b/frontend/src/views/settings/pages/GeneralPage.vue index 8aee13a..91410bc 100644 --- a/frontend/src/views/settings/pages/GeneralPage.vue +++ b/frontend/src/views/settings/pages/GeneralPage.vue @@ -23,24 +23,57 @@ const migrationProgress = ref(new MigrationProgress({ let pollingTimer: number | null = null; const isPolling = ref(false); +// 进度条显示控制 +const showProgress = ref(false); +const progressError = ref(''); +let hideProgressTimer: any = null; + // 开始轮询迁移进度 const startPolling = () => { if (isPolling.value) return; isPolling.value = true; + showProgress.value = true; + progressError.value = ''; + + // 立即重置迁移进度状态,避免从之前的失败状态渐变 + migrationProgress.value = new MigrationProgress({ + status: MigrationStatus.MigrationStatusMigrating, + progress: 0 + }); + pollingTimer = window.setInterval(async () => { try { const progress = await MigrationService.GetProgress(); migrationProgress.value = progress; + + const { status, error } = progress; + const isCompleted = [MigrationStatus.MigrationStatusCompleted, MigrationStatus.MigrationStatusFailed].includes(status); - // 如果迁移完成或失败,停止轮询 - if (progress.status === MigrationStatus.MigrationStatusCompleted || progress.status === MigrationStatus.MigrationStatusFailed) { + if (isCompleted) { stopPolling(); + + // 设置错误信息(如果是失败状态) + progressError.value = (status === MigrationStatus.MigrationStatusFailed) ? (error || 'Migration failed') : ''; + + const delay = status === MigrationStatus.MigrationStatusCompleted ? 3000 : 5000; + hideProgressTimer = setTimeout(hideProgress, delay); } } catch (error) { 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; }; -// 迁移消息链 -interface MigrationMessage { - id: number; - content: string; - type: 'start' | 'progress' | 'success' | 'error'; - timestamp: number; -} - -const migrationMessages = ref([]); -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() - }; +// 隐藏进度条 +const hideProgress = () => { + showProgress.value = false; + progressError.value = ''; - migrationMessages.value.push(message); - showMessages.value = true; -}; - -// 清除所有消息 -const clearMigrationMessages = () => { - migrationMessages.value = []; - showMessages.value = false; -}; - -// 监听迁移进度变化 -watch(() => migrationProgress.value, (progress, oldProgress) => { - // 清除之前的隐藏定时器 - if (hideMessagesTimer) { - clearTimeout(hideMessagesTimer); - hideMessagesTimer = null; + // 重置迁移状态,避免下次显示时状态不正确 + migrationProgress.value = new MigrationProgress({ + status: MigrationStatus.MigrationStatusCompleted, + progress: 0 + }); + + if (hideProgressTimer) { + clearTimeout(hideProgressTimer); + hideProgressTimer = 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 migrationComplete = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusCompleted); -const migrationFailed = computed(() => migrationProgress.value.status === MigrationStatus.MigrationStatusFailed); -// 进度条样式和宽度 -const progressBarClass = computed(() => { - switch (migrationProgress.value.status) { - case MigrationStatus.MigrationStatusMigrating: - return 'migrating'; - case MigrationStatus.MigrationStatusCompleted: - return 'success'; - case MigrationStatus.MigrationStatusFailed: - return 'error'; - default: - return ''; - } -}); +// 进度条样式 - 使用 Map 简化条件判断 +const statusClassMap = new Map([ + [MigrationStatus.MigrationStatusMigrating, 'migrating'], + [MigrationStatus.MigrationStatusCompleted, 'success'], + [MigrationStatus.MigrationStatusFailed, 'error'] +]); + +const progressBarClass = computed(() => + showProgress.value ? statusClassMap.get(migrationProgress.value.status) ?? '' : '' +); const progressBarWidth = computed(() => { - // 只有在显示消息且正在迁移时才显示进度条 - if (showMessages.value && isMigrating.value) { - return migrationProgress.value.progress + '%'; - } else if (showMessages.value && (migrationComplete.value || migrationFailed.value)) { - // 迁移完成或失败时,短暂显示100%,然后随着消息隐藏而隐藏 - return '100%'; - } - return '0%'; + if (!showProgress.value) return '0%'; + return isMigrating.value ? `${migrationProgress.value.progress}%` : '100%'; }); // 重置确认状态 @@ -206,6 +158,12 @@ const enableSystemTray = computed({ set: (value: boolean) => configStore.setEnableSystemTray(value) }); +// 计算属性 - 开机启动 +const startAtLogin = computed({ + get: () => configStore.config.general.startAtLogin, + set: (value: boolean) => configStore.setStartAtLogin(value) +}); + // 修饰键配置 - 只读计算属性 const modifierKeys = computed(() => ({ ctrl: configStore.config.general.globalHotkey.ctrl, @@ -253,19 +211,20 @@ const confirmReset = async () => { await configStore.resetConfig(); }; -// 计算热键预览文本 +// 计算热键预览文本 - 使用现代语法简化 const hotkeyPreview = computed(() => { if (!enableGlobalHotkey.value) return ''; - const hotkey = configStore.config.general.globalHotkey; - const parts: string[] = []; - if (hotkey.ctrl) parts.push('Ctrl'); - if (hotkey.shift) parts.push('Shift'); - if (hotkey.alt) parts.push('Alt'); - if (hotkey.win) parts.push('Win'); - if (hotkey.key) parts.push(hotkey.key); + const { ctrl, shift, alt, win, key } = configStore.config.general.globalHotkey; + const modifiers = [ + ctrl && 'Ctrl', + shift && 'Shift', + alt && 'Alt', + win && 'Win', + key + ].filter(Boolean); - return parts.join(' + '); + return modifiers.join(' + '); }); // 数据路径配置 @@ -275,41 +234,54 @@ const currentDataPath = computed(() => configStore.config.general.dataPath); const selectDataDirectory = async () => { if (isMigrating.value) return; - const selectedPath = await DialogService.SelectDirectory(); - - if (selectedPath && selectedPath.trim() && selectedPath !== currentDataPath.value) { - // 清除之前的消息 - clearMigrationMessages(); + try { + const selectedPath = await DialogService.SelectDirectory(); + + // 检查用户是否取消了选择或路径为空 + if (!selectedPath || !selectedPath.trim() || selectedPath === currentDataPath.value) { + return; + } + const oldPath = currentDataPath.value; + const newPath = selectedPath.trim(); + + // 清除之前的进度状态 + hideProgress(); // 开始轮询迁移进度 startPolling(); // 开始迁移 try { - await safeCall(async () => { - const oldPath = currentDataPath.value; - const newPath = selectedPath.trim(); - - await MigrationService.MigrateDirectory(oldPath, newPath); - await configStore.setDataPath(newPath); - }, ''); + await MigrationService.MigrateDirectory(oldPath, newPath); + await configStore.setDataPath(newPath); } catch (error) { - // 发生错误时清除消息并停止轮询 - clearMigrationMessages(); 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(() => { stopPolling(); + hideProgress(); if (resetConfirmTimer) { clearTimeout(resetConfirmTimer); } - if (hideMessagesTimer) { - clearTimeout(hideMessagesTimer); - } }); @@ -363,6 +335,12 @@ onUnmounted(() => { + + + + + +
@@ -380,27 +358,23 @@ onUnmounted(() => { :title="t('settings.clickToSelectPath')" :disabled="isMigrating" /> +
-
- -
- -
- {{ message.content }} -
-
-
-
- -
+ + + +
+ {{ progressError }} +
+
@@ -612,108 +586,46 @@ onUnmounted(() => { position: absolute; bottom: 0; left: 0; - height: 2px; + height: 3px; background-color: transparent; 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; opacity: 0; + z-index: 1; &.active { opacity: 1; &.migrating { - background-color: #3b82f6; - animation: progress-wave 2s infinite; + background: linear-gradient(90deg, #22c55e, #16a34a); + animation: progress-pulse 2s ease-in-out infinite; + transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } &.success { background-color: #22c55e; + animation: none; + transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } &.error { background-color: #ef4444; + animation: none; + transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); } } } } - .migration-status-container { - margin-top: 8px; - min-height: 0; - overflow: hidden; - transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); - - .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); + .progress-error { + margin-top: 6px; + font-size: 12px; + color: #ef4444; + padding: 0 2px; + line-height: 1.4; opacity: 1; - transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); - - &::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; - } + transition: all 0.3s ease; } } @@ -749,16 +661,15 @@ onUnmounted(() => { } } -// 进度条波浪动画 -@keyframes progress-wave { - 0% { - background-position: 0% 50%; +// 进度条脉冲动画 +@keyframes progress-pulse { + 0%, 100% { + opacity: 0.8; + transform: scaleY(1); } 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; + opacity: 1; + transform: scaleY(1.1); } } @@ -787,58 +698,22 @@ onUnmounted(() => { } } -// Vue Transition 动画 -.fade-slide-enter-active { - transition: all 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); +// 错误提示动画 +.error-fade-enter-active { + transition: all 0.3s ease; } -.fade-slide-leave-active { - transition: all 0.3s cubic-bezier(0.4, 0, 0.6, 1); +.error-fade-leave-active { + transition: all 0.3s ease; } -.fade-slide-enter-from { +.error-fade-enter-from { opacity: 0; - transform: translateY(-8px); - max-height: 0; - margin-top: 0; - margin-bottom: 0; + transform: translateY(-4px); } -.fade-slide-leave-to { +.error-fade-leave-to { opacity: 0; - transform: translateY(-5px); - 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); + transform: translateY(-4px); } \ No newline at end of file diff --git a/go.mod b/go.mod index 9f18278..e825c5d 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/fsnotify/fsnotify v1.9.0 github.com/spf13/viper v1.20.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.9 + golang.org/x/sys v0.33.0 ) require ( @@ -57,7 +58,6 @@ require ( golang.org/x/crypto v0.39.0 // indirect golang.org/x/exp v0.0.0-20250506013437-ce4c2cf36ca6 // 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 gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect diff --git a/internal/events/tray_events.go b/internal/events/tray_events.go index f8278af..8941569 100644 --- a/internal/events/tray_events.go +++ b/internal/events/tray_events.go @@ -29,8 +29,6 @@ func RegisterTrayEvents(app *application.App, systray *application.SystemTray, m trayService.HandleWindowClose() }) - // 不再拦截窗口最小化事件,让任务栏点击保持正常行为 - // 最小化到托盘的逻辑由前端标题栏按钮直接处理 } // RegisterTrayMenuEvents 注册系统托盘菜单事件 diff --git a/internal/models/config.go b/internal/models/config.go index 0958a15..35366de 100644 --- a/internal/models/config.go +++ b/internal/models/config.go @@ -43,6 +43,7 @@ type GeneralConfig struct { AlwaysOnTop bool `json:"alwaysOnTop"` // 窗口是否置顶 DataPath string `json:"dataPath"` // 数据存储路径 EnableSystemTray bool `json:"enableSystemTray"` // 是否启用系统托盘 + StartAtLogin bool `json:"startAtLogin"` // 开机启动设置 // 全局热键设置 EnableGlobalHotkey bool `json:"enableGlobalHotkey"` // 是否启用全局热键 @@ -115,7 +116,8 @@ func NewDefaultAppConfig() *AppConfig { General: GeneralConfig{ AlwaysOnTop: false, DataPath: dataDir, - EnableSystemTray: true, // 默认启用系统托盘 + EnableSystemTray: true, + StartAtLogin: false, EnableGlobalHotkey: false, GlobalHotkey: HotkeyCombo{ Ctrl: false, @@ -140,7 +142,7 @@ func NewDefaultAppConfig() *AppConfig { }, Appearance: AppearanceConfig{ Language: LangZhCN, - SystemTheme: SystemThemeDark, // 默认使用深色系统主题 + SystemTheme: SystemThemeAuto, // 默认使用深色系统主题 }, Updates: UpdatesConfig{}, Metadata: ConfigMetadata{ diff --git a/internal/models/document.go b/internal/models/document.go index db788da..0d1306b 100644 --- a/internal/models/document.go +++ b/internal/models/document.go @@ -36,6 +36,6 @@ func NewDefaultDocument() *Document { LastUpdated: now, CreatedAt: now, }, - Content: "// 在此处编写文本...", + Content: "\n∞∞∞text-a\n", } } diff --git a/internal/services/service_manager.go b/internal/services/service_manager.go index d070239..d5cd95f 100644 --- a/internal/services/service_manager.go +++ b/internal/services/service_manager.go @@ -17,6 +17,7 @@ type ServiceManager struct { dialogService *DialogService trayService *TrayService keyBindingService *KeyBindingService + startupService *StartupService logger *log.LoggerService } @@ -49,6 +50,9 @@ func NewServiceManager() *ServiceManager { // 初始化快捷键服务 keyBindingService := NewKeyBindingService(logger) + // 初始化开机启动服务 + startupService := NewStartupService(configService, logger) + // 使用新的配置通知系统设置热键配置变更监听 err := configService.SetHotkeyChangeCallback(func(enable bool, hotkey *models.HotkeyCombo) error { return hotkeyService.UpdateHotkey(enable, hotkey) @@ -83,6 +87,7 @@ func NewServiceManager() *ServiceManager { dialogService: dialogService, trayService: trayService, keyBindingService: keyBindingService, + startupService: startupService, logger: logger, } } @@ -98,6 +103,7 @@ func (sm *ServiceManager) GetServices() []application.Service { application.NewService(sm.dialogService), application.NewService(sm.trayService), application.NewService(sm.keyBindingService), + application.NewService(sm.startupService), } } @@ -130,3 +136,8 @@ func (sm *ServiceManager) GetTrayService() *TrayService { func (sm *ServiceManager) GetKeyBindingService() *KeyBindingService { return sm.keyBindingService } + +// GetStartupService 获取开机启动服务实例 +func (sm *ServiceManager) GetStartupService() *StartupService { + return sm.startupService +} diff --git a/internal/services/startup_darwin.go b/internal/services/startup_darwin.go new file mode 100644 index 0000000..8d6c254 --- /dev/null +++ b/internal/services/startup_darwin.go @@ -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 +} diff --git a/internal/services/startup_linux.go b/internal/services/startup_linux.go new file mode 100644 index 0000000..c51fe9b --- /dev/null +++ b/internal/services/startup_linux.go @@ -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) +} diff --git a/internal/services/startup_service.go b/internal/services/startup_service.go new file mode 100644 index 0000000..e966cde --- /dev/null +++ b/internal/services/startup_service.go @@ -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 +} diff --git a/internal/services/startup_windows.go b/internal/services/startup_windows.go new file mode 100644 index 0000000..ffd5d89 --- /dev/null +++ b/internal/services/startup_windows.go @@ -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 +}