From fc83bab9f7b924efe23f04b1b5d498e6cc22cf45 Mon Sep 17 00:00:00 2001 From: Benjamin Hardy Date: Sat, 7 Mar 2026 15:48:27 +0100 Subject: [PATCH] feat: added PWA support --- app.py | 12 ++++++- static/icons/icon-192x192.png | Bin 0 -> 2235 bytes static/icons/icon-512x512.png | Bin 0 -> 6882 bytes static/manifest.json | 23 +++++++++++++ static/offline.html | 29 ++++++++++++++++ static/sw.js | 62 ++++++++++++++++++++++++++++++++++ templates/index.html | 12 +++++++ templates/login.html | 13 +++++++ 8 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 static/icons/icon-192x192.png create mode 100644 static/icons/icon-512x512.png create mode 100644 static/manifest.json create mode 100644 static/offline.html create mode 100644 static/sw.js diff --git a/app.py b/app.py index 1cb70e9..124daff 100644 --- a/app.py +++ b/app.py @@ -30,7 +30,7 @@ APP_PASSWORD = os.environ.get("PASSWORD", "") def require_login(): if not APP_PASSWORD: return - if request.endpoint in ("login", "static"): + if request.endpoint in ("login", "static", "service_worker", "offline"): return if not session.get("authenticated"): if request.path.startswith("/api/"): @@ -170,6 +170,16 @@ def run_download(job_id: str, urls: list[str], options: dict): jobs[job_id]["output"] = jobs[job_id].get("output", []) + [str(e)] +@app.route("/sw.js") +def service_worker(): + return send_from_directory("static", "sw.js", mimetype="application/javascript") + + +@app.route("/offline") +def offline(): + return send_from_directory("static", "offline.html") + + @app.route("/login", methods=["GET", "POST"]) def login(): if request.method == "POST": diff --git a/static/icons/icon-192x192.png b/static/icons/icon-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..48bf0e3233cce8b6efa548f5784dfb37392fda22 GIT binary patch literal 2235 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 aYdY)9o>Td}VOaR>3K$!l*DpS87yLik_kNB5 literal 0 HcmV?d00001 diff --git a/static/manifest.json b/static/manifest.json new file mode 100644 index 0000000..44efcf6 --- /dev/null +++ b/static/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "Votify Web", + "short_name": "Votify", + "description": "Music download manager", + "start_url": "/", + "display": "standalone", + "background_color": "#121212", + "theme_color": "#1db954", + "icons": [ + { + "src": "/static/icons/icon-192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/static/icons/icon-512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ] +} diff --git a/static/offline.html b/static/offline.html new file mode 100644 index 0000000..eba9720 --- /dev/null +++ b/static/offline.html @@ -0,0 +1,29 @@ + + + + + + Votify Web - Offline + + + +
+

Votify Web

+

You are currently offline.
Please check your connection and try again.

+ +
+ + diff --git a/static/sw.js b/static/sw.js new file mode 100644 index 0000000..866c988 --- /dev/null +++ b/static/sw.js @@ -0,0 +1,62 @@ +const CACHE_NAME = 'votify-v1'; +const APP_SHELL = [ + '/', + '/static/favicon.ico', + '/static/manifest.json', + '/static/icons/icon-192x192.png', + '/static/icons/icon-512x512.png', + '/offline', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME) + .then(cache => cache.addAll(APP_SHELL)) + .then(() => self.skipWaiting()) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then(keys => + Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) + ).then(() => self.clients.claim()) + ); +}); + +self.addEventListener('fetch', (event) => { + const url = new URL(event.request.url); + + // API calls: network only + if (url.pathname.startsWith('/api/')) { + return; + } + + // Navigation requests: network first, cache fallback, then offline page + if (event.request.mode === 'navigate') { + event.respondWith( + fetch(event.request) + .then(response => { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); + return response; + }) + .catch(() => caches.match(event.request).then(r => r || caches.match('/offline'))) + ); + return; + } + + // Static assets: cache first, network fallback + if (url.pathname.startsWith('/static/')) { + event.respondWith( + caches.match(event.request).then(cached => + cached || fetch(event.request).then(response => { + const clone = response.clone(); + caches.open(CACHE_NAME).then(cache => cache.put(event.request, clone)); + return response; + }) + ) + ); + return; + } +}); diff --git a/templates/index.html b/templates/index.html index 97e4cdc..7409dee 100644 --- a/templates/index.html +++ b/templates/index.html @@ -5,6 +5,12 @@ Votify Web + + + + + +