diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a0708390..3cdc738e 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -157,6 +157,12 @@ fn export_runtime_logs(path: String) -> Result { crate::logging::export_logs_to_file(&path) } +#[tauri::command] +fn frontend_debug_log(scope: String, message: String) -> Result<(), String> { + crate::app_deprintln!("[frontend][{}] {}", scope, message); + Ok(()) +} + #[tauri::command] fn set_subsonic_wire_user_agent( user_agent: String, @@ -3639,6 +3645,7 @@ pub fn run() { set_linux_webkit_smooth_scrolling, set_logging_mode, export_runtime_logs, + frontend_debug_log, set_subsonic_wire_user_agent, no_compositing_mode, is_tiling_wm_cmd, diff --git a/src/App.tsx b/src/App.tsx index 8a74a925..79e890fc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -28,6 +28,7 @@ import Login from './pages/Login'; import AlbumDetail from './pages/AlbumDetail'; import MostPlayed from './pages/MostPlayed'; import RandomAlbums from './pages/RandomAlbums'; +import LuckyMixPage from './pages/LuckyMix'; import SearchResults from './pages/SearchResults'; import Playlists from './pages/Playlists'; import PlaylistDetail from './pages/PlaylistDetail'; @@ -452,6 +453,7 @@ function AppShell() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/src/components/MobileMoreOverlay.tsx b/src/components/MobileMoreOverlay.tsx index 9cdc3846..850006a5 100644 --- a/src/components/MobileMoreOverlay.tsx +++ b/src/components/MobileMoreOverlay.tsx @@ -6,6 +6,7 @@ import { useSidebarStore } from '../store/sidebarStore'; import { useAuthStore } from '../store/authStore'; import { useOfflineStore } from '../store/offlineStore'; import { ALL_NAV_ITEMS } from '../config/navItems'; +import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; const BOTTOM_NAV_ROUTES = new Set(['/', '/albums', '/now-playing']); @@ -16,6 +17,8 @@ export default function MobileMoreOverlay({ onClose }: { onClose: () => void }) const serverId = useAuthStore(s => s.activeServerId ?? ''); const offlineAlbums = useOfflineStore(s => s.albums); const hasOfflineContent = Object.values(offlineAlbums).some(a => a.serverId === serverId); + const luckyMixBase = useLuckyMixAvailable(); + const luckyMixAvailable = luckyMixBase && randomNavMode === 'separate'; const items = sidebarItems .filter(cfg => { @@ -25,6 +28,7 @@ export default function MobileMoreOverlay({ onClose }: { onClose: () => void }) 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; + if (cfg.id === 'luckyMix' && !luckyMixAvailable) return false; return true; }) .map(cfg => ALL_NAV_ITEMS[cfg.id]); diff --git a/src/components/QueuePanel.tsx b/src/components/QueuePanel.tsx index 254e378e..a9627f8a 100644 --- a/src/components/QueuePanel.tsx +++ b/src/components/QueuePanel.tsx @@ -17,6 +17,7 @@ import LyricsPane from './LyricsPane'; import NowPlayingInfo from './NowPlayingInfo'; import { TFunction } from 'i18next'; import OverlayScrollArea from './OverlayScrollArea'; +import { useLuckyMixStore } from '../store/luckyMixStore'; function formatTime(seconds: number): string { if (!seconds || isNaN(seconds)) return '0:00'; @@ -269,6 +270,7 @@ export default function QueuePanel() { const activeTab = useLyricsStore(s => s.activeTab); const setTab = useLyricsStore(s => s.setTab); + const luckyRolling = useLuckyMixStore(s => s.isRolling); const [showRemainingTime, setShowRemainingTime] = useState(false); const [showCrossfadePopover, setShowCrossfadePopover] = useState(false); @@ -657,7 +659,8 @@ export default function QueuePanel() { {t('queue.emptyQueue')} ) : ( - queue.map((track, idx) => { + <> + {queue.map((track, idx) => { const isPlaying = idx === queueIndex; const isFirstAutoAdded = track.autoAdded && (idx === 0 || !queue[idx - 1].autoAdded); const isFirstRadioAdded = track.radioAdded && (idx === 0 || !queue[idx - 1].radioAdded); @@ -726,9 +729,36 @@ export default function QueuePanel() { {formatTime(track.duration)} + {luckyRolling && isPlaying && ( + + )} ); - }) + })} + )} ) : activeTab === 'lyrics' ? ( diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index d244d2af..772763a5 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -27,6 +27,7 @@ import { isSidebarNavItemUserHideable, type SidebarNavDropTarget, } from '../utils/sidebarNavReorder'; +import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; const SIDEBAR_NAV_LONG_PRESS_MS = 1000; const SIDEBAR_NAV_LONG_PRESS_MOVE_CANCEL_PX = 10; @@ -68,6 +69,10 @@ export default function Sidebar({ const sidebarItems = useSidebarStore(s => s.items); const setSidebarItems = useSidebarStore(s => s.setItems); const randomNavMode = useAuthStore(s => s.randomNavMode); + const luckyMixBase = useLuckyMixAvailable(); + // Sidebar surfaces Lucky Mix as its own entry only in "separate" nav mode — + // in hub mode it lives inside the Build-a-Mix landing page instead. + const luckyMixAvailable = luckyMixBase && randomNavMode === 'separate'; const [libraryDropdownOpen, setLibraryDropdownOpen] = useState(false); const [playlistsExpanded, setPlaylistsExpanded] = useState(false); const playlistsRaw = usePlaylistStore(s => s.playlists); @@ -95,8 +100,13 @@ export default function Sidebar({ [sidebarItems], ); const visibleLibraryConfigs = useMemo( - () => libraryItemsForReorder.filter(c => c.visible), - [libraryItemsForReorder], + () => + libraryItemsForReorder.filter(c => { + if (!c.visible) return false; + if (c.id === 'luckyMix' && !luckyMixAvailable) return false; + return true; + }), + [libraryItemsForReorder, luckyMixAvailable], ); const visibleSystemConfigs = useMemo( () => systemItemsForReorder.filter(c => c.visible), diff --git a/src/config/navItems.ts b/src/config/navItems.ts index 3cf474c2..f85c40a6 100644 --- a/src/config/navItems.ts +++ b/src/config/navItems.ts @@ -2,7 +2,7 @@ import React from 'react'; import { Disc3, Users, Music4, Radio, Heart, BarChart3, HelpCircle, Tags, ListMusic, Cast, TrendingUp, - FolderOpen, HardDriveUpload, Wand2, Shuffle, Dices, + FolderOpen, HardDriveUpload, Wand2, Shuffle, Dices, Sparkles, } from 'lucide-react'; export interface NavItemMeta { @@ -20,6 +20,7 @@ export const ALL_NAV_ITEMS: Record = { randomPicker: { icon: Wand2, labelKey: 'sidebar.randomPicker', to: '/random', section: 'library' }, randomMix: { icon: Shuffle, labelKey: 'sidebar.randomMix', to: '/random/mix', section: 'library' }, randomAlbums: { icon: Dices, labelKey: 'sidebar.randomAlbums', to: '/random/albums', section: 'library' }, + luckyMix: { icon: Sparkles, labelKey: 'sidebar.feelingLucky', to: '/lucky-mix', section: 'library' }, artists: { icon: Users, labelKey: 'sidebar.artists', to: '/artists', section: 'library' }, genres: { icon: Tags, labelKey: 'sidebar.genres', to: '/genres', section: 'library' }, favorites: { icon: Heart, labelKey: 'sidebar.favorites', to: '/favorites', section: 'library' }, diff --git a/src/hooks/useLuckyMixAvailable.ts b/src/hooks/useLuckyMixAvailable.ts new file mode 100644 index 00000000..2c2af063 --- /dev/null +++ b/src/hooks/useLuckyMixAvailable.ts @@ -0,0 +1,40 @@ +import { useAuthStore } from '../store/authStore'; + +/** + * Whether "Lucky Mix" should be exposed as a navigable menu/card entry. + * + * Single source of truth for the gate — previously this logic was inlined in + * Sidebar, MobileMoreOverlay, RandomLanding, Settings/SidebarCustomizer, and + * the sidebarNavReorder filter. All call sites share the same three-way + * predicate: + * 1. User hasn't hidden it via the Settings toggle. + * 2. AudioMuse is enabled for the active server (feature depends on + * audiomuse-backed similar-track quality). + * 3. An active server exists at all. + * + * Callers that additionally care about the "split vs hub" navigation mode + * should combine this with `randomNavMode === 'separate'` explicitly — that's + * an orthogonal UI placement concern, not an availability concern. + */ +export function isLuckyMixAvailable(args: { + activeServerId: string | null | undefined; + audiomuseByServer: Record; + showLuckyMixMenu: boolean; +}): boolean { + const { activeServerId, audiomuseByServer, showLuckyMixMenu } = args; + if (!showLuckyMixMenu) return false; + if (!activeServerId) return false; + return Boolean(audiomuseByServer[activeServerId]); +} + +/** + * React hook form — subscribes to the three authStore slices the predicate + * depends on, so any user-facing change (toggle flip, server switch, AudioMuse + * toggle on/off) re-renders the caller automatically. + */ +export function useLuckyMixAvailable(): boolean { + const activeServerId = useAuthStore(s => s.activeServerId); + const audiomuseByServer = useAuthStore(s => s.audiomuseNavidromeByServer); + const showLuckyMixMenu = useAuthStore(s => s.showLuckyMixMenu); + return isLuckyMixAvailable({ activeServerId, audiomuseByServer, showLuckyMixMenu }); +} diff --git a/src/locales/de.ts b/src/locales/de.ts index f3c913ef..ce1874b1 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -31,6 +31,7 @@ export const deTranslation = { expandPlaylists: 'Playlists ausklappen', collapsePlaylists: 'Playlists einklappen', more: 'Mehr', + feelingLucky: 'Glücks-Mix', }, home: { hero: 'Featured', @@ -283,6 +284,8 @@ export const deTranslation = { mixByTracksDesc: 'Zufällige Auswahl aus deiner gesamten Mediathek', mixByAlbums: 'Mix nach Alben', mixByAlbumsDesc: 'Zufällige Alben für neue Entdeckungen', + mixByLucky: 'Glücks-Mix', + mixByLuckyDesc: 'Smarter Instant Mix aus Top-Künstlern, Alben und Bewertungen', }, randomAlbums: { title: 'Zufallsalben', @@ -335,6 +338,12 @@ export const deTranslation = { filterPanelDesc: 'Genre-Tag oder Künstlername in der Liste anklicken, um ihn aus zukünftigen Mixes auszuschließen.', genreClickHint: 'Genre-Tag anklicken,\num es als Filter-Keyword hinzuzufügen.\nPrüft Genre, Titel, Album & Künstler.', }, + luckyMix: { + done: 'Glücks-Mix bereit: {{count}} Titel', + failed: 'Glücks-Mix konnte nicht erstellt werden. Bitte erneut versuchen.', + unavailable: 'Glücks-Mix ist für diesen Server nicht verfügbar.', + cancelTooltip: 'Glücks-Mix-Erstellung abbrechen', + }, albums: { title: 'Alle Alben', sortByName: 'A–Z (Album)', @@ -720,6 +729,8 @@ export const deTranslation = { showChangelogOnUpdate: "'Was ist neu' bei Update anzeigen", showChangelogOnUpdateDesc: 'Blendet nach einem Update einen dezenten Changelog-Banner über Now Playing ein. Klick öffnet die Release Notes, X blendet ihn aus.', randomMixTitle: 'Zufallsmix-Blacklist', + luckyMixMenuTitle: 'Glücks-Mix im Menü anzeigen', + luckyMixMenuDesc: 'Aktiviert Glücks-Mix im "Mix erstellen"-Hub und als separaten Menüeintrag bei getrennter Navigation. Sichtbar nur bei aktiviertem AudioMuse auf dem aktiven Server.', randomMixBlacklistTitle: 'Eigene Filter-Keywords', randomMixBlacklistDesc: 'Songs werden ausgeschlossen, wenn ein Keyword auf Genre, Titel, Album oder Künstler zutrifft (aktiv wenn die Checkbox oben an ist).', randomMixBlacklistPlaceholder: 'Keyword hinzufügen…', @@ -755,7 +766,7 @@ export const deTranslation = { sidebarDrag: 'Ziehen zum Umsortieren', sidebarFixed: 'Immer sichtbar', randomNavSplitTitle: 'Mix-Navigation aufteilen', - randomNavSplitDesc: '"Zufallsmix" und "Zufallsalben" als separate Sidebar-Einträge statt als "Mix erstellen"-Hub anzeigen.', + randomNavSplitDesc: '"Zufallsmix", "Zufallsalben" und "Glücks-Mix" als separate Sidebar-Einträge statt als "Mix erstellen"-Hub anzeigen.', tabInput: 'Eingabe', tabUsers: 'Benutzer', shortcutsReset: 'Auf Standard zurücksetzen', diff --git a/src/locales/en.ts b/src/locales/en.ts index c9fc5e43..2103e92c 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -32,6 +32,7 @@ export const enTranslation = { expandPlaylists: 'Expand playlists', collapsePlaylists: 'Collapse playlists', more: 'More', + feelingLucky: 'Lucky Mix', }, home: { hero: 'Featured', @@ -284,6 +285,8 @@ export const enTranslation = { mixByTracksDesc: 'Random selection of tracks from your entire library', mixByAlbums: 'Mix by Albums', mixByAlbumsDesc: 'Random album picks for your next discovery', + mixByLucky: 'Lucky Mix', + mixByLuckyDesc: 'Smart instant mix from your top artists, albums, and ratings', }, randomAlbums: { title: 'Random Albums', @@ -336,6 +339,12 @@ export const enTranslation = { filterPanelDesc: 'Click a genre tag or artist name in the tracklist below to block it from future mixes.', genreClickHint: 'Click a genre tag to add it\nas a filter keyword.\nMatches genre, title, album & artist.', }, + luckyMix: { + done: 'Lucky Mix ready: {{count}} tracks', + failed: 'Could not build Lucky Mix. Try again.', + unavailable: 'Lucky Mix is unavailable for this server.', + cancelTooltip: 'Cancel Lucky Mix build', + }, albums: { title: 'All Albums', sortByName: 'A–Z (Album)', @@ -722,6 +731,8 @@ export const enTranslation = { showChangelogOnUpdate: "Show 'What's New' on update", showChangelogOnUpdateDesc: "Show a discreet changelog banner above Now Playing after an update. Click opens the release notes; X dismisses it.", randomMixTitle: 'Random Mix Blacklist', + luckyMixMenuTitle: 'Show Lucky Mix in menu', + luckyMixMenuDesc: 'Enables Lucky Mix in Build a Mix and as a separate menu item when split navigation is on. Visible only when AudioMuse is enabled on the active server.', randomMixBlacklistTitle: 'Custom Filter Keywords', randomMixBlacklistDesc: 'Songs are excluded when any keyword matches their genre, title, album, or artist (active when the checkbox above is on).', randomMixBlacklistPlaceholder: 'Add keyword…', @@ -757,7 +768,7 @@ export const enTranslation = { sidebarDrag: 'Drag to reorder', sidebarFixed: 'Always visible', randomNavSplitTitle: 'Split Mix navigation', - randomNavSplitDesc: 'Show "Random Mix" and "Random Albums" as separate sidebar entries instead of the "Build a Mix" hub.', + randomNavSplitDesc: 'Show "Random Mix", "Random Albums", and "Lucky Mix" as separate sidebar entries instead of the "Build a Mix" hub.', tabInput: 'Input', tabUsers: 'Users', tabSystem: 'System', diff --git a/src/locales/es.ts b/src/locales/es.ts index 5fcb5cc6..1373f94f 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -32,6 +32,7 @@ export const esTranslation = { expandPlaylists: 'Expandir listas', collapsePlaylists: 'Colapsar listas', more: 'Más', + feelingLucky: 'Mezcla Suerte', }, home: { hero: 'Destacado', @@ -284,6 +285,8 @@ export const esTranslation = { mixByTracksDesc: 'Selección aleatoria de canciones de toda tu biblioteca', mixByAlbums: 'Mezcla por Álbumes', mixByAlbumsDesc: 'Álbumes aleatorios para tu próximo descubrimiento', + mixByLucky: 'Mezcla Suerte', + mixByLuckyDesc: 'Instant Mix inteligente con artistas, álbumes y valoraciones destacadas', }, randomAlbums: { title: 'Álbumes Aleatorios', @@ -336,6 +339,12 @@ export const esTranslation = { filterPanelDesc: 'Click en una etiqueta de género o nombre de artista en la lista para bloquearlo de futuras mezclas.', genreClickHint: 'Click en una etiqueta de género para agregarla\ncomo palabra clave filtrada.\nBusca en género, título, álbum y artista.', }, + luckyMix: { + done: 'Mezcla Suerte lista: {{count}} canciones', + failed: 'No se pudo crear la Mezcla Suerte. Inténtalo de nuevo.', + unavailable: 'Mezcla Suerte no está disponible para este servidor.', + cancelTooltip: 'Cancelar creación de Mezcla Suerte', + }, albums: { title: 'Todos los Álbumes', sortByName: 'A–Z (Álbum)', @@ -712,6 +721,8 @@ export const esTranslation = { showChangelogOnUpdate: "Mostrar 'Novedades' al actualizar", showChangelogOnUpdateDesc: 'Muestra un discreto banner de changelog encima de Now Playing tras una actualización. Clic abre las notas de la versión; X lo oculta.', randomMixTitle: 'Lista negra de Mezcla Aleatoria', + luckyMixMenuTitle: 'Mostrar Mezcla Suerte en el menú', + luckyMixMenuDesc: 'Activa Mezcla Suerte en "Crear Mezcla" y como elemento de menú separado cuando la navegación dividida está activa. Solo visible cuando AudioMuse está activo en el servidor actual.', randomMixBlacklistTitle: 'Palabras Clave de Filtro Personalizadas', randomMixBlacklistDesc: 'Las canciones se excluyen cuando cualquier palabra clave coincide con su género, título, álbum o artista (activo cuando el checkbox de arriba está activado).', randomMixBlacklistPlaceholder: 'Agregar palabra clave…', @@ -748,7 +759,7 @@ export const esTranslation = { sidebarDrag: 'Arrastra para reordenar', sidebarFixed: 'Siempre visible', randomNavSplitTitle: 'Dividir navegación Mix', - randomNavSplitDesc: 'Mostrar "Mezcla Aleatoria" y "Álbumes Aleatorios" como entradas separadas en la barra lateral en lugar del hub "Crear Mezcla".', + randomNavSplitDesc: 'Mostrar "Mezcla Aleatoria", "Álbumes Aleatorios" y "Mezcla Suerte" como entradas separadas en la barra lateral en lugar del hub "Crear Mezcla".', tabInput: 'Entrada', tabUsers: 'Usuarios', tabSystem: 'Sistema', diff --git a/src/locales/fr.ts b/src/locales/fr.ts index 6bd479da..d64e30b6 100644 --- a/src/locales/fr.ts +++ b/src/locales/fr.ts @@ -31,6 +31,7 @@ export const frTranslation = { expandPlaylists: 'Développer les playlists', collapsePlaylists: 'Réduire les playlists', more: 'Plus', + feelingLucky: 'Mix Chance', }, home: { hero: 'En vedette', @@ -283,6 +284,8 @@ export const frTranslation = { mixByTracksDesc: 'Sélection aléatoire de titres depuis toute votre médiathèque', mixByAlbums: 'Mix par albums', mixByAlbumsDesc: 'Albums aléatoires pour vos prochaines découvertes', + mixByLucky: 'Mix Chance', + mixByLuckyDesc: 'Instant Mix intelligent basé sur artistes, albums et notes élevées', }, randomAlbums: { title: 'Albums aléatoires', @@ -335,6 +338,12 @@ export const frTranslation = { filterPanelDesc: 'Cliquez sur un tag de genre ou un nom d\'artiste dans la liste pour l\'exclure des futurs mix.', genreClickHint: 'Cliquez sur un tag de genre pour l\'ajouter\ncomme mot-clé de filtre.\nCorrespond au genre, titre, album et artiste.', }, + luckyMix: { + done: 'Mix Chance prêt : {{count}} titres', + failed: 'Impossible de créer le Mix Chance. Réessayez.', + unavailable: 'Mix Chance n\'est pas disponible pour ce serveur.', + cancelTooltip: 'Annuler la création du Mix Chance', + }, albums: { title: 'Tous les albums', sortByName: 'A–Z (Album)', @@ -707,6 +716,8 @@ export const frTranslation = { showChangelogOnUpdate: "Afficher 'Quoi de neuf' lors des mises à jour", showChangelogOnUpdateDesc: "Affiche une bannière discrète du changelog au-dessus de Now Playing après une mise à jour. Un clic ouvre les notes de version, le X la masque.", randomMixTitle: 'Liste noire du mix aléatoire', + luckyMixMenuTitle: 'Afficher Mix Chance dans le menu', + luckyMixMenuDesc: 'Active Mix Chance dans "Créer un mix" et comme entrée séparée quand la navigation est scindée. Visible uniquement si AudioMuse est actif sur le serveur courant.', randomMixBlacklistTitle: 'Mots-clés de filtre personnalisés', randomMixBlacklistDesc: 'Les morceaux sont exclus si un mot-clé correspond à leur genre, titre, album ou artiste (actif quand la case ci-dessus est cochée).', randomMixBlacklistPlaceholder: 'Ajouter un mot-clé…', @@ -743,7 +754,7 @@ export const frTranslation = { sidebarDrag: 'Glisser pour réorganiser', sidebarFixed: 'Toujours visible', randomNavSplitTitle: 'Diviser la navigation Mix', - randomNavSplitDesc: 'Afficher "Mix Aléatoire" et "Albums Aléatoires" comme entrées séparées dans la barre latérale plutôt que le hub "Créer un Mix".', + randomNavSplitDesc: 'Afficher "Mix Aléatoire", "Albums Aléatoires" et "Mix Chance" comme entrées séparées dans la barre latérale plutôt que le hub "Créer un Mix".', tabInput: 'Entrée', tabUsers: 'Utilisateurs', shortcutsReset: 'Réinitialiser', diff --git a/src/locales/nb.ts b/src/locales/nb.ts index 3b8bf921..69967fab 100644 --- a/src/locales/nb.ts +++ b/src/locales/nb.ts @@ -31,6 +31,7 @@ export const nbTranslation = { expandPlaylists: 'Utvid spillelister', collapsePlaylists: 'Skjul spillelister', more: 'Mer', + feelingLucky: 'Lykkemiks', }, home: { hero: 'Utvalgt', @@ -283,6 +284,8 @@ export const nbTranslation = { mixByTracksDesc: 'Tilfeldig utvalg av spor fra hele biblioteket ditt', mixByAlbums: 'Miks etter album', mixByAlbumsDesc: 'Tilfeldige album for nye oppdagelser', + mixByLucky: 'Lykkemiks', + mixByLuckyDesc: 'Smart Instant Mix fra toppartister, album og gode vurderinger', }, randomAlbums: { title: 'Tilfeldige album', @@ -335,6 +338,12 @@ export const nbTranslation = { filterPanelDesc: 'Klikk på en sjanger-tag eller på artistnavnet i sporlisten nedenfor, for å blokkere den fra fremtidige mikser.', genreClickHint: 'Klikk på en sjanger-tag for å legge den til\nsom et filternøkkelord.\nSamsvarer med sjanger, tittel, album og artist.', }, + luckyMix: { + done: 'Lykkemiks klar: {{count}} spor', + failed: 'Kunne ikke lage Lykkemiksen. Prøv igjen.', + unavailable: 'Lykkemiks er ikke tilgjengelig for denne serveren.', + cancelTooltip: 'Avbryt Lykkemiks-bygging', + }, albums: { title: 'Alle album', sortByName: 'A–Å (Album)', @@ -706,6 +715,8 @@ export const nbTranslation = { showChangelogOnUpdate: "Vis 'Hva er nytt' ved oppdatering til ny versjon", showChangelogOnUpdateDesc: "Viser et diskret changelog-banner over Now Playing etter en oppdatering. Klikk åpner versjonsnotatene; X skjuler det.", randomMixTitle: 'Svarteliste for tilfeldig miks', + luckyMixMenuTitle: 'Vis Lykkemiks i menyen', + luckyMixMenuDesc: 'Aktiverer Lykkemiks i "Lag en miks" og som eget menypunkt når delt navigasjon er aktiv. Vises bare når AudioMuse er aktiv på gjeldende server.', randomMixBlacklistTitle: 'Egendefinerte filternøkkelord', randomMixBlacklistDesc: 'Sanger ekskluderes når et nøkkelord samsvarer med sjanger, tittel, album eller artist (aktiv når avkrysningsboksen ovenfor er på).', randomMixBlacklistPlaceholder: 'Legg til nøkkelord…', @@ -742,7 +753,7 @@ export const nbTranslation = { sidebarDrag: 'Dra for å endre rekkefølge', sidebarFixed: 'Alltid synlig', randomNavSplitTitle: 'Del Mix-navigasjon', - randomNavSplitDesc: 'Vis "Tilfeldig miks" og "Tilfeldige album" som separate sidefeltsoppføringer i stedet for "Lag en miks"-huben.', + randomNavSplitDesc: 'Vis "Tilfeldig miks", "Tilfeldige album" og "Lykkemiks" som separate sidefeltsoppføringer i stedet for "Lag en miks"-huben.', tabShortcuts: 'Snarveier', tabUsers: 'Brukere', tabSystem: 'System', diff --git a/src/locales/nl.ts b/src/locales/nl.ts index b5fe75b8..f32bcb3e 100644 --- a/src/locales/nl.ts +++ b/src/locales/nl.ts @@ -31,6 +31,7 @@ export const nlTranslation = { expandPlaylists: 'Afspeellijsten uitklappen', collapsePlaylists: 'Afspeellijsten inklappen', more: 'Meer', + feelingLucky: 'Geluksmix', }, home: { hero: 'Uitgelicht', @@ -282,6 +283,8 @@ export const nlTranslation = { mixByTracksDesc: 'Willekeurige selectie uit je volledige mediatheek', mixByAlbums: 'Mix op albums', mixByAlbumsDesc: 'Willekeurige albums voor nieuwe ontdekkingen', + mixByLucky: 'Geluksmix', + mixByLuckyDesc: 'Slimme Instant Mix op basis van topartiesten, albums en hoge beoordelingen', }, randomAlbums: { title: 'Willekeurige albums', @@ -334,6 +337,12 @@ export const nlTranslation = { filterPanelDesc: 'Klik op een genre-tag of artiestennaam in de lijst om deze uit toekomstige mixes te weren.', genreClickHint: 'Klik op een genre-tag om het\ntoe te voegen als filtertrefwoord.\nVergelijkt genre, titel, album & artiest.', }, + luckyMix: { + done: 'Geluksmix klaar: {{count}} nummers', + failed: 'Kon de Geluksmix niet maken. Probeer opnieuw.', + unavailable: 'Geluksmix is niet beschikbaar voor deze server.', + cancelTooltip: 'Geluksmix-opbouw annuleren', + }, albums: { title: 'Alle albums', sortByName: 'A–Z (Album)', @@ -706,6 +715,8 @@ export const nlTranslation = { showChangelogOnUpdate: "'Wat is nieuw' tonen bij update", showChangelogOnUpdateDesc: 'Toont een discrete changelog-banner boven Now Playing na een update. Klik opent de release-notities; X verbergt hem.', randomMixTitle: 'Willekeurige mix-blacklist', + luckyMixMenuTitle: 'Toon Geluksmix in menu', + luckyMixMenuDesc: 'Schakelt Geluksmix in bij "Mix samenstellen" en als apart menu-item bij gesplitste navigatie. Alleen zichtbaar wanneer AudioMuse actief is op de huidige server.', randomMixBlacklistTitle: 'Aangepaste filtertrefwoorden', randomMixBlacklistDesc: 'Nummers worden uitgesloten als een trefwoord overeenkomt met hun genre, titel, album of artiest (actief wanneer het selectievakje hierboven is aangevinkt).', randomMixBlacklistPlaceholder: 'Trefwoord toevoegen…', @@ -742,7 +753,7 @@ export const nlTranslation = { sidebarDrag: 'Slepen om te herordenen', sidebarFixed: 'Altijd zichtbaar', randomNavSplitTitle: 'Mix-navigatie splitsen', - randomNavSplitDesc: 'Toon "Willekeurige mix" en "Willekeurige albums" als afzonderlijke zijbalkitems in plaats van de "Mix samenstellen"-hub.', + randomNavSplitDesc: 'Toon "Willekeurige mix", "Willekeurige albums" en "Geluksmix" als afzonderlijke zijbalkitems in plaats van de "Mix samenstellen"-hub.', tabInput: 'Invoer', tabUsers: 'Gebruikers', shortcutsReset: 'Standaard herstellen', diff --git a/src/locales/ru.ts b/src/locales/ru.ts index cd4cb1ce..c2b16385 100644 --- a/src/locales/ru.ts +++ b/src/locales/ru.ts @@ -32,6 +32,7 @@ export const ruTranslation = { expandPlaylists: 'Развернуть плейлисты', collapsePlaylists: 'Свернуть плейлисты', more: 'Ещё', + feelingLucky: 'Мне повезёт', }, home: { hero: 'Подборка', @@ -297,6 +298,8 @@ export const ruTranslation = { mixByTracksDesc: 'Случайная подборка треков со всей медиатеки', mixByAlbums: 'Микс по альбомам', mixByAlbumsDesc: 'Случайная подборка альбомов для открытий', + mixByLucky: 'Мне повезёт', + mixByLuckyDesc: 'Умный Instant Mix из топ-артистов, альбомов и высоких оценок', }, randomAlbums: { title: 'Случайные альбомы', @@ -354,6 +357,12 @@ export const ruTranslation = { genreClickHint: 'Нажмите на тег жанра — слово попадёт в фильтр.\nУчитываются жанр, название, альбом и исполнитель.', }, + luckyMix: { + done: '«Мне повезёт» готово: {{count}} треков', + failed: 'Не удалось собрать микс «Мне повезёт». Попробуйте ещё раз.', + unavailable: '«Мне повезёт» недоступен на этом сервере.', + cancelTooltip: 'Отменить сборку микса «Мне повезёт»', + }, albums: { title: 'Все альбомы', sortByName: 'А–Я (альбом)', @@ -745,6 +754,9 @@ export const ruTranslation = { showChangelogOnUpdate: 'Показывать «Что нового» после обновления', showChangelogOnUpdateDesc: 'После обновления над «Сейчас играет» появится ненавязчивый баннер журнала изменений. Клик открывает заметки о выпуске, X скрывает.', randomMixTitle: 'Чёрный список случайного микса', + luckyMixMenuTitle: 'Показывать «Мне повезёт» в меню', + luckyMixMenuDesc: + 'Включает «Мне повезёт» в «Собрать микс», а при раздельной навигации — отдельным пунктом меню. Видно только при включённом AudioMuse на активном сервере.', randomMixBlacklistTitle: 'Свои слова-фильтры', randomMixBlacklistDesc: 'Треки скрываются, если слово встречается в жанре, названии, альбоме или исполнителе (когда включён фильтр выше).', @@ -784,7 +796,8 @@ export const ruTranslation = { sidebarDrag: 'Перетащите для порядка', sidebarFixed: 'Всегда показывать', randomNavSplitTitle: 'Разделить навигацию микса', - randomNavSplitDesc: 'Показывать «Случайный микс» и «Случайные альбомы» как отдельные пункты боковой панели вместо хаба «Собрать микс».', + randomNavSplitDesc: + 'Показывать «Случайный микс», «Случайные альбомы» и «Мне повезёт» как отдельные пункты боковой панели вместо хаба «Собрать микс».', tabInput: 'Ввод', tabUsers: 'Пользователи', tabSystem: 'Система', diff --git a/src/locales/zh.ts b/src/locales/zh.ts index e3ae9e57..e2f436b6 100644 --- a/src/locales/zh.ts +++ b/src/locales/zh.ts @@ -31,6 +31,7 @@ export const zhTranslation = { expandPlaylists: '展开播放列表', collapsePlaylists: '收起播放列表', more: '更多', + feelingLucky: '好运混音', }, home: { hero: '精选', @@ -281,6 +282,8 @@ export const zhTranslation = { mixByTracksDesc: '从整个媒体库随机选取曲目', mixByAlbums: '按专辑混音', mixByAlbumsDesc: '随机选取专辑,探索新音乐', + mixByLucky: '好运混音', + mixByLuckyDesc: '基于高频艺人、专辑和高评分歌曲的智能 Instant Mix', }, randomAlbums: { title: '随机专辑', @@ -333,6 +336,12 @@ export const zhTranslation = { filterPanelDesc: '点击下方列表中的流派标签或艺术家名称,将其从未来的混音中排除。', genreClickHint: '点击流派标签将其添加为过滤关键词。\\n匹配流派、标题、专辑和艺术家。', }, + luckyMix: { + done: '好运混音已就绪:{{count}} 首', + failed: '生成好运混音失败,请重试。', + unavailable: '当前服务器不支持好运混音。', + cancelTooltip: '取消生成好运混音', + }, albums: { title: '全部专辑', sortByName: '按名称排序 (A-Z)', @@ -701,6 +710,8 @@ export const zhTranslation = { showChangelogOnUpdate: '更新时显示"新功能"', showChangelogOnUpdateDesc: '更新后在「正在播放」上方显示一个低调的更新日志横幅。点击打开发行说明,X 按钮关闭。', randomMixTitle: '随机混音黑名单', + luckyMixMenuTitle: '在菜单中显示“好运混音”', + luckyMixMenuDesc: '在“创建混音”中启用“好运混音”,并在分离导航时作为独立菜单项显示。仅当当前服务器启用 AudioMuse 时可见。', randomMixBlacklistTitle: '自定义过滤关键词', randomMixBlacklistDesc: '当任何关键词匹配流派、标题、专辑或艺术家时,歌曲将被排除(当上方复选框开启时生效)。', randomMixBlacklistPlaceholder: '添加关键词…', @@ -737,7 +748,7 @@ export const zhTranslation = { sidebarDrag: '拖动以重新排序', sidebarFixed: '始终显示', randomNavSplitTitle: '拆分混音导航', - randomNavSplitDesc: '在侧边栏中将"随机混音"和"随机专辑"显示为独立条目,而非"创建混音"合并入口。', + randomNavSplitDesc: '在侧边栏中将“随机混音”、“随机专辑”和“好运混音”显示为独立条目,而非“创建混音”合并入口。', tabInput: '输入', tabUsers: '用户', tabSystem: '系统', diff --git a/src/pages/LuckyMix.tsx b/src/pages/LuckyMix.tsx new file mode 100644 index 00000000..871d4c98 --- /dev/null +++ b/src/pages/LuckyMix.tsx @@ -0,0 +1,17 @@ +import { useEffect, useRef } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { buildAndPlayLuckyMix } from '../utils/luckyMix'; + +export default function LuckyMixPage() { + const navigate = useNavigate(); + const startedRef = useRef(false); + + useEffect(() => { + if (startedRef.current) return; + startedRef.current = true; + void buildAndPlayLuckyMix(); + navigate('/now-playing', { replace: true }); + }, [navigate]); + + return null; +} diff --git a/src/pages/RandomLanding.tsx b/src/pages/RandomLanding.tsx index de4370d1..da844fa2 100644 --- a/src/pages/RandomLanding.tsx +++ b/src/pages/RandomLanding.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { Shuffle, Dices } from 'lucide-react'; +import { Shuffle, Dices, Sparkles } from 'lucide-react'; +import { useLuckyMixAvailable } from '../hooks/useLuckyMixAvailable'; interface MixCard { icon: React.ElementType; @@ -28,11 +29,25 @@ const CARDS: MixCard[] = [ export default function RandomLanding() { const { t } = useTranslation(); const navigate = useNavigate(); + // RandomLanding is only reachable in "hub" nav mode, so we don't need to + // gate on randomNavMode here — availability alone is enough. + const luckyMixAvailable = useLuckyMixAvailable(); + const cards = luckyMixAvailable + ? [ + ...CARDS, + { + icon: Sparkles, + labelKey: 'randomLanding.mixByLucky', + descKey: 'randomLanding.mixByLuckyDesc', + to: '/lucky-mix', + }, + ] + : CARDS; return (
- {CARDS.map(({ icon: Icon, labelKey, descKey, to }) => ( + {cards.map(({ icon: Icon, labelKey, descKey, to }) => (