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