diff --git a/src/core/i18n.ts b/src/core/i18n.ts index ee94972..21339a8 100644 --- a/src/core/i18n.ts +++ b/src/core/i18n.ts @@ -12,6 +12,10 @@ export interface Translations { error: string; notAvailable: string; + // App update + updateAvailable: string; + updateReload: string; + // Navigation nav: { calendar: string; @@ -548,6 +552,9 @@ const translations: Record = { error: 'Error', notAvailable: 'Not available', + updateAvailable: 'New version available', + updateReload: 'Update', + nav: { calendar: '📅 Calendar', service: '📖 Service', @@ -1116,6 +1123,9 @@ const translations: Record = { error: 'Ошибка', notAvailable: 'Недоступно', + updateAvailable: 'Достꙋ́пна новаѧ вєрсі́ѧ', + updateReload: 'Обнови́ти', + nav: { calendar: '📅 Календарь', service: '📖 Устав', @@ -1684,6 +1694,9 @@ const translations: Record = { error: 'Ошибка', notAvailable: 'Недоступно', + updateAvailable: 'Доступна новая версия', + updateReload: 'Обновить', + nav: { calendar: '📅 Календарь', service: '📖 Уставъ', diff --git a/src/main.ts b/src/main.ts index 0820842..1df3a6a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,32 @@ import './styles/main.css'; +import { registerSW } from 'virtual:pwa-register'; import { App } from './ui/app'; +import { getTranslations, type LanguageCode } from './core/i18n'; +import { loadSettings } from './ui/settings-view'; + +const updateSW = registerSW({ + immediate: true, + onNeedRefresh: () => showUpdateToast(), +}); + +function showUpdateToast() { + if (document.getElementById('pwa-update-toast')) return; + const t = getTranslations(loadSettings().language as LanguageCode); + const toast = document.createElement('div'); + toast.id = 'pwa-update-toast'; + toast.className = 'fixed bottom-4 left-1/2 -translate-x-1/2 z-[60] bg-surface border border-gold/40 rounded-lg shadow-xl px-4 py-3 flex items-center gap-3 max-w-[calc(100vw-2rem)]'; + toast.innerHTML = ` + ${t.updateAvailable} + + + `; + document.body.appendChild(toast); + toast.querySelector('#pwa-update-btn')?.addEventListener('click', () => { + (toast.querySelector('#pwa-update-btn') as HTMLButtonElement).disabled = true; + updateSW(true); + }); + toast.querySelector('#pwa-update-dismiss')?.addEventListener('click', () => toast.remove()); +} const app = new App(document.getElementById('app')!); app.init(); diff --git a/src/ui/settings-view.ts b/src/ui/settings-view.ts index 8f47687..61843b9 100644 --- a/src/ui/settings-view.ts +++ b/src/ui/settings-view.ts @@ -319,6 +319,7 @@ export class SettingsView { ${t.settings.aboutLicense}

+

build ${__BUILD_DATE__}

`; diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts index 11f02fe..9860a3a 100644 --- a/src/vite-env.d.ts +++ b/src/vite-env.d.ts @@ -1 +1,4 @@ /// +/// + +declare const __BUILD_DATE__: string; diff --git a/vite.config.ts b/vite.config.ts index 1a5afbc..6f593f6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,11 +6,10 @@ import { VitePWA } from 'vite-plugin-pwa'; export default defineConfig({ plugins: [ tailwindcss(), - VitePWA({ - registerType: 'autoUpdate', + VitePWA({ registerType: 'prompt', workbox: { - skipWaiting: true, - clientsClaim: true, + skipWaiting: false, + clientsClaim: false, globPatterns: ['**/*.{js,css,html,woff,png}'], globIgnores: ['**/data/**'], maximumFileSizeToCacheInBytes: 20 * 1024 * 1024, @@ -66,5 +65,8 @@ export default defineConfig({ outDir: 'dist', sourcemap: true, }, + define: { + __BUILD_DATE__: JSON.stringify(new Date().toISOString().slice(0, 10)), + }, publicDir: 'static', }); \ No newline at end of file