diff --git a/src/app.html b/src/app.html
index c7e23ff..283010a 100644
--- a/src/app.html
+++ b/src/app.html
@@ -3,12 +3,14 @@
-
+
+
+
diff --git a/src/lib/assets/apple-touch-icon.png b/src/lib/assets/apple-touch-icon.png
deleted file mode 100644
index fbdf197..0000000
Binary files a/src/lib/assets/apple-touch-icon.png and /dev/null differ
diff --git a/src/lib/assets/owlboard-logo-square.svg b/src/lib/assets/owlboard-logo-square.svg
new file mode 100644
index 0000000..c24503c
--- /dev/null
+++ b/src/lib/assets/owlboard-logo-square.svg
@@ -0,0 +1 @@
+
diff --git a/src/lib/preferences.svelte.ts b/src/lib/preferences.svelte.ts
new file mode 100644
index 0000000..6de0e82
--- /dev/null
+++ b/src/lib/preferences.svelte.ts
@@ -0,0 +1,43 @@
+import { browser } from '$app/environment';
+
+export interface Preferences {
+ locationEnabled: boolean;
+}
+
+const STORAGE_KEY = 'ob_pref';
+
+const DEFAULT_PREFS: Preferences = {
+ locationEnabled: false,
+};
+
+function loadPrefs(): Preferences {
+ if (!browser) return DEFAULT_PREFS;
+ const stored = localStorage.getItem(STORAGE_KEY);
+ if (!stored) return DEFAULT_PREFS;
+
+ try {
+ return { ...DEFAULT_PREFS, ...JSON.parse(stored) };
+ } catch {
+ return DEFAULT_PREFS;
+ }
+}
+
+class PreferencesStore {
+ current = $state(loadPrefs());
+
+ constructor() {
+ $effect.root(() => {
+ $effect(() => {
+ if (browser) {
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.current));
+ }
+ });
+ });
+ }
+
+ toggleLocation(enabled: boolean) {
+ this.current.locationEnabled = enabled;
+ }
+}
+
+export const prefs = new PreferencesStore();
\ No newline at end of file
diff --git a/src/lib/quick-links.svelte.ts b/src/lib/quick-links.svelte.ts
index e897e22..95d948e 100644
--- a/src/lib/quick-links.svelte.ts
+++ b/src/lib/quick-links.svelte.ts
@@ -11,14 +11,18 @@ const MAX_ENTRIES: number = RETURNED_LENGTH * 4;
class QuickLinksService {
#links = $state([]);
- constructor() {
- if (typeof window !== 'undefined') {
- const saved = localStorage.getItem('ql');
- if (saved) {
- this.#links = JSON.parse(saved);
- }
- }
- }
+constructor() {
+ if (typeof window !== 'undefined') {
+ const saved = localStorage.getItem('ql');
+ if (saved) {
+ try {
+ this.#links = JSON.parse(saved);
+ } catch {
+ this.#links = [];
+ }
+ }
+ }
+ }
get list(): QuickLink[] {
return this.#links.slice(0, RETURNED_LENGTH);
@@ -53,8 +57,17 @@ class QuickLinksService {
this.#links = sorted.slice(0, MAX_ENTRIES);
- localStorage.setItem('ql', JSON.stringify(this.#links));
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('ql', JSON.stringify(this.#links));
+ }
}
+
+ reset() {
+ this.#links = [];
+ if (typeof window !== 'undefined') {
+ localStorage.removeItem('ql');
+ }
+ }
}
export const quickLinks = new QuickLinksService();
diff --git a/src/service-worker.ts b/src/service-worker.ts
index 6b137ee..4873a8a 100644
--- a/src/service-worker.ts
+++ b/src/service-worker.ts
@@ -6,9 +6,17 @@ import { build, files, version } from '$service-worker';
const sw = self as unknown as ServiceWorkerGlobalScope;
const CACHE_NAME = `owlboard-cache-${version}`;
-const ASSETS = [...build, ...files, '/'];
+const ASSETS = [
+ ...build,
+ ...files.filter(file =>
+ !file.endsWith('manifest.json') &&
+ !file.startsWith('/icons/')
+ ),
+ '/',
+];
sw.addEventListener('install', (event) => {
+ sw.skipWaiting();
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
);
@@ -16,11 +24,14 @@ sw.addEventListener('install', (event) => {
sw.addEventListener('activate', (event) => {
event.waitUntil(
- caches.keys().then((keys) => {
- return Promise.all(
- keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
- );
- })
+ Promise.all([
+ caches.keys().then((keys) => {
+ return Promise.all(
+ keys.filter((key) => key !== CACHE_NAME).map((key) => caches.delete(key))
+ );
+ }),
+ sw.clients.claim()
+ ])
);
});
@@ -30,6 +41,12 @@ sw.addEventListener('fetch', (event) => {
const url = new URL(request.url);
+ // Uncached static assets
+ if (url.pathname.endsWith('manifest.json') || url.pathname.includes('/icons/')) {
+ event.respondWith(fetch(request));
+ return;
+ }
+
// 1. Static Assets (Cache-First)
if (ASSETS.includes(url.pathname) || url.pathname.startsWith('/_app/')) {
event.respondWith(
diff --git a/static/icons/apple-touch-icon.png b/static/icons/apple-touch-icon.png
new file mode 100644
index 0000000..3d8e628
Binary files /dev/null and b/static/icons/apple-touch-icon.png differ
diff --git a/static/icons/icon-192.png b/static/icons/icon-192.png
new file mode 100644
index 0000000..3987fd6
Binary files /dev/null and b/static/icons/icon-192.png differ
diff --git a/static/icons/icon-512.png b/static/icons/icon-512.png
new file mode 100644
index 0000000..e750475
Binary files /dev/null and b/static/icons/icon-512.png differ
diff --git a/static/icons/maskable-512.png b/static/icons/maskable-512.png
new file mode 100644
index 0000000..48e9d67
Binary files /dev/null and b/static/icons/maskable-512.png differ
diff --git a/static/manifest.json b/static/manifest.json
new file mode 100644
index 0000000..435f0f5
--- /dev/null
+++ b/static/manifest.json
@@ -0,0 +1,62 @@
+{
+ "name": "OwlBoard",
+ "short_name": "OwlBoard",
+ "description": "Real-time UK rail data and operational tools for staff and enthusiasts.",
+ "id": "/",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "display_override": [
+ "window-controls-overlay",
+ "standalone",
+ "minimal-ui"
+ ],
+ "background_color": "#404c55",
+ "theme_color": "#4fd1d1",
+ "orientation": "portrait",
+ "categories": [
+ "transportation",
+ "utilities"
+ ],
+ "dir": "ltr",
+ "lang": "en-GB",
+ "prefer_related_applications": false,
+ "icons": [
+ {
+ "src": "/icons/icon-192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/icon-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/icons/maskable-512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Find PIS",
+ "short_name": "PIS",
+ "description": "Find PIS Codes fast",
+ "url": "/pis",
+ "icons": [
+ {
+ "src": "/icons/icon-192.png",
+ "sizes": "192x192"
+ }
+ ]
+ }
+ ],
+ "handle_links": "preferred",
+ "launch_handler": {
+ "client_mode": "focus-existing"
+ }
+}
\ No newline at end of file