audio streaming app plyr.fm
38
fork

Configure Feed

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

fix: glass styling for feed switcher + tag filtering on for-you (#1284)

two issues from staging review:

1. segmented control used opaque --bg-secondary background and --radius-xl,
visually inconsistent with track items and cards which use translucent
--track-bg/--track-border and --radius-md. switched to match.

2. tag filters were hidden when viewing for-you feed. added optional
`tags` query param to GET /for-you/ — filters candidates to tracks
with at least one matching tag (same inclusive semantics as /tracks/).
ForYouCache now supports setTags(), and the homepage shows tag filters
regardless of feed mode.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

authored by

nate nowack
Claude Opus 4.6 (1M context)
and committed by
GitHub
25de94c7 62688585

+54 -19
+14
backend/src/backend/api/for_you.py
··· 274 274 auth_session: AuthSession = Depends(require_auth), 275 275 cursor: str | None = Query(None), 276 276 limit: int = Query(30, ge=1, le=50), 277 + tags: list[str] | None = Query(None), 277 278 ) -> ForYouResponse: 278 279 """Personalized feed for the authenticated user. 279 280 ··· 341 342 hidden_set = {r.track_id for r in hidden_rows} 342 343 if hidden_set: 343 344 track_ids = [tid for tid in track_ids if tid not in hidden_set] 345 + 346 + # apply active tag filter (inclusive — keep only tracks with at least one matching tag) 347 + if tags and track_ids: 348 + tagged_rows = ( 349 + await db.execute( 350 + select(TrackTag.track_id) 351 + .join(Tag, TrackTag.tag_id == Tag.id) 352 + .where(Tag.name.in_(tags)) 353 + .where(TrackTag.track_id.in_(track_ids)) 354 + ) 355 + ).all() 356 + tagged_set = {r.track_id for r in tagged_rows} 357 + track_ids = [tid for tid in track_ids if tid in tagged_set] 344 358 345 359 # page window 346 360 page_ids = track_ids[offset : offset + limit]
+18 -2
frontend/src/lib/for-you.svelte.ts
··· 15 15 nextCursor = $state<string | null>(null); 16 16 hasMore = $state(false); 17 17 coldStart = $state(false); 18 + activeTags = $state<string[]>([]); 19 + 20 + private buildUrl(): URL { 21 + const url = new URL(`${API_URL}/for-you/`); 22 + for (const tag of this.activeTags) { 23 + url.searchParams.append('tags', tag); 24 + } 25 + return url; 26 + } 18 27 19 28 async fetch(force = false): Promise<void> { 20 29 if (!force && this.loading) return; 21 30 22 31 this.loading = true; 23 32 try { 24 - const response = await fetch(`${API_URL}/for-you/`, { 33 + const response = await fetch(this.buildUrl().toString(), { 25 34 credentials: 'include' 26 35 }); 27 36 if (!response.ok) { ··· 48 57 49 58 this.loadingMore = true; 50 59 try { 51 - const url = new URL(`${API_URL}/for-you/`); 60 + const url = this.buildUrl(); 52 61 url.searchParams.set('cursor', this.nextCursor); 53 62 54 63 const response = await fetch(url.toString(), { ··· 72 81 this.nextCursor = null; 73 82 this.hasMore = false; 74 83 this.coldStart = false; 84 + } 85 + 86 + setTags(tags: string[]): void { 87 + this.activeTags = tags; 88 + this.nextCursor = null; 89 + this.hasMore = false; 90 + this.fetch(true); 75 91 } 76 92 } 77 93
+21 -16
frontend/src/routes/+page.svelte
··· 346 346 </div> 347 347 {/if} 348 348 </div> 349 - {#if feedMode === 'latest'} 350 - <div class="filter-row"> 351 - <TagFilter 352 - onTagsChange={(tags) => tracksCache.setTags(tags)} 353 - hiddenTags={preferences.hiddenTags} 354 - /> 355 - <HiddenTagsFilter /> 356 - </div> 357 - {/if} 349 + <div class="filter-row"> 350 + <TagFilter 351 + onTagsChange={(tags) => { 352 + if (feedMode === 'for-you') { 353 + forYouCache.setTags(tags); 354 + } else { 355 + tracksCache.setTags(tags); 356 + } 357 + }} 358 + hiddenTags={preferences.hiddenTags} 359 + /> 360 + <HiddenTagsFilter /> 361 + </div> 358 362 {#if showLoading} 359 363 <div class="loading-container"> 360 364 <WaveLoading size="lg" message="loading tracks..." /> ··· 427 431 .feed-switcher { 428 432 display: flex; 429 433 gap: 0.25rem; 430 - background: var(--bg-secondary); 431 - border: 1px solid var(--border-subtle); 432 - border-radius: var(--radius-xl); 434 + background: var(--track-bg, var(--bg-secondary)); 435 + border: 1px solid var(--track-border, var(--border-subtle)); 436 + border-radius: var(--radius-md); 433 437 padding: 0.2rem; 434 438 } 435 439 436 440 .feed-tab { 437 441 background: transparent; 438 442 border: 1px solid transparent; 439 - border-radius: var(--radius-xl); 440 - padding: 0.35rem 0.85rem; 443 + border-radius: var(--radius-sm); 444 + padding: 0.3rem 0.75rem; 441 445 font: inherit; 442 446 font-size: var(--text-sm); 443 447 color: var(--text-tertiary); ··· 449 453 450 454 .feed-tab:hover:not(.active) { 451 455 color: var(--text-secondary); 456 + background: var(--track-border, rgba(255, 255, 255, 0.06)); 452 457 } 453 458 454 459 .feed-tab.active { 455 - background: color-mix(in srgb, var(--accent) 15%, transparent); 456 - border-color: var(--accent); 460 + background: color-mix(in srgb, var(--accent) 12%, var(--track-bg, transparent)); 461 + border-color: color-mix(in srgb, var(--accent) 20%, var(--track-border, transparent)); 457 462 color: var(--accent); 458 463 font-weight: 600; 459 464 }
+1 -1
loq.toml
··· 232 232 233 233 [[rules]] 234 234 path = "frontend/src/routes/+page.svelte" 235 - max_lines = 630 235 + max_lines = 635 236 236 237 237 [[rules]] 238 238 path = "frontend/src/lib/components/AlbumUploadForm.svelte"