browse the protocol like its 2008 ibex.desertthunder.dev
ubuntu atproto svelte
7

Configure Feed

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

feat: startup sound with tray controls

Owais Jamil (Jun 17, 2026, 10:51 AM -0500) 06354648 f8e5778d

+461 -4
+5
.changeset/heavy-regions-talk.md
··· 1 + --- 2 + 'intrepid-ibex': minor 3 + --- 4 + 5 + add localStorage backed startup sound from ubuntu
+102 -4
src/lib/components/GnomeTray.svelte
··· 2 2 import { onMount } from 'svelte'; 3 3 import { accountSetup } from '$lib/atproto/setup.svelte'; 4 4 import { desktopSession } from '$lib/desktop-session.svelte'; 5 + import { startupSound } from '$lib/sounds.svelte'; 5 6 6 7 let now = $state(new Date()); 8 + let isSoundMenuOpen = $state(false); 7 9 8 10 const panelTime = $derived( 9 11 now.toLocaleString([], { ··· 18 20 ); 19 21 20 22 onMount(() => { 23 + startupSound.initialize(); 24 + 21 25 const interval = window.setInterval(() => { 22 26 now = new Date(); 23 27 }, 1000); 24 28 25 29 return () => window.clearInterval(interval); 26 30 }); 31 + 32 + function replayStartupSound() { 33 + isSoundMenuOpen = false; 34 + void startupSound.replay(); 35 + } 36 + 37 + function syncSoundMenuState(event: Event) { 38 + const target = event.currentTarget; 39 + if (!(target instanceof HTMLDetailsElement)) return; 40 + 41 + isSoundMenuOpen = target.open; 42 + } 43 + 44 + function setStartupSoundMuted(event: Event) { 45 + const target = event.currentTarget; 46 + if (!(target instanceof HTMLInputElement)) return; 47 + 48 + startupSound.setMuted(target.checked); 49 + } 27 50 </script> 28 51 29 52 <aside class="panel-tray" aria-label="Status tray"> ··· 45 68 <button class="tray-button" type="button" onclick={() => desktopSession.lock()} title="Lock screen"> 46 69 <img src="/icons/humanity/status/network-wireless-encrypted.svg" alt="Lock screen" width="16" height="16" /> 47 70 </button> 48 - <a class="tray-button" href="https://plyr.fm" target="_blank" rel="noopener noreferrer" title="Open plyr.fm"> 49 - <img src="/icons/humanity/status/audio-volume-medium.svg" alt="Open plyr.fm" width="16" height="16" /> 50 - </a> 71 + <details class="sound-menu" open={isSoundMenuOpen} ontoggle={syncSoundMenuState}> 72 + <summary class="tray-button" title="Sound"> 73 + {#if startupSound.isMuted} 74 + <img src="/icons/humanity/status/audio-volume-muted.svg" alt="Sound muted" width="16" height="16" /> 75 + {:else} 76 + <img src="/icons/humanity/status/audio-volume-medium.svg" alt="Sound" width="16" height="16" /> 77 + {/if} 78 + </summary> 79 + <div class="sound-popover"> 80 + <button class="sound-menu-item" type="button" onclick={replayStartupSound} disabled={startupSound.isMuted}> 81 + <img src="/icons/humanity/status/audio-volume-medium.svg" alt="" width="16" height="16" /> 82 + <span>Replay startup sound</span> 83 + </button> 84 + <label class="sound-menu-item"> 85 + <input type="checkbox" checked={startupSound.isMuted} onchange={setStartupSoundMuted} /> 86 + <span>Mute startup sound</span> 87 + </label> 88 + </div> 89 + </details> 51 90 <a class="tray-button" href="https://time.is" target="_blank"> 52 91 <time datetime={now.toISOString()}>{panelTime}</time> 53 92 </a> ··· 83 122 } 84 123 85 124 .account-chip:hover, 86 - .tray-button:hover { 125 + .tray-button:hover, 126 + .sound-menu[open] .tray-button { 87 127 background: rgb(255 238 203 / 0.16); 88 128 border-color: rgb(255 255 255 / 0.18) rgb(0 0 0 / 0.35) rgb(0 0 0 / 0.45) rgb(255 255 255 / 0.16); 129 + } 130 + 131 + .sound-menu { 132 + position: relative; 133 + } 134 + 135 + .sound-menu summary { 136 + list-style: none; 137 + } 138 + 139 + .sound-menu summary::-webkit-details-marker { 140 + display: none; 141 + } 142 + 143 + .sound-popover { 144 + position: absolute; 145 + top: calc(100% + 3px); 146 + right: 0; 147 + display: grid; 148 + gap: 1px; 149 + min-width: 13rem; 150 + padding: var(--space-1); 151 + color: var(--text); 152 + background: linear-gradient(#f4e6cc, #cfb58c); 153 + border: 1px solid #4d2c17; 154 + box-shadow: 0 10px 22px rgb(0 0 0 / 0.42); 155 + text-shadow: none; 156 + z-index: 10; 157 + } 158 + 159 + .sound-menu-item { 160 + display: inline-flex; 161 + align-items: center; 162 + gap: var(--space-2); 163 + width: 100%; 164 + min-height: 1.6rem; 165 + padding: 0 var(--space-2); 166 + color: var(--text); 167 + border: 1px solid transparent; 168 + border-radius: var(--radius-1); 169 + } 170 + 171 + button.sound-menu-item { 172 + cursor: default; 173 + } 174 + 175 + button.sound-menu-item:hover:not(:disabled), 176 + .sound-menu-item:has(input:hover) { 177 + color: white; 178 + background: var(--selection); 179 + } 180 + 181 + button.sound-menu-item:disabled { 182 + opacity: 0.55; 183 + } 184 + 185 + .sound-menu-item input { 186 + margin: 0; 89 187 } 90 188 91 189 .account-chip img {
+104
src/lib/sounds.svelte.ts
··· 1 + import { browser } from '$app/environment'; 2 + 3 + const STARTUP_SOUND_PLAYED_KEY = 'intrepid-ibex-startup-sound-played'; 4 + const STARTUP_SOUND_MUTED_KEY = 'intrepid-ibex-startup-sound-muted'; 5 + const STARTUP_SOUND_SRC = '/sounds/login.ogg'; 6 + const STARTUP_SOUND_VOLUME = 0.2; 7 + 8 + class StartupSoundState { 9 + hasPlayed = $state(false); 10 + isMuted = $state(false); 11 + 12 + private activeAudio: HTMLAudioElement | null = null; 13 + private initialized = false; 14 + 15 + initialize() { 16 + if (!browser || this.initialized) return; 17 + 18 + this.initialized = true; 19 + this.hasPlayed = this.wasPlayedBefore(); 20 + this.isMuted = this.wasMutedBefore(); 21 + } 22 + 23 + async playOnStartup() { 24 + this.initialize(); 25 + if (this.hasPlayed || this.isMuted) return; 26 + 27 + await this.play(); 28 + } 29 + 30 + async replay() { 31 + this.initialize(); 32 + await this.play(); 33 + } 34 + 35 + setMuted(isMuted: boolean) { 36 + this.initialize(); 37 + this.isMuted = isMuted; 38 + 39 + if (isMuted && this.activeAudio) { 40 + this.activeAudio.pause(); 41 + this.activeAudio.currentTime = 0; 42 + this.activeAudio = null; 43 + } 44 + 45 + try { 46 + localStorage.setItem(STARTUP_SOUND_MUTED_KEY, String(isMuted)); 47 + } catch { 48 + // Storage can be unavailable in restricted browser contexts. 49 + } 50 + } 51 + 52 + private async play() { 53 + if (!browser || this.isMuted || this.activeAudio) return; 54 + 55 + const audio = new Audio(STARTUP_SOUND_SRC); 56 + audio.volume = STARTUP_SOUND_VOLUME; 57 + audio.preload = 'auto'; 58 + this.activeAudio = audio; 59 + 60 + audio.addEventListener( 61 + 'ended', 62 + () => { 63 + if (this.activeAudio === audio) this.activeAudio = null; 64 + }, 65 + { once: true } 66 + ); 67 + 68 + try { 69 + await audio.play(); 70 + this.markPlayed(); 71 + } catch { 72 + if (this.activeAudio === audio) this.activeAudio = null; 73 + } 74 + } 75 + 76 + private markPlayed() { 77 + this.hasPlayed = true; 78 + 79 + try { 80 + localStorage.setItem(STARTUP_SOUND_PLAYED_KEY, 'true'); 81 + } catch (err) { 82 + const message = err instanceof Error ? err.message : String(err); 83 + console.debug('browser storage unavailable', message); 84 + } 85 + } 86 + 87 + private wasPlayedBefore() { 88 + try { 89 + return localStorage.getItem(STARTUP_SOUND_PLAYED_KEY) === 'true'; 90 + } catch { 91 + return false; 92 + } 93 + } 94 + 95 + private wasMutedBefore() { 96 + try { 97 + return localStorage.getItem(STARTUP_SOUND_MUTED_KEY) === 'true'; 98 + } catch { 99 + return false; 100 + } 101 + } 102 + } 103 + 104 + export const startupSound = new StartupSoundState();
+2
src/routes/+layout.svelte
··· 9 9 import { accountSetup } from '$lib/atproto/setup.svelte'; 10 10 import favicon from '$lib/assets/favicon.svg'; 11 11 import { desktopSession } from '$lib/desktop-session.svelte'; 12 + import { startupSound } from '$lib/sounds.svelte'; 12 13 import { errorMessage } from '$lib/utils/errors'; 13 14 import { windowManager } from '$lib/window-manager.svelte'; 14 15 import AboutComputer from '$lib/components/AboutComputer.svelte'; ··· 165 166 accountSetup.load(); 166 167 setMainWindowTitle(); 167 168 bootStatus = 'ready'; 169 + void startupSound.playOnStartup(); 168 170 } catch (error) { 169 171 bootError = errorMessage(error, 'Could not start the local cache.'); 170 172 bootStatus = 'error';
+248
static/icons/humanity/status/audio-volume-muted.svg
··· 1 + <?xml version="1.0" encoding="UTF-8" standalone="no"?> 2 + <!-- Created with Inkscape (http://www.inkscape.org/) --> 3 + 4 + <svg 5 + xmlns:svg="http://www.w3.org/2000/svg" 6 + xmlns="http://www.w3.org/2000/svg" 7 + xmlns:xlink="http://www.w3.org/1999/xlink" 8 + version="1.0" 9 + width="16" 10 + height="16" 11 + id="svg2"> 12 + <defs 13 + id="defs4"> 14 + <linearGradient 15 + id="linearGradient3703"> 16 + <stop 17 + id="stop3705" 18 + style="stop-color:#888a85;stop-opacity:1" 19 + offset="0" /> 20 + <stop 21 + id="stop3707" 22 + style="stop-color:#d4d4d0;stop-opacity:1" 23 + offset="0.40036523" /> 24 + <stop 25 + id="stop3709" 26 + style="stop-color:#555753;stop-opacity:1" 27 + offset="1" /> 28 + </linearGradient> 29 + <linearGradient 30 + x1="12.706047" 31 + y1="20.760689" 32 + x2="12.706047" 33 + y2="25.978554" 34 + id="linearGradient9150" 35 + xlink:href="#linearGradient3703" 36 + gradientUnits="userSpaceOnUse" 37 + gradientTransform="matrix(0.48020412,0,0,0.5749477,303.88003,175.06369)" /> 38 + <linearGradient 39 + id="linearGradient3608-2"> 40 + <stop 41 + id="stop3610-6" 42 + style="stop-color:#1b1b1b;stop-opacity:1" 43 + offset="0" /> 44 + <stop 45 + id="stop3647" 46 + style="stop-color:#969696;stop-opacity:1" 47 + offset="0.5" /> 48 + <stop 49 + id="stop3612-1" 50 + style="stop-color:#6f6f6f;stop-opacity:1" 51 + offset="1" /> 52 + </linearGradient> 53 + <linearGradient 54 + x1="3.3790166" 55 + y1="19.495045" 56 + x2="3.3790166" 57 + y2="25.613855" 58 + id="linearGradient9147" 59 + xlink:href="#linearGradient3637" 60 + gradientUnits="userSpaceOnUse" 61 + gradientTransform="matrix(0.49477423,0,0,0.49029127,305.30971,177.44175)" /> 62 + <linearGradient 63 + x1="12.25" 64 + y1="19.75" 65 + x2="10.410554" 66 + y2="38.483665" 67 + id="linearGradient9143" 68 + xlink:href="#linearGradient21598" 69 + gradientUnits="userSpaceOnUse" 70 + gradientTransform="matrix(0.32408242,0,0,0.4755459,305.60766,177.69925)" /> 71 + <linearGradient 72 + id="linearGradient21598"> 73 + <stop 74 + id="stop21600" 75 + style="stop-color:#ffffff;stop-opacity:1" 76 + offset="0" /> 77 + <stop 78 + id="stop21602" 79 + style="stop-color:#ffffff;stop-opacity:0" 80 + offset="1" /> 81 + </linearGradient> 82 + <linearGradient 83 + x1="2.3957696" 84 + y1="32.771046" 85 + x2="2.3455546" 86 + y2="26.589766" 87 + id="linearGradient2876" 88 + xlink:href="#linearGradient3608-2" 89 + gradientUnits="userSpaceOnUse" 90 + gradientTransform="matrix(0.86311373,0,0,0.80102924,304.95708,164.70082)" /> 91 + <linearGradient 92 + x1="19.916945" 93 + y1="31.018301" 94 + x2="19.916945" 95 + y2="14.024532" 96 + id="linearGradient9130" 97 + xlink:href="#linearGradient3608-2" 98 + gradientUnits="userSpaceOnUse" 99 + gradientTransform="matrix(0.9980931,0,0,0.588451,291.10255,174.74725)" /> 100 + <linearGradient 101 + x1="313.98148" 102 + y1="187" 103 + x2="312.95602" 104 + y2="187.01263" 105 + id="linearGradient4248" 106 + xlink:href="#linearGradient4242" 107 + gradientUnits="userSpaceOnUse" 108 + gradientTransform="translate(-1.9745022,-0.01262924)" /> 109 + <linearGradient 110 + id="linearGradient4242"> 111 + <stop 112 + id="stop4244" 113 + style="stop-color:#1b1b1b;stop-opacity:1" 114 + offset="0" /> 115 + <stop 116 + id="stop4246" 117 + style="stop-color:#000000;stop-opacity:0" 118 + offset="1" /> 119 + </linearGradient> 120 + <linearGradient 121 + id="linearGradient3637"> 122 + <stop 123 + id="stop3639" 124 + style="stop-color:#888a85;stop-opacity:1" 125 + offset="0" /> 126 + <stop 127 + id="stop3641" 128 + style="stop-color:#d4d4d0;stop-opacity:1" 129 + offset="0.42949009" /> 130 + <stop 131 + id="stop3643" 132 + style="stop-color:#555753;stop-opacity:1" 133 + offset="1" /> 134 + </linearGradient> 135 + <linearGradient 136 + id="linearGradient3608-2-1"> 137 + <stop 138 + id="stop3610-7" 139 + style="stop-color:#1b1b1b;stop-opacity:1" 140 + offset="0" /> 141 + <stop 142 + id="stop3612-2" 143 + style="stop-color:#6f6f6f;stop-opacity:1" 144 + offset="1" /> 145 + </linearGradient> 146 + <linearGradient 147 + x1="309.00433" 148 + y1="193.00002" 149 + x2="309.00433" 150 + y2="184.00002" 151 + id="linearGradient3662" 152 + xlink:href="#linearGradient3608-2-1" 153 + gradientUnits="userSpaceOnUse" /> 154 + <linearGradient 155 + x1="308.98154" 156 + y1="193" 157 + x2="308.98154" 158 + y2="184" 159 + id="linearGradient3670" 160 + xlink:href="#linearGradient3608-2-1" 161 + gradientUnits="userSpaceOnUse" /> 162 + <linearGradient 163 + x1="310.4946" 164 + y1="194.19289" 165 + x2="310.74371" 166 + y2="182.20787" 167 + id="linearGradient3678" 168 + xlink:href="#linearGradient3608-2-1" 169 + gradientUnits="userSpaceOnUse" /> 170 + </defs> 171 + <g 172 + id="g2859"> 173 + <rect 174 + width="2" 175 + height="1" 176 + rx="0" 177 + ry="0" 178 + x="14" 179 + y="7" 180 + id="rect2384" 181 + style="opacity:0.4;fill:#606060;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> 182 + <rect 183 + width="2" 184 + height="1" 185 + rx="0" 186 + ry="0" 187 + x="11" 188 + y="7" 189 + id="rect3158" 190 + style="opacity:0.4;fill:#606060;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> 191 + <rect 192 + width="2" 193 + height="1" 194 + rx="0" 195 + ry="0" 196 + x="8" 197 + y="7" 198 + id="rect3162" 199 + style="opacity:0.4;fill:#606060;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> 200 + <g 201 + transform="matrix(1.0000114,0,0,1,-304.98506,-181)" 202 + id="g4019" 203 + style="display:inline;enable-background:new"> 204 + <path 205 + d="m 311.9815,181 c -0.72031,3.16221 -4.339,2.61296 -6.49992,7.5 2.16092,4.88705 5.77961,4.33779 6.49992,7.5 l 0,-15 z" 206 + id="path1887" 207 + style="fill:url(#linearGradient9150);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient3678);stroke-width:0.99999952;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;enable-background:new" /> 208 + <rect 209 + width="3.0144327" 210 + height="7.9916568" 211 + rx="0.94472277" 212 + ry="1.4383463" 213 + x="305.48575" 214 + y="184.50418" 215 + id="rect1898" 216 + style="fill:url(#linearGradient9147);fill-opacity:1;stroke:url(#linearGradient3662);stroke-width:1.00833738;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.20000057;display:inline;enable-background:new" /> 217 + <path 218 + d="m 312.51668,182.68837 c -1.32954,2.09988 -4.65216,2.49127 -5.0794,5.76939 0.23645,3.38674 4.78227,3.98154 4.95202,5.97547 l -0.13794,-11.50298 0.26532,-0.24188 z" 219 + id="path21595" 220 + style="opacity:0.41999996;fill:none;stroke:url(#linearGradient9143);stroke-width:0.92961943;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;display:inline;enable-background:new" /> 221 + <path 222 + d="m 308.58154,189 3.99995,-1 0,-3 -3.99995,3 0,1 z" 223 + id="path4732" 224 + style="opacity:0.41999996;fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline;enable-background:new" /> 225 + <path 226 + d="m 306.98156,184.4995 1.10897,0 c 0.21689,0 0.39151,0.23597 0.39151,0.52908 l 0,6.94284 c 0,0.29311 -0.17462,0.52908 -0.39151,0.52908 l -1.10897,0" 227 + id="rect1881" 228 + style="fill:url(#linearGradient2876);fill-opacity:1;stroke:url(#linearGradient3670);stroke-width:0.99899137;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dashoffset:1.20000057;display:inline;enable-background:new" /> 229 + <rect 230 + width="1.9927369" 231 + height="13.950581" 232 + rx="0.87766522" 233 + ry="0.58845085" 234 + x="310.48877" 235 + y="181.54942" 236 + id="rect1889" 237 + style="fill:url(#linearGradient9130);fill-opacity:1;stroke:url(#linearGradient4248);stroke-width:0.99999946;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:1.20000057;display:inline;enable-background:new" /> 238 + <path 239 + d="m 307.98156,186.5 a 0.5,0.5000005 0 0 1 -1,0 0.5,0.5000005 0 1 1 1,0 z" 240 + id="path5619" 241 + style="opacity:0.6;fill:#ffffff;fill-opacity:1;stroke:none;display:inline;enable-background:new" /> 242 + <path 243 + d="m 311.98151,183.5 a 0.5,0.5000005 0 0 1 -1,0 0.5,0.5000005 0 1 1 1,0 z" 244 + id="path4273" 245 + style="opacity:0.6;fill:#ffffff;fill-opacity:1;stroke:none;display:inline;enable-background:new" /> 246 + </g> 247 + </g> 248 + </svg>
static/sounds/login.ogg

This is a binary file and will not be displayed.