index.tsx
⎇
Raw
1/* @refresh reload */
2import { createSignal } from "solid-js";
3import { render } from "solid-js/web";
4import "./style.css";
5import "./components/Dialog.css";
6
7import { registerSW } from "virtual:pwa-register";
8import App from "./App";
9import { loadDataFromDB } from "./offline";
10import { toast } from "./utils";
11
12const root = document.getElementById("root");
13if (!(root instanceof HTMLElement)) {
14 throw new Error(
15 "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?",
16 );
17}
18
19navigator?.storage?.persist()?.catch(() => {});
20
21function hasTouch() {
22 return (
23 "ontouchstart" in document.documentElement ||
24 navigator.maxTouchPoints > 0 ||
25 ("msMaxTouchPoints" in navigator &&
26 typeof navigator.msMaxTouchPoints === "number" &&
27 navigator.msMaxTouchPoints > 0)
28 );
29}
30
31if (hasTouch()) {
32 // remove all the :hover stylesheets
33 try {
34 // prevent exception on browsers not supporting DOM styleSheets properly
35 for (const si in document.styleSheets) {
36 const styleSheet = document.styleSheets[si];
37 if (!styleSheet.cssRules) continue;
38
39 for (let ri = styleSheet.cssRules.length - 1; ri >= 0; ri--) {
40 const rule = styleSheet.cssRules[ri];
41 if (!("selectorText" in rule) || typeof rule.selectorText !== "string" || !rule.selectorText) continue;
42
43 if (rule.selectorText.match(":hover")) {
44 styleSheet.deleteRule(ri);
45 }
46 }
47 }
48 } catch {}
49}
50
51//deletes every local trace of the app, so the next load starts from a clean slate.
52//resolves to an error message when something could not be cleared, undefined on success
53async function clearLocalData(): Promise<string | undefined> {
54 localStorage.clear();
55
56 //unregister first: the old service worker would otherwise keep serving the stale precached
57 //assets after the reload, defeating the whole point of the reset
58 if ("serviceWorker" in navigator) {
59 const registrations = await navigator.serviceWorker.getRegistrations().catch(() => []);
60 await Promise.all(registrations.map((registration) => registration.unregister().catch(() => false)));
61 }
62 if ("caches" in window) {
63 const cacheNames = await caches.keys().catch(() => []);
64 //awaited, otherwise the reload races the cleanup and can leave caches behind
65 await Promise.all(cacheNames.map((cacheName) => caches.delete(cacheName).catch(() => false)));
66 }
67
68 //onsuccess alone can hang forever: another open tab blocks the delete, and errors never resolve
69 return await new Promise<string | undefined>((resolve) => {
70 const request = indexedDB.deleteDatabase("music-files");
71 request.onsuccess = () => resolve(undefined);
72 request.onerror = () => resolve("Deleting the local database failed, close all other tabs and try again.");
73 request.onblocked = () =>
74 resolve("Another tab still has this app open, close it and try again to delete the local database.");
75 });
76}
77
78function ResetDialog() {
79 const [error, setError] = createSignal<string>();
80 const [resetting, setResetting] = createSignal(false);
81
82 async function reset() {
83 setResetting(true);
84 setError(undefined);
85 const failure = await clearLocalData().catch((e) => e?.toString() ?? "Unknown error");
86 if (failure) {
87 setError(failure);
88 setResetting(false);
89 return;
90 }
91 location.hash = "";
92 location.reload();
93 }
94
95 return (
96 <div style={{ display: "flex", height: "100%", "align-items": "center" }}>
97 <dialog open>
98 <div>
99 Your local version of this application is not compatible with the newly loaded one, click "Reset" to delete
100 all stored settings, files and cached and refresh the page.
101 {error() && <div style={{ color: "rgb(255, 24, 24)" }}>{error()}</div>}
102 <button style={{ margin: "auto" }} disabled={resetting()} onClick={reset}>
103 {resetting() ? "Resetting..." : "Reset"}
104 </button>
105 </div>
106 </dialog>
107 </div>
108 );
109}
110
111const currentVersion = "1";
112const version = localStorage.getItem("app-version");
113if (version && version !== currentVersion) {
114 //TODO: add a way to at least export playlists before resetting
115 root.innerHTML = "";
116 render(() => <ResetDialog />, root);
117} else {
118 if (!version) {
119 try {
120 localStorage.setItem("app-version", currentVersion);
121 } catch {}
122 }
123
124 //the auto-injected registration only ever checks on a cold navigation, which for an installed
125 //player that stays open for days means new deploys are never picked up
126 const updateCheckInterval = 60 * 60 * 1000;
127 registerSW({
128 immediate: true,
129 onRegisteredSW: (_swScriptUrl, registration) => {
130 if (!registration) return;
131 setInterval(() => {
132 if (navigator.onLine) registration.update().catch(() => {});
133 }, updateCheckInterval);
134 },
135 //replaces the default automatic reload, which would cut off playback mid-track
136 onNeedReload: () => toast("A new version was installed, reload the page to use it", "info"),
137 });
138
139 loadDataFromDB()
140 .then(() => {
141 root.innerHTML = "";
142 render(() => <App />, root);
143 })
144 .catch((e) => {
145 root.innerHTML = e.toString();
146 console.error(e);
147 });
148}
149