A music player that connects to your cloud/distributed storage.
at v4 161 lines 4.6 kB view raw
1import * as Build from "./build.js"; 2import * as Dashboard from "./dashboard.js"; 3import * as Grid from "./grid.js"; 4import * as Guide from "./guide.js"; 5import * as Nav from "./nav.js"; 6 7/** Base pathname of the app (e.g. "/" at root, "/diffuse/" in a subdirectory). */ 8const BASE_PATHNAME = new URL(document.baseURI).pathname; 9 10/** 11 * Strips the app's base path prefix from an absolute pathname, 12 * returning a root-relative path like "/build". 13 * 14 * @param {string} pathname 15 */ 16function relativePathname(pathname) { 17 const stripped = pathname.replace(/\/$/, ""); 18 const base = BASE_PATHNAME.replace(/\/$/, ""); 19 return base.length > 0 && stripped.startsWith(base) 20 ? stripped.slice(base.length) 21 : stripped; 22} 23 24/** 25 * @param {URL} url 26 */ 27async function initJsBasedOnPage(url) { 28 const path = relativePathname(url.pathname); 29 30 Nav.update(); 31 Nav.watchResize(); 32 33 Grid.setupFilter(); 34 Grid.insertToggleButtons(); 35 await Grid.monitorToggleButtonStates(); 36 await Grid.setupOutputIndicator(); 37 38 switch (path) { 39 case "/build": 40 Build.renderEditor(); 41 Build.handleBuildFormSubmit(); 42 Build.listenForExamplesEdit(); 43 await Build.editFacetFromURL(); 44 break; 45 case "/dashboard": 46 await Dashboard.renderList(); 47 break; 48 case "/guide": 49 Guide.setupSampleButton(); 50 break; 51 default: 52 break; 53 } 54} 55 56initJsBasedOnPage(new URL(location.href)); 57 58// Partial page updates for kitchen navigation using the Navigation API. 59// Intercepts nav link clicks, fetches the new page, and swaps <main> content 60// instead of doing a full page load. 61 62if ("navigation" in globalThis) { 63 /** @type {any} */ (globalThis).navigation.addEventListener( 64 "navigate", 65 navigateHandler, 66 ); 67} 68 69/** @param {any} event */ 70function navigateHandler(event) { 71 if (!event.canIntercept) return; 72 73 const url = new URL(event.destination.url); 74 if (url.origin !== location.origin) return; 75 76 // Only intercept paths one level deep 77 const relative = relativePathname(url.pathname); 78 const parts = relative.split("/").filter(Boolean); 79 if (parts.length === 0) return; 80 if (parts.length > 2) return; 81 82 // Skip the loader page 83 if (parts[0] === "l") return; 84 if (parts.includes("chronicle")) return; 85 86 event.intercept({ 87 scroll: "manual", 88 async handler() { 89 const navLinks = /** @type {HTMLAnchorElement[]} */ ([ 90 ...document.querySelectorAll("#diffuse-nav a, #nav-overflow-menu a"), 91 ]); 92 const stripSlash = (/** @type {string} */ p) => p.replace(/^\//, ""); 93 const navLink = navLinks.find( 94 (a) => 95 stripSlash(new URL(a.href).pathname) === stripSlash(url.pathname), 96 ); 97 98 const icon = navLink?.querySelector("i"); 99 const originalIconClass = icon?.className; 100 let addedSpinner = /** @type {HTMLElement | undefined} */ (undefined); 101 102 const loadingTimer = navLink 103 ? setTimeout(() => { 104 if (icon) { 105 icon.className = "ph-bold ph-spinner animate-spin"; 106 } else { 107 addedSpinner = document.createElement("i"); 108 addedSpinner.className = "ph-bold ph-spinner animate-spin"; 109 const span = navLink.querySelector("span"); 110 (span ?? navLink).prepend(addedSpinner); 111 } 112 }, 250) 113 : undefined; 114 115 let html; 116 117 try { 118 const response = await fetch(url); 119 if (!response.ok) throw new Error(`${response.status}`); 120 html = await response.text(); 121 } catch { 122 clearTimeout(loadingTimer); 123 if (icon && originalIconClass !== undefined) icon.className = originalIconClass; 124 addedSpinner?.remove(); 125 location.href = url.href; 126 return; 127 } finally { 128 clearTimeout(loadingTimer); 129 if (icon && originalIconClass !== undefined) icon.className = originalIconClass; 130 addedSpinner?.remove(); 131 } 132 133 const parser = new DOMParser(); 134 const doc = parser.parseFromString(html, "text/html"); 135 136 const newMain = doc.querySelector("main"); 137 const currentMain = document.querySelector("main"); 138 139 if (!newMain || !currentMain) { 140 location.href = url.href; 141 return; 142 } 143 144 document.title = doc.title; 145 146 // Replace <main> content 147 const range = document.createRange(); 148 range.selectNode(currentMain); 149 const documentFragment = range.createContextualFragment( 150 newMain.innerHTML ?? "", 151 ); 152 153 currentMain.innerHTML = ""; 154 currentMain.append(documentFragment); 155 156 initJsBasedOnPage(url); 157 158 window.scrollTo({ top: 0, behavior: "instant" }); 159 }, 160 }); 161}