diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 71408a5..ff583cc 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1709,7 +1709,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2001,7 +2001,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -2969,7 +2969,7 @@ dependencies = [ "libc", "percent-encoding", "pin-project-lite", - "socket2 0.6.4", + "socket2 0.5.10", "system-configuration", "tokio", "tower-service", @@ -2989,7 +2989,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core 0.61.2", ] [[package]] @@ -3758,7 +3758,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -4990,7 +4990,7 @@ dependencies = [ "quinn-udp", "rustc-hash 2.1.2", "rustls 0.23.40", - "socket2 0.6.4", + "socket2 0.5.10", "thiserror 2.0.18", "tokio", "tracing", @@ -5027,9 +5027,9 @@ dependencies = [ "cfg_aliases", "libc", "once_cell", - "socket2 0.6.4", + "socket2 0.5.10", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5631,7 +5631,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -5732,7 +5732,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -6306,7 +6306,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7111,7 +7111,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] @@ -7640,7 +7640,7 @@ dependencies = [ "png 0.18.1", "serde", "thiserror 2.0.18", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -7680,7 +7680,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -8200,7 +8200,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.59.0", ] [[package]] diff --git a/src/App.vue b/src/App.vue index 7e328de..eb10191 100644 --- a/src/App.vue +++ b/src/App.vue @@ -5,6 +5,7 @@ import { storeToRefs } from 'pinia' import { onMounted, onUnmounted, watch } from 'vue' import { RouterView } from 'vue-router' import AppNotifications from '@/components/ui/notification/AppNotifications.vue' +import UpdateNotification from '@/components/UpdateNotification.vue' import { useAppUpdater } from '@/composables/useAppUpdater' import { useAccountStore } from '@/store/accountStore' import { useAppStore } from '@/store/appStore' @@ -41,4 +42,5 @@ onUnmounted(() => { diff --git a/src/assets/index.css b/src/assets/index.css index 8e97e98..c5296e5 100644 --- a/src/assets/index.css +++ b/src/assets/index.css @@ -15,6 +15,10 @@ --accent-foreground: 222.2 47.4% 11.2%; --destructive: 0 84.2% 60.2%; --destructive-foreground: 210 40% 98%; + --success: 142.1 70.6% 35.5%; + --success-foreground: 0 0% 100%; + --success-muted: 138 76% 95%; + --success-border: 140 58% 85%; --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 220 13% 60%; @@ -43,6 +47,10 @@ --accent-foreground: 210 40% 98%; --destructive: 0 62.8% 30.6%; --destructive-foreground: 210 40% 98%; + --success: 142.1 70% 45%; + --success-foreground: 0 0% 100%; + --success-muted: 142 35% 16%; + --success-border: 142 30% 25%; --border: 217.2 32.6% 17.5%; --input: 217.2 32.6% 17.5%; --ring: 212.7 26.8% 68%; diff --git a/src/components/UpdateNotification.vue b/src/components/UpdateNotification.vue new file mode 100644 index 0000000..c4cae62 --- /dev/null +++ b/src/components/UpdateNotification.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/composables/useAppUpdater.ts b/src/composables/useAppUpdater.ts index bf698c2..0f39efa 100644 --- a/src/composables/useAppUpdater.ts +++ b/src/composables/useAppUpdater.ts @@ -1,28 +1,35 @@ import type { DownloadEvent, Update } from '@tauri-apps/plugin-updater' import { relaunch } from '@tauri-apps/plugin-process' import { check } from '@tauri-apps/plugin-updater' +import { useLocalStorage } from '@vueuse/core' import { computed, ref } from 'vue' import { useI18n } from 'vue-i18n' import { toast } from './useNotifications' +// Module-scope state ensures `useAppUpdater()` behaves as a shared composable: +// every call site reads from and writes to the same refs (App.vue triggers the +// check, UpdateNotification.vue renders the result, SettingsPage.vue inspects it). +const skipVersion = useLocalStorage('sqlkit-skip-version', '') +const updateAvailable = ref(false) +const updateInfo = ref(null) +const isChecking = ref(false) +const isDownloading = ref(false) +const isInstalling = ref(false) +const isRestarting = ref(false) + +/** Accumulated downloaded bytes for the current update download */ +const downloadedBytes = ref(0) +/** Total content length (from Started event) */ +const contentLength = ref(0) +/** Download progress as percentage 0–100, null when not downloading */ +const downloadProgress = computed(() => { + if (!isDownloading.value || contentLength.value <= 0) + return null + return Math.round((downloadedBytes.value / contentLength.value) * 100) +}) + export function useAppUpdater() { const { t } = useI18n() - const updateAvailable = ref(false) - const updateInfo = ref(null) - const isChecking = ref(false) - const isDownloading = ref(false) - const isInstalling = ref(false) - - /** Accumulated downloaded bytes for the current update download */ - const downloadedBytes = ref(0) - /** Total content length (from Started event) */ - const contentLength = ref(0) - /** Download progress as percentage 0–100, null when not downloading */ - const downloadProgress = computed(() => { - if (!isDownloading.value || contentLength.value <= 0) - return null - return Math.round((downloadedBytes.value / contentLength.value) * 100) - }) const checkForUpdates = async (showToast = false): Promise => { if (isChecking.value) @@ -32,7 +39,7 @@ export function useAppUpdater() { isChecking.value = true const update = await check() - if (update) { + if (update && update.version !== skipVersion.value) { updateAvailable.value = true updateInfo.value = update @@ -73,9 +80,16 @@ export function useAppUpdater() { isDownloading.value = true downloadedBytes.value = 0 contentLength.value = 0 - toast.info(t('updater.downloading')) - await updateInfo.value.downloadAndInstall((event: DownloadEvent) => { + // Re-check to get a fresh download URL + const freshUpdate = await check() + if (!freshUpdate) { + isDownloading.value = false + return + } + updateInfo.value = freshUpdate + + await freshUpdate.downloadAndInstall((event: DownloadEvent) => { if (event.event === 'Started') { contentLength.value = event.data.contentLength ?? 0 } @@ -85,30 +99,46 @@ export function useAppUpdater() { else if (event.event === 'Finished') { isDownloading.value = false isInstalling.value = true - toast.info(t('updater.installing')) } }) - - toast.success(t('updater.installed')) - - setTimeout(async () => { - await relaunch() - }, 1500) } catch (error) { console.error('Failed to install update:', error) toast.error(t('updater.installFailed'), { description: error instanceof Error ? error.message : String(error), }) - } - finally { isDownloading.value = false isInstalling.value = false - updateAvailable.value = false - updateInfo.value = null downloadedBytes.value = 0 contentLength.value = 0 + return } + + isRestarting.value = true + isInstalling.value = false + + const relaunchTimeout = setTimeout(() => { + isRestarting.value = false + }, 30000) + + try { + await relaunch() + } + catch { + clearTimeout(relaunchTimeout) + isRestarting.value = false + toast.error(t('updater.installFailed')) + } + } + + const skipUpdate = () => { + if (updateInfo.value) { + skipVersion.value = updateInfo.value.version + } + updateAvailable.value = false + updateInfo.value = null + downloadedBytes.value = 0 + contentLength.value = 0 } const dismissUpdate = () => { @@ -124,9 +154,11 @@ export function useAppUpdater() { isChecking, isDownloading, isInstalling, + isRestarting, downloadProgress, checkForUpdates, downloadAndInstall, + skipUpdate, dismissUpdate, } } diff --git a/src/lang/enUS.ts b/src/lang/enUS.ts index f899301..dc5fe19 100644 --- a/src/lang/enUS.ts +++ b/src/lang/enUS.ts @@ -1407,8 +1407,12 @@ export const enUS = { checkFailed: 'Failed to check for updates', downloading: 'Downloading update...', installing: 'Installing update...', - installed: 'Update installed. Restarting...', installFailed: 'Failed to install update', + skip: 'Skip This Version', + later: 'Later', + updateNow: 'Update Now', + restarting: 'Restarting...', + downloadingPercent: 'Downloading {percent}%', }, errors: { unexpected: 'Unexpected error: {error}', diff --git a/src/lang/zhCN.ts b/src/lang/zhCN.ts index b1a892a..18e8942 100644 --- a/src/lang/zhCN.ts +++ b/src/lang/zhCN.ts @@ -1407,8 +1407,12 @@ export const zhCN = { checkFailed: '检查更新失败', downloading: '正在下载更新...', installing: '正在安装更新...', - installed: '更新已安装,正在重启...', installFailed: '安装更新失败', + skip: '跳过此版本', + later: '稍后', + updateNow: '立即更新', + restarting: '正在重启...', + downloadingPercent: '下载中 {percent}%', }, errors: { unexpected: '意外错误:{error}', diff --git a/uno.config.ts b/uno.config.ts index 4c9b6bd..40fa6bb 100644 --- a/uno.config.ts +++ b/uno.config.ts @@ -43,6 +43,12 @@ export default defineConfig({ DEFAULT: 'hsl(var(--destructive))', foreground: 'hsl(var(--destructive-foreground))', }, + success: { + DEFAULT: 'hsl(var(--success))', + foreground: 'hsl(var(--success-foreground))', + muted: 'hsl(var(--success-muted))', + border: 'hsl(var(--success-border))', + }, muted: { DEFAULT: 'hsl(var(--muted))', foreground: 'hsl(var(--muted-foreground))', @@ -105,5 +111,9 @@ export default defineConfig({ 'text-destructive', 'hover:bg-destructive/10', 'hover:text-destructive-foreground', + 'bg-success', + 'text-success', + 'border-success-border', + 'bg-success-muted', ], })