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.

feat: data export/import tool

+277 -3
+7 -3
src/facets/index.vto
··· 22 22 title: "Tools / Automatic Queue" 23 23 desc: > 24 24 Everything you need to automatically put tracks into the queue. 25 - - url: "facets/tools/v3-import.html" 26 - title: "Tools / V3.x Import" 25 + - url: "facets/tools/export-import.html" 26 + title: "Tools / Export & Import" 27 27 desc: > 28 - Import data from Diffuse v3. 28 + Export all data as a JSON snapshot, or restore from a previously exported file. 29 29 - url: "facets/tools/split-view.html" 30 30 title: "Tools / Split View" 31 31 desc: > 32 32 Arrange multiple facets side-by-side in a resizable split-panel layout. 33 + - url: "facets/tools/v3-import.html" 34 + title: "Tools / V3.x Import" 35 + desc: > 36 + Import data from Diffuse v3. 33 37 - url: "themes/webamp/browser/facet.html" 34 38 title: "Webamp / Browser" 35 39 desc: >
+96
src/facets/tools/export-import.html
··· 1 + <main> 2 + <div class="panel"> 3 + <p> 4 + Export all Diffuse data to a JSON snapshot, or restore from a previously 5 + exported file. Importing replaces all data in that category. 6 + </p> 7 + <hr /> 8 + <div class="row"> 9 + <button id="export" disabled> 10 + <span class="with-icon"><i class="ph-bold ph-download-simple"></i> Export all data</span> 11 + </button> 12 + </div> 13 + <hr /> 14 + <div class="row"> 15 + <label class="with-icon" for="file"> 16 + <i class="ph-bold ph-upload"></i> 17 + <input id="file" type="file" accept=".json,application/json" /> 18 + </label> 19 + </div> 20 + <hr /> 21 + <div class="row"> 22 + <button id="import-tracks" disabled> 23 + <span class="with-icon"><i class="ph-bold ph-music-note"></i> Import tracks</span> 24 + </button> 25 + </div> 26 + <div class="row"> 27 + <button id="import-playlists" disabled> 28 + <span class="with-icon"><i class="ph-bold ph-playlist"></i> Import playlists</span> 29 + </button> 30 + </div> 31 + <div class="row"> 32 + <button id="import-facets" disabled> 33 + <span class="with-icon"><i class="ph-bold ph-squares-four"></i> Import facets</span> 34 + </button> 35 + </div> 36 + <div class="row"> 37 + <button id="import-themes" disabled> 38 + <span class="with-icon"><i class="ph-bold ph-palette"></i> Import themes</span> 39 + </button> 40 + </div> 41 + <div id="status" class="status" hidden></div> 42 + </div> 43 + </main> 44 + 45 + <style> 46 + @import "./styles/base.css"; 47 + @import "./styles/wireframe/ui.css"; 48 + @import "./vendor/@phosphor-icons/bold/style.css"; 49 + 50 + body { 51 + display: flex; 52 + align-items: center; 53 + justify-content: center; 54 + height: 100dvh; 55 + } 56 + 57 + main { 58 + padding: var(--space-md); 59 + } 60 + 61 + p { 62 + line-height: var(--leading-relaxed); 63 + max-width: 42ch; 64 + } 65 + 66 + label { 67 + flex: 1; 68 + } 69 + 70 + input[type="file"] { 71 + flex: 1; 72 + } 73 + 74 + .status { 75 + margin-top: var(--space-sm); 76 + font-size: var(--text-sm); 77 + } 78 + 79 + .status--success { 80 + color: var(--accent); 81 + 82 + @media (prefers-color-scheme: dark) { 83 + color: var(--accent-twist-4); 84 + } 85 + } 86 + 87 + .status--error { 88 + color: var(--accent-twist-5); 89 + 90 + @media (prefers-color-scheme: dark) { 91 + color: var(--accent-twist-3); 92 + } 93 + } 94 + </style> 95 + 96 + <script type="module" src="./export-import.inline.js"></script>
+174
src/facets/tools/export-import.inline.js
··· 1 + import * as Output from "~/common/output.js"; 2 + import foundation from "~/common/facets/foundation.js"; 3 + import { effect } from "~/common/signal.js"; 4 + 5 + // Setup 6 + const output = foundation.orchestrator.output(); 7 + 8 + // Elements 9 + const exportBtn = 10 + /** @type {HTMLButtonElement} */ (document.querySelector("#export")); 11 + const fileInput = 12 + /** @type {HTMLInputElement} */ (document.querySelector("#file")); 13 + const importTracksBtn = 14 + /** @type {HTMLButtonElement} */ (document.querySelector("#import-tracks")); 15 + const importPlaylistsBtn = 16 + /** @type {HTMLButtonElement} */ (document.querySelector( 17 + "#import-playlists", 18 + )); 19 + const importFacetsBtn = 20 + /** @type {HTMLButtonElement} */ (document.querySelector("#import-facets")); 21 + const importThemesBtn = 22 + /** @type {HTMLButtonElement} */ (document.querySelector("#import-themes")); 23 + const statusEl = /** @type {HTMLElement} */ (document.querySelector("#status")); 24 + 25 + /** @type {Record<string, any> | null} */ 26 + let json = null; 27 + 28 + /** 29 + * Show a status message. 30 + * @param {string} message 31 + * @param {"success" | "error"} type 32 + */ 33 + function showStatus(message, type) { 34 + statusEl.textContent = message; 35 + statusEl.className = `status status--${type}`; 36 + statusEl.hidden = false; 37 + } 38 + 39 + // Enable export button once output is ready 40 + effect(() => { 41 + exportBtn.disabled = !output.ready(); 42 + }); 43 + 44 + // Export all data as a JSON snapshot 45 + exportBtn.onclick = async () => { 46 + await Output.waitUntilLoaded(output.facets); 47 + await Output.waitUntilLoaded(output.playlistItems); 48 + await Output.waitUntilLoaded(output.themes); 49 + await Output.waitUntilLoaded(output.tracks); 50 + 51 + const data = { 52 + exportedAt: new Date().toISOString(), 53 + tracks: output.tracks.collection() ?? [], 54 + playlistItems: output.playlistItems.collection() ?? [], 55 + facets: output.facets.collection() ?? [], 56 + themes: output.themes.collection() ?? [], 57 + }; 58 + 59 + const blob = new Blob([JSON.stringify(data, null, 2)], { 60 + type: "application/json", 61 + }); 62 + 63 + const url = URL.createObjectURL(blob); 64 + const a = document.createElement("a"); 65 + a.href = url; 66 + a.download = `diffuse-${new Date().toISOString().slice(0, 10)}.json`; 67 + document.body.append(a); 68 + a.click(); 69 + a.remove(); 70 + 71 + URL.revokeObjectURL(url); 72 + }; 73 + 74 + // Parse file on selection 75 + fileInput.onchange = async () => { 76 + const file = fileInput.files?.[0]; 77 + 78 + json = null; 79 + statusEl.hidden = true; 80 + importTracksBtn.disabled = true; 81 + importPlaylistsBtn.disabled = true; 82 + importFacetsBtn.disabled = true; 83 + importThemesBtn.disabled = true; 84 + 85 + if (!file) return; 86 + 87 + try { 88 + json = JSON.parse(await file.text()); 89 + } catch (err) { 90 + console.error("Failed to parse JSON:", err); 91 + showStatus( 92 + `Failed to parse JSON: ${/** @type {Error} */ (err).message}`, 93 + "error", 94 + ); 95 + return; 96 + } 97 + 98 + if (Array.isArray(json?.tracks) && json.tracks.length > 0) { 99 + importTracksBtn.disabled = false; 100 + } 101 + 102 + if (Array.isArray(json?.playlistItems) && json.playlistItems.length > 0) { 103 + importPlaylistsBtn.disabled = false; 104 + } 105 + 106 + if (Array.isArray(json?.facets) && json.facets.length > 0) { 107 + importFacetsBtn.disabled = false; 108 + } 109 + 110 + if (Array.isArray(json?.themes) && json.themes.length > 0) { 111 + importThemesBtn.disabled = false; 112 + } 113 + }; 114 + 115 + // Import tracks 116 + importTracksBtn.onclick = async () => { 117 + /** @type {any[]} */ 118 + const tracks = json?.tracks; 119 + if (!Array.isArray(tracks) || tracks.length === 0) return; 120 + 121 + try { 122 + await output.tracks.save(tracks); 123 + showStatus(`Imported ${tracks.length} track(s).`, "success"); 124 + } catch (err) { 125 + console.error("Import failed:", err); 126 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 127 + } 128 + }; 129 + 130 + // Import playlists 131 + importPlaylistsBtn.onclick = async () => { 132 + /** @type {any[]} */ 133 + const playlistItems = json?.playlistItems; 134 + if (!Array.isArray(playlistItems) || playlistItems.length === 0) return; 135 + 136 + try { 137 + await output.playlistItems.save(playlistItems); 138 + const playlistCount = new Set(playlistItems.map((p) => p.playlist)).size; 139 + showStatus(`Imported ${playlistCount} playlist(s).`, "success"); 140 + } catch (err) { 141 + console.error("Import failed:", err); 142 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 143 + } 144 + }; 145 + 146 + // Import facets 147 + importFacetsBtn.onclick = async () => { 148 + /** @type {any[]} */ 149 + const facets = json?.facets; 150 + if (!Array.isArray(facets) || facets.length === 0) return; 151 + 152 + try { 153 + await output.facets.save(facets); 154 + showStatus(`Imported ${facets.length} facet(s).`, "success"); 155 + } catch (err) { 156 + console.error("Import failed:", err); 157 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 158 + } 159 + }; 160 + 161 + // Import themes 162 + importThemesBtn.onclick = async () => { 163 + /** @type {any[]} */ 164 + const themes = json?.themes; 165 + if (!Array.isArray(themes) || themes.length === 0) return; 166 + 167 + try { 168 + await output.themes.save(themes); 169 + showStatus(`Imported ${themes.length} theme(s).`, "success"); 170 + } catch (err) { 171 + console.error("Import failed:", err); 172 + showStatus(`Import failed: ${/** @type {Error} */ (err).message}`, "error"); 173 + } 174 + };