A music player that connects to your cloud/distributed storage.
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: split-view facet

+459 -2
+9 -1
src/common/loader.js
··· 213 213 }), 214 214 ); 215 215 216 - return value.html ?? ""; 216 + if (value.html) { 217 + return value.html; 218 + } 219 + 220 + if (value.uri) { 221 + return loadURI(value.uri); 222 + } 223 + 224 + return ""; 217 225 } 218 226 219 227 /**
+4
src/facets/index.vto
··· 26 26 title: "Tools / V3.x Import" 27 27 desc: > 28 28 Import data from Diffuse v3. 29 + - url: "facets/tools/split-view.html" 30 + title: "Tools / Split View" 31 + desc: > 32 + Arrange multiple facets side-by-side in a resizable split-panel layout. 29 33 - url: "themes/webamp/browser/facet.html" 30 34 title: "Webamp / Browser" 31 35 desc: >
+127
src/facets/tools/split-view.html
··· 1 + <link rel="stylesheet" href="vendor/@awesome.me/webawesome/styles/themes/default.css" /> 2 + <link rel="stylesheet" href="vendor/@awesome.me/webawesome/styles/utilities/layout.css" /> 3 + <link rel="stylesheet" href="vendor/@awesome.me/webawesome/styles/utilities/gap.css" /> 4 + 5 + <div class="wa-theme-shoelace"> 6 + <main id="layout"></main> 7 + 8 + <div id="edit-bar"> 9 + <wa-button 10 + id="edit-toggle" 11 + appearance="filled" 12 + variant="neutral" 13 + size="small" 14 + pill 15 + aria-label="Edit layout" 16 + > 17 + <wa-icon id="edit-icon" name="border-all"></wa-icon> 18 + </wa-button> 19 + </div> 20 + 21 + <wa-dialog id="facet-picker" label="Choose a facet" style="--width: 360px"> 22 + <div class="wa-stack wa-gap-s"> 23 + <wa-select id="facet-select" placeholder="Built-in facets…"> 24 + <wa-option value="themes/blur/artwork-controller/facet.html" 25 + >Blur / Artwork controller</wa-option 26 + > 27 + <wa-option value="facets/tools/auto-queue.html">Tools / Automatic Queue</wa-option> 28 + <wa-option value="facets/tools/split-view.html">Tools / Split View</wa-option> 29 + <wa-option value="facets/tools/v3-import.html">Tools / V3.x Import</wa-option> 30 + <wa-option value="themes/webamp/browser/facet.html">Webamp / Browser</wa-option> 31 + <wa-option value="themes/webamp/configurators/input/facet.html" 32 + >Webamp / Input Configurator</wa-option 33 + > 34 + <wa-option value="themes/webamp/configurators/output/facet.html" 35 + >Webamp / Output Configurator</wa-option 36 + > 37 + </wa-select> 38 + <wa-input 39 + id="custom-path" 40 + placeholder="facets/tools/auto-queue.html" 41 + style="flex: 1" 42 + ></wa-input> 43 + <wa-button id="custom-confirm" variant="neutral" appearance="filled" pill>Load</wa-button> 44 + </div> 45 + </wa-dialog> 46 + </div> 47 + 48 + <style> 49 + body { 50 + margin: 0; 51 + height: 100dvh; 52 + overflow: hidden; 53 + } 54 + 55 + #layout, 56 + #layout > * { 57 + height: 100%; 58 + } 59 + 60 + wa-split-panel { 61 + height: 100%; 62 + } 63 + 64 + [slot="start"], 65 + [slot="end"] { 66 + height: 100%; 67 + } 68 + 69 + .pane { 70 + position: relative; 71 + height: 100%; 72 + display: flex; 73 + flex-direction: column; 74 + } 75 + 76 + .pane iframe { 77 + flex: 1; 78 + border: none; 79 + width: 100%; 80 + } 81 + 82 + .dragging iframe { 83 + pointer-events: none; 84 + } 85 + 86 + .pane-overlay { 87 + position: absolute; 88 + inset: 0; 89 + display: none; 90 + flex-direction: column; 91 + align-items: center; 92 + justify-content: center; 93 + gap: var(--wa-space-xs); 94 + background: color-mix(in srgb, var(--wa-color-surface-default) 85%, transparent); 95 + backdrop-filter: blur(4px); 96 + z-index: 10; 97 + padding: var(--wa-space-m); 98 + } 99 + 100 + .pane--empty .pane-overlay { 101 + display: flex; 102 + } 103 + 104 + .edit-mode .pane:not(.pane--empty) .pane-overlay { 105 + display: flex; 106 + } 107 + 108 + .pane-name { 109 + font-size: var(--wa-font-size-xs); 110 + color: var(--wa-color-text-quiet); 111 + margin-bottom: var(--wa-space-2xs); 112 + text-align: center; 113 + } 114 + 115 + #edit-bar { 116 + position: fixed; 117 + top: var(--wa-space-xs); 118 + left: var(--wa-space-xs); 119 + z-index: 100; 120 + } 121 + 122 + #facet-select { 123 + width: 100%; 124 + } 125 + </style> 126 + 127 + <script type="module" src="./split-view.inline.js"></script>
+317
src/facets/tools/split-view.inline.js
··· 1 + import "@awesome.me/webawesome/dist/components/split-panel/split-panel.js"; 2 + import "@awesome.me/webawesome/dist/components/dialog/dialog.js"; 3 + import "@awesome.me/webawesome/dist/components/button/button.js"; 4 + import "@awesome.me/webawesome/dist/components/input/input.js"; 5 + import "@awesome.me/webawesome/dist/components/icon/icon.js"; 6 + import "@awesome.me/webawesome/dist/components/select/select.js"; 7 + import "@awesome.me/webawesome/dist/components/option/option.js"; 8 + 9 + /** 10 + * @import { default as WaSplitPanel } from "@awesome.me/webawesome/dist/components/split-panel/split-panel.js" 11 + * @import { default as WaDialog } from "@awesome.me/webawesome/dist/components/dialog/dialog.js" 12 + * @import { default as WaInput } from "@awesome.me/webawesome/dist/components/input/input.js" 13 + * @import { default as WaIcon } from "@awesome.me/webawesome/dist/components/icon/icon.js" 14 + * @import { default as WaSelect } from "@awesome.me/webawesome/dist/components/select/select.js" 15 + */ 16 + 17 + /** 18 + * @typedef {{ type: "pane", facet: string | null }} PaneNode 19 + * @typedef {{ type: "split", orientation: "horizontal" | "vertical", position: number, start: Node, end: Node }} SplitNode 20 + * @typedef {PaneNode | SplitNode} Node 21 + */ 22 + 23 + const STORAGE_KEY = "diffuse:split-view:layout"; 24 + 25 + // ─── State ─────────────────────────────────────────────────────────────────── 26 + 27 + /** @type {Node} */ 28 + let state = loadState(); 29 + 30 + let editMode = false; 31 + 32 + /** @type {string | null} */ 33 + let pendingPaneId = null; 34 + 35 + /** @returns {Node} */ 36 + function loadState() { 37 + try { 38 + return /** @type {Node} */ (JSON.parse( 39 + localStorage.getItem(STORAGE_KEY) ?? "null", 40 + )) ?? defaultState(); 41 + } catch { 42 + return defaultState(); 43 + } 44 + } 45 + 46 + function saveState() { 47 + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); 48 + } 49 + 50 + /** @returns {PaneNode} */ 51 + function defaultState() { 52 + return { type: "pane", facet: null }; 53 + } 54 + 55 + // ─── Tree helpers ───────────────────────────────────────────────────────────── 56 + // Nodes are identified by a dot-separated path: "root", "root.start", "root.end.start" 57 + 58 + /** 59 + * @param {string} path 60 + * @returns {Node} 61 + */ 62 + function getNode(path) { 63 + if (path === "root") return state; 64 + const parts = path.replace(/^root\./, "").split("."); 65 + /** @type {any} */ 66 + let node = state; 67 + for (const part of parts) node = node[part]; 68 + return /** @type {Node} */ (node); 69 + } 70 + 71 + /** 72 + * @param {string} path 73 + * @param {Node} newNode 74 + */ 75 + function replaceNode(path, newNode) { 76 + if (path === "root") { 77 + state = newNode; 78 + return; 79 + } 80 + const parts = path.split("."); 81 + const parentPath = parts.slice(0, -1).join("."); 82 + const childKey = parts[parts.length - 1]; 83 + const parent = /** @type {any} */ (getNode(parentPath)); 84 + parent[childKey] = newNode; 85 + } 86 + 87 + // ─── Mutations ──────────────────────────────────────────────────────────────── 88 + 89 + /** 90 + * @param {string} nodePath 91 + * @param {"horizontal" | "vertical"} orientation 92 + */ 93 + function splitPane(nodePath, orientation) { 94 + const existing = getNode(nodePath); 95 + replaceNode(nodePath, { 96 + type: "split", 97 + orientation, 98 + position: 50, 99 + start: existing, 100 + end: { type: "pane", facet: null }, 101 + }); 102 + saveState(); 103 + render(); 104 + } 105 + 106 + /** @param {string} nodePath */ 107 + function removePane(nodePath) { 108 + if (nodePath === "root") return; 109 + const parts = nodePath.split("."); 110 + const parentPath = parts.slice(0, -1).join("."); 111 + const childKey = parts[parts.length - 1]; 112 + const siblingKey = childKey === "start" ? "end" : "start"; 113 + const parent = /** @type {any} */ (getNode(parentPath)); 114 + const sibling = /** @type {Node} */ (parent[siblingKey]); 115 + replaceNode(parentPath, sibling); 116 + saveState(); 117 + render(); 118 + } 119 + 120 + /** 121 + * @param {string} nodePath 122 + * @param {string} facetPath 123 + */ 124 + function setFacet(nodePath, facetPath) { 125 + const pane = /** @type {PaneNode} */ (getNode(nodePath)); 126 + pane.facet = facetPath; 127 + saveState(); 128 + render(); 129 + } 130 + 131 + // ─── Rendering ──────────────────────────────────────────────────────────────── 132 + 133 + const layout = /** @type {HTMLElement} */ (document.querySelector("#layout")); 134 + 135 + function render() { 136 + layout.innerHTML = ""; 137 + layout.appendChild(renderNode(state, "root", true)); 138 + layout.classList.toggle("edit-mode", editMode); 139 + } 140 + 141 + /** 142 + * @param {Node} node 143 + * @param {string} path 144 + * @param {boolean} isRoot 145 + * @returns {HTMLElement} 146 + */ 147 + function renderNode(node, path, isRoot) { 148 + if (node.type === "split") { 149 + const panel = 150 + /** @type {WaSplitPanel} */ (document.createElement("wa-split-panel")); 151 + panel.position = node.position; 152 + if (node.orientation === "vertical") panel.orientation = "vertical"; 153 + 154 + const startSlot = document.createElement("div"); 155 + startSlot.slot = "start"; 156 + startSlot.appendChild(renderNode(node.start, path + ".start", false)); 157 + 158 + const endSlot = document.createElement("div"); 159 + endSlot.slot = "end"; 160 + endSlot.appendChild(renderNode(node.end, path + ".end", false)); 161 + 162 + panel.appendChild(startSlot); 163 + panel.appendChild(endSlot); 164 + 165 + panel.addEventListener("wa-reposition", () => { 166 + const splitNode = /** @type {SplitNode} */ (getNode(path)); 167 + splitNode.position = panel.position; 168 + saveState(); 169 + }); 170 + 171 + return panel; 172 + } 173 + 174 + // Pane 175 + const pane = document.createElement("div"); 176 + pane.className = "pane" + (node.facet ? "" : " pane--empty"); 177 + 178 + if (node.facet) { 179 + const iframe = document.createElement("iframe"); 180 + const uri = node.facet.includes("://") 181 + ? node.facet 182 + : `diffuse://${node.facet}`; 183 + iframe.src = "facets/l/?uri=" + encodeURIComponent(uri); 184 + iframe.allow = "autoplay"; 185 + pane.appendChild(iframe); 186 + } 187 + 188 + const overlay = document.createElement("div"); 189 + overlay.className = "pane-overlay"; 190 + 191 + if (node.facet) { 192 + const title = 193 + document.querySelector(`#facet-select wa-option[value="${node.facet}"]`) 194 + ?.textContent?.trim() ?? node.facet; 195 + const label = document.createElement("div"); 196 + label.className = "pane-name"; 197 + label.style.fontWeight = "700"; 198 + label.textContent = title; 199 + overlay.appendChild(label); 200 + } 201 + 202 + overlay.appendChild( 203 + makeWaButton( 204 + node.facet ? "Change facet" : "+ Add facet", 205 + "neutral", 206 + "filled", 207 + () => openPicker(path), 208 + ), 209 + ); 210 + overlay.appendChild( 211 + makeWaButton( 212 + "Split left / right", 213 + "neutral", 214 + "outlined", 215 + () => splitPane(path, "horizontal"), 216 + ), 217 + ); 218 + overlay.appendChild( 219 + makeWaButton( 220 + "Split top / bottom", 221 + "neutral", 222 + "outlined", 223 + () => splitPane(path, "vertical"), 224 + ), 225 + ); 226 + 227 + if (!isRoot) { 228 + overlay.appendChild( 229 + makeWaButton("Remove", "danger", "outlined", () => removePane(path)), 230 + ); 231 + } 232 + 233 + pane.appendChild(overlay); 234 + return pane; 235 + } 236 + 237 + /** 238 + * @param {string} text 239 + * @param {string} variant 240 + * @param {string} appearance 241 + * @param {() => void} onClick 242 + * @returns {HTMLElement} 243 + */ 244 + function makeWaButton(text, variant, appearance, onClick) { 245 + const btn = document.createElement("wa-button"); 246 + btn.setAttribute("variant", variant); 247 + btn.setAttribute("appearance", appearance); 248 + btn.setAttribute("size", "small"); 249 + btn.style.width = "100%"; 250 + btn.textContent = text; 251 + btn.addEventListener("click", (e) => { 252 + e.stopPropagation(); 253 + onClick(); 254 + }); 255 + return btn; 256 + } 257 + 258 + // ─── Divider drag: disable iframe pointer events while dragging ─────────────── 259 + 260 + document.addEventListener("mousedown", (e) => { 261 + const isDivider = e.composedPath().some( 262 + (el) => el instanceof Element && el.getAttribute("part") === "divider", 263 + ); 264 + if (isDivider) layout.classList.add("dragging"); 265 + }, { capture: true }); 266 + 267 + document.addEventListener("mouseup", () => { 268 + layout.classList.remove("dragging"); 269 + }); 270 + 271 + // ─── Edit mode ──────────────────────────────────────────────────────────────── 272 + 273 + const editToggle = 274 + /** @type {HTMLElement} */ (document.querySelector("#edit-toggle")); 275 + const editIcon = /** @type {WaIcon} */ (document.querySelector("#edit-icon")); 276 + 277 + editToggle.addEventListener("click", () => { 278 + editMode = !editMode; 279 + editToggle.setAttribute("aria-label", editMode ? "Done" : "Edit layout"); 280 + editIcon.name = editMode ? "xmark" : "border-all"; 281 + layout.classList.toggle("edit-mode", editMode); 282 + }); 283 + 284 + // ─── Facet picker ───────────────────────────────────────────────────────────── 285 + 286 + const pickerDialog = 287 + /** @type {WaDialog} */ (document.querySelector("#facet-picker")); 288 + const facetSelect = 289 + /** @type {WaSelect} */ (document.querySelector("#facet-select")); 290 + const customPath = 291 + /** @type {WaInput} */ (document.querySelector("#custom-path")); 292 + const customConfirm = 293 + /** @type {HTMLElement} */ (document.querySelector("#custom-confirm")); 294 + 295 + customConfirm.addEventListener("click", () => { 296 + const val = customPath.value?.trim() || /** @type {string} */ 297 + (facetSelect.value) || ""; 298 + if (val && pendingPaneId !== null) setFacet(pendingPaneId, val); 299 + pendingPaneId = null; 300 + pickerDialog.open = false; 301 + }); 302 + 303 + pickerDialog.addEventListener("wa-hide", (e) => { 304 + if (e.target === pickerDialog) pendingPaneId = null; 305 + }); 306 + 307 + /** @param {string} nodePath */ 308 + function openPicker(nodePath) { 309 + pendingPaneId = nodePath; 310 + facetSelect.value = null; 311 + customPath.value = ""; 312 + pickerDialog.open = true; 313 + } 314 + 315 + // ─── Init ───────────────────────────────────────────────────────────────────── 316 + 317 + render();
+2 -1
src/themes/webamp/common/ui.js
··· 7 7 const tr = event.target.tagName === "TR" 8 8 ? event.target 9 9 : event.target.closest("tr"); 10 + 10 11 if (!tr) return; 12 + if (tr.closest("thead")) return; 11 13 12 14 tr.parentElement?.querySelector("tr.highlighted")?.classList.remove( 13 15 "highlighted", ··· 15 17 16 18 tr.classList.add("highlighted"); 17 19 } 18 -