feat(immersive): unify tab navigation with view mode toggle
Consolidates navigation into single bar with mode toggle: - Split mode tabs: Lyrics, Track Info, Suggestions, Queue - Focus mode tabs: Coverflow, Lyrics, Queue - Mode toggle button (lotus/split-view icons) - V key toggles view mode, 1-3 for focus tabs
This commit is contained in:
1 parent
aea99a66d6
commit
2004a59660
2 files changed
+129
-115
No files matched your search
@@ -1,16 +1,19 @@
|
||||
<script lang="ts">
|
||||
import { Disc3, LayoutGrid, Mic2, ListMusic, Music2, Info, Radio, Maximize, Minimize, ChevronDown, X, Square, Copy, Minus } from 'lucide-svelte';
|
||||
import { Disc3, Mic2, ListMusic, Music2, Info, Radio, Maximize, Minimize, ChevronDown, X, Square, Copy, Minus } from 'lucide-svelte';
|
||||
import { t } from '$lib/i18n';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
|
||||
export type ImmersiveTab = 'lyrics' | 'trackInfo' | 'suggestions' | 'queue';
|
||||
export type DisplayMode = 'coverflow' | 'split' | 'lyrics-focus' | 'queue-focus';
|
||||
export type FocusTab = 'coverflow' | 'lyrics-focus' | 'queue-focus';
|
||||
export type ViewMode = 'focus' | 'split';
|
||||
|
||||
interface Props {
|
||||
viewMode: ViewMode;
|
||||
activeTab: ImmersiveTab;
|
||||
displayMode: DisplayMode;
|
||||
activeFocusTab: FocusTab;
|
||||
onViewModeChange: (mode: ViewMode) => void;
|
||||
onTabChange: (tab: ImmersiveTab) => void;
|
||||
onDisplayModeChange: (mode: DisplayMode) => void;
|
||||
onFocusTabChange: (tab: FocusTab) => void;
|
||||
onClose: () => void;
|
||||
onCloseApp?: () => void;
|
||||
visible?: boolean;
|
||||
@@ -25,10 +28,12 @@
|
||||
}
|
||||
|
||||
let {
|
||||
viewMode,
|
||||
activeTab,
|
||||
displayMode,
|
||||
activeFocusTab,
|
||||
onViewModeChange,
|
||||
onTabChange,
|
||||
onDisplayModeChange,
|
||||
onFocusTabChange,
|
||||
onClose,
|
||||
onCloseApp,
|
||||
visible = true,
|
||||
@@ -69,45 +74,55 @@
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = $derived([
|
||||
// Split mode tabs
|
||||
const splitTabs = $derived([
|
||||
{ id: 'lyrics' as const, label: $t('player.lyrics'), icon: Music2, enabled: hasLyrics },
|
||||
{ id: 'trackInfo' as const, label: $t('player.trackInfo') || 'Track Info', icon: Info, enabled: hasTrackInfo },
|
||||
{ id: 'suggestions' as const, label: $t('player.suggestions') || 'Suggestions', icon: Radio, enabled: hasSuggestions },
|
||||
{ id: 'queue' as const, label: $t('player.queue') || 'Queue', icon: ListMusic, enabled: true },
|
||||
].filter(tab => tab.enabled));
|
||||
|
||||
const displayModes: { id: DisplayMode; icon: typeof Disc3; title: string }[] = [
|
||||
{ id: 'coverflow', icon: Disc3, title: 'Coverflow (1)' },
|
||||
{ id: 'split', icon: LayoutGrid, title: 'Split View (2)' },
|
||||
{ id: 'lyrics-focus', icon: Mic2, title: 'Lyrics Focus (3)' },
|
||||
{ id: 'queue-focus', icon: ListMusic, title: 'Queue Focus (4)' },
|
||||
// Focus mode tabs
|
||||
const focusTabs: { id: FocusTab; label: string; icon: typeof Disc3 }[] = [
|
||||
{ id: 'coverflow', label: 'Coverflow', icon: Disc3 },
|
||||
{ id: 'lyrics-focus', label: 'Lyrics', icon: Mic2 },
|
||||
{ id: 'queue-focus', label: 'Queue', icon: ListMusic },
|
||||
];
|
||||
|
||||
// Only show tabs in split mode
|
||||
const showTabs = $derived(displayMode === 'split');
|
||||
const isFocusMode = $derived(viewMode === 'focus');
|
||||
</script>
|
||||
|
||||
<header class="immersive-header" class:visible>
|
||||
<!-- Left: Display mode switcher -->
|
||||
<!-- Left: View mode toggle -->
|
||||
<div class="header-left">
|
||||
<div class="mode-switcher">
|
||||
{#each displayModes as mode (mode.id)}
|
||||
<button
|
||||
class="mode-btn"
|
||||
class:active={displayMode === mode.id}
|
||||
onclick={() => onDisplayModeChange(mode.id)}
|
||||
title={mode.title}
|
||||
>
|
||||
<mode.icon size={18} />
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<button
|
||||
class="mode-toggle"
|
||||
onclick={() => onViewModeChange(isFocusMode ? 'split' : 'focus')}
|
||||
title={isFocusMode ? 'Switch to Split View' : 'Switch to Focus View'}
|
||||
>
|
||||
<img
|
||||
src={isFocusMode ? '/lotus.svg' : '/split-view.svg'}
|
||||
alt={isFocusMode ? 'Focus Mode' : 'Split Mode'}
|
||||
class="mode-icon"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Center: Tabs (only in split mode) -->
|
||||
{#if showTabs}
|
||||
<nav class="tabs">
|
||||
{#each tabs as tab (tab.id)}
|
||||
<!-- Center: Tabs (change based on view mode) -->
|
||||
<nav class="tabs">
|
||||
{#if isFocusMode}
|
||||
{#each focusTabs as tab (tab.id)}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeFocusTab === tab.id}
|
||||
onclick={() => onFocusTabChange(tab.id)}
|
||||
>
|
||||
<tab.icon size={16} />
|
||||
<span class="tab-label">{tab.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
{:else}
|
||||
{#each splitTabs as tab (tab.id)}
|
||||
<button
|
||||
class="tab"
|
||||
class:active={activeTab === tab.id}
|
||||
@@ -117,10 +132,8 @@
|
||||
<span class="tab-label">{tab.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</nav>
|
||||
{:else}
|
||||
<div class="tabs-placeholder"></div>
|
||||
{/if}
|
||||
{/if}
|
||||
</nav>
|
||||
|
||||
<!-- Right: Expandable Window Controls -->
|
||||
<div class="header-actions">
|
||||
@@ -211,46 +224,38 @@
|
||||
}
|
||||
|
||||
.header-left {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
flex: 0 0 auto;
|
||||
min-width: 50px;
|
||||
}
|
||||
|
||||
.mode-switcher {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 4px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.mode-btn {
|
||||
.mode-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
padding: 8px;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 12px;
|
||||
cursor: pointer;
|
||||
transition: all 150ms ease;
|
||||
}
|
||||
|
||||
.mode-btn:hover {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
.mode-toggle:hover {
|
||||
background: rgba(0, 0, 0, 0.6);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.mode-btn.active {
|
||||
color: var(--text-primary, white);
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
.mode-icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
filter: invert(1) opacity(0.85);
|
||||
transition: filter 150ms ease;
|
||||
}
|
||||
|
||||
.tabs-placeholder {
|
||||
flex: 1;
|
||||
.mode-toggle:hover .mode-icon {
|
||||
filter: invert(1) opacity(1);
|
||||
}
|
||||
|
||||
.tabs {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import ImmersiveBackground from './ImmersiveBackground.svelte';
|
||||
import ImmersiveArtwork from './ImmersiveArtwork.svelte';
|
||||
import ImmersiveHeader, { type ImmersiveTab, type DisplayMode } from './ImmersiveHeader.svelte';
|
||||
import ImmersiveHeader, { type ImmersiveTab, type FocusTab, type ViewMode } from './ImmersiveHeader.svelte';
|
||||
import PlayerControlsCompact from './PlayerControlsCompact.svelte';
|
||||
import LyricsPanel from './panels/LyricsPanel.svelte';
|
||||
import TrackInfoPanel from './panels/TrackInfoPanel.svelte';
|
||||
@@ -115,8 +115,9 @@
|
||||
}: Props = $props();
|
||||
|
||||
// UI State
|
||||
let displayMode: DisplayMode = $state('split');
|
||||
let viewMode: ViewMode = $state('split');
|
||||
let activeTab: ImmersiveTab = $state('lyrics');
|
||||
let activeFocusTab: FocusTab = $state('coverflow');
|
||||
let showUI = $state(true);
|
||||
let hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let isFullscreen = $state(false);
|
||||
@@ -210,35 +211,38 @@
|
||||
onSeek(Math.min(duration, currentTime + 5));
|
||||
}
|
||||
break;
|
||||
// Display mode shortcuts
|
||||
case '1':
|
||||
displayMode = 'coverflow';
|
||||
// View mode toggle
|
||||
case 'v':
|
||||
case 'V':
|
||||
viewMode = viewMode === 'split' ? 'focus' : 'split';
|
||||
break;
|
||||
case '2':
|
||||
displayMode = 'split';
|
||||
break;
|
||||
case '3':
|
||||
displayMode = 'lyrics-focus';
|
||||
break;
|
||||
case '4':
|
||||
displayMode = 'queue-focus';
|
||||
break;
|
||||
// Tab shortcuts (only in split mode)
|
||||
// Tab shortcuts (split mode)
|
||||
case 'l':
|
||||
case 'L':
|
||||
if (displayMode === 'split') activeTab = 'lyrics';
|
||||
if (viewMode === 'split') activeTab = 'lyrics';
|
||||
break;
|
||||
case 't':
|
||||
case 'T':
|
||||
if (displayMode === 'split' && enableCredits) activeTab = 'trackInfo';
|
||||
if (viewMode === 'split' && enableCredits) activeTab = 'trackInfo';
|
||||
break;
|
||||
case 's':
|
||||
case 'S':
|
||||
if (displayMode === 'split' && enableSuggestions) activeTab = 'suggestions';
|
||||
if (viewMode === 'split' && enableSuggestions) activeTab = 'suggestions';
|
||||
break;
|
||||
case 'q':
|
||||
case 'Q':
|
||||
if (displayMode === 'split') activeTab = 'queue';
|
||||
if (viewMode === 'split') activeTab = 'queue';
|
||||
else if (viewMode === 'focus') activeFocusTab = 'queue-focus';
|
||||
break;
|
||||
// Focus mode tabs
|
||||
case '1':
|
||||
if (viewMode === 'focus') activeFocusTab = 'coverflow';
|
||||
break;
|
||||
case '2':
|
||||
if (viewMode === 'focus') activeFocusTab = 'lyrics-focus';
|
||||
break;
|
||||
case '3':
|
||||
if (viewMode === 'focus') activeFocusTab = 'queue-focus';
|
||||
break;
|
||||
}
|
||||
resetHideTimer();
|
||||
@@ -280,10 +284,12 @@
|
||||
|
||||
<!-- Header with mode switcher -->
|
||||
<ImmersiveHeader
|
||||
{viewMode}
|
||||
{activeTab}
|
||||
{displayMode}
|
||||
{activeFocusTab}
|
||||
onViewModeChange={(mode) => viewMode = mode}
|
||||
onTabChange={(tab) => activeTab = tab}
|
||||
onDisplayModeChange={(mode) => displayMode = mode}
|
||||
onFocusTabChange={(tab) => activeFocusTab = tab}
|
||||
onClose={handleExitImmersive}
|
||||
visible={showUI}
|
||||
hasLyrics={true}
|
||||
@@ -296,39 +302,42 @@
|
||||
onMinimize={minimizeWindow}
|
||||
/>
|
||||
|
||||
<!-- Content based on display mode -->
|
||||
{#if displayMode === 'coverflow'}
|
||||
<!-- Coverflow: Centered artwork -->
|
||||
<CoverflowPanel
|
||||
{artwork}
|
||||
{trackTitle}
|
||||
{artist}
|
||||
{album}
|
||||
{isPlaying}
|
||||
{quality}
|
||||
{bitDepth}
|
||||
{samplingRate}
|
||||
/>
|
||||
{:else if displayMode === 'lyrics-focus'}
|
||||
<!-- Lyrics Focus: Single line, large, centered -->
|
||||
<LyricsFocusPanel
|
||||
lines={lyricsLines}
|
||||
activeIndex={lyricsActiveIndex}
|
||||
isLoading={lyricsLoading}
|
||||
error={lyricsError}
|
||||
/>
|
||||
{:else if displayMode === 'queue-focus'}
|
||||
<!-- Queue Focus: Full screen queue -->
|
||||
<div class="focus-panel">
|
||||
<div class="focus-panel-content queue-content">
|
||||
<QueuePanel
|
||||
tracks={queueTracks}
|
||||
currentIndex={queueCurrentIndex}
|
||||
onPlayTrack={(index) => onQueuePlayTrack?.(index)}
|
||||
onClear={onQueueClear}
|
||||
/>
|
||||
<!-- Content based on view mode -->
|
||||
{#if viewMode === 'focus'}
|
||||
<!-- Focus Mode Views -->
|
||||
{#if activeFocusTab === 'coverflow'}
|
||||
<!-- Coverflow: Centered artwork -->
|
||||
<CoverflowPanel
|
||||
{artwork}
|
||||
{trackTitle}
|
||||
{artist}
|
||||
{album}
|
||||
{isPlaying}
|
||||
{quality}
|
||||
{bitDepth}
|
||||
{samplingRate}
|
||||
/>
|
||||
{:else if activeFocusTab === 'lyrics-focus'}
|
||||
<!-- Lyrics Focus: Single line, large, centered -->
|
||||
<LyricsFocusPanel
|
||||
lines={lyricsLines}
|
||||
activeIndex={lyricsActiveIndex}
|
||||
isLoading={lyricsLoading}
|
||||
error={lyricsError}
|
||||
/>
|
||||
{:else if activeFocusTab === 'queue-focus'}
|
||||
<!-- Queue Focus: Full screen queue -->
|
||||
<div class="focus-panel">
|
||||
<div class="focus-panel-content queue-content">
|
||||
<QueuePanel
|
||||
tracks={queueTracks}
|
||||
currentIndex={queueCurrentIndex}
|
||||
onPlayTrack={(index) => onQueuePlayTrack?.(index)}
|
||||
onClear={onQueueClear}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<!-- Split: Artwork + Panel side by side -->
|
||||
<div class="immersive-main">
|
||||
|
||||
Reference in new issue
Block a user