audio streaming app plyr.fm
38
fork

Configure Feed

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

fix: PDS tooltip hover — use delayed hide (#1110)

* fix: PDS tooltip hover — use delayed hide like LikersTooltip

CSS :hover broke because the gap between the ? icon and dropdown
content caused the tooltip to vanish before the user could click
the link. Switch to JS enter/leave with 150ms hide timeout,
matching the established LikersTooltip pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* add auto-tag tooltip, extract reusable InfoTooltip component

- extract generic InfoTooltip with delayed hide and snippet content
- refactor PdsTooltip to use InfoTooltip
- add auto-tag tooltip linking to docs.plyr.fm/artists/#auto-tagging
- add auto-tagging section to artists docs as anchor target

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6
and committed by
GitHub
748a34f1 aff4f119

+120 -65
+4
docs/artists.md
··· 26 26 27 27 the audio and metadata are stored on your PDS. if your PDS can't accept the file (blob size limits), plyr.fm falls back to R2 storage — but the metadata record always lives in your repo. 28 28 29 + ### auto-tagging 30 + 31 + when you upload a track, you can opt in to **auto-tag with recommended genres**. plyr.fm runs ML genre classification on the audio and suggests tags automatically — you can accept, remove, or add your own. auto-suggested tags typically appear within a few seconds of upload. 32 + 29 33 ## embeds 30 34 31 35 share your audio anywhere with embed iframes. plyr.fm supports track, playlist, and album embeds.
+100
frontend/src/lib/components/InfoTooltip.svelte
··· 1 + <script lang="ts"> 2 + import type { Snippet } from "svelte"; 3 + 4 + interface Props { 5 + label?: string; 6 + children: Snippet; 7 + } 8 + 9 + let { label = "more info", children }: Props = $props(); 10 + 11 + let show = $state(false); 12 + let hideTimeout: ReturnType<typeof setTimeout> | null = null; 13 + 14 + function enter() { 15 + if (hideTimeout) { 16 + clearTimeout(hideTimeout); 17 + hideTimeout = null; 18 + } 19 + show = true; 20 + } 21 + 22 + function leave() { 23 + hideTimeout = setTimeout(() => { 24 + show = false; 25 + hideTimeout = null; 26 + }, 150); 27 + } 28 + </script> 29 + 30 + <span 31 + class="tooltip-wrapper" 32 + role="button" 33 + tabindex="0" 34 + aria-label={label} 35 + aria-expanded={show} 36 + onmouseenter={enter} 37 + onmouseleave={leave} 38 + onfocus={enter} 39 + onblur={leave} 40 + > 41 + <span class="tooltip-icon">?</span> 42 + {#if show} 43 + <span 44 + class="tooltip-content" 45 + role="tooltip" 46 + onmouseenter={enter} 47 + onmouseleave={leave} 48 + > 49 + {@render children()} 50 + </span> 51 + {/if} 52 + </span> 53 + 54 + <style> 55 + .tooltip-wrapper { 56 + position: relative; 57 + display: inline-flex; 58 + } 59 + 60 + .tooltip-icon { 61 + display: inline-flex; 62 + align-items: center; 63 + justify-content: center; 64 + width: 1.1rem; 65 + height: 1.1rem; 66 + border-radius: 50%; 67 + border: 1px solid var(--text-tertiary); 68 + color: var(--text-tertiary); 69 + font-size: 0.7rem; 70 + font-weight: 600; 71 + cursor: help; 72 + line-height: 1; 73 + } 74 + 75 + .tooltip-content { 76 + position: absolute; 77 + left: 50%; 78 + top: calc(100% + 0.5rem); 79 + transform: translateX(-50%); 80 + background: var(--bg-primary); 81 + border: 1px solid var(--border-default); 82 + border-radius: var(--radius-sm); 83 + padding: 0.5rem 0.75rem; 84 + font-size: var(--text-sm); 85 + color: var(--text-secondary); 86 + white-space: nowrap; 87 + z-index: 10; 88 + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 89 + pointer-events: auto; 90 + } 91 + 92 + .tooltip-content a { 93 + color: var(--accent); 94 + text-decoration: none; 95 + } 96 + 97 + .tooltip-content a:hover { 98 + text-decoration: underline; 99 + } 100 + </style>
+10 -64
frontend/src/lib/components/PdsTooltip.svelte
··· 1 1 <script lang="ts"> 2 2 import { preferences } from "$lib/preferences.svelte"; 3 + import InfoTooltip from "./InfoTooltip.svelte"; 3 4 4 5 let enabled = $derived( 5 6 preferences.uiSettings.pds_audio_uploads_enabled ?? false, 6 7 ); 7 8 </script> 8 9 9 - <span class="tooltip-wrapper"> 10 - <span class="tooltip-icon">?</span> 11 - <span class="tooltip-content"> 12 - {#if enabled} 13 - uploads are stored on your PDS. 14 - <a href="https://docs.plyr.fm/artists/#your-data" target="_blank" rel="noopener">learn more</a> 15 - {:else} 16 - PDS audio uploads available in <a href="/settings">settings</a>. 17 - <a href="https://docs.plyr.fm/artists/#your-data" target="_blank" rel="noopener">learn more</a> 18 - {/if} 19 - </span> 20 - </span> 21 - 22 - <style> 23 - .tooltip-wrapper { 24 - position: relative; 25 - display: inline-flex; 26 - } 27 - 28 - .tooltip-icon { 29 - display: inline-flex; 30 - align-items: center; 31 - justify-content: center; 32 - width: 1.1rem; 33 - height: 1.1rem; 34 - border-radius: 50%; 35 - border: 1px solid var(--text-tertiary); 36 - color: var(--text-tertiary); 37 - font-size: 0.7rem; 38 - font-weight: 600; 39 - cursor: help; 40 - line-height: 1; 41 - } 42 - 43 - .tooltip-content { 44 - display: none; 45 - position: absolute; 46 - left: 50%; 47 - top: calc(100% + 0.5rem); 48 - transform: translateX(-50%); 49 - background: var(--bg-primary); 50 - border: 1px solid var(--border-default); 51 - border-radius: var(--radius-sm); 52 - padding: 0.5rem 0.75rem; 53 - font-size: var(--text-sm); 54 - color: var(--text-secondary); 55 - white-space: nowrap; 56 - z-index: 10; 57 - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); 58 - } 59 - 60 - .tooltip-wrapper:hover .tooltip-content { 61 - display: block; 62 - } 63 - 64 - .tooltip-content a { 65 - color: var(--accent); 66 - text-decoration: none; 67 - } 68 - 69 - .tooltip-content a:hover { 70 - text-decoration: underline; 71 - } 72 - </style> 10 + <InfoTooltip label="PDS storage info"> 11 + {#if enabled} 12 + uploads are stored on your PDS. 13 + <a href="https://docs.plyr.fm/artists/#your-data" target="_blank" rel="noopener">learn more</a> 14 + {:else} 15 + PDS audio uploads available in <a href="/settings">settings</a>. 16 + <a href="https://docs.plyr.fm/artists/#your-data" target="_blank" rel="noopener">learn more</a> 17 + {/if} 18 + </InfoTooltip>
+5
frontend/src/routes/upload/+page.svelte
··· 5 5 import HandleSearch from "$lib/components/HandleSearch.svelte"; 6 6 import AlbumSelect from "$lib/components/AlbumSelect.svelte"; 7 7 import PdsTooltip from "$lib/components/PdsTooltip.svelte"; 8 + import InfoTooltip from "$lib/components/InfoTooltip.svelte"; 8 9 import WaveLoading from "$lib/components/WaveLoading.svelte"; 9 10 import TagInput from "$lib/components/TagInput.svelte"; 10 11 import type { FeaturedArtist, AlbumSummary, Artist } from "$lib/types"; ··· 335 336 <label class="checkbox-label" style="margin-top: 0.75rem;"> 336 337 <input type="checkbox" bind:checked={autoTag} /> 337 338 <span class="checkbox-text">auto-tag with recommended genres</span> 339 + <InfoTooltip label="auto-tagging info"> 340 + ML genre classification suggests tags from your audio. 341 + <a href="https://docs.plyr.fm/artists/#auto-tagging" target="_blank" rel="noopener">learn more</a> 342 + </InfoTooltip> 338 343 </label> 339 344 </div> 340 345
+1 -1
loq.toml
··· 183 183 184 184 [[rules]] 185 185 path = "frontend/src/routes/upload/+page.svelte" 186 - max_lines = 720 186 + max_lines = 725 187 187 188 188 [[rules]] 189 189 path = "services/moderation/src/admin.rs"