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

refactor: simplify architecture

+549 -1064
+1 -1
_config.ts
··· 194 194 key = "uri"; 195 195 } 196 196 197 - return `kitchen/l/?${key}=${encodeURIComponent(text)}`; 197 + return `l/?${key}=${encodeURIComponent(text)}`; 198 198 }); 199 199 200 200 ////////////////////////////////////////////
+1 -6
docs/ARCHITECTURE.md
··· 25 25 `src/definitions/` are lexicons, JSON schemas that describe data in the system. 26 26 27 27 28 - ## Themes 29 - 30 - Like orchestrator components, these are compositions of elements. Unlike orchestrators however, it doesn't compose by the use of selectors, instead we write the custom elements as HTML and use the DOM as the composition layer. Alternatively, custom elements can be created in Javascript and then added to the DOM from there. 31 - 32 - 33 28 ## Other directories 34 29 35 30 - `src/common`: Common Javascript code shared by various components and/or themes. 36 31 - `src/styles`: Common CSS shared by themes, the index page or facets. 37 32 - `src/favicons`, `src/fonts`, `src/images` are binary assets for themes and the index page (`src/index.vto`) 38 - - `src/_components` and `src/_includes` are templates used in `.vto` templates, again themes and index page. 33 + - `src/_components` and `src/_includes` are templates used in `.vto` templates.
+3 -1
src/_components/element.vto
··· 1 1 <div class="element"> 2 - <h3>{{title}}</h3> 2 + {{ set slug = title.toLowerCase().replace(/\s+/g, '-') }} 3 + 4 + <h3 id="{{slug}}">{{title}}</h3> 3 5 4 6 <p> 5 7 <em>{{content}}</em>
src/_components/facets/examples.vto src/_components/examples.vto
src/_components/facets/grid.vto src/_components/grid.vto
-52
src/_components/facets/nav.vto
··· 1 - {{ set colorClass = (path) => url === "/" + path ? 'button--bg-twist-2' : 'button--transparent' }} 2 - {{ set slug = (cat) => cat.toLowerCase().replace(/\s+/g, '-') }} 3 - 4 - <nav> 5 - <a href="kitchen/guide/" class="button {{ colorClass("kitchen/guide/") }} button--border"> 6 - <span class="with-icon"> 7 - <i class="ph-fill ph-book-open-text"></i> 8 - Guide 9 - </span> 10 - </a> 11 - 12 - <a href="kitchen/you/" class="button {{ colorClass("kitchen/you/") }} button--border"> 13 - <span class="with-icon"> 14 - <i class="ph-fill ph-person"></i> 15 - Your collection 16 - </span> 17 - </a> 18 - 19 - <a href="kitchen/" class="button {{ colorClass("kitchen/") }} button--border"> 20 - <span class="with-icon"> 21 - <i class="ph-fill ph-sparkle"></i> 22 - Featured 23 - </span> 24 - </a> 25 - 26 - <a href="kitchen/build/" class="button {{ colorClass("kitchen/build/") }} button--border"> 27 - <span class="with-icon"> 28 - <i class="ph-fill ph-hammer"></i> 29 - Build 30 - </span> 31 - </a> 32 - 33 - <div class="divider"></div> 34 - 35 - <a href="kitchen/data/" class="button {{ colorClass("kitchen/data/") }} button--border"> 36 - Input & Output 37 - </a> 38 - 39 - <a href="kitchen/playback/" class="button {{ colorClass("kitchen/playback/") }} button--border"> 40 - Playback 41 - </a> 42 - 43 - <a href="kitchen/browsing/" class="button {{ colorClass("kitchen/browsing/") }} button--border"> 44 - Browsing 45 - </a> 46 - 47 - <a href="kitchen/misc/" class="button {{ colorClass("kitchen/misc/") }} button--border"> 48 - <span class="with-icon"> 49 - <i class="ph-fill ph-treasure-chest"></i> 50 - </span> 51 - </a> 52 - </nav>
+52
src/_components/nav.vto
··· 1 + {{ set colorClass = (path) => url === "/" + path ? 'button--bg-twist-2' : 'button--transparent' }} 2 + {{ set slug = (cat) => cat.toLowerCase().replace(/\s+/g, '-') }} 3 + 4 + <nav> 5 + <a href="guide/" class="button {{ colorClass("guide/") }} button--border"> 6 + <span class="with-icon"> 7 + <i class="ph-fill ph-book-open-text"></i> 8 + Guide 9 + </span> 10 + </a> 11 + 12 + <a href="dashboard/" class="button {{ colorClass("dashboard/") }} button--border"> 13 + <span class="with-icon"> 14 + <i class="ph-fill ph-person"></i> 15 + Your Diffuse 16 + </span> 17 + </a> 18 + 19 + <a href="featured/" class="button {{ colorClass("featured/") }} button--border"> 20 + <span class="with-icon"> 21 + <i class="ph-fill ph-sparkle"></i> 22 + Featured 23 + </span> 24 + </a> 25 + 26 + <a href="build/" class="button {{ colorClass("build/") }} button--border"> 27 + <span class="with-icon"> 28 + <i class="ph-fill ph-hammer"></i> 29 + Build 30 + </span> 31 + </a> 32 + 33 + <div class="divider"></div> 34 + 35 + <a href="data/" class="button {{ colorClass("data/") }} button--border"> 36 + Input & Output 37 + </a> 38 + 39 + <a href="playback/" class="button {{ colorClass("playback/") }} button--border"> 40 + Playback 41 + </a> 42 + 43 + <a href="browsing/" class="button {{ colorClass("browsing/") }} button--border"> 44 + Browsing 45 + </a> 46 + 47 + <a href="misc/" class="button {{ colorClass("misc/") }} button--border"> 48 + <span class="with-icon"> 49 + <i class="ph-fill ph-treasure-chest"></i> 50 + </span> 51 + </a> 52 + </nav>
+1 -1
src/_includes/layouts/kitchen-category.vto
··· 3 3 --- 4 4 5 5 <section> 6 - {{ await comp.facets.grid({ id: slug, items: categoryFacets }) }} 6 + {{ await comp.grid({ id: slug, items: categoryFacets }) }} 7 7 </section>
+9 -9
src/_includes/layouts/kitchen.vto
··· 1 1 --- 2 2 layout: layouts/diffuse.vto 3 3 base: ../../ 4 - title: Facets | Diffuse 4 + title: Diffuse 5 5 6 6 styles: 7 7 - styles/base.css ··· 11 11 - vendor/@phosphor-icons/fill/style.css 12 12 13 13 scripts: 14 - - kitchen/common/ppr.js 14 + - common/pages/ppr.js 15 15 - common/pages/version-upgrade.js 16 16 --- 17 17 ··· 22 22 </style> 23 23 24 24 <header style="overflow: hidden"> 25 - <div style="min-width: var(--container-md)"> 26 - <div class="diffuse-logo-container"> 25 + <div> 26 + <div class="diffuse-logo-container diffuse-logo-container--condensed"> 27 27 <a href="./" style="display: inline-block;"> 28 28 {{ await comp.diffuse.logo() }} 29 29 </a> 30 + </div> 31 + </div> 32 + <div class="dither-mask filler filler--bg-twist-2"> 33 + <div id="status--filler-container" style="color: var(--accent-twist-2)"> 30 34 {{ await comp.diffuse.status() }} 31 35 </div> 32 - <p class="construct dither-mask" style="margin-top: 0; max-width: none;"> 33 - Kitchen 34 - </p> 35 36 </div> 36 - <div class="dither-mask filler filler--bg-twist-2"></div> 37 37 </header> 38 38 39 39 <main> 40 - {{ await comp.facets.nav({ url, facets }) }} 40 + {{ await comp.nav({ url, facets }) }} 41 41 42 42 {{ content }} 43 43 </main>
+315
src/elements.vto
··· 1 + --- 2 + layout: layouts/diffuse.vto 3 + base: ../ 4 + title: Elements | Diffuse 5 + 6 + styles: 7 + - styles/base.css 8 + - styles/diffuse/page.css 9 + - vendor/@phosphor-icons/bold/style.css 10 + - vendor/@phosphor-icons/fill/style.css 11 + 12 + scripts: 13 + - index.js 14 + - common/pages/version-upgrade.js 15 + 16 + # ELEMENTS 17 + 18 + configurators: 19 + - url: "components/configurator/input/element.js" 20 + title: "Input" 21 + desc: "Allows for multiple inputs to be used at once." 22 + - url: "components/configurator/output/element.js" 23 + title: "Output" 24 + desc: "Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory." 25 + - url: "components/configurator/scrobbles/element.js" 26 + title: "Scrobbles" 27 + desc: "Configure multiple scrobblers (music trackers)." 28 + 29 + engines: 30 + - url: "components/engine/audio/element.js" 31 + title: "Audio" 32 + desc: "Plays audio through audio elements." 33 + - url: "components/engine/queue/element.js" 34 + title: "Queue" 35 + desc: "A queue for tracks." 36 + - url: "components/engine/repeat-shuffle/element.js" 37 + title: "Repeat & Shuffle" 38 + desc: "Signals synced with local storage (classified by group) that decide if audio should be repeated and if the queue should be shuffled when filling it." 39 + - url: "components/engine/scope/element.js" 40 + title: "Scope" 41 + desc: > 42 + Signals that could influence the scope of a set of tracks. 43 + 44 + input: 45 + - url: "components/input/https/element.js" 46 + title: "HTTPS" 47 + desc: > 48 + HTTPS URLs to audio files or streams. 49 + - title: "HTTPS (JSON)" 50 + desc: > 51 + Generate tracks based on HTTPS servers that provide JSON (directory) listings. 52 + todo: true 53 + - url: "components/input/icecast/element.js" 54 + title: "Icecast" 55 + desc: > 56 + Icecast internet radio streams. Fetches ICY metadata to populate track information. 57 + - url: "components/input/local/element.js" 58 + title: "Local" 59 + desc: > 60 + Audio files or directories from your local device, using the browser's File System Access API. 61 + - url: "components/input/opensubsonic/element.js" 62 + title: "Opensubsonic" 63 + desc: > 64 + Add any (open)subsonic server. 65 + - url: "components/input/s3/element.js" 66 + title: "S3" 67 + desc: > 68 + AWS S3 and services that provide the same surface API such as Cloudflare R2. 69 + - title: "WebDAV" 70 + desc: > 71 + Add any WebDAV server. 72 + todo: true 73 + 74 + orchestrators: 75 + - url: "components/orchestrator/auto-queue/element.js" 76 + title: "Automatic Queue" 77 + desc: > 78 + Fill the queue automatically with non-manual items (shuffled or regular, based on repeat-shuffle engine). 79 + - url: "components/orchestrator/favourites/element.js" 80 + title: "Favourites" 81 + desc: > 82 + Mark tracks as favourites. Automatically creates an unordered 'Favourites' playlist. 83 + - url: "components/orchestrator/input/element.js" 84 + title: "Input" 85 + desc: "**A default input configuration.** Contains all the inputs provided here." 86 + - url: "components/orchestrator/media-session/element.js" 87 + title: "Media Session" 88 + desc: "Keeps the browser/os media session in sync with queue and audio state. Adds handlers for previous, next, seek to, etc." 89 + - url: "components/orchestrator/offline/element.js" 90 + title: "Offline" 91 + desc: "Registers a service worker that makes the page available offline. Resources (except audio & video) are cached as they load and served from cache when offline." 92 + - url: "components/orchestrator/output/element.js" 93 + title: "Output" 94 + desc: "**A default output configuration.** Contains all the outputs provided here along with the relevant transformers." 95 + - url: "components/orchestrator/path-collections/element.js" 96 + title: "Path Collections" 97 + desc: "Wraps an output element to generate ephemeral playlists based on the first path segment of each track's URI. Ephemeral items are excluded from storage." 98 + - url: "components/orchestrator/process-tracks/element.js" 99 + title: "Process Inputs Into Tracks" 100 + desc: "Whenever the cached tracks are initially loaded through the passed output element it will list tracks by using the passed input element. Afterwards it loops over all tracks and checks if metadata needs to be fetched. If anything has changed, it'll pass the results to the output element." 101 + - url: "components/orchestrator/queue-audio/element.js" 102 + title: "Queue ⭤ Audio" 103 + desc: "Connects the given queue engine to the given audio engine." 104 + - url: "components/orchestrator/scrobble-audio/element.js" 105 + title: "Scrobble ⭤ Audio" 106 + desc: "Connects the audio engine with a scrobbler element. Calls `nowPlaying` when a track starts playing and `scrobble` once the user has listened long enough." 107 + - url: "components/orchestrator/sources/element.js" 108 + title: "Sources" 109 + desc: "Monitor tracks from the given output to form a list of sources based on the input's sources return value." 110 + - url: "components/orchestrator/scoped-tracks/element.js" 111 + title: "Scoped Tracks" 112 + desc: "Supplies the tracks from the given output to the given search processor whenever the tracks collection changes. Additionally it can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a `tracks` signal similar to `output.tracks.collection`" 113 + 114 + output: 115 + - url: "components/output/polymorphic/indexed-db/element.js" 116 + title: "Polymorphic / IndexedDB" 117 + desc: "Stores output into the local indexedDB. Supports any type of data that indexedDB supports." 118 + - url: "components/output/bytes/s3/element.js" 119 + title: "Bytes / S3" 120 + desc: > 121 + Store output data on AWS S3 or compatible services such as Cloudflare R2. 122 + - url: "components/output/raw/atproto/element.js" 123 + title: "Raw / AT Protocol" 124 + desc: > 125 + Store your user data on the storage associated with your ATProtocol identity. Data is lexicon shaped by default so this element takes in that data directly without any transformations. 126 + 127 + processors: 128 + - url: "components/processor/artwork/element.js" 129 + title: "Artwork" 130 + desc: "Fetches cover art for a given set of tracks, stored locally in indexedDB. Checks the audio metadata first, then MusicBrainz and uses Last.fm as the fallback." 131 + - url: "components/processor/metadata/element.js" 132 + title: "Metadata" 133 + desc: "Fetch audio metadata for a given set of tracks, adding to the `Track` object." 134 + - url: "components/processor/search/element.js" 135 + title: "Search" 136 + desc: "Provides a way to search through a collection of tracks, powered by orama.js" 137 + 138 + supplements: 139 + - url: "components/supplement/last.fm/element.js" 140 + title: "Last.fm Scrobbler" 141 + - title: "ListenBrainz Scrobbler" 142 + todo: true 143 + - title: "Rocksky Scrobbler" 144 + todo: true 145 + - title: "Teal.fm Scrobbler" 146 + todo: true 147 + 148 + transformers: 149 + - title: "Output / Bytes / Automerge" 150 + desc: "Translate data to and from an Automerge CRDT." 151 + url: "components/transformer/output/bytes/automerge/element.js" 152 + todo: true 153 + - title: "Output / Bytes / Cambria Lenses" 154 + desc: "Uses the Cambria library to seamlessly translate between data schemas so that no data migration is needed." 155 + todo: true 156 + - title: "Output / Bytes / DASL Sync" 157 + desc: "Syncs data between local and remote using CID-based diffing and performs union merges with tombstone tracking when both sides have diverged." 158 + url: "components/transformer/output/bytes/dasl-sync/element.js" 159 + - title: "Output / Bytes / JSON" 160 + desc: "Raw data schema output ⇄ JSON Uint8Array." 161 + url: "components/transformer/output/bytes/json/element.js" 162 + - title: "Output / Raw / AT Protocol Sync" 163 + desc: "Wraps an AT Protocol output with a local IndexedDB cache. Uses the repo revision to skip unnecessary fetches and performs union merges with tombstone tracking when both local and remote have diverged." 164 + url: "components/transformer/output/raw/atproto-sync/element.js" 165 + - title: "Output / Refiner / Default" 166 + desc: "The task of a refiner transformer is to remove the output state that is not meant to be saved to storage. For example, ephemeral tracks; this transformer will keep them in memory, but they will not be present in the output. **Ideally this is part of every theme, but you may swap it out with another transformer that might provide better defaults.**" 167 + url: "components/transformer/output/refiner/default/element.js" 168 + - title: "Output / Refiner / Track URI Passkey" 169 + desc: "Encrypts track URIs using a passkey-derived PRF key. On read, decrypts `encrypted://` URIs transparently; on write, re-encrypts all URIs before passing downstream. Tracks that cannot be decrypted are held separately and excluded from the visible collection." 170 + url: "components/transformer/output/refiner/track-uri-passkey/element.js" 171 + - title: "Output / String / JSON" 172 + desc: "Raw data schema output ⇄ JSON UTF8 string." 173 + url: "components/transformer/output/string/json/element.js" 174 + 175 + # DEFINITIONS 176 + 177 + definitions: 178 + - title: "Output / Collaboration" 179 + desc: > 180 + Represents a collaboration between multiple collaborators on a subject, such as a playlist. 181 + url: "definitions/output/collaboration.json" 182 + - title: "Output / Facet" 183 + desc: > 184 + Facet pointer or HTML snippet. 185 + url: "definitions/output/facet.json" 186 + - title: "Output / Playlist Item" 187 + desc: > 188 + Represents a single item in a playlist. Tracks are matched based on the given criteria. A playlist is formed by grouping items by their playlist property. 189 + url: "definitions/output/playlistItem.json" 190 + - title: "Output / Playlist Item Bundle" 191 + desc: > 192 + A bundle of playlist items. 193 + url: "definitions/output/playlistItemBundle.json" 194 + - title: "Output / Progress" 195 + desc: > 196 + Used to track progress of (long) audio playback. 197 + todo: true 198 + - title: "Output / Track" 199 + desc: > 200 + Represents audio that can be played, or a placeholder for a source of tracks. Contains a URI that will resolve to the audio. 201 + url: "definitions/output/track.json" 202 + - title: "Output / Track Bundle" 203 + desc: > 204 + A bundle of tracks. 205 + url: "definitions/output/trackBundle.json" 206 + 207 + --- 208 + 209 + <header> 210 + <div> 211 + <div class="diffuse-logo-container"> 212 + <a href="./" style="display: inline-block;"> 213 + {{ await comp.diffuse.logo() }} 214 + </a> 215 + </div> 216 + <p class="construct dither-mask"> 217 + Elements 218 + </p> 219 + <p> 220 + Diffuse was built using these custom elements (aka. web components), consume these using the <a href="facets/#builder">build tool</a>, the Javascript <a href="https://jsr.io/@toko/diffuse">package</a>, or the linked Javascript files down below. 221 + </p> 222 + <ul class="table-of-contents"> 223 + <li><a href="elements/#configurators">Configurators</a></li> 224 + <li><a href="elements/#engines">Engines</a></li> 225 + <li><a href="elements/#input">Input</a></li> 226 + <li><a href="elements/#orchestrators">Orchestrators</a></li> 227 + <li><a href="elements/#output">Output</a></li> 228 + <li><a href="elements/#processors">Processors</a></li> 229 + <li><a href="elements/#supplements">Supplements</a></li> 230 + <li><a href="elements/#transformers">Transformers</a></li> 231 + <!----> 232 + <li style="margin-top: var(--space-xs);"><a href="#definitions">Definitions</a></li> 233 + </ul> 234 + </div> 235 + <div class="dither-mask filler"></div> 236 + </header> 237 + <main> 238 + <!-- ELEMENTS --> 239 + <section> 240 + <div class="columns"> 241 + {{ await comp.element({ 242 + title: "Configurators", 243 + items: configurators, 244 + content: ` 245 + Elements that serve as an intermediate in order to make a particular kind of element configurable. In other words, these allow for an element to be swapped out with another that takes the same set of the actions and data output. 246 + ` 247 + }) }} 248 + 249 + {{ await comp.element({ 250 + title: "Engines", 251 + items: engines, 252 + content: ` 253 + Elements with each a singular purpose and don't have any UI. There are specialised UI and orchestrator elements that control these. 254 + ` 255 + }) }} 256 + 257 + {{ await comp.element({ 258 + title: "Input", 259 + items: input, 260 + content: ` 261 + Inputs are sources of audio tracks. Each track is an entry in the list of possible items to play. These can be files or streams, static or dynamic. 262 + ` 263 + }) }} 264 + 265 + {{ await comp.element({ 266 + title: "Orchestrators", 267 + items: orchestrators, 268 + content: ` 269 + These too are element compositions. However, unlike themes, these are purely logical. Mostly exist in order to construct sensible defaults to use across themes and other compositions. 270 + ` 271 + }) }} 272 + 273 + {{ await comp.element({ 274 + title: "Output", 275 + items: output, 276 + content: ` 277 + Output is application-derived data such as playlists. These elements can receive such data and keep it around. These are categorised by the type of data they ingest, or many types in the case of polymorphic. Optionally use transformers to convert output into the expected format. 278 + ` 279 + }) }} 280 + 281 + {{ await comp.element({ 282 + title: "Processors", 283 + items: processors, 284 + content: ` 285 + These elements work with the output generated by the input elements to add more data to them, or process them in some other way. 286 + ` 287 + }) }} 288 + 289 + {{ await comp.element({ 290 + title: "Supplements", 291 + items: supplements, 292 + content: ` 293 + Additional elements, such as scrobblers. 294 + ` 295 + }) }} 296 + 297 + {{ await comp.element({ 298 + title: "Transformers", 299 + items: transformers, 300 + content: ` 301 + Transform data from one format or schema into another. See schema section below for more information. Just as configurators, these are intermediates and require to have the same set of actions as the element it targets. 302 + ` 303 + }) }} 304 + </div> 305 + </section> 306 + 307 + <!-- DEFINITIONS --> 308 + <section> 309 + <h2 id="definitions">Definitions</h2> 310 + 311 + <p>All of the elements here are built with these data definitions in mind. That said, you can mix elements that use different definitions; you just have to put a transformer between them in order to translate between them, if needed.</p> 312 + 313 + {{ await comp.list({ items: definitions }) }} 314 + </section> 315 + </main>
+2 -2
src/facets/misc/split-view/index.inline.js
··· 186 186 const uri = node.facet.includes("://") 187 187 ? node.facet 188 188 : `diffuse://${node.facet}`; 189 - iframe.src = "facets/l/?uri=" + encodeURIComponent(uri); 189 + iframe.src = "l/?uri=" + encodeURIComponent(uri); 190 190 iframe.allow = "autoplay"; 191 191 pane.appendChild(iframe); 192 192 } ··· 359 359 const uri = node.facet.includes("://") 360 360 ? node.facet 361 361 : `diffuse://${node.facet}`; 362 - const src = "facets/l/?uri=" + encodeURIComponent(uri); 362 + const src = "l/?uri=" + encodeURIComponent(uri); 363 363 return `${indent}<div class="pane"> 364 364 ${inner}<iframe src="${src}" allow="autoplay"></iframe> 365 365 ${indent}</div>`;
+11
src/featured.vto
··· 1 + --- 2 + layout: layouts/kitchen.vto 3 + base: ../ 4 + title: Featured | Diffuse 5 + --- 6 + 7 + <h1 hidden>Featured</h1> 8 + 9 + <section> 10 + {{ await comp.grid({ id: "featured", items: facets.filter(f => f.featured) }) }} 11 + </section>
+84
src/guide.vto
··· 1 + --- 2 + layout: layouts/kitchen.vto 3 + base: ../ 4 + title: Guide | Diffuse 5 + --- 6 + 7 + <h1 hidden>Guide</h1> 8 + 9 + <div class="columns"> 10 + <section> 11 + <h3>Tutorial</h3> 12 + 13 + <p> 14 + <strong>Diffuse is not your typical streaming service, we have to add sources of audio so we have stuff to play.</strong> This button below adds some demo content, so you can experiment with the software right away. 15 + </p> 16 + 17 + <p> 18 + <button id="add-sample-content" class="button--bg-twist-2"> 19 + <span>Add sample content</span> 20 + </button> 21 + </p> 22 + 23 + <p> 24 + Now we should explore what is possible with our audio. Because Diffuse is cooperative and malleable software, our interface can look like anything, and we can pick the features we like. That might sound overwhelming, so let's keep it simple for now. 25 + </p> 26 + 27 + <p> 28 + <strong>Let's pick an interface</strong> that automatically puts audio from our collection into the queue, and another to play what got put into the queue. 29 + </p> 30 + 31 + <p> 32 + <span class="button-row"> 33 + <a class="button button--bg-twist-2" href="{{ ('facets/playback/auto-queue/index.html') |> facetLoaderURL }}" target="_blank"> 34 + <span class="with-icon"> 35 + <i class="ph-fill ph-number-circle-one"></i> 36 + Fill up queue 37 + </span> 38 + </a> 39 + <a class="button button--bg-twist-2" href="{{ ('themes/blur/artwork-controller/facet/index.html') |> facetLoaderURL }}" target="_blank"> 40 + <span class="with-icon"> 41 + <i class="ph-fill ph-number-circle-two"></i> 42 + Play audio 43 + </span> 44 + </a> 45 + </span> 46 + </p> 47 + 48 + <p> 49 + <em>So you said I could pick the features and interfaces that I liked, how does that work?</em> 50 + </p> 51 + 52 + <p> 53 + To do that, we have to look at the other pages shown in the navigation here, such as the <a href="featured/">featured page</a>. There you'll be able to browse through all the features and interfaces that are provided by Diffuse. 54 + </p> 55 + 56 + <p> 57 + <strong>To use a feature, you click the toggle to enable it</strong>, this will add it to your software. Interfaces can be added too, but it's not required, you can try them out right away by clicking the link in their title. For interfaces that are more like traditional web applications, delivering more encompassing experiences, look at <a href="themes/">themes</a>. 58 + </p> 59 + 60 + <p> 61 + TODO: explain adding your own inputs, syncing user data, etc. 62 + </p> 63 + </section> 64 + 65 + <section> 66 + <h3>Concept</h3> 67 + 68 + <p> 69 + Diffuse is unlike traditional software; instead of combining several features into a single user interface and producing data output, we do the opposite, we start with the data and work our way up from there. 70 + </p> 71 + 72 + <p> 73 + <strong>It provides every user the ability to choose which features and interfaces they want to layer on top of their data.</strong> 74 + </p> 75 + 76 + <p> 77 + These features and interfaces are housed into units that we call "facets". They consist of <a href="elements/">Diffuse elements</a> that are connected, they broadcast their state and your data that has been updated. 78 + </p> 79 + 80 + <p> 81 + Finally, facets are just regular web pages so they can live wherever. We save them to the user-data storage that's configured and give the user the option to share it. This means that you can load features and interfaces from other people, <strong>building software cooperatively</strong>. 82 + </p> 83 + </section> 84 + </div>
+7 -5
src/index.js src/common/pages/guide.js
··· 1 + import { insertPreludes } from "../facets/prelude.js"; 2 + 1 3 //////////////////////////////////////////// 2 4 // DEMO 3 5 //////////////////////////////////////////// 4 6 5 - import { insertPreludes } from "./common/facets/prelude.js"; 6 - 7 - const addDemoBtn = document.querySelector("#add-sample-content"); 8 - 9 7 async function addSampleContent() { 8 + const addDemoBtn = document.querySelector("#add-sample-content"); 10 9 if (!addDemoBtn) return; 11 10 12 11 addDemoBtn.setAttribute("disabled", ""); ··· 60 59 </span>`; 61 60 } 62 61 63 - addDemoBtn?.addEventListener("click", addSampleContent); 62 + export function setupSampleButton() { 63 + const addDemoBtn = document.querySelector("#add-sample-content"); 64 + addDemoBtn?.addEventListener("click", addSampleContent); 65 + }
+10 -414
src/index.vto
··· 11 11 - index.js 12 12 - common/pages/version-upgrade.js 13 13 14 - # ELEMENTS 15 - 16 - configurators: 17 - - url: "components/configurator/input/element.js" 18 - title: "Input" 19 - desc: "Allows for multiple inputs to be used at once." 20 - - url: "components/configurator/output/element.js" 21 - title: "Output" 22 - desc: "Enables the user to configure a specific output. If no default output is set, it creates a temporary session by storing everything in memory." 23 - - url: "components/configurator/scrobbles/element.js" 24 - title: "Scrobbles" 25 - desc: "Configure multiple scrobblers (music trackers)." 26 - 27 - engines: 28 - - url: "components/engine/audio/element.js" 29 - title: "Audio" 30 - desc: "Plays audio through audio elements." 31 - - url: "components/engine/queue/element.js" 32 - title: "Queue" 33 - desc: "A queue for tracks." 34 - - url: "components/engine/repeat-shuffle/element.js" 35 - title: "Repeat & Shuffle" 36 - desc: "Signals synced with local storage (classified by group) that decide if audio should be repeated and if the queue should be shuffled when filling it." 37 - - url: "components/engine/scope/element.js" 38 - title: "Scope" 39 - desc: > 40 - Signals that could influence the scope of a set of tracks. 41 - 42 - input: 43 - - url: "components/input/https/element.js" 44 - title: "HTTPS" 45 - desc: > 46 - HTTPS URLs to audio files or streams. 47 - - title: "HTTPS (JSON)" 48 - desc: > 49 - Generate tracks based on HTTPS servers that provide JSON (directory) listings. 50 - todo: true 51 - - url: "components/input/icecast/element.js" 52 - title: "Icecast" 53 - desc: > 54 - Icecast internet radio streams. Fetches ICY metadata to populate track information. 55 - - url: "components/input/local/element.js" 56 - title: "Local" 57 - desc: > 58 - Audio files or directories from your local device, using the browser's File System Access API. 59 - - url: "components/input/opensubsonic/element.js" 60 - title: "Opensubsonic" 61 - desc: > 62 - Add any (open)subsonic server. 63 - - url: "components/input/s3/element.js" 64 - title: "S3" 65 - desc: > 66 - AWS S3 and services that provide the same surface API such as Cloudflare R2. 67 - - title: "WebDAV" 68 - desc: > 69 - Add any WebDAV server. 70 - todo: true 71 - 72 - orchestrators: 73 - - url: "components/orchestrator/auto-queue/element.js" 74 - title: "Automatic Queue" 75 - desc: > 76 - Fill the queue automatically with non-manual items (shuffled or regular, based on repeat-shuffle engine). 77 - - url: "components/orchestrator/favourites/element.js" 78 - title: "Favourites" 79 - desc: > 80 - Mark tracks as favourites. Automatically creates an unordered 'Favourites' playlist. 81 - - url: "components/orchestrator/input/element.js" 82 - title: "Input" 83 - desc: "**A default input configuration.** Contains all the inputs provided here." 84 - - url: "components/orchestrator/media-session/element.js" 85 - title: "Media Session" 86 - desc: "Keeps the browser/os media session in sync with queue and audio state. Adds handlers for previous, next, seek to, etc." 87 - - url: "components/orchestrator/offline/element.js" 88 - title: "Offline" 89 - desc: "Registers a service worker that makes the page available offline. Resources (except audio & video) are cached as they load and served from cache when offline." 90 - - url: "components/orchestrator/output/element.js" 91 - title: "Output" 92 - desc: "**A default output configuration.** Contains all the outputs provided here along with the relevant transformers." 93 - - url: "components/orchestrator/path-collections/element.js" 94 - title: "Path Collections" 95 - desc: "Wraps an output element to generate ephemeral playlists based on the first path segment of each track's URI. Ephemeral items are excluded from storage." 96 - - url: "components/orchestrator/process-tracks/element.js" 97 - title: "Process Inputs Into Tracks" 98 - desc: "Whenever the cached tracks are initially loaded through the passed output element it will list tracks by using the passed input element. Afterwards it loops over all tracks and checks if metadata needs to be fetched. If anything has changed, it'll pass the results to the output element." 99 - - url: "components/orchestrator/queue-audio/element.js" 100 - title: "Queue ⭤ Audio" 101 - desc: "Connects the given queue engine to the given audio engine." 102 - - url: "components/orchestrator/scrobble-audio/element.js" 103 - title: "Scrobble ⭤ Audio" 104 - desc: "Connects the audio engine with a scrobbler element. Calls `nowPlaying` when a track starts playing and `scrobble` once the user has listened long enough." 105 - - url: "components/orchestrator/sources/element.js" 106 - title: "Sources" 107 - desc: "Monitor tracks from the given output to form a list of sources based on the input's sources return value." 108 - - url: "components/orchestrator/scoped-tracks/element.js" 109 - title: "Scoped Tracks" 110 - desc: "Supplies the tracks from the given output to the given search processor whenever the tracks collection changes. Additionally it can perform a search and other ways to reduce the scope of tracks based on the given scope engine. Provides a `tracks` signal similar to `output.tracks.collection`" 111 - 112 - output: 113 - - url: "components/output/polymorphic/indexed-db/element.js" 114 - title: "Polymorphic / IndexedDB" 115 - desc: "Stores output into the local indexedDB. Supports any type of data that indexedDB supports." 116 - - url: "components/output/bytes/s3/element.js" 117 - title: "Bytes / S3" 118 - desc: > 119 - Store output data on AWS S3 or compatible services such as Cloudflare R2. 120 - - url: "components/output/raw/atproto/element.js" 121 - title: "Raw / AT Protocol" 122 - desc: > 123 - Store your user data on the storage associated with your ATProtocol identity. Data is lexicon shaped by default so this element takes in that data directly without any transformations. 124 - 125 - processors: 126 - - url: "components/processor/artwork/element.js" 127 - title: "Artwork" 128 - desc: "Fetches cover art for a given set of tracks, stored locally in indexedDB. Checks the audio metadata first, then MusicBrainz and uses Last.fm as the fallback." 129 - - url: "components/processor/metadata/element.js" 130 - title: "Metadata" 131 - desc: "Fetch audio metadata for a given set of tracks, adding to the `Track` object." 132 - - url: "components/processor/search/element.js" 133 - title: "Search" 134 - desc: "Provides a way to search through a collection of tracks, powered by orama.js" 135 - 136 - supplements: 137 - - url: "components/supplement/last.fm/element.js" 138 - title: "Last.fm Scrobbler" 139 - - title: "ListenBrainz Scrobbler" 140 - todo: true 141 - - title: "Rocksky Scrobbler" 142 - todo: true 143 - - title: "Teal.fm Scrobbler" 144 - todo: true 145 - 146 - transformers: 147 - - title: "Output / Bytes / Automerge" 148 - desc: "Translate data to and from an Automerge CRDT." 149 - url: "components/transformer/output/bytes/automerge/element.js" 150 - todo: true 151 - - title: "Output / Bytes / Cambria Lenses" 152 - desc: "Uses the Cambria library to seamlessly translate between data schemas so that no data migration is needed." 153 - todo: true 154 - - title: "Output / Bytes / DASL Sync" 155 - desc: "Syncs data between local and remote using CID-based diffing and performs union merges with tombstone tracking when both sides have diverged." 156 - url: "components/transformer/output/bytes/dasl-sync/element.js" 157 - - title: "Output / Bytes / JSON" 158 - desc: "Raw data schema output ⇄ JSON Uint8Array." 159 - url: "components/transformer/output/bytes/json/element.js" 160 - - title: "Output / Raw / AT Protocol Sync" 161 - desc: "Wraps an AT Protocol output with a local IndexedDB cache. Uses the repo revision to skip unnecessary fetches and performs union merges with tombstone tracking when both local and remote have diverged." 162 - url: "components/transformer/output/raw/atproto-sync/element.js" 163 - - title: "Output / Refiner / Default" 164 - desc: "The task of a refiner transformer is to remove the output state that is not meant to be saved to storage. For example, ephemeral tracks; this transformer will keep them in memory, but they will not be present in the output. **Ideally this is part of every theme, but you may swap it out with another transformer that might provide better defaults.**" 165 - url: "components/transformer/output/refiner/default/element.js" 166 - - title: "Output / Refiner / Track URI Passkey" 167 - desc: "Encrypts track URIs using a passkey-derived PRF key. On read, decrypts `encrypted://` URIs transparently; on write, re-encrypts all URIs before passing downstream. Tracks that cannot be decrypted are held separately and excluded from the visible collection." 168 - url: "components/transformer/output/refiner/track-uri-passkey/element.js" 169 - - title: "Output / String / JSON" 170 - desc: "Raw data schema output ⇄ JSON UTF8 string." 171 - url: "components/transformer/output/string/json/element.js" 172 - 173 - # DEFINITIONS 174 - 175 - definitions: 176 - - title: "Output / Collaboration" 177 - desc: > 178 - Represents a collaboration between multiple collaborators on a subject, such as a playlist. 179 - url: "definitions/output/collaboration.json" 180 - - title: "Output / Facet" 181 - desc: > 182 - Facet pointer or HTML snippet. 183 - url: "definitions/output/facet.json" 184 - - title: "Output / Playlist Item" 185 - desc: > 186 - Represents a single item in a playlist. Tracks are matched based on the given criteria. A playlist is formed by grouping items by their playlist property. 187 - url: "definitions/output/playlistItem.json" 188 - - title: "Output / Playlist Item Bundle" 189 - desc: > 190 - A bundle of playlist items. 191 - url: "definitions/output/playlistItemBundle.json" 192 - - title: "Output / Progress" 193 - desc: > 194 - Used to track progress of (long) audio playback. 195 - todo: true 196 - - title: "Output / Track" 197 - desc: > 198 - Represents audio that can be played, or a placeholder for a source of tracks. Contains a URI that will resolve to the audio. 199 - url: "definitions/output/track.json" 200 - - title: "Output / Track Bundle" 201 - desc: > 202 - A bundle of tracks. 203 - url: "definitions/output/trackBundle.json" 204 - 205 14 # LINKS 206 15 207 16 links: ··· 237 46 <i class="ph-fill ph-crane"></i> 238 47 <strong style="font-weight: 700;">WORK IN PROGRESS</strong> 239 48 </p> 49 + <p style="margin: var(--space-lg) 0"> 50 + <a class="button" href="dashboard/">Open Diffuse</a> 51 + </p> 240 52 <ul class="table-of-contents"> 241 - <li><a href="#usage">Usage</a></li> 242 - <li><a href="#developers">Developers</a></li> 243 - <li><a href="#definitions">Definitions</a></li> 244 - <li><a href="#links">Links</a></li> 245 - </ul> 53 + <li><a href="guide/">Guide</a></li> 54 + <li><a href="featured/">Featured</a></li> 55 + </ul> 246 56 <p> 247 - <small>Built by <a href="https://tokono.ma">tokono.ma</a></small> 57 + <small style="line-height: var(--leading-relaxed)"> 58 + Built with <a href="elements/">Diffuse elements</a><br /> 59 + Created by <a href="https://tokono.ma">tokono.ma</a> 60 + </small> 248 61 </p> 249 62 </div> 250 63 <div class="dither-mask filler"></div> 251 64 </header> 252 65 <main> 253 - <!-- USAGE --> 254 - <section> 255 - <h2 id="usage">Usage</h2> 256 - 257 - <div class="columns"> 258 - <div class="element"> 259 - <p> 260 - <strong style="color: var(--accent)">Diffuse is not your typical streaming service, we have to add sources of audio so we have stuff to play.</strong> This button below adds some demo content, so you can experiment with the software right away. 261 - </p> 262 - 263 - <p> 264 - <button id="add-sample-content"> 265 - <span>Add sample content</span> 266 - </button> 267 - </p> 268 - </div> 269 - 270 - <div class="element"> 271 - <p> 272 - If you do already have a place where you keep your audio files and want to connect it first thing, browse through all the various services that can be connected to Diffuse. 273 - </p> 274 - 275 - <p><em>Look for "Connect ..."</em></p> 276 - 277 - <p> 278 - <a class="button" href="kitchen/data/" target="_blank">Discover integrations</a> 279 - </p> 280 - </div> 281 - </div> 282 - 283 - <div class="columns" style="margin-top: var(--space-xl)"> 284 - <div class="element"> 285 - <p> 286 - Great, one of the hardest parts is already done. Now we should explore what is possible with our audio. Because Diffuse is cooperative and malleable software, our interface can look like anything, and we can pick the features we like. That might sound overwhelming, so let's keep it simple for now. 287 - </p> 288 - </div> 289 - 290 - <div class="element"> 291 - <p> 292 - <strong>Let's pick an interface</strong> that automatically puts audio from our collection into the queue, and another to play what got put into the queue. 293 - </p> 294 - 295 - <p> 296 - <span class="button-row"> 297 - <a class="button button--bg-twist-1" href="{{ ('facets/playback/auto-queue/index.html') |> facetLoaderURL }}" target="_blank"> 298 - <span class="with-icon"> 299 - <i class="ph-fill ph-number-circle-one"></i> 300 - Fill up queue 301 - </span> 302 - </a> 303 - <a class="button button--bg-twist-1" href="{{ ('themes/blur/artwork-controller/facet/index.html') |> facetLoaderURL }}" target="_blank"> 304 - <span class="with-icon"> 305 - <i class="ph-fill ph-number-circle-two"></i> 306 - Play audio 307 - </span> 308 - </a> 309 - </span> 310 - </p> 311 - </div> 312 - </div> 313 - 314 - <div class="columns" style="margin-top: var(--space-xl)"> 315 - <div class="element"> 316 - <p> 317 - <em>So you said I could pick the features and interfaces that I liked, how does that work?</em> 318 - </p> 319 - 320 - <p> 321 - To do that, we have to visit our <strong style="color: var(--accent-twist-2)">kitchen</strong>. There you'll be able to browse through all the features and interfaces that are provided by Diffuse. To use a feature, you click the toggle to enable it, this will add it to your software. Interfaces can be added too, but it's not required, you can try them out right away by clicking the link in their title. 322 - </p> 323 - 324 - <p> 325 - <a class="button button--bg-twist-2" href="kitchen/" target="_blank">Explore the kitchen</a> 326 - </p> 327 - </div> 328 - 329 - <div class="element"> 330 - <p> 331 - <em>What you mean, my software? What is software anyways?</em> 332 - </p> 333 - 334 - <p> 335 - When we say software, we mean, having a user interface or code that manipulates your data in some way. Because you are picking those pieces that change how it looks and behaves, you are composing your own software, you made it yours. 336 - </p> 337 - </div> 338 - </div> 339 - 340 - <div class="columns" style="margin-top: var(--space-xl)"> 341 - <div class="element"> 342 - <p> 343 - <em>What is a feature exactly?</em> 344 - </p> 345 - 346 - <p> 347 - A <strong style="color: var(--accent-twist-4)">feature</strong> is a piece of code that expands the capabilities of your software or that makes it behave in a certain kind of way. It runs each time you open an interface, but may not necessarily be activated. Some examples, support for a particular audio source, or always add things automatically to the queue. 348 - </p> 349 - </div> 350 - 351 - <div class="element"> 352 - <p> 353 - <em>What makes it cooperative?</em> 354 - </p> 355 - 356 - <p> 357 - These features and interfaces aren't required to be built into Diffuse, they can come from any place on the web. This makes it cooperative, you can share your features and interfaces, and use ones from other people. Any of these can be edited by clicking the edit button on the "your software" page. 358 - </p> 359 - </div> 360 - </div> 361 - 362 - <div class="columns" style="margin-top: var(--space-xl)"> 363 - <div class="element"> 364 - <p> 365 - <em>How do I sync my data with my other devices?</em> 366 - </p> 367 - 368 - <p> 369 - # 370 - </p> 371 - </div> 372 - </div> 373 - </section> 374 - 375 - <!-- ELEMENTS --> 376 - <section> 377 - <h2 id="developers">Developers</h2> 378 - 379 - <p> 380 - If you're a programmer, you can use the (web) components of the system. These custom elements can be combined into an entire music player and browser, or whatever you want to build. 381 - </p> 382 - 383 - <p> 384 - Consume these using the kitchen <a href="facets/#builder">build tool</a>, the Javascript <a href="https://jsr.io/@toko/diffuse">package</a>, or the linked Javascript files down below. 385 - </p> 386 - 387 - <div class="columns"> 388 - {{ await comp.element({ 389 - title: "Configurators", 390 - items: configurators, 391 - content: ` 392 - Elements that serve as an intermediate in order to make a particular kind of element configurable. In other words, these allow for an element to be swapped out with another that takes the same set of the actions and data output. 393 - ` 394 - }) }} 395 - 396 - {{ await comp.element({ 397 - title: "Engines", 398 - items: engines, 399 - content: ` 400 - Elements with each a singular purpose and don't have any UI. There are specialised UI and orchestrator elements that control these. 401 - ` 402 - }) }} 403 - 404 - {{ await comp.element({ 405 - title: "Input", 406 - items: input, 407 - content: ` 408 - Inputs are sources of audio tracks. Each track is an entry in the list of possible items to play. These can be files or streams, static or dynamic. 409 - ` 410 - }) }} 411 - 412 - {{ await comp.element({ 413 - title: "Orchestrators", 414 - items: orchestrators, 415 - content: ` 416 - These too are element compositions. However, unlike themes, these are purely logical. Mostly exist in order to construct sensible defaults to use across themes and other compositions. 417 - ` 418 - }) }} 419 - 420 - {{ await comp.element({ 421 - title: "Output", 422 - items: output, 423 - content: ` 424 - Output is application-derived data such as playlists. These elements can receive such data and keep it around. These are categorised by the type of data they ingest, or many types in the case of polymorphic. Optionally use transformers to convert output into the expected format. 425 - ` 426 - }) }} 427 - 428 - {{ await comp.element({ 429 - title: "Processors", 430 - items: processors, 431 - content: ` 432 - These elements work with the output generated by the input elements to add more data to them, or process them in some other way. 433 - ` 434 - }) }} 435 - 436 - {{ await comp.element({ 437 - title: "Supplements", 438 - items: supplements, 439 - content: ` 440 - Additional elements, such as scrobblers. 441 - ` 442 - }) }} 443 - 444 - {{ await comp.element({ 445 - title: "Transformers", 446 - items: transformers, 447 - content: ` 448 - Transform data from one format or schema into another. See schema section below for more information. Just as configurators, these are intermediates and require to have the same set of actions as the element it targets. 449 - ` 450 - }) }} 451 - </div> 452 - </section> 453 - 454 - <!-- DEFINITIONS + LINKS --> 455 - <div class="columns"> 456 - <section> 457 - <h2 id="definitions">Definitions</h2> 458 - 459 - <p>All of the elements here are built with these data definitions in mind. That said, you can mix elements that use different definitions; you just have to put a transformer between them in order to translate between them, if needed.</p> 460 - 461 - {{ await comp.list({ items: definitions }) }} 462 - </section> 463 - 464 - <section> 465 - <h2 id="links">Links</h2> 466 - 467 - {{ await comp.list({ items: links }) }} 468 - </section> 469 - </div> 470 66 </main>
+3 -3
src/kitchen/build.vto src/build.vto
··· 1 1 --- 2 2 layout: layouts/kitchen.vto 3 - base: ../../ 4 - title: Build | Kitchen | Diffuse 3 + base: ../ 4 + title: Build | Diffuse 5 5 6 6 examples: 7 7 - url: "facets/examples/now-playing/index.html" ··· 99 99 Some simple examples to help you understand how to build your own facet. Click the edit button to load them into the code editor above. 100 100 </p> 101 101 102 - {{ await comp.facets.examples({ id: "examples", items: examples }) }} 102 + {{ await comp.examples({ id: "examples", items: examples }) }} 103 103 104 104 <h2 id="notes">Notes</h2> 105 105 <p>
+2 -2
src/kitchen/category.page.ts src/category.page.ts
··· 14 14 for (const category of categories) { 15 15 const slug = category.toLowerCase().replace(/\s+/g, "-"); 16 16 yield { 17 - url: `/kitchen/${slug}/`, 18 - title: `${category} | Kitchen | Diffuse`, 17 + url: `/${slug}/`, 18 + title: `${category} | Diffuse`, 19 19 category, 20 20 slug, 21 21 categoryFacets: facets.filter((f) => f.category === category),
+1 -1
src/kitchen/common/build.js src/common/pages/build.js
··· 114 114 break; 115 115 case "save+open": 116 116 await saveFacet(facet); 117 - globalThis.open(`./facets/l/?id=${facet.id}`, "blank"); 117 + globalThis.open(`./l/?id=${facet.id}`, "blank"); 118 118 break; 119 119 } 120 120 };
src/kitchen/common/crud.js src/common/pages/crud.js
src/kitchen/common/grid.js src/common/pages/grid.js
src/kitchen/common/output.js src/common/pages/output.js
+10 -7
src/kitchen/common/ppr.js src/common/pages/ppr.js
··· 1 1 import * as Build from "./build.js"; 2 + import * as Dashboard from "./dashboard.js"; 2 3 import * as Grid from "./grid.js"; 3 - import * as You from "./you.js"; 4 + import * as Guide from "./guide.js"; 4 5 5 6 /** Base pathname of the app (e.g. "/" at root, "/diffuse/" in a subdirectory). */ 6 7 const BASE_PATHNAME = new URL(document.baseURI).pathname; 7 8 8 9 /** 9 10 * Strips the app's base path prefix from an absolute pathname, 10 - * returning a root-relative path like "/kitchen/build". 11 + * returning a root-relative path like "/build". 11 12 * 12 13 * @param {string} pathname 13 14 */ ··· 29 30 await Grid.monitorToggleButtonStates(); 30 31 31 32 switch (path) { 32 - case "/kitchen/build": 33 + case "/build": 33 34 Build.renderEditor(); 34 35 Build.handleBuildFormSubmit(); 35 36 Build.listenForExamplesEdit(); 36 37 await Build.editFacetFromURL(); 37 38 break; 38 - case "/kitchen/you": 39 - await You.renderList(); 39 + case "/dashboard": 40 + await Dashboard.renderList(); 40 41 break; 42 + case "/guide": 43 + Guide.setupSampleButton(); 41 44 default: 42 45 break; 43 46 } ··· 63 66 const url = new URL(event.destination.url); 64 67 if (url.origin !== location.origin) return; 65 68 66 - // Only intercept /kitchen/[section]/ paths (not deeper sub-paths like /kitchen/misc/*) 69 + // Only intercept paths one level deep 67 70 const relative = relativePathname(url.pathname); 68 71 const parts = relative.split("/").filter(Boolean); 69 - if (parts[0] !== "kitchen") return; 72 + if (parts.length === 0) return; 70 73 if (parts.length > 2) return; 71 74 72 75 // Skip the loader page
+4 -4
src/kitchen/common/you.js src/common/pages/dashboard.js
··· 40 40 <p> 41 41 <span> 42 42 You haven't saved anything yet. Add a facet by browsing the <a 43 - href="kitchen/" 43 + href="featured/" 44 44 >featured ones</a> or any of the other categories. You can click the toggle 45 45 to quickly add or remove from your collection. Alternatively, add one using 46 46 an URI: ··· 207 207 const loading = html` 208 208 <div class="with-icon"> 209 209 <i class="ph-bold ph-spinner animate-spin"></i> 210 - Loading items 210 + Loading your software 211 211 </div> 212 212 `; 213 213 ··· 238 238 ` 239 239 : html` 240 240 <a 241 - href="facets/l/?id=${c 241 + href="l/?id=${c 242 242 .id}" 243 243 style="display: inline-block; padding: var(--space-3xs) 0" 244 244 > ··· 287 287 <a 288 288 class="button button--transparent" 289 289 title="Edit" 290 - href="kitchen/build/?id=${encodeURIComponent(c.id)}" 290 + href="build/?id=${encodeURIComponent(c.id)}" 291 291 > 292 292 <i class="ph-fill ph-code-block"></i> 293 293 </a>
-46
src/kitchen/guide.vto
··· 1 - --- 2 - layout: layouts/kitchen.vto 3 - base: ../../ 4 - title: Guide | Kitchen | Diffuse 5 - --- 6 - 7 - <h1 hidden>Guide</h1> 8 - 9 - <div class="columns"> 10 - <section> 11 - <h3>Getting started</h3> 12 - 13 - <p> 14 - To get started you can browse existing facets here on these pages, find one you like and then either you click the link in the title in case it's an <strong>interface</strong>. Or, you save it to your collection by using the toggle if it's a <strong>feature</strong>, which will activate it. 15 - </p> 16 - <p> 17 - For example, say you want to play music; two options would be: (1) <a href="{{ ('themes/webamp/browser/facet/index.html') |> facetLoaderURL }}">browse</a> for a specific song and add it to the queue, or (2) <a href="{{ ('facets/playback/auto-queue/index.html') |> facetLoaderURL }}">automatically</a> add a bunch of shuffled songs to the queue. Next, you need a way to play the items you added to the queue. That's where a <a href="{{ ('themes/blur/artwork-controller/facet/index.html') |> facetLoaderURL }}">controller</a> could be used. 18 - </p> 19 - <p> 20 - <em>You might ask, why can't I do all of this in just one window? That's what <a href="themes/">themes</a> are for, if you need something more streamlined. If you however want a customised experience, or prefer certain interfaces for certain things with an infinite number of optional features, that's what facets are for.</em> 21 - </p> 22 - <p> 23 - <small><i class="ph-fill ph-info"></i> Every facet has access to your audio collection and your user data, along with any other shared state, be mindful of which facets you're interacting with.</small> 24 - </p> 25 - </section> 26 - 27 - <section> 28 - <h3>Concept</h3> 29 - 30 - <p> 31 - Facets are unlike traditional software; instead of combining several features into a single user interface and producing data output, we do the opposite, we start with the data and work our way up from there. 32 - </p> 33 - 34 - <p> 35 - <strong>It provides every user the ability to choose what features and interface they want to layer on top of their data.</strong> 36 - </p> 37 - 38 - <p> 39 - Facets can provide any amount of features and user interface they see fit. The key here is that the user can combine facets because the components that consist of it are connected, they broadcast their state and your data that has been updated. 40 - </p> 41 - 42 - <p> 43 - Finally, facets are just regular web pages so they can live wherever. We save them to the user-data storage that's configured and give the user the option to share it. This means that you can load facets from other people, <strong>building software cooperatively</strong>. 44 - </p> 45 - </section> 46 - </div>
-11
src/kitchen/index.vto
··· 1 - --- 2 - layout: layouts/kitchen.vto 3 - base: ../ 4 - title: Featured | Kitchen | Diffuse 5 - --- 6 - 7 - <h1 hidden>Featured</h1> 8 - 9 - <section> 10 - {{ await comp.facets.grid({ id: "featured", items: facets.filter(f => f.featured) }) }} 11 - </section>
src/kitchen/l/index.js src/l/index.js
+1 -1
src/kitchen/l/index.vto src/l/index.vto
··· 8 8 - styles/loader.css 9 9 10 10 scripts: 11 - - facets/l/index.js 11 + - l/index.js 12 12 --- 13 13 14 14 <div id="container">
+3 -3
src/kitchen/you.vto src/dashboard.vto
··· 1 1 --- 2 2 layout: layouts/kitchen.vto 3 - base: ../../ 4 - title: Your collection | Kitchen | Diffuse 3 + base: ../ 4 + title: Your collection | Diffuse 5 5 --- 6 6 7 7 <h1 hidden>Your collection</h1> ··· 11 11 <div id="list" data-rendered="f"> 12 12 <div class="with-icon"> 13 13 <i class="ph-bold ph-spinner animate-spin"></i> 14 - Loading items 14 + Loading your software 15 15 </div> 16 16 </div> 17 17 </section>
+8 -8
src/styles/diffuse/colors.css
··· 4 4 --color-2: oklch(98.369% 0.01834 67.664); 5 5 --color-3: oklch(26.787% 0.00168 186.65); 6 6 7 - /* Green */ 8 - --accent: hsl(82, 39%, 30.2%); 7 + /* Accent */ 8 + --accent: oklch(0.49552082493478433 0.08959833768631711 126.18581796616421); 9 9 10 10 /* Based on accent */ 11 11 --accent-twist-1: oklch(0.4394087182327507 0.07236154661033459 126.18581796616421); ··· 28 28 29 29 @media (prefers-color-scheme: dark) { 30 30 :root { 31 - --accent: #9e86b8; 31 + --accent: oklch(0.9588262863391736 0.06116191577190922 140.4794385054261); 32 32 33 33 /* Based on accent */ 34 - --accent-twist-1: oklch(0.8304625409973806 0.04463693607878053 306.25919097736113); 35 - --accent-twist-2: oklch(0.8598598403285739 0.0562638076130437 71.2592389751063); 36 - --accent-twist-3: oklch(0.9098598401854464 0.04000001621772218 71.25925756817853); 37 - --accent-twist-4: oklch(0.759859834391464 0.0968658786601377 181.2591638095228); 38 - --accent-twist-5: oklch(0.5798598316017731 0.08000002153797516 181.25916489227464); 34 + --accent-twist-1: oklch(0.990000017539932 0.040000057532729824 140.4794373213467); 35 + --accent-twist-2: oklch(0.8588262791075975 0.05264925002780416 250.47942154450243); 36 + --accent-twist-3: oklch(0.6788262741405414 0.07999998302468042 250.47944272057788); 37 + --accent-twist-4: oklch(0.758826283102734 0.08415801238169258 15.479456004219076); 38 + --accent-twist-5: oklch(0.4288262770395823 0.05680666043681931 15.47945273215004); 39 39 40 40 /* Derivatives */ 41 41 --bg-color: var(--color-3);
+21
src/styles/diffuse/page.css
··· 459 459 460 460 small { 461 461 font-size: var(--fs-xs); 462 + 463 + a { 464 + text-underline-offset: 3px; 465 + } 462 466 } 463 467 464 468 .button-col { ··· 576 580 opacity: 0.25; 577 581 } 578 582 } 583 + 584 + &.diffuse-logo-container--condensed { 585 + margin-bottom: var(--space-lg); 586 + } 579 587 } 580 588 581 589 /** ··· 614 622 text-decoration: none; 615 623 transition-duration: 250ms; 616 624 transition-property: opacity; 625 + 626 + &:hover, 627 + &:focus { 628 + opacity: 1; 629 + } 617 630 } 618 631 619 632 a.hidden { ··· 625 638 position: relative; 626 639 top: 1px; 627 640 } 641 + } 642 + 643 + #status--filler-container { 644 + align-items: center; 645 + display: flex; 646 + height: 100%; 647 + justify-content: end; 648 + padding: 0 var(--space-lg); 628 649 } 629 650 630 651 /**
-291
src/themes/index.js
··· 1 - import { html, render } from "lit-html"; 2 - import { keyed } from "lit-html/directives/keyed.js"; 3 - 4 - import { basicSetup, EditorView } from "codemirror"; 5 - import { css as langCss } from "@codemirror/lang-css"; 6 - import { html as langHtml } from "@codemirror/lang-html"; 7 - import { javascript as langJs } from "@codemirror/lang-javascript"; 8 - import { autocompletion } from "@codemirror/autocomplete"; 9 - 10 - import * as TID from "@atcute/tid"; 11 - 12 - import * as CID from "~/common/cid.js"; 13 - import foundation from "~/common/facets/foundation.js"; 14 - import { effect, signal } from "~/common/signal.js"; 15 - import { themeFromURI } from "~/common/themes/utils.js"; 16 - import { loadURI } from "~/common/loader.js"; 17 - 18 - /** 19 - * @import {Theme} from "~/definitions/types.d.ts" 20 - */ 21 - 22 - //////////////////////////////////////////// 23 - // SAVE & FORK 24 - //////////////////////////////////////////// 25 - 26 - document.body.addEventListener( 27 - "click", 28 - /** 29 - * @param {MouseEvent} event 30 - */ 31 - async (event) => { 32 - const target = /** @type {HTMLElement} */ (event.target); 33 - const rel = target.getAttribute("rel"); 34 - if (!rel) return; 35 - 36 - const uri = target.closest("li")?.getAttribute("data-uri"); 37 - if (!uri) return; 38 - 39 - const name = target.closest("li")?.getAttribute("data-name"); 40 - if (!name) return; 41 - 42 - switch (rel) { 43 - case "fork": { 44 - const theme = await themeFromURI({ name, uri }, { fetchHTML: true }); 45 - editTheme(theme); 46 - document.querySelector("#build")?.scrollIntoView(); 47 - break; 48 - } 49 - case "save": { 50 - const theme = await themeFromURI({ name, uri }, { fetchHTML: false }); 51 - const out = await foundation.orchestrator.output(); 52 - 53 - const col = out.themes.collection(); 54 - out.themes.save([ 55 - ...(col.state === "loaded" ? col.data : []), 56 - theme, 57 - ]); 58 - break; 59 - } 60 - } 61 - }, 62 - ); 63 - 64 - //////////////////////////////////////////// 65 - // YOUR COLLECTION 66 - //////////////////////////////////////////// 67 - 68 - /** @type {HTMLElement | null} */ 69 - const listEl = document.querySelector("#list"); 70 - if (!listEl) throw new Error("List element not found"); 71 - 72 - listEl.innerHTML = ""; 73 - 74 - const output = await foundation.orchestrator.output(); 75 - 76 - effect(() => { 77 - const themesCol = output.themes.collection(); 78 - const col = themesCol.state === "loaded" 79 - ? [...themesCol.data].sort((a, b) => { 80 - return a.name.toLocaleLowerCase().localeCompare(b.name.toLocaleLowerCase()); 81 - }) 82 - : []; 83 - 84 - const h = col.length && themesCol.state === "loaded" 85 - ? html` 86 - <ul> 87 - ${col.map((c) => 88 - keyed( 89 - c.id, 90 - html` 91 - <li> 92 - <div style="position: relative;"> 93 - <a href="themes/l/?id=${c.id}"> 94 - ${c.name} 95 - </a> 96 - <button 97 - class="button--fixed button--transparent" 98 - popovertarget="theme-menu-col-${c.id}" 99 - style="anchor-name: --theme-anchor-col-${c 100 - .id}; position: absolute; right: 0; top: 50%; transform: translateY(-50%);" 101 - > 102 - <i class="ph-fill ph-dots-three-circle"></i> 103 - </button> 104 - </div> 105 - <div class="list-description"> 106 - ${c.uri && !c.html 107 - ? html` 108 - <span class="with-icon"> 109 - <i class="ph-fill ph-binoculars"></i> 110 - <span>Tracking the original <a href="${c 111 - .uri}">URI</a></span> 112 - </span> 113 - ` 114 - : html` 115 - <span class="with-icon"> 116 - <i class="ph-fill ph-code"></i> 117 - <span>Custom code</span> 118 - </span> 119 - `} 120 - </div> 121 - 122 - <!-- Dropdown Menu --> 123 - <div 124 - id="theme-menu-col-${c.id}" 125 - class="dropdown" 126 - style="position-anchor: --theme-anchor-col-${c.id}" 127 - popover 128 - > 129 - <a href="themes/l/?id=${c.id}"> 130 - <span class="with-icon"> 131 - <i class="ph-fill ph-globe"></i> Open 132 - </span> 133 - </a> 134 - <a @click="${() => editTheme(c)}"> 135 - <span class="with-icon"> 136 - <i class="ph-fill ph-cursor-text"></i> Edit 137 - </span> 138 - </a> 139 - <a @click="${deleteTheme({ id: c.id })}"> 140 - <span class="with-icon"> 141 - <i class="ph-fill ph-eraser"></i> Delete 142 - </span> 143 - </a> 144 - </div> 145 - </li> 146 - `, 147 - ) 148 - )} 149 - </ul> 150 - ` 151 - : themesCol.state === "loaded" 152 - ? emptyThemesList 153 - : html` 154 - <div class="with-icon" style="font-size: var(--fs-sm);"> 155 - <i class="ph-bold ph-spinner-gap"></i> 156 - Loading items 157 - </div> 158 - `; 159 - 160 - render(h, listEl); 161 - }); 162 - 163 - const emptyThemesList = html` 164 - <p style="margin-bottom: 0;"> 165 - <i class="ph-fill ph-info"></i> You have not saved any themes yet. 166 - </p> 167 - `; 168 - 169 - /** 170 - * @param {{ id: string }} _ 171 - */ 172 - function deleteTheme({ id }) { 173 - return () => { 174 - const c = confirm("Are you sure you want to delete this theme?"); 175 - if (!c) return; 176 - 177 - const col = output.themes.collection(); 178 - if (col.state !== "loaded") return; 179 - output.themes.save(col.data.filter((c) => !(c.id === id))); 180 - }; 181 - } 182 - 183 - //////////////////////////////////////////// 184 - // BUILD 185 - //////////////////////////////////////////// 186 - 187 - const $editingTheme = signal(/** @type {Theme | null} */ (null)); 188 - 189 - // Code editor 190 - const editorContainer = document.body.querySelector("#html-input-container"); 191 - if (!editorContainer) throw new Error("Editor container not found"); 192 - 193 - const editor = new EditorView({ 194 - parent: editorContainer, 195 - doc: ``.trim(), 196 - extensions: [ 197 - basicSetup, 198 - langHtml(), 199 - langCss(), 200 - langJs(), 201 - autocompletion(), 202 - ], 203 - }); 204 - 205 - // Form submit 206 - document.querySelector("#build-form")?.addEventListener( 207 - "submit", 208 - onBuildSubmit, 209 - ); 210 - 211 - /** 212 - * @param {Event} event 213 - */ 214 - async function onBuildSubmit(event) { 215 - event.preventDefault(); 216 - 217 - const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 218 - "#name-input", 219 - )); 220 - 221 - const html = editor.state.doc.toString(); 222 - const cid = await CID.create(0x55, new TextEncoder().encode(html)); 223 - const name = nameEl?.value ?? "nameless"; 224 - 225 - /** @type {Theme} */ 226 - const theme = $editingTheme.value 227 - ? { 228 - ...$editingTheme.value, 229 - cid, 230 - html, 231 - name, 232 - } 233 - : { 234 - $type: "sh.diffuse.output.theme", 235 - id: TID.now(), 236 - cid, 237 - html, 238 - name, 239 - }; 240 - 241 - switch (/** @type {any} */ (event).submitter.name) { 242 - case "save": 243 - await saveTheme(theme); 244 - break; 245 - case "save+open": 246 - await saveTheme(theme); 247 - globalThis.open(`./themes/l/?id=${theme.id}`, "blank"); 248 - break; 249 - } 250 - } 251 - 252 - /** 253 - * @param {Theme} ogTheme 254 - */ 255 - async function editTheme(ogTheme) { 256 - const theme = { ...ogTheme }; 257 - const nameEl = /** @type {HTMLInputElement | null} */ (document.querySelector( 258 - "#name-input", 259 - )); 260 - 261 - if (!nameEl) return; 262 - 263 - // Make sure HTML is loaded 264 - if (!theme.html && theme.uri) { 265 - const html = await loadURI(theme.uri); 266 - const cid = await CID.create(0x55, new TextEncoder().encode(html)); 267 - 268 - theme.html = html; 269 - theme.cid = cid; 270 - } 271 - 272 - $editingTheme.value = theme; 273 - nameEl.value = theme.name; 274 - 275 - editor.dispatch({ 276 - changes: { from: 0, to: editor.state.doc.length, insert: theme.html }, 277 - }); 278 - } 279 - 280 - /** 281 - * @param {Theme} theme 282 - */ 283 - async function saveTheme(theme) { 284 - const col = output.themes.collection(); 285 - const data = col.state === "loaded" ? col.data : []; 286 - const colWithoutId = data.filter((c) => c.id !== theme.id); 287 - await output.themes.save([...colWithoutId, { 288 - ...theme, 289 - updatedAt: new Date().toISOString(), 290 - }]); 291 - }
-130
src/themes/index.vto
··· 1 - --- 2 - layout: layouts/diffuse.vto 3 - base: ../ 4 - 5 - styles: 6 - - styles/base.css 7 - - styles/diffuse/page.css 8 - - vendor/@phosphor-icons/bold/style.css 9 - - vendor/@phosphor-icons/fill/style.css 10 - 11 - scripts: 12 - - themes/index.js 13 - - common/pages/version-upgrade.js 14 - 15 - # THEMES 16 - 17 - builtIn: 18 - - url: "themes/blur/" 19 - title: "Blur" 20 - todo: true 21 - desc: > 22 - **A theme with an Apple-inspired playback view.** Features two audio players instead of the usual one. 23 - - url: "themes/webamp/" 24 - title: "Webamp" 25 - desc: > 26 - **Winamp 2 + Windows 98**. Uses Webamp as the audio player connected to various Diffuse elements. Also features a desktop-like Windows 98 environment in which you can open "programs" that control the used Diffuse elements. 27 - --- 28 - 29 - <header> 30 - <div> 31 - <div class="diffuse-logo-container"> 32 - <a href="./" style="display: inline-block;"> 33 - {{ await comp.diffuse.logo() }} 34 - </a> 35 - {{ await comp.diffuse.status() }} 36 - </div> 37 - 38 - <p class="construct dither-mask" style="margin-top: 0"> 39 - Themes 40 - </p> 41 - 42 - <p> 43 - Themes are element compositions and provide a traditional browser web application way of 44 - using them. In other words, pretty much the whole thing, besides your data, lives inside a single browser tab. 45 - </p> 46 - 47 - <p> 48 - <small> 49 - <strong><i class="ph-fill ph-info"></i></strong> 50 - Each theme is unique, not just a skin; each one might have a totally different feature set. 51 - </small> 52 - </p> 53 - 54 - <ul class="table-of-contents"> 55 - <li><a href="themes/#built-in">Built-in</a></li> 56 - <li><a href="themes/#community">Community</a></li> 57 - <li><a href="themes/#collection">Your collection</a></li> 58 - <li><a href="themes/#build">Build</a></li> 59 - </ul> 60 - </div> 61 - <div class="dither-mask filler"></div> 62 - </header> 63 - <main> 64 - <!-- BUILT-IN --> 65 - <div class="columns"> 66 - <section class="flex"> 67 - <h2 id="built-in">Built-in</h2> 68 - 69 - {{ await comp.list({ items: builtIn }) }} 70 - </section> 71 - 72 - <section class="flex"> 73 - <h2 id="community">Community</h2> 74 - <p> 75 - Check out some themes from the community and load them here. 76 - </p> 77 - <p> 78 - <small><i class="ph-fill ph-info"></i> Nothing here yet, too early.</small> 79 - </p> 80 - </section> 81 - </div> 82 - 83 - <!-- YOUR COLLECTION --> 84 - <div class="columns"> 85 - <section class="flex"> 86 - <h2 id="collection">Your collection</h2> 87 - <div id="list"> 88 - <div class="with-icon" style="font-size: var(--fs-sm);"> 89 - <i class="ph-bold ph-spinner-gap"></i> 90 - Loading items 91 - </div> 92 - </div> 93 - </section> 94 - 95 - <section class="flex"></section> 96 - </div> 97 - 98 - <!-- / --> 99 - <div class="dither-mask filler" style="height: var(--space-2xl); margin-top: var(--space-2xl);"></div> 100 - 101 - <!-- BUILD --> 102 - <section> 103 - <h2 id="build">Build</h2> 104 - 105 - <form id="build-form" class="columns"> 106 - <div class="flex"> 107 - <p style="margin-top: 0"> 108 - If you know a bit of HTML & Javascript, you can write your own or plug in some code you found elsewhere: 109 - </p> 110 - 111 - <div id="html-input-container" class="code-editor monospace-font"> 112 - </div> 113 - </div> 114 - 115 - <div class="flex"> 116 - <p style="margin-top: 0"> 117 - Your code here will be loaded in a dedicated page, it'll be injected into a <code>&lt;iframe&gt;</code> element in the body. You have access to the elements listed on the <a href="./#elements">index page</a> and the facets <a href="facets/#foundation">foundation</a>. 118 - </p> 119 - <input id="name-input" type="text" placeholder="Name" name="name" required /> 120 - <p> 121 - <span class="button-row"> 122 - <button name="save">Save</button> 123 - <button name="save+open">Save &amp; Open</button> 124 - </span> 125 - </p> 126 - </div> 127 - </form> 128 - </section> 129 - 130 - </main>
-12
src/themes/l/index.css
··· 1 - body { 2 - margin: 0; 3 - overflow: hidden; 4 - padding: 0; 5 - } 6 - 7 - iframe { 8 - border: 0; 9 - display: block; 10 - height: 100dvh; 11 - width: 100%; 12 - }
-33
src/themes/l/index.js
··· 1 - import foundation from "~/common/facets/foundation.js"; 2 - import * as CID from "~/common/cid.js"; 3 - import { createLoader, renderError } from "~/common/loader.js"; 4 - 5 - const output = await foundation.orchestrator.output(); 6 - 7 - createLoader({ 8 - $type: "sh.diffuse.output.theme", 9 - label: "Theme", 10 - source: () => output.themes, 11 - async render(theme) { 12 - if (theme.cid) { 13 - const valid = await CID.verify( 14 - new TextEncoder().encode(theme.html ?? ""), 15 - theme.cid, 16 - ); 17 - 18 - if (!valid) { 19 - renderError( 20 - document.body, 21 - "CID mismatch: HTML content does not match the CID", 22 - ); 23 - return; 24 - } 25 - } 26 - 27 - const iframe = document.createElement("iframe"); 28 - iframe.srcdoc = theme.html ?? ""; 29 - 30 - document.body.innerHTML = ""; 31 - document.body.append(iframe); 32 - }, 33 - });
-21
src/themes/l/index.vto
··· 1 - --- 2 - layout: layouts/diffuse.vto 3 - base: ../../ 4 - 5 - styles: 6 - - vendor/@phosphor-icons/fill/style.css 7 - - styles/base.css 8 - - styles/loader.css 9 - - themes/l/index.css 10 - 11 - scripts: 12 - - themes/l/index.js 13 - --- 14 - 15 - <div id="container"> 16 - <div class="diffuse"> 17 - <div id="diffuse-loader" class="flex"> 18 - <i class="ph-fill ph-music-notes animate-bounce"></i> 19 - </div> 20 - </div> 21 - </div>