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 0000000..48bf0e3 Binary files /dev/null and b/static/icons/icon-192x192.png differ diff --git a/static/icons/icon-512x512.png b/static/icons/icon-512x512.png new file mode 100644 index 0000000..8f0bf57 Binary files /dev/null and b/static/icons/icon-512x512.png differ 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 + + + + + +