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

Configure Feed

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

fix: feature detection for local file input

+100 -77
+77 -72
src/components/input/local/common.js
··· 14 14 //////////////////////////////////////////// 15 15 16 16 /** 17 - * @param {string} uriString 18 - * @returns {{ tid: string; path: string } | undefined} 19 - */ 20 - export function parseURI(uriString) { 21 - try { 22 - const url = new URL(uriString); 23 - if (url.protocol !== `${SCHEME}:`) return undefined; 24 - if (!url.host) return undefined; 25 - 26 - return { 27 - tid: url.host, 28 - path: safeDecodeURIComponent(url.pathname), 29 - }; 30 - } catch { 31 - return undefined; 32 - } 33 - } 34 - 35 - /** 36 17 * @param {string} tid 37 18 * @param {string} [path] 38 19 */ ··· 45 26 } 46 27 47 28 /** 29 + * @param {FileSystemDirectoryHandle} dirHandle 30 + * @param {string} [basePath] 31 + * @returns {Promise<string[]>} 32 + */ 33 + export async function enumerateAudioFiles(dirHandle, basePath = "/") { 34 + const results = []; 35 + 36 + for await (const [name, handle] of /** @type {any} */ (dirHandle).entries()) { 37 + const entryPath = basePath + name; 38 + 39 + if (handle.kind === "directory") { 40 + const sub = await enumerateAudioFiles( 41 + /** @type {FileSystemDirectoryHandle} */ (handle), 42 + entryPath + "/", 43 + ); 44 + results.push(...sub); 45 + } else if (isAudioFile(name)) { 46 + results.push(entryPath); 47 + } 48 + } 49 + 50 + return results; 51 + } 52 + 53 + /** 54 + * @param {FileSystemHandle} handle 55 + * @param {string} path 56 + * @returns {Promise<FileSystemFileHandle>} 57 + */ 58 + export async function getHandleFile(handle, path) { 59 + if (handle.kind === "file") { 60 + return /** @type {FileSystemFileHandle} */ (handle); 61 + } 62 + 63 + const parts = path.replace(/^\//, "").split("/").filter(Boolean); 64 + let current = /** @type {FileSystemDirectoryHandle} */ (handle); 65 + 66 + for (const part of parts.slice(0, -1)) { 67 + current = await current.getDirectoryHandle(part); 68 + } 69 + 70 + return current.getFileHandle(/** @type {string} */ (parts.at(-1))); 71 + } 72 + 73 + /** 48 74 * @param {Track[]} tracks 49 75 * @returns {Record<string, { tid: string; tracks: Track[] }>} 50 76 */ ··· 90 116 return acc; 91 117 } 92 118 93 - /** 94 - * @param {Track[]} tracks 95 - * @returns {Record<string, string>} 96 - */ 97 - export function tidsFromTracks(tracks) { 98 - /** @type {Record<string, string>} */ 99 - const acc = {}; 100 - 101 - tracks.forEach((track) => { 102 - const parsed = parseURI(track.uri); 103 - if (!parsed) return; 104 - acc[parsed.tid] = parsed.tid; 105 - }); 106 - 107 - return acc; 119 + export function isSupported() { 120 + return typeof (/** @type {any} */ (globalThis).showDirectoryPicker) !== 121 + "undefined"; 108 122 } 109 123 110 124 /** ··· 116 130 } 117 131 118 132 /** 119 - * @param {Record<string, FileSystemHandle>} handles 133 + * @param {string} uriString 134 + * @returns {{ tid: string; path: string } | undefined} 120 135 */ 121 - export async function saveHandles(handles) { 122 - await IDB.set(IDB_HANDLES, handles); 136 + export function parseURI(uriString) { 137 + try { 138 + const url = new URL(uriString); 139 + if (url.protocol !== `${SCHEME}:`) return undefined; 140 + if (!url.host) return undefined; 141 + 142 + return { 143 + tid: url.host, 144 + path: safeDecodeURIComponent(url.pathname), 145 + }; 146 + } catch { 147 + return undefined; 148 + } 123 149 } 124 150 125 151 /** 126 - * @param {FileSystemHandle} handle 127 - * @param {string} path 128 - * @returns {Promise<FileSystemFileHandle>} 152 + * @param {Record<string, FileSystemHandle>} handles 129 153 */ 130 - export async function getHandleFile(handle, path) { 131 - if (handle.kind === "file") { 132 - return /** @type {FileSystemFileHandle} */ (handle); 133 - } 134 - 135 - const parts = path.replace(/^\//, "").split("/").filter(Boolean); 136 - let current = /** @type {FileSystemDirectoryHandle} */ (handle); 137 - 138 - for (const part of parts.slice(0, -1)) { 139 - current = await current.getDirectoryHandle(part); 140 - } 141 - 142 - return current.getFileHandle(/** @type {string} */ (parts.at(-1))); 154 + export async function saveHandles(handles) { 155 + await IDB.set(IDB_HANDLES, handles); 143 156 } 144 157 145 158 /** 146 - * @param {FileSystemDirectoryHandle} dirHandle 147 - * @param {string} [basePath] 148 - * @returns {Promise<string[]>} 159 + * @param {Track[]} tracks 160 + * @returns {Record<string, string>} 149 161 */ 150 - export async function enumerateAudioFiles(dirHandle, basePath = "/") { 151 - const results = []; 162 + export function tidsFromTracks(tracks) { 163 + /** @type {Record<string, string>} */ 164 + const acc = {}; 152 165 153 - for await (const [name, handle] of /** @type {any} */ (dirHandle).entries()) { 154 - const entryPath = basePath + name; 155 - 156 - if (handle.kind === "directory") { 157 - const sub = await enumerateAudioFiles( 158 - /** @type {FileSystemDirectoryHandle} */ (handle), 159 - entryPath + "/", 160 - ); 161 - results.push(...sub); 162 - } else if (isAudioFile(name)) { 163 - results.push(entryPath); 164 - } 165 - } 166 + tracks.forEach((track) => { 167 + const parsed = parseURI(track.uri); 168 + if (!parsed) return; 169 + acc[parsed.tid] = parsed.tid; 170 + }); 166 171 167 - return results; 172 + return acc; 168 173 }
+2 -1
src/components/input/local/worker.js
··· 7 7 getHandleFile, 8 8 groupTracksByTid, 9 9 groupUrisByTid, 10 + isSupported, 10 11 loadHandles, 11 12 parseURI, 12 13 saveHandles, ··· 26 27 * @type {Actions['consult']} 27 28 */ 28 29 export async function consult(fileUriOrScheme) { 29 - if (typeof FileSystemHandle === "undefined") { 30 + if (!isSupported()) { 30 31 return { supported: false, reason: "No browser support" }; 31 32 } 32 33
+21 -4
src/themes/webamp/configurators/input/element.js
··· 10 10 11 11 import { buildURI as buildOpenSubsonicURI } from "~/components/input/opensubsonic/common.js"; 12 12 import { buildURI as buildS3URI } from "~/components/input/s3/common.js"; 13 + import { isSupported as supportsLocalFsAccess } from "~/components/input/local/common.js"; 13 14 14 15 import { SCHEME as HTTPS_SCHEME } from "~/components/input/https/constants.js"; 15 16 import { SCHEME as LOCAL_SCHEME } from "~/components/input/local/constants.js"; ··· 529 530 </fieldset> 530 531 531 532 <fieldset> 532 - <p class="button-row"> 533 - <button @click="${this.#addLocalDirectory}">Add directory</button> 534 - <button @click="${this.#addLocalFiles}">Add files</button> 535 - </p> 533 + ${supportsLocalFsAccess() 534 + ? html` 535 + <p class="button-row"> 536 + <button @click="${this.#addLocalDirectory}"> 537 + Add directory 538 + </button> 539 + <button @click="${this.#addLocalFiles}">Add files</button> 540 + </p> 541 + ` 542 + : html` 543 + <p class="with-icon with-icon--large"> 544 + <img 545 + src="images/icons/windows_98/msg_warning-0.png" 546 + width="24" 547 + /> 548 + Your browser does not support the File System Access API.<br /> 549 + Use a Chromium-based browser to add local files. 550 + <!-- TODO: Add an alternative facet where you can just drag & drop your local files. --> 551 + </p> 552 + `} 536 553 </fieldset> 537 554 </div> 538 555 `;