a simple web player for subsonic tinysub.devins.page
subsonic navidrome javascript
11
fork

Configure Feed

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

feat: tauri system context menu

+116 -56
+3 -2
package.json
··· 20 20 "modern-normalize": "^3.0.1", 21 21 "prettier-plugin-svelte": "^3.5.1", 22 22 "spark-md5": "^3.0.2", 23 - "svelte-check": "^4.4.8", 24 23 "svelte": "^5.55.5", 24 + "svelte-check": "^4.4.8", 25 25 "typescript": "~6.0.3", 26 - "vite": "^8.0.10" 26 + "vite": "^8.0.10", 27 + "@tauri-apps/api": "^2.11.0" 27 28 }, 28 29 "prettier": { 29 30 "useTabs": true,
+8
pnpm-lock.yaml
··· 11 11 '@sveltejs/vite-plugin-svelte': 12 12 specifier: ^7.1.1 13 13 version: 7.1.1(svelte@5.55.5)(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)(terser@5.46.2)) 14 + '@tauri-apps/api': 15 + specifier: ^2.11.0 16 + version: 2.11.0 14 17 '@tauri-apps/cli': 15 18 specifier: ^2.11.1 16 19 version: 2.11.1 ··· 199 202 peerDependencies: 200 203 svelte: ^5.46.4 201 204 vite: ^8.0.0-beta.7 || ^8.0.0 205 + 206 + '@tauri-apps/api@2.11.0': 207 + resolution: {integrity: sha512-7CinYODhky9lmO23xHnUFv0Xt43fbtWMyxZcLcRBlFkcgXKuEirBvHpmtJ89YMhyeGcq20Wuc47Fa4XjyniywA==} 202 208 203 209 '@tauri-apps/cli-darwin-arm64@2.11.1': 204 210 resolution: {integrity: sha512-6eEKMBXsQPCuM1EmvrjT2+aBuxWQuFdKdW8pzNuNQtpq45nEEpBlD5gr8pUeAyOU1DQKlkFaEc/MPBxb/Pfjtg==} ··· 722 728 svelte: 5.55.5 723 729 vite: 8.0.10(@types/node@25.6.0)(jiti@2.6.1)(terser@5.46.2) 724 730 vitefu: 1.1.3(vite@8.0.10(@types/node@25.6.0)(jiti@2.6.1)(terser@5.46.2)) 731 + 732 + '@tauri-apps/api@2.11.0': {} 725 733 726 734 '@tauri-apps/cli-darwin-arm64@2.11.1': 727 735 optional: true
+105 -54
src/lib/ContextMenu.svelte
··· 16 16 onclose: () => void; 17 17 }>(); 18 18 19 + const isTauri = 20 + typeof window !== "undefined" && !!(window as any).__TAURI_INTERNALS__; 21 + 19 22 let active = $state<number | null>(null), 20 23 subActive = $state<number | null>(null), 21 24 width = $state(0), 22 25 height = $state(0), 23 26 element = $state<HTMLElement>(); 24 27 28 + async function showTauriMenu() { 29 + try { 30 + const { Menu, MenuItem, Submenu, PredefinedMenuItem } = 31 + await import("@tauri-apps/api/menu"); 32 + 33 + const createItems = async (items: Item[]): Promise<any[]> => { 34 + return Promise.all( 35 + items.map(async (item) => { 36 + if (item.type === "separator") { 37 + return await PredefinedMenuItem.new({ item: "Separator" }); 38 + } 39 + if (item.items) { 40 + return await Submenu.new({ 41 + text: item.label || "", 42 + items: await createItems(item.items), 43 + }); 44 + } 45 + return await MenuItem.new({ 46 + text: item.label || "", 47 + enabled: !item.disabled, 48 + action: () => { 49 + item.action?.(); 50 + onclose(); 51 + }, 52 + }); 53 + }), 54 + ); 55 + }; 56 + 57 + const menu = await Menu.new({ 58 + items: await createItems(items), 59 + }); 60 + 61 + await menu.popup(); 62 + } catch (err) { 63 + console.error("failed to show tauri menu:", err); 64 + } finally { 65 + onclose(); 66 + } 67 + } 68 + 25 69 onMount(() => { 70 + if (isTauri) { 71 + showTauriMenu(); 72 + return; 73 + } 74 + 26 75 const prevFocus = document.activeElement as HTMLElement | null; 27 76 element?.focus(); 28 77 return () => prevFocus?.focus(); ··· 132 181 </button> 133 182 {/snippet} 134 183 135 - <!-- svelte-ignore a11y_no_static_element_interactions --> 136 - <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 137 - <!-- svelte-ignore a11y_click_events_have_key_events --> 138 - <div 139 - class="overlay" 140 - onclick={onclose} 141 - oncontextmenu={(event) => { 142 - event.preventDefault(); 143 - onclose(); 144 - }} 145 - > 184 + {#if !isTauri} 185 + <!-- svelte-ignore a11y_no_static_element_interactions --> 186 + <!-- svelte-ignore a11y_no_noninteractive_tabindex --> 187 + <!-- svelte-ignore a11y_click_events_have_key_events --> 146 188 <div 147 - bind:this={element} 148 - class="menu" 149 - style="top: {top}px; left: {left}px;" 150 - onclick={(event) => event.stopPropagation()} 151 - onkeydown={onKey} 152 - bind:clientWidth={width} 153 - bind:clientHeight={height} 154 - tabindex="0" 189 + class="overlay" 190 + onclick={onclose} 191 + oncontextmenu={(event) => { 192 + event.preventDefault(); 193 + onclose(); 194 + }} 155 195 > 156 - {#each items as item, index} 157 - {#if item.type === "separator"} 158 - <div class="separator"></div> 159 - {:else} 160 - <div 161 - class="row" 162 - onmouseleave={() => { 163 - if (subActive === null) active = null; 164 - }} 165 - > 166 - {@render menuButton( 167 - item, 168 - active === index && subActive === null, 169 - false, 170 - )} 171 - {#if item.items && active === index} 172 - <div 173 - class="submenu" 174 - class:flip-x={left + width + 160 > window.innerWidth} 175 - class:flip-y={top + index * 32 + item.items.length * 32 > 176 - window.innerHeight} 177 - > 178 - {#each item.items as sub, subIndex} 179 - {#if sub.type === "separator"} 180 - <div class="separator"></div> 181 - {:else} 182 - {@render menuButton(sub, subActive === subIndex, true)} 183 - {/if} 184 - {/each} 185 - </div> 186 - {/if} 187 - </div> 188 - {/if} 189 - {/each} 196 + <div 197 + bind:this={element} 198 + class="menu" 199 + style="top: {top}px; left: {left}px;" 200 + onclick={(event) => event.stopPropagation()} 201 + onkeydown={onKey} 202 + bind:clientWidth={width} 203 + bind:clientHeight={height} 204 + tabindex="0" 205 + > 206 + {#each items as item, index} 207 + {#if item.type === "separator"} 208 + <div class="separator"></div> 209 + {:else} 210 + <div 211 + class="row" 212 + onmouseleave={() => { 213 + if (subActive === null) active = null; 214 + }} 215 + > 216 + {@render menuButton( 217 + item, 218 + active === index && subActive === null, 219 + false, 220 + )} 221 + {#if item.items && active === index} 222 + <div 223 + class="submenu" 224 + class:flip-x={left + width + 160 > window.innerWidth} 225 + class:flip-y={top + index * 32 + item.items.length * 32 > 226 + window.innerHeight} 227 + > 228 + {#each item.items as sub, subIndex} 229 + {#if sub.type === "separator"} 230 + <div class="separator"></div> 231 + {:else} 232 + {@render menuButton(sub, subActive === subIndex, true)} 233 + {/if} 234 + {/each} 235 + </div> 236 + {/if} 237 + </div> 238 + {/if} 239 + {/each} 240 + </div> 190 241 </div> 191 - </div> 242 + {/if} 192 243 193 244 <style> 194 245 .overlay {