diff --git a/src/components/BottomNav.tsx b/src/components/BottomNav.tsx index b8cad603..ba019207 100644 --- a/src/components/BottomNav.tsx +++ b/src/components/BottomNav.tsx @@ -1,9 +1,10 @@ import { useState } from 'react'; import { NavLink } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Disc3, Search, Music4, AudioLines } from 'lucide-react'; +import { Disc3, Search, Music4, AudioLines, MoreHorizontal } from 'lucide-react'; import { usePlayerStore } from '../store/playerStore'; import MobileSearchOverlay from './MobileSearchOverlay'; +import MobileMoreOverlay from './MobileMoreOverlay'; const NAV_ITEMS = [ { to: '/', end: true, icon: Disc3, labelKey: 'sidebar.mainstage' }, @@ -16,6 +17,7 @@ export default function BottomNav() { const isPlaying = usePlayerStore(s => s.isPlaying); const currentTrack = usePlayerStore(s => s.currentTrack); const [searchOpen, setSearchOpen] = useState(false); + const [moreOpen, setMoreOpen] = useState(false); return ( <> @@ -47,9 +49,21 @@ export default function BottomNav() { {t('search.title')} + + {searchOpen && setSearchOpen(false)} />} + {moreOpen && setMoreOpen(false)} />} ); } diff --git a/src/components/MobileMoreOverlay.tsx b/src/components/MobileMoreOverlay.tsx new file mode 100644 index 00000000..9cdc3846 --- /dev/null +++ b/src/components/MobileMoreOverlay.tsx @@ -0,0 +1,73 @@ +import { createPortal } from 'react-dom'; +import { NavLink } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { Settings, HardDriveDownload } from 'lucide-react'; +import { useSidebarStore } from '../store/sidebarStore'; +import { useAuthStore } from '../store/authStore'; +import { useOfflineStore } from '../store/offlineStore'; +import { ALL_NAV_ITEMS } from '../config/navItems'; + +const BOTTOM_NAV_ROUTES = new Set(['/', '/albums', '/now-playing']); + +export default function MobileMoreOverlay({ onClose }: { onClose: () => void }) { + const { t } = useTranslation(); + const sidebarItems = useSidebarStore(s => s.items); + const randomNavMode = useAuthStore(s => s.randomNavMode); + const serverId = useAuthStore(s => s.activeServerId ?? ''); + const offlineAlbums = useOfflineStore(s => s.albums); + const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); + + const items = sidebarItems + .filter(cfg => { + if (!cfg?.visible) return false; + const item = ALL_NAV_ITEMS[cfg.id]; + if (!item) return false; + if (BOTTOM_NAV_ROUTES.has(item.to)) return false; + if (randomNavMode === 'hub' && (cfg.id === 'randomMix' || cfg.id === 'randomAlbums')) return false; + if (randomNavMode === 'separate' && cfg.id === 'randomPicker') return false; + return true; + }) + .map(cfg => ALL_NAV_ITEMS[cfg.id]); + + return createPortal( + <> +
+
+
+
+ {items.map(item => ( + `mobile-more-item${isActive ? ' active' : ''}`} + onClick={onClose} + > + + {t(item.labelKey)} + + ))} + {hasOfflineContent && ( + `mobile-more-item${isActive ? ' active' : ''}`} + onClick={onClose} + > + + {t('sidebar.offlineLibrary')} + + )} + `mobile-more-item${isActive ? ' active' : ''}`} + onClick={onClose} + > + + {t('sidebar.settings')} + +
+
+ , + document.body + ); +} diff --git a/src/locales/de.ts b/src/locales/de.ts index 057079ac..db69174f 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -30,6 +30,7 @@ export const deTranslation = { allLibraries: 'Alle Bibliotheken', expandPlaylists: 'Playlists ausklappen', collapsePlaylists: 'Playlists einklappen', + more: 'Mehr', }, home: { hero: 'Featured', diff --git a/src/locales/en.ts b/src/locales/en.ts index d2310084..d5788fb3 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -31,6 +31,7 @@ export const enTranslation = { allLibraries: 'All libraries', expandPlaylists: 'Expand playlists', collapsePlaylists: 'Collapse playlists', + more: 'More', }, home: { hero: 'Featured', diff --git a/src/locales/es.ts b/src/locales/es.ts index ea752371..1d7de27e 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -31,6 +31,7 @@ export const esTranslation = { allLibraries: 'Todas las bibliotecas', expandPlaylists: 'Expandir listas', collapsePlaylists: 'Colapsar listas', + more: 'Más', }, home: { hero: 'Destacado', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index d87e1829..351684b6 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -30,6 +30,7 @@ export const frTranslation = { allLibraries: 'Toutes les bibliothèques', expandPlaylists: 'Développer les playlists', collapsePlaylists: 'Réduire les playlists', + more: 'Plus', }, home: { hero: 'En vedette', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index a39f53ee..f5d55d00 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -30,6 +30,7 @@ export const nbTranslation = { allLibraries: 'Alle biblioteker', expandPlaylists: 'Utvid spillelister', collapsePlaylists: 'Skjul spillelister', + more: 'Mer', }, home: { hero: 'Utvalgt', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index 053d5f77..7e9fb520 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -30,6 +30,7 @@ export const nlTranslation = { allLibraries: 'Alle bibliotheken', expandPlaylists: 'Afspeellijsten uitklappen', collapsePlaylists: 'Afspeellijsten inklappen', + more: 'Meer', }, home: { hero: 'Uitgelicht', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index 534c298a..89e69734 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -31,6 +31,7 @@ export const ruTranslation = { allLibraries: 'Все библиотеки', expandPlaylists: 'Развернуть плейлисты', collapsePlaylists: 'Свернуть плейлисты', + more: 'Ещё', }, home: { hero: 'Подборка', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index 64f2e632..bae60a32 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -30,6 +30,7 @@ export const zhTranslation = { allLibraries: '所有资料库', expandPlaylists: '展开播放列表', collapsePlaylists: '收起播放列表', + more: '更多', }, home: { hero: '精选', diff --git a/src/pages/ArtistDetail.tsx b/src/pages/ArtistDetail.tsx index 8c2fee5f..55a53535 100644 --- a/src/pages/ArtistDetail.tsx +++ b/src/pages/ArtistDetail.tsx @@ -4,7 +4,8 @@ import { getArtist, getArtistInfo, getTopSongs, getSimilarSongs2, getAlbum, sear import AlbumCard from '../components/AlbumCard'; import CachedImage from '../components/CachedImage'; import CoverLightbox from '../components/CoverLightbox'; -import { ArrowLeft, Users, ExternalLink, Heart, Play, Shuffle, Radio, HardDriveDownload, Check, Camera, Loader2 } from 'lucide-react'; +import { ArrowLeft, Users, ExternalLink, Heart, Play, Shuffle, Radio, HardDriveDownload, Check, Camera, Loader2, ChevronDown, ChevronUp } from 'lucide-react'; +import { useIsMobile } from '../hooks/useIsMobile'; import { open } from '@tauri-apps/plugin-shell'; import { usePlayerStore, songToTrack } from '../store/playerStore'; import { useOfflineStore } from '../store/offlineStore'; @@ -15,6 +16,7 @@ import { lastfmGetSimilarArtists, lastfmIsConfigured } from '../api/lastfm'; import LastfmIcon from '../components/LastfmIcon'; import { invalidateCoverArt } from '../utils/imageCache'; import { showToast } from '../utils/toast'; +import { extractCoverColors } from '../utils/dynamicColors'; import StarRating from '../components/StarRating'; function formatDuration(seconds: number): string { @@ -62,7 +64,10 @@ export default function ArtistDetail() { const [lightboxOpen, setLightboxOpen] = useState(false); const [bioExpanded, setBioExpanded] = useState(false); const [uploading, setUploading] = useState(false); + const [similarCollapsed, setSimilarCollapsed] = useState(true); + const isMobile = useIsMobile(); const [coverRevision, setCoverRevision] = useState(0); + const [avatarGlow, setAvatarGlow] = useState(''); const imageInputRef = useRef(null); const playTrack = usePlayerStore(state => state.playTrack); @@ -91,6 +96,7 @@ export default function ArtistDetail() { setInfo(null); setTopSongs([]); setFeaturedAlbums([]); + setAvatarGlow(''); getArtist(id).then(artistData => { if (cancelled) return; setArtist(artistData.artist); @@ -449,7 +455,14 @@ export default function ArtistDetail() { )}
-
+
{coverId ? ( @@ -538,15 +552,25 @@ export default function ArtistDetail() { {playAllLoading ?
: } {t('artistDetail.playAll')} - )} - {albums.length > 0 && (() => { const progress = id ? bulkProgress[id] : undefined; @@ -564,9 +588,9 @@ export default function ArtistDetail() { {isDownloading ?
: isDone ? : } - {isDownloading + {!isMobile && (isDownloading ? t('artistDetail.offlineDownloading', { done: progress.done, total: progress.total }) - : isDone ? t('artistDetail.offlineCached') : t('artistDetail.cacheOffline')} + : isDone ? t('artistDetail.offlineCached') : t('artistDetail.cacheOffline'))} ); })()} @@ -665,9 +689,20 @@ export default function ArtistDetail() { {showSimilarSection && ( <> -

- {t('artistDetail.similarArtists')} -

+
+

+ {t('artistDetail.similarArtists')} +

+ {isMobile && (() => { + const list = showAudiomuseSimilar ? serverSimilarArtists : similarArtists; + return list.length > 5 ? ( + + ) : null; + })()} +
{showLastfmSimilar && similarLoading ? (
@@ -675,15 +710,17 @@ export default function ArtistDetail() {
) : (
- {(showAudiomuseSimilar ? serverSimilarArtists : similarArtists).map(a => ( - - ))} + {(showAudiomuseSimilar ? serverSimilarArtists : similarArtists) + .slice(0, isMobile && similarCollapsed ? 5 : undefined) + .map(a => ( + + ))}
)} @@ -695,7 +732,7 @@ export default function ArtistDetail() { {albums.length > 0 ? ( -
+
{albums.map(a => )}
) : ( @@ -715,7 +752,7 @@ export default function ArtistDetail() { ))}
) : ( -
+
{featuredAlbums.map(a => )}
)} diff --git a/src/pages/RandomMix.tsx b/src/pages/RandomMix.tsx index 398f2dee..a6c2c608 100644 --- a/src/pages/RandomMix.tsx +++ b/src/pages/RandomMix.tsx @@ -5,6 +5,7 @@ import { useAuthStore } from '../store/authStore'; import { Play, RefreshCw, ChevronDown, ChevronUp, Heart } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useDragDrop } from '../contexts/DragDropContext'; +import { useIsMobile } from '../hooks/useIsMobile'; import { fetchRandomMixSongsUntilFull, getMixMinRatingsConfigFromAuth, @@ -38,6 +39,7 @@ export default function RandomMix() { const starredOverrides = usePlayerStore(s => s.starredOverrides); const setStarredOverride = usePlayerStore(s => s.setStarredOverride); const [contextMenuSongId, setContextMenuSongId] = useState(null); + const isMobile = useIsMobile(); const psyDrag = useDragDrop(); const [starredSongs, setStarredSongs] = useState>(new Set()); const { @@ -68,6 +70,10 @@ export default function RandomMix() { const [blacklistOpen, setBlacklistOpen] = useState(false); const [newGenre, setNewGenre] = useState(''); + // Mobile collapsible panels + const [filtersExpanded, setFiltersExpanded] = useState(false); + const [genreMixExpanded, setGenreMixExpanded] = useState(false); + // Genre Mix state const [serverGenres, setServerGenres] = useState([]); const [allAvailableGenres, setAllAvailableGenres] = useState([]); @@ -217,7 +223,7 @@ export default function RandomMix() { {/* ── Filter + Genre Mix panel ─────────────────────────────── */}
{/* Left: Blacklist */}
-
- {t('randomMix.filterPanelTitle')} -
-

- {t('randomMix.filterPanelDesc')} -

- -