feat(sidebar): add collapsible Favorites section
- Add chevron button to expand/collapse Favorites sub-items - Show Artists, Tracks, Albums as nested items when expanded - Respect user's tab order from favorites preferences - Add popover menu for collapsed sidebar mode - Use translations for all labels
This commit is contained in:
1 parent
0ef72c2499
commit
75aab4edb5
1 file changed
+210
-9
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { tick } from 'svelte';
|
import { tick } from 'svelte';
|
||||||
import { Search, Home, HardDrive, Plus, RefreshCw, ChevronDown, ChevronUp, Heart, ListMusic, Import, Settings, MoreHorizontal, ArrowUpDown, ChevronRight, ChevronLeft, Folder, FolderPlus, X } from 'lucide-svelte';
|
import { Search, Home, HardDrive, Plus, RefreshCw, ChevronDown, ChevronUp, Heart, ListMusic, Import, Settings, MoreHorizontal, ArrowUpDown, ChevronRight, ChevronLeft, Folder, FolderPlus, X, User, Disc, Music } from 'lucide-svelte';
|
||||||
|
import type { FavoritesPreferences } from '$lib/types';
|
||||||
import { invoke } from '@tauri-apps/api/core';
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import NavigationItem from './NavigationItem.svelte';
|
import NavigationItem from './NavigationItem.svelte';
|
||||||
@@ -103,6 +104,12 @@
|
|||||||
let playlistsCollapsed = $state(false);
|
let playlistsCollapsed = $state(false);
|
||||||
let localLibraryCollapsed = $state(false);
|
let localLibraryCollapsed = $state(false);
|
||||||
|
|
||||||
|
// Favorites section state
|
||||||
|
let favoritesExpanded = $state(false);
|
||||||
|
let favoritesTabOrder = $state<string[]>(['tracks', 'albums', 'artists']);
|
||||||
|
let showFavoritesMenu = $state(false);
|
||||||
|
let favoritesMenuPos = $state({ x: 0, y: 0 });
|
||||||
|
|
||||||
// Sidebar search state - synced with SearchView
|
// Sidebar search state - synced with SearchView
|
||||||
let sidebarSearchQuery = $state(getSearchQuery());
|
let sidebarSearchQuery = $state(getSearchQuery());
|
||||||
let sidebarSearchInput: HTMLInputElement;
|
let sidebarSearchInput: HTMLInputElement;
|
||||||
@@ -699,12 +706,23 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function loadFavoritesPreferences() {
|
||||||
|
try {
|
||||||
|
const prefs = await invoke<FavoritesPreferences>('get_favorites_preferences');
|
||||||
|
// Filter out 'playlists' from tab order for sidebar display
|
||||||
|
favoritesTabOrder = (prefs.tab_order || ['tracks', 'albums', 'artists']).filter(t => t !== 'playlists');
|
||||||
|
} catch (err) {
|
||||||
|
console.debug('[Sidebar] Failed to load favorites preferences:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
loadSortPreference();
|
loadSortPreference();
|
||||||
loadUserPlaylists();
|
loadUserPlaylists();
|
||||||
loadPlaylistSettings();
|
loadPlaylistSettings();
|
||||||
loadLocalTrackCounts();
|
loadLocalTrackCounts();
|
||||||
loadFolders(); // Load playlist folders
|
loadFolders(); // Load playlist folders
|
||||||
|
loadFavoritesPreferences(); // Load favorites tab order
|
||||||
|
|
||||||
// Subscribe to offline state changes
|
// Subscribe to offline state changes
|
||||||
const unsubscribeOffline = subscribeOffline(() => {
|
const unsubscribeOffline = subscribeOffline(() => {
|
||||||
@@ -918,6 +936,12 @@
|
|||||||
closeFolderPopover();
|
closeFolderPopover();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (showFavoritesMenu) {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
if (!target.closest('.favorites-popover') && !target.closest('.favorites-section .nav-item')) {
|
||||||
|
showFavoritesMenu = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -977,18 +1001,97 @@
|
|||||||
</NavigationItem>
|
</NavigationItem>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Favorites Section (standalone) -->
|
<!-- Favorites Section (collapsible) -->
|
||||||
<nav class="nav-section">
|
<nav class="nav-section favorites-section">
|
||||||
<NavigationItem
|
{#if isExpanded}
|
||||||
label={$t('nav.favorites')}
|
<button
|
||||||
active={activeView.startsWith('favorites-')}
|
class="nav-item favorites-header"
|
||||||
|
class:active={activeView.startsWith('favorites-')}
|
||||||
onclick={() => handleViewChange('favorites')}
|
onclick={() => handleViewChange('favorites')}
|
||||||
showLabel={isExpanded}
|
|
||||||
>
|
>
|
||||||
{#snippet icon()}<Heart size={14} />{/snippet}
|
<div class="icon-container">
|
||||||
</NavigationItem>
|
<Heart size={14} />
|
||||||
|
</div>
|
||||||
|
<span class="label">{$t('nav.favorites')}</span>
|
||||||
|
<button
|
||||||
|
class="favorites-chevron"
|
||||||
|
class:expanded={favoritesExpanded}
|
||||||
|
onclick={(e) => { e.stopPropagation(); favoritesExpanded = !favoritesExpanded; }}
|
||||||
|
title={favoritesExpanded ? 'Collapse' : 'Expand'}
|
||||||
|
>
|
||||||
|
<ChevronRight size={12} />
|
||||||
|
</button>
|
||||||
|
</button>
|
||||||
|
{#if favoritesExpanded}
|
||||||
|
<div class="favorites-subitems">
|
||||||
|
{#each favoritesTabOrder as tab}
|
||||||
|
<button
|
||||||
|
class="nav-item favorites-subitem"
|
||||||
|
class:active={activeView === `favorites-${tab}`}
|
||||||
|
onclick={() => handleViewChange(`favorites-${tab}`)}
|
||||||
|
>
|
||||||
|
<div class="icon-container">
|
||||||
|
{#if tab === 'artists'}
|
||||||
|
<User size={12} />
|
||||||
|
{:else if tab === 'albums'}
|
||||||
|
<Disc size={12} />
|
||||||
|
{:else if tab === 'tracks'}
|
||||||
|
<Music size={12} />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
<span class="label">{$t(`favorites.${tab}`)}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
<!-- Collapsed sidebar: show heart with menu on click -->
|
||||||
|
<button
|
||||||
|
class="nav-item"
|
||||||
|
class:active={activeView.startsWith('favorites-')}
|
||||||
|
onclick={(e) => {
|
||||||
|
const rect = e.currentTarget.getBoundingClientRect();
|
||||||
|
favoritesMenuPos = { x: rect.right + 8, y: rect.top };
|
||||||
|
showFavoritesMenu = !showFavoritesMenu;
|
||||||
|
}}
|
||||||
|
title={$t('nav.favorites')}
|
||||||
|
>
|
||||||
|
<div class="icon-container">
|
||||||
|
<Heart size={14} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
<!-- Favorites menu popover (when sidebar collapsed) -->
|
||||||
|
{#if showFavoritesMenu && !isExpanded}
|
||||||
|
<div
|
||||||
|
class="favorites-popover"
|
||||||
|
style="left: {favoritesMenuPos.x}px; top: {favoritesMenuPos.y}px;"
|
||||||
|
>
|
||||||
|
<button class="popover-item" onclick={() => { handleViewChange('favorites'); showFavoritesMenu = false; }}>
|
||||||
|
<Heart size={14} />
|
||||||
|
<span>{$t('favorites.title')}</span>
|
||||||
|
</button>
|
||||||
|
<div class="popover-divider"></div>
|
||||||
|
{#each favoritesTabOrder as tab}
|
||||||
|
<button
|
||||||
|
class="popover-item"
|
||||||
|
onclick={() => { handleViewChange(`favorites-${tab}`); showFavoritesMenu = false; }}
|
||||||
|
>
|
||||||
|
{#if tab === 'artists'}
|
||||||
|
<User size={14} />
|
||||||
|
{:else if tab === 'albums'}
|
||||||
|
<Disc size={14} />
|
||||||
|
{:else if tab === 'tracks'}
|
||||||
|
<Music size={14} />
|
||||||
|
{/if}
|
||||||
|
<span>{$t(`favorites.${tab}`)}</span>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Playlists Section (hidden in offline mode) -->
|
<!-- Playlists Section (hidden in offline mode) -->
|
||||||
{#if !isOffline}
|
{#if !isOffline}
|
||||||
<div class="section playlists-section">
|
<div class="section playlists-section">
|
||||||
@@ -2123,4 +2226,102 @@
|
|||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Favorites Section */
|
||||||
|
.favorites-section {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-header {
|
||||||
|
position: relative;
|
||||||
|
padding-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-chevron {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
padding: 0;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted);
|
||||||
|
transition: transform 150ms ease, color 150ms ease, background 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-chevron:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-chevron.expanded {
|
||||||
|
transform: translateY(-50%) rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-subitems {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-left: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-subitem {
|
||||||
|
padding: 6px 10px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.favorites-subitem .icon-container {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Favorites Popover (collapsed sidebar) */
|
||||||
|
.favorites-popover {
|
||||||
|
position: fixed;
|
||||||
|
z-index: 9999;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
padding: 6px;
|
||||||
|
min-width: 160px;
|
||||||
|
animation: fade-in 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade-in {
|
||||||
|
from { opacity: 0; transform: translateY(-4px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
transition: background-color 150ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-item:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.popover-divider {
|
||||||
|
height: 1px;
|
||||||
|
background: var(--border-subtle);
|
||||||
|
margin: 6px 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
Reference in new issue
Block a user