From c831537cefc8fc5fb13f97f0d4a1ccb2b2970522 Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Wed, 13 May 2026 18:22:25 +0000 Subject: [PATCH] Implement PWA. --- frontend/index.html | 8 +++-- frontend/package.json | 1 + frontend/public/mediahive-32.webp | Bin 0 -> 874 bytes frontend/public/mediahive.webp | Bin 0 -> 5638 bytes frontend/src/App.vue | 6 ++++ frontend/src/main.ts | 6 ++++ frontend/src/pwa.ts | 8 +++++ frontend/src/vite-env.d.ts | 1 + frontend/vite.config.ts | 52 +++++++++++++++++++++++++++++- 9 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 frontend/public/mediahive-32.webp create mode 100644 frontend/public/mediahive.webp create mode 100644 frontend/src/pwa.ts diff --git a/frontend/index.html b/frontend/index.html index 67901dc..b149186 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,10 +2,12 @@ - - - + + + + + MediaHive diff --git a/frontend/package.json b/frontend/package.json index 2a36843..47c83d9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@vitejs/plugin-vue": "^5.0.0", "typescript": "^5.3.0", "vite": "^5.0.0", + "vite-plugin-pwa": "^1.3.0", "vue-tsc": "^2.0.0" } } diff --git a/frontend/public/mediahive-32.webp b/frontend/public/mediahive-32.webp new file mode 100644 index 0000000000000000000000000000000000000000..15df1148f145dfe9a4a2ca7a461e8167044cd3ea GIT binary patch literal 874 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 literal 0 HcmV?d00001 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 22805a0..1b76b86 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -219,6 +219,10 @@ interface ScoredMediaItem { const searchCategories = ref([]); +function requestInitialFullscreen() { + void document.documentElement.requestFullscreen(); +} + // Focus state per page for Escape navigation const focusStateMap = new Map(); // Track the last viewed item ID to restore focus to the right card @@ -308,10 +312,12 @@ function handleEscapeKey(event: KeyboardEvent) { onMounted(() => { document.addEventListener('keydown', handleEscapeKey); + window.addEventListener('click', requestInitialFullscreen, { once: true }); }); onUnmounted(() => { document.removeEventListener('keydown', handleEscapeKey); + window.removeEventListener('click', requestInitialFullscreen); }); // Search query stored in ref (not URL-based) diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 8e5cde2..e5e0140 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -9,4 +9,10 @@ import { installGamepadNavigation } from './composables/useGamepadNavigation' installKeyboardNavigation() installGamepadNavigation() +if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) { + navigator.serviceWorker.addEventListener('controllerchange', () => { + window.location.reload() + }, { once: true }) +} + createApp(App).use(router).mount('#app') diff --git a/frontend/src/pwa.ts b/frontend/src/pwa.ts new file mode 100644 index 0000000..650e249 --- /dev/null +++ b/frontend/src/pwa.ts @@ -0,0 +1,8 @@ +import { registerSW } from 'virtual:pwa-register' + +export function registerMediaHivePwa(): void { + const updateServiceWorker = registerSW({ + immediate: true, + onNeedRefresh: () => void updateServiceWorker(true), + }) +} diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts index 323c78a..0dcaebc 100644 --- a/frontend/src/vite-env.d.ts +++ b/frontend/src/vite-env.d.ts @@ -1,4 +1,5 @@ /// +/// declare module '*.vue' { import type { DefineComponent } from 'vue' diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index df2a826..d6ecc5b 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,10 +1,60 @@ import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; +import { VitePWA } from "vite-plugin-pwa"; import fastapiVue from './vite-plugin-fastapi.js' // https://vitejs.dev/config/ export default defineConfig(async () => ({ - plugins: [fastapiVue(), vue()], + plugins: [ + fastapiVue(), + vue(), + VitePWA({ + registerType: "autoUpdate", + injectRegister: "script", + manifest: { + id: "/", + name: "MediaHive", + short_name: "MediaHive", + description: "Movies and Series", + start_url: "/", + scope: "/", + display_override: ["window-controls-overlay", "fullscreen", "standalone"], + display: "fullscreen", + background_color: "#0a0a0a", + theme_color: "#0a0a0a", + icons: [ + { + src: "/mediahive-32.webp", + sizes: "32x32", + type: "image/webp" + }, + { + src: "/mediahive.webp", + sizes: "192x192", + type: "image/webp" + } + ] + }, + workbox: { + cleanupOutdatedCaches: true, + clientsClaim: true, + skipWaiting: true, + manifestTransforms: [ + async (entries) => { + const manifest = entries.map((entry) => + entry.url === "index.html" ? { ...entry, url: "/" } : entry + ); + return { manifest, warnings: [] }; + }, + ], + navigateFallback: null, + navigateFallbackDenylist: [/^\/api\//], + }, + devOptions: { + enabled: false, + }, + }), + ], // Vite dev server options clearScreen: false,