Experiment to rebuild Diffuse using web applets.
0
fork

Configure Feed

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

feat: reorg & add new concepts

+219 -198
+15
src/components/Applet.astro
··· 1 + --- 2 + import List from "../components/List.astro"; 3 + 4 + const { list, title } = Astro.props; 5 + --- 6 + 7 + <div class="applet"> 8 + <h3>{title}</h3> 9 + 10 + <p> 11 + <em><slot /></em> 12 + </p> 13 + 14 + <List items={list} /> 15 + </div>
+17
src/components/List.astro
··· 1 + --- 2 + const { items } = Astro.props; 3 + --- 4 + 5 + <ul> 6 + { 7 + items.map((item: { title: string; url: string }) => ( 8 + <li> 9 + {item.title.startsWith("(TODO) ") ? ( 10 + <span>{item.title}</span> 11 + ) : ( 12 + <a href={item.url}>{item.title}</a> 13 + )} 14 + </li> 15 + )) 16 + } 17 + </ul>
+4 -4
src/pages/configurator/storage/output/_applet.astro src/pages/configurator/output/_applet.astro
··· 57 57 type ListItem<M> = { activated: boolean; icon: string; method: M; title: string }; 58 58 59 59 const DEFAULT_METHOD: Method = "browser"; 60 - const LOCALSTORAGE_KEY = "applets/configurator/storage/output/active-storage"; 61 - const CUSTOM_KEY = "applets/configurator/storage/output/custom-applet"; 60 + const LOCALSTORAGE_KEY = "applets/configurator/output/active-output"; 61 + const CUSTOM_KEY = "applets/configurator/output/custom-applet"; 62 62 63 63 const h = ( 64 64 tag: string, ··· 87 87 // Applet connections 88 88 const storage = { 89 89 output: { 90 - indexedDB: await applet("../../../storage/output/indexed-db/", { container }), 91 - nativeFs: await applet("../../../storage/output/native-fs/", { container }), 90 + indexedDB: await applet("../../../output/indexed-db/", { container }), 91 + nativeFs: await applet("../../../output/native-fs/", { container }), 92 92 }, 93 93 }; 94 94
+2 -2
src/pages/configurator/storage/output/_manifest.json src/pages/configurator/output/_manifest.json
··· 1 1 { 2 - "name": "diffuse/configurator/storage/output", 3 - "title": "Diffuse Configurator | Storage | Output", 2 + "name": "diffuse/configurator/output", 3 + "title": "Diffuse Configurator | Output", 4 4 "entrypoint": "index.html", 5 5 "actions": { 6 6 "get": {
src/pages/configurator/storage/output/index.astro src/pages/configurator/output/index.astro
+97 -113
src/pages/index.astro
··· 1 1 --- 2 + import Applet from "../components/Applet.astro"; 3 + import List from "../components/List.astro"; 2 4 import Page from "../layouts/page.astro"; 3 5 4 - // import "@picocss/pico/css/pico.colors.css"; 5 6 import "../styles/pages/index.css"; 6 7 7 - const configurators = [{ url: "configurator/storage/output/", title: "Storage / Output" }]; 8 + // Types 9 + type Ref = { 10 + url: string; 11 + title: string; 12 + }; 13 + 14 + // Links 15 + const WEB_APPLETS_HREF = "https://unternet.co/docs/web-applets/introduction"; 16 + 17 + // Themes 18 + const themes = [{ url: "themes/pilot/", title: "Pilot" }]; 19 + 20 + // Abstractions 21 + // TODO 22 + 23 + // Applets 24 + const configurators = [{ url: "configurator/output/", title: "Output" }]; 8 25 9 26 const engines = [ 10 27 { url: "engine/audio/", title: "Audio" }, 11 28 { url: "engine/queue/", title: "Queue" }, 12 29 ]; 13 30 31 + const input = [ 32 + { url: "input/native-fs", title: "(TODO) Native File System" }, 33 + { url: "input/s3-compatible", title: "(TODO) S3-Compatible API" }, 34 + ]; 35 + 14 36 const orchestrators = [ 15 - { url: "orchestrator/queue/", title: "Queue" }, 16 - { url: "orchestrator/storage/", title: "Storage" }, 37 + { url: "orchestrator/output-management/", title: "Output management" }, 38 + { url: "orchestrator/single-queue/", title: "Single queue" }, 17 39 ]; 18 40 19 - const storages = [ 20 - { url: "storage/output/indexed-db", title: "Output / IndexedDB" }, 21 - { url: "storage/output/native-fs", title: "Output / Native File System" }, 41 + const output = [ 42 + { url: "output/indexed-db", title: "IndexedDB" }, 43 + { url: "output/native-fs", title: "Native File System" }, 22 44 ]; 23 45 24 - const processors = []; 25 - 26 - const themes = [{ url: "themes/pilot/", title: "Pilot" }]; 46 + const processors = [ 47 + { url: "processor/artwork", title: "(TODO) Artwork fetcher" }, 48 + { url: "processor/http-metadata", title: "(TODO) HTTP(S) metadata fetcher" }, 49 + ]; 27 50 --- 28 51 29 52 <Page title="Diffuse"> ··· 36 59 href="/images/diffuse-current.svg#diffuse"></use> 37 60 </svg> 38 61 </h1> 62 + <p> 63 + Diffuse is a collection of <a href={WEB_APPLETS_HREF}>web applets</a> that make it possible to 64 + listen to audio from various sources on your devices and the web, and to create the ideal digital 65 + listening experience for you. 66 + </p> 67 + <p> 68 + These applets can be used in various ways. The main ways so far are: (a) through <a 69 + href="#themes">themes</a 70 + >, a traditional browser (web application) approach, and (b) <a href="#abstractions" 71 + >abstractions</a 72 + > for non-browser systems. 73 + </p> 39 74 </header> 40 75 <main> 76 + <!-- THEMES --> 41 77 <section> 42 - <h2>Themes</h2> 78 + <h2 id="themes">Themes</h2> 43 79 44 80 <p> 45 - <em 46 - >Themes are “applet compositions” and opt for a traditional way of interacting with them, 47 - like a regular (web) app:</em 48 - > 81 + Themes are “applet compositions” and provide a traditional browser web application way of 82 + using them. Each theme is unique, not just a skin (eg. not like winamp skins). 49 83 </p> 50 84 51 - <ul> 52 - { 53 - themes.map((item: any) => ( 54 - <li> 55 - <a href={item.url}>{item.title}</a> 56 - </li> 57 - )) 58 - } 59 - </ul> 60 - </section> 61 - 62 - <section> 63 - <h2>Applets</h2> 64 - 65 - <h3>Configurators</h3> 66 - 67 85 <p> 68 - <em 69 - >Applets that serve as an intermediate in order to make a particular kind of applet 70 - configurable. In other words, these allow for an applet to be swapped out with another 71 - that takes the same actions and data output.</em 72 - > 86 + For example, most themes here will limit the currently playing audio tracks to one item, but 87 + you might as well create a DJ theme that can play multiple items at the same time. 73 88 </p> 74 89 75 - <ul> 76 - { 77 - configurators.map((item: any) => ( 78 - <li> 79 - <a href={item.url}>{item.title}</a> 80 - </li> 81 - )) 82 - } 83 - </ul> 90 + <List items={themes} /> 91 + </section> 84 92 85 - <h3>Engines</h3> 93 + <!-- ABSTRACTIONS --> 94 + <section> 95 + <h2 id="abstractions">Abstractions</h2> 86 96 87 97 <p> 88 - <em 89 - >Applets with each a singular purpose and don't have any UI. There are specialised UI 90 - applets in themes that control these.</em 91 - > 98 + These are applet configurations that enable certain use cases outside the traditional web 99 + app experience. Just like themes, these include various assumptions of how certain parts of 100 + the system should interact. 92 101 </p> 93 102 94 - <ul> 95 - { 96 - engines.map((item: any) => ( 97 - <li> 98 - <a href={item.url}>{item.title}</a> 99 - </li> 100 - )) 101 - } 102 - </ul> 103 + <p><em>TODO: Enable intelligent user (ai) agent use-case.</em></p> 104 + 105 + <List items={[]} /> 106 + </section> 103 107 104 - <h3>Orchestrators</h3> 108 + <!-- APPLETS --> 109 + <section> 110 + <h2 id="applets">Applets</h2> 105 111 106 112 <p> 107 - <em 108 - >These too are applet compositions. However, unlike themes, these are purely logical, and 109 - reuse applet instances from the parent context (when available). Mostly exist in order to 110 - construct sensible defaults (eg. applet connections you want to reuse across themes).</em 111 - > 113 + Applets are <a href={WEB_APPLETS_HREF}>web applets</a>, the components of the system. These 114 + are then recombined into an entire music player experience, or whatever you want to build. 112 115 </p> 113 116 114 - <ul> 115 - { 116 - orchestrators.map((item: any) => ( 117 - <li> 118 - <a href={item.url}>{item.title}</a> 119 - </li> 120 - )) 121 - } 122 - </ul> 117 + <div class="columns"> 118 + <Applet title="Configurators" list={configurators}> 119 + Applets that serve as an intermediate in order to make a particular kind of applet 120 + configurable. In other words, these allow for an applet to be swapped out with another 121 + that takes the same actions and data output. 122 + </Applet> 123 123 124 - <h3>Processors</h3> 124 + <Applet title="Engines" list={engines}> 125 + Applets with each a singular purpose and don't have any UI. There are specialised UI 126 + applets in themes that control these. 127 + </Applet> 125 128 126 - <p> 127 - <em 128 - >These applets interact with the bytes provided by the data storage applets, or provide 129 - to. This processed data can then be passed on to, for example, the UI layer and engine 130 - applets.</em 131 - > 132 - </p> 129 + <Applet title="Input" list={input}> 130 + Inputs are sources of audio tracks. Each track is an entry in the list of possible items 131 + to play. These can be files or streams. Or in other words, static or dynamic. 132 + </Applet> 133 133 134 - <ul> 135 - { 136 - processors.map((item: any) => ( 137 - <li> 138 - <a href={item.url}>{item.title}</a> 139 - </li> 140 - )) 141 - } 142 - </ul> 143 - 144 - <h3>Storages</h3> 145 - 146 - <p> 147 - <em 148 - >Input and output managers of the system. Where input is audio files or streams, and 149 - output is derived data such as a music playlist and your processed input (tracks).</em 150 - > 151 - </p> 134 + <Applet title="Orchestrators" list={orchestrators}> 135 + These too are applet compositions. However, unlike themes, these are purely logical, and 136 + reuse applet instances from the parent context (when available). Mostly exist in order to 137 + construct sensible defaults to use across themes and abstractions. 138 + </Applet> 152 139 153 - <ul> 154 - { 155 - storages.map((item: any) => ( 156 - <li> 157 - <a href={item.url}>{item.title}</a> 158 - </li> 159 - )) 160 - } 161 - </ul> 140 + <Applet title="Output" list={output}> 141 + Output is application-derived data such as playlists. These applets can receive such data 142 + and keep it around. 143 + </Applet> 162 144 163 - <h3>Supplements</h3> 145 + <Applet title="Processors" list={processors}> 146 + These applets interact with the bytes provided by the input applets. This processed data 147 + can then be passed on to other applets. 148 + </Applet> 164 149 165 - <p> 166 - <em>Additional applets, such as scrobblers.</em> 167 - </p> 150 + <Applet title="Supplements" list={[]}>Additional applets, such as scrobblers.</Applet> 151 + </div> 168 152 </section> 169 153 </main> 170 154 </Page>
+6
src/pages/orchestrator/output-management/_manifest.json
··· 1 + { 2 + "name": "diffuse/orchestrator/output-management", 3 + "title": "Diffuse Orchestrator | Output management", 4 + "entrypoint": "index.html", 5 + "actions": {} 6 + }
+3 -3
src/pages/orchestrator/queue/_applet.astro src/pages/orchestrator/single-queue/_applet.astro
··· 20 20 }; 21 21 22 22 const orchestrator = { 23 - storage: await applet<Output>("../../orchestrator/storage", { context: self.parent }), 23 + output: await applet<Output>("../../orchestrator/output-management", { context: self.parent }), 24 24 }; 25 25 26 26 //////////////////////////////////////////// ··· 33 33 // into a usable audio URL. 34 34 engine.queue.sendAction( 35 35 "add", 36 - orchestrator.storage.data.tracks.map((track: Track) => { 36 + orchestrator.output.data.tracks.map((track: Track) => { 37 37 return { 38 38 expiresAt: Infinity, 39 39 id: track.id, ··· 101 101 // 📦 STORAGE 102 102 //////////////////////////////////////////// 103 103 reactive( 104 - orchestrator.storage, 104 + orchestrator.output, 105 105 (data) => (data ? comparable(data.tracks) : undefined), 106 106 (hash) => { 107 107 if (hash) fill();
-11
src/pages/orchestrator/queue/_manifest.json
··· 1 - { 2 - "name": "diffuse/orchestrator/queue", 3 - "title": "Diffuse Orchestrator | Queue", 4 - "entrypoint": "index.html", 5 - "actions": { 6 - "fill": { 7 - "title": "Fill", 8 - "description": "Fill up the queue." 9 - } 10 - } 11 - }
src/pages/orchestrator/queue/index.astro src/pages/orchestrator/single-queue/index.astro
+11
src/pages/orchestrator/single-queue/_manifest.json
··· 1 + { 2 + "name": "diffuse/orchestrator/single-queue", 3 + "title": "Diffuse Orchestrator | Single queue", 4 + "entrypoint": "index.html", 5 + "actions": { 6 + "fill": { 7 + "title": "Fill", 8 + "description": "Fill up the queue." 9 + } 10 + } 11 + }
+7 -9
src/pages/orchestrator/storage/_applet.astro src/pages/orchestrator/output-management/_applet.astro
··· 13 13 14 14 // Applet connections 15 15 const configurator = { 16 - storage: { 17 - output: await applet("../../configurator/storage/output", { context: self.parent }), 18 - }, 16 + output: await applet("../../configurator/output", { context: self.parent }), 19 17 }; 20 18 21 19 // Sample content ··· 56 54 //////////////////////////////////////////// 57 55 async function loadSources(): Promise<Source[]> { 58 56 // TODO: This is not concurrency safe! 59 - await configurator.storage.output.sendAction("get", { 57 + await configurator.output.sendAction("get", { 60 58 name: "sources.json", 61 59 }); 62 60 63 - const data = configurator.storage.output.data; 61 + const data = configurator.output.data; 64 62 65 63 if (!data) { 66 64 saveSources([SAMPLE_SOURCE]); ··· 72 70 73 71 async function loadTracks(): Promise<Track[]> { 74 72 // TODO: This is not concurrency safe! 75 - await configurator.storage.output.sendAction("get", { 73 + await configurator.output.sendAction("get", { 76 74 name: "tracks.json", 77 75 }); 78 76 79 - const data = configurator.storage.output.data; 77 + const data = configurator.output.data; 80 78 81 79 if (!data) { 82 80 saveTracks(SAMPLE_TRACKS); ··· 96 94 function saveSources(sources: Source[]) { 97 95 const data = encode(sources); 98 96 99 - configurator.storage.output.sendAction("put", { 97 + configurator.output.sendAction("put", { 100 98 name: "sources.json", 101 99 data, 102 100 }); ··· 105 103 function saveTracks(tracks: Track[]) { 106 104 const data = encode(tracks); 107 105 108 - configurator.storage.output.sendAction("put", { 106 + configurator.output.sendAction("put", { 109 107 name: "tracks.json", 110 108 data, 111 109 });
-6
src/pages/orchestrator/storage/_manifest.json
··· 1 - { 2 - "name": "diffuse/orchestrator/storage", 3 - "title": "Diffuse Orchestrator | Storage", 4 - "entrypoint": "index.html", 5 - "actions": {} 6 - }
src/pages/orchestrator/storage/index.astro src/pages/orchestrator/output-management/index.astro
+1 -1
src/pages/storage/output/indexed-db/_applet.astro src/pages/output/indexed-db/_applet.astro
··· 7 7 //////////////////////////////////////////// 8 8 // SETUP 9 9 //////////////////////////////////////////// 10 - const IDB_PREFIX = "@applets/storage/output/indexed-db"; 10 + const IDB_PREFIX = "@applets/output/indexed-db"; 11 11 const context = applets.register(); 12 12 13 13 ////////////////////////////////////////////
+2 -2
src/pages/storage/output/indexed-db/_manifest.json src/pages/output/native-fs/_manifest.json
··· 1 1 { 2 - "name": "diffuse/storage/output/indexed-db", 3 - "title": "Diffuse Storage | Output | IndexedDB", 2 + "name": "diffuse/output/native-fs", 3 + "title": "Diffuse Output | Native File System", 4 4 "entrypoint": "index.html", 5 5 "actions": { 6 6 "get": {
src/pages/storage/output/indexed-db/index.astro src/pages/output/indexed-db/index.astro
+3 -3
src/pages/storage/output/native-fs/_applet.astro src/pages/output/native-fs/_applet.astro
··· 8 8 //////////////////////////////////////////// 9 9 // SETUP 10 10 //////////////////////////////////////////// 11 - const IDB_PREFIX = "@applets/storage/output/native-fs"; 11 + const IDB_PREFIX = "@applets/output/native-fs"; 12 12 const IDB_DEVICE_KEY = `${IDB_PREFIX}/device`; 13 13 14 14 const context = applets.register(); ··· 17 17 // ACTIONS 18 18 //////////////////////////////////////////// 19 19 const get: OutputGetter = async ({ name }) => { 20 - const handle: FileSystemDirectoryHandle | null = await IDB.get(IDB_DEVICE_KEY); 20 + const handle = await IDB.get(IDB_DEVICE_KEY); 21 21 if (!handle) throw new Error("Storage not configured properly, handle not found."); 22 22 23 23 try { ··· 34 34 }; 35 35 36 36 const put: OutputSetter = async ({ data, name }) => { 37 - const handle: FileSystemDirectoryHandle | null = await IDB.get(IDB_DEVICE_KEY); 37 + const handle = await IDB.get(IDB_DEVICE_KEY); 38 38 if (!handle) throw new Error("Storage not configured properly, handle not found."); 39 39 const fileHandle = await handle.getFileHandle(name, { create: true }); 40 40 const stream = await fileHandle.createWritable();
+2 -2
src/pages/storage/output/native-fs/_manifest.json src/pages/output/indexed-db/_manifest.json
··· 1 1 { 2 - "name": "diffuse/storage/output/native-fs", 3 - "title": "Diffuse Storage | Output | Native File System", 2 + "name": "diffuse/output/indexed-db", 3 + "title": "Diffuse Output | IndexedDB", 4 4 "entrypoint": "index.html", 5 5 "actions": { 6 6 "get": {
src/pages/storage/output/native-fs/index.astro src/pages/output/native-fs/index.astro
+4 -16
src/scripts/themes/pilot/index.ts
··· 15 15 import type * as AudioUI from "@applets/themes/pilot/ui/audio/types.d.ts"; 16 16 17 17 const _configurator = { 18 - storage: { 19 - output: await applet("../../configurator/storage/output"), 20 - }, 18 + output: await applet("../../configurator/output"), 21 19 }; 22 20 23 21 const engine = { ··· 26 24 }; 27 25 28 26 const _orchestrator = { 29 - queue: await applet("../../orchestrator/queue"), 30 - storage: await applet<Output>("../../orchestrator/storage"), 27 + queue: await applet("../../orchestrator/single-queue"), 28 + output: await applet<Output>("../../orchestrator/output-management"), 31 29 }; 32 30 33 31 const ui = { 34 32 audio: await applet<AudioUI.State>("ui/audio", { setHeight: true }), 35 33 }; 36 34 37 - // NOTE: 38 - // Themes are just limited imaginations. 39 - // 40 - // For example, this theme limits the currently playing audio to one item. 41 - // But you might as well create a DJ "theme" that plays multiple items at 42 - // the same time. With that in mind, you could abstract things here that are 43 - // reused across multiple themes. But it might also be good to keep the code 44 - // repetition because there are some defaults hidden in here. 45 - 46 35 //////////////////////////////////////////// 47 36 // ⚙️ [Connections → Engines] 48 37 // 🔉 AUDIO ··· 53 42 54 43 reactive( 55 44 engine.audio, 56 - (data) => 57 - data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false, 45 + (data) => data.items[engine.queue.data.now?.id ?? Infinity]?.isPlaying ?? false, 58 46 (isPlaying) => ui.audio.sendAction("modifyIsPlaying", isPlaying), 59 47 ); 60 48
+43 -24
src/styles/pages/index.css
··· 5 5 font-size: var(--fs-base); 6 6 7 7 /* Colors */ 8 - --palm-leaf: #32472b; 9 - --mana-tree: #4c7541; 10 - --beyond-the-pines: #6c824a; 11 - --rural-green: #89854f; 12 - --wasteland: #a18a5a; 13 - --incense: #ac9b7d; 8 + --color-1: oklch(4.1308% 0.25306 109.22); 9 + --color-2: oklch(98.369% 0.01834 67.664); 10 + /* --accent: oklch(57.002% 0.25722 258.17); */ 11 + --accent: oklch(86.947% 0.25527 28.789); 12 + 13 + --bg-color: var(--color-2); 14 + --text-color: var(--color-1); 15 + } 16 + 17 + @media (prefers-color-scheme: dark) { 18 + /* TODO: */ 19 + /* :root { 20 + --bg-color: oklch(from var(--color-1) calc(l + 0.125) c h); 21 + --text-color: var(--color-2); 22 + } */ 14 23 } 15 24 16 25 @supports (font-variation-settings: normal) { 17 26 :root { 18 27 font-family: "InterVariable", sans-serif; 19 28 font-feature-settings: 20 - "ss03" 2, 21 - "ss02" 2; 29 + "zero" 2, 30 + "ss03" 2; 22 31 font-optical-sizing: auto; 23 32 } 24 33 } 25 34 26 35 body { 27 - background: var(--palm-leaf); 28 - color: var(--incense); 36 + background-color: var(--bg-color); 37 + color: var(--text-color); 29 38 } 30 39 31 40 header, ··· 33 42 margin: var(--space-md) var(--space-lg); 34 43 } 35 44 36 - main { 37 - display: flex; 38 - flex-wrap: wrap; 39 - gap: 0 var(--space-3xl); 40 - } 41 - 42 - main > section { 43 - /* flex: 1; */ 44 - min-width: min(var(--container-xs), 100%); 45 - width: 32.5%; 46 - } 47 - 48 45 a { 49 46 color: inherit; 50 47 text-underline-offset: 6px; 51 48 } 52 49 53 50 h1 svg { 54 - fill: oklch(from var(--palm-leaf) calc(l + 0.125) c h); 51 + fill: oklch(from var(--bg-color) calc(l - 0.5) c h); 52 + opacity: 0.125; 53 + 54 + @media (prefers-color-scheme: dark) { 55 + & { 56 + fill: var(--text-color); 57 + opacity: 0.375; 58 + } 59 + } 55 60 } 56 61 57 62 h2 { 58 - color: var(--beyond-the-pines); 63 + /* color: oklch(from var(--bg-color) calc(l - 0.25) c h); */ 64 + color: var(--accent); 65 + 59 66 font-size: var(--fs-xl); 60 67 font-weight: 900; 68 + letter-spacing: -0.0125em; 61 69 line-height: 1; 62 70 margin: var(--space-2xl) 0 var(--space-md); 63 71 text-transform: uppercase; ··· 86 94 margin: var(--space-sm) 0; 87 95 max-width: var(--container-sm); 88 96 } 97 + 98 + .columns { 99 + display: flex; 100 + flex-wrap: wrap; 101 + gap: 0 var(--space-3xl); 102 + } 103 + 104 + .applet { 105 + min-width: min(var(--container-xs), 100%); 106 + width: 32.5%; 107 + }
+2 -2
src/styles/themes/pilot/index.css
··· 50 50 /*********************************** 51 51 * Applets (No UI) 52 52 ***********************************/ 53 + iframe[src*="/configurator/"], 53 54 iframe[src*="/engine/"], 54 - iframe[src*="/orchestrator/"], 55 - iframe[src*="/storage/"] { 55 + iframe[src*="/orchestrator/"] { 56 56 height: 0; 57 57 left: 110vw; 58 58 opacity: 0;