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