diff --git a/src/App.vue b/src/App.vue index 3f45eba7d9..bd57e681f7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -59,6 +59,7 @@ const { isDownloadingUpdate, downloadProgress, updateReady, + hasUpdateAvailable, openUrl, checkUpdates, openLatestRelease, @@ -68,6 +69,8 @@ const { const { setupFileDrop } = useFileDrop(); const isDesktop = isTauriRuntime(); +const UPDATE_CHECK_INTERVAL_MS = 60 * 60 * 1000; +let updateCheckTimer: ReturnType | undefined; const needsAuth = ref(!isDesktop); const authenticated = ref(isDesktop); const setupRequired = ref(false); @@ -527,6 +530,7 @@ onMounted(async () => { initApp(); setupFileDrop().catch(() => {}); checkUpdates({ silent: true }); + updateCheckTimer = setInterval(() => checkUpdates({ silent: true }), UPDATE_CHECK_INTERVAL_MS); api .getAppVersion() .then((v) => { @@ -538,6 +542,9 @@ onMounted(async () => { onUnmounted(() => { cleanupTauriListeners(); + if (updateCheckTimer) { + clearInterval(updateCheckTimer); + } window.removeEventListener("keydown", handleKeydown, true); document.removeEventListener("contextmenu", handleContextMenu); }); @@ -559,6 +566,7 @@ onUnmounted(() => { :show-history="showHistory" :show-driver-store="showDriverStore" :checking-updates="checkingUpdates" + :has-update-available="hasUpdateAvailable" :has-connections="connectionStore.connections.length > 0" :has-sql-file-connections="hasSqlFileConnections" @new-connection="showConnectionDialog = true" diff --git a/src/components/layout/AppToolbar.vue b/src/components/layout/AppToolbar.vue index 374a1f33a8..1cae5bf14a 100644 --- a/src/components/layout/AppToolbar.vue +++ b/src/components/layout/AppToolbar.vue @@ -45,6 +45,7 @@ defineProps<{ showHistory: boolean; showDriverStore: boolean; checkingUpdates: boolean; + hasUpdateAvailable: boolean; hasConnections: boolean; hasSqlFileConnections: boolean; }>(); @@ -159,9 +160,19 @@ function onToolbarDblClick(e: MouseEvent) { - {{ t("updates.check") }} diff --git a/src/composables/useAppUpdater.ts b/src/composables/useAppUpdater.ts index c6a253032f..4b780e1410 100644 --- a/src/composables/useAppUpdater.ts +++ b/src/composables/useAppUpdater.ts @@ -1,4 +1,4 @@ -import { ref } from "vue"; +import { computed, ref } from "vue"; import { useI18n } from "vue-i18n"; import { isTauriRuntime } from "@/lib/tauriRuntime"; import { useToast } from "@/composables/useToast"; @@ -15,6 +15,7 @@ export function useAppUpdater() { const isDownloadingUpdate = ref(false); const downloadProgress = ref(0); const updateReady = ref(false); + const hasUpdateAvailable = computed(() => updateInfo.value?.update_available === true); const latestReleaseUrl = "https://github.com/t8y2/dbx/releases/latest"; function openUrl(url: string) { @@ -31,9 +32,12 @@ export function useAppUpdater() { updateCheckMessage.value = ""; try { const info = await api.checkForUpdates(); + const wasUpdateAvailable = hasUpdateAvailable.value; updateInfo.value = info; if (info.update_available) { - showUpdateDialog.value = true; + if (!options.silent || !wasUpdateAvailable) { + showUpdateDialog.value = true; + } } else if (!options.silent) { updateCheckMessage.value = t("updates.upToDate", { version: info.current_version }); showUpdateDialog.value = true; @@ -107,6 +111,7 @@ export function useAppUpdater() { isDownloadingUpdate, downloadProgress, updateReady, + hasUpdateAvailable, latestReleaseUrl, openUrl, checkUpdates, diff --git a/tests/appUpdateBadge.test.ts b/tests/appUpdateBadge.test.ts new file mode 100644 index 0000000000..0140c5e878 --- /dev/null +++ b/tests/appUpdateBadge.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from "node:fs"; +import { strict as assert } from "node:assert"; +import test from "node:test"; + +test("toolbar update button can show a red update badge", () => { + const source = readFileSync("src/components/layout/AppToolbar.vue", "utf8"); + + assert.match(source, /hasUpdateAvailable/); + assert.match(source, /v-if="hasUpdateAvailable"/); + assert.match(source, /bg-red-500/); +}); + +test("app schedules hourly silent update checks and clears the timer", () => { + const source = readFileSync("src/App.vue", "utf8"); + + assert.match(source, /UPDATE_CHECK_INTERVAL_MS\s*=\s*60\s*\*\s*60\s*\*\s*1000/); + assert.match(source, /setInterval\(\(\)\s*=>\s*checkUpdates\(\{\s*silent:\s*true\s*}\)/); + assert.match(source, /clearInterval\(updateCheckTimer\)/); +}); + +test("app passes update availability to the toolbar badge", () => { + const source = readFileSync("src/App.vue", "utf8"); + + assert.match(source, /:has-update-available="hasUpdateAvailable"/); +});