feat(phase37): implement clickable context tooltip navigation

StackIcon Component:
- Now accepts onClick prop
- Changed from div to button element
- Added arrow (→) that appears on hover
- Tooltip text changed to "Click to go to: {source}"
- Visual feedback: hover background + active scale

Navigation Logic (+page.svelte):
- Added handleContextNavigation function
- Switch based on context_type:
  - album → handleAlbumClick
  - playlist → navigate + set selectedPlaylistId
  - artist_top → handleArtistClick
  - favorites → navigate to favorites
  - home_list → navigate to home
  - search → navigate to search
- Import getCurrentContext from playbackContextStore

UI Integration:
- ExpandedPlayer: Added onContextClick prop, passed to StackIcon
- FocusMode: Added onContextClick prop, passed to StackIcon
- Both receive handleContextNavigation callback

Phase 3.7 COMPLETE ✅

cp
This commit is contained in:
vicrodh committed 2026-01-18 03:47:11 -06:00
1 parent b070884ee8
commit 584650fee1
4 files changed
+112 -7

No files matched your search

+3 -1
View File
@@ -50,6 +50,7 @@
onOpenFocusMode?: () => void;
onCast?: () => void;
isCastConnected?: boolean;
onContextClick?: () => void;
// Lyrics props
lyricsLines?: LyricsLine[];
lyricsActiveIndex?: number;
@@ -89,6 +90,7 @@
onOpenFocusMode,
onCast,
isCastConnected = false,
onContextClick,
lyricsLines = [],
lyricsActiveIndex = -1,
lyricsActiveProgress = 0,
@@ -247,7 +249,7 @@
<div class="track-info">
<h1 class="title">{trackTitle}</h1>
<div class="artist-album-row">
<StackIcon size={16} class="stack-icon" />
<StackIcon size={16} class="stack-icon" onClick={onContextClick} />
<h2 class="artist">{artist}</h2>
<span class="separator">·</span>
<h3 class="album">{album}</h3>
+3 -1
View File
@@ -22,6 +22,7 @@
currentTime: number;
duration: number;
onSeek: (time: number) => void;
onContextClick?: () => void;
// Lyrics props
lyricsLines?: LyricsLine[];
lyricsActiveIndex?: number;
@@ -44,6 +45,7 @@
currentTime,
duration,
onSeek,
onContextClick,
lyricsLines = [],
lyricsActiveIndex = -1,
lyricsActiveProgress = 0,
@@ -187,7 +189,7 @@
<div class="track-meta">
<div class="track-title">{trackTitle}</div>
<div class="track-artist-row">
<StackIcon size={12} class="stack-icon" />
<StackIcon size={12} class="stack-icon" onClick={onContextClick} />
<div class="track-artist">{artist}</div>
</div>
</div>
+42 -4
View File
@@ -6,14 +6,23 @@
interface Props {
size?: number;
class?: string;
onClick?: () => void;
}
let { size = 16, class: className = '' }: Props = $props();
let { size = 16, class: className = '', onClick }: Props = $props();
let context = $state(getCurrentContext());
let displayInfo = $state(context ? getContextDisplayInfo() : null);
let showIcon = $state(getCachedPreferences().show_context_icon);
function handleClick(e: MouseEvent) {
e.stopPropagation();
if (onClick) {
console.log('[StackIcon] Clicked - navigating to source');
onClick();
}
}
// Subscribe to context changes
$effect(() => {
const unsubscribe = subscribe(() => {
@@ -43,9 +52,15 @@
</script>
{#if context && displayInfo && showIcon}
<div class="stack-icon-wrapper {className}" title="Playing from: {displayInfo}">
<button
class="stack-icon-wrapper {className}"
title="Click to go to: {displayInfo}"
onclick={handleClick}
type="button"
>
<Layers size={size} strokeWidth={2} />
</div>
<span class="arrow">→</span>
</button>
{/if}
<style>
@@ -53,13 +68,36 @@
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
color: var(--text-secondary);
opacity: 0.7;
transition: opacity 0.2s;
transition: all 0.2s;
flex-shrink: 0;
background: none;
border: none;
padding: 4px 6px;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
}
.stack-icon-wrapper:hover {
opacity: 1;
background: rgba(255, 255, 255, 0.05);
color: var(--text-primary);
}
.stack-icon-wrapper:active {
transform: scale(0.95);
}
.arrow {
opacity: 0;
transition: opacity 0.2s;
margin-left: -2px;
}
.stack-icon-wrapper:hover .arrow {
opacity: 1;
}
</style>
+64 -1
View File
@@ -29,7 +29,8 @@
import {
initPlaybackContextStore,
setPlaybackContext,
clearPlaybackContext
clearPlaybackContext,
getCurrentContext
} from '$lib/stores/playbackContextStore';
import {
initPlaybackPreferences,
@@ -371,6 +372,66 @@
}
}
/**
* Navigate to the source of current playback context
*/
async function handleContextNavigation() {
const context = getCurrentContext();
if (!context) {
console.log('[ContextNav] No context available');
return;
}
console.log('[ContextNav] Navigating to:', context);
try {
switch (context.context_type) {
case 'album':
// Navigate to album page
await handleAlbumClick(context.context_id);
break;
case 'playlist':
// Navigate to playlist page
const playlistId = parseInt(context.context_id);
if (!isNaN(playlistId)) {
selectedPlaylistId = playlistId;
navigateTo('playlist');
}
break;
case 'artist_top':
// Navigate to artist page
const artistId = parseInt(context.context_id);
if (!isNaN(artistId)) {
await handleArtistClick(artistId);
}
break;
case 'favorites':
// Navigate to favorites page
navigateTo('favorites');
break;
case 'home_list':
// Navigate to home page
navigateTo('home');
break;
case 'search':
// Navigate to search (could restore query if needed)
navigateTo('search');
break;
default:
console.warn('[ContextNav] Unknown context type:', context.context_type);
}
} catch (err) {
console.error('[ContextNav] Navigation failed:', err);
showToast('Failed to navigate to source', 'error');
}
}
interface ArtistAlbumsResponse {
items: QobuzAlbum[];
total: number;
@@ -2201,6 +2262,7 @@
handleAlbumClick(currentTrack.albumId);
}
}}
onContextClick={handleContextNavigation}
/>
{:else}
<NowPlayingBar
@@ -2293,6 +2355,7 @@
lyricsSynced={lyricsIsSynced}
lyricsLoading={lyricsStatus === 'loading'}
lyricsError={lyricsStatus === 'error' ? lyricsError : (lyricsStatus === 'not_found' ? 'No lyrics found' : null)}
onContextClick={handleContextNavigation}
/>
{/if}