-
-
-
{t('queue.title')}
- {queue.length > 0 && (
-
- {queue.length} {queue.length === 1 ? t('queue.trackSingular') : t('queue.trackPlural')} · {formatDuration(totalSecs)}
+ {/* ── About the Artist ── */}
+ {(artistInfo?.biography || artistInfo?.largeImageUrl) && (
+
+
+
{t('nowPlaying.aboutArtist')}
+ {currentTrack.artistId && (
+
+ )}
+
+
+ {artistInfo.largeImageUrl && (
+

+ )}
+ {artistInfo.biography && (
+
+
+
+
+ )}
+
)}
-
-
-
-
-
-
-
-
-
{ e.preventDefault(); e.dataTransfer.dropEffect = isDraggingInternalRef.current ? 'move' : 'copy'; }}
- onDragOver={e => { e.preventDefault(); e.dataTransfer.dropEffect = isDraggingInternalRef.current ? 'move' : 'copy'; }}
- onDrop={onDropQueue}
- >
- {queue.length === 0 ? (
-
{t('queue.emptyQueue')}
- ) : (
- queue.map((track, idx) => {
- const isActive = idx === queueIndex;
- const isDragging = draggedIdx === idx;
- const isDragOver = dragOverIdx === idx;
- let dragStyle: React.CSSProperties = {};
- if (isDragging) dragStyle = { opacity: 0.4, background: 'var(--bg-hover)' };
- else if (isDragOver && draggedIdx !== null) {
- dragStyle = draggedIdx > idx
- ? { borderTop: '2px solid var(--accent)', paddingTop: '6px', marginTop: '-2px' }
- : { borderBottom: '2px solid var(--accent)', paddingBottom: '6px', marginBottom: '-2px' };
- }
-
- return (
-
playTrack(track, queue)}
- onContextMenu={e => { e.preventDefault(); usePlayerStore.getState().openContextMenu(e.clientX, e.clientY, track, 'queue-item', idx); }}
- draggable
- onDragStart={e => onDragStart(e, idx)}
- onDragEnter={e => onDragEnterItem(e)}
- onDragOver={e => onDragOverItem(e, idx)}
- onDragEnd={onDragEnd}
- style={dragStyle}
- >
-
- {isActive
- ?
- :
{idx + 1}
- }
-
-
-
{track.title}
-
{track.artist} · {track.album}
-
-
{formatTime(track.duration)}
-
+ {/* ── From this Album ── */}
+ {albumTracks.length > 0 && (
+
+
+
{t('nowPlaying.fromAlbum')}: {currentTrack.album}
+ {currentTrack.albumId && (
+
+ )}
- );
- })
- )}
-
+
+ {albumTracks.map(track => {
+ const isActive = track.id === currentTrack.id;
+ return (
+
currentTrack.albumId && navigate(`/album/${currentTrack.albumId}`)}
+ >
+
+ {isActive
+ ?
+ : track.track ?? '—'
+ }
+
+ {track.title}
+ {formatTime(track.duration)}
+
+ );
+ })}
+
+
+ )}
+ >
+ ) : (
+
+
+
{t('nowPlaying.nothingPlaying')}
+
+ )}
-
- {saveModalOpen && (
-
setSaveModalOpen(false)}
- onSave={async name => {
- try { await createPlaylist(name, queue.map(t => t.id)); setSaveModalOpen(false); }
- catch (e) { console.error(e); }
- }}
- />
- )}
- {loadModalOpen && (
- setLoadModalOpen(false)}
- onLoad={async id => {
- try {
- const data = await getPlaylist(id);
- const tracks: Track[] = data.songs.map(s => ({
- id: s.id, title: s.title, artist: s.artist, album: s.album,
- albumId: s.albumId, artistId: s.artistId, duration: s.duration, coverArt: s.coverArt,
- track: s.track, year: s.year, bitRate: s.bitRate, suffix: s.suffix, userRating: s.userRating,
- }));
- if (tracks.length > 0) { clearQueue(); playTrack(tracks[0], tracks); }
- setLoadModalOpen(false);
- } catch (e) { console.error(e); }
- }}
- />
- )}
);
}
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index fe22f9be..46f71a3c 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -4,7 +4,7 @@ import changelogRaw from '../../CHANGELOG.md?raw';
import { useNavigate, useLocation } from 'react-router-dom';
import {
Wifi, WifiOff, Globe, Music2, Sliders, LogOut, CheckCircle2, FolderOpen,
- Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play
+ Palette, Server, Plus, Trash2, Eye, EyeOff, Info, ExternalLink, Shuffle, X, Play, Type, Keyboard
} from 'lucide-react';
import { open as openUrl } from '@tauri-apps/plugin-shell';
import { lastfmGetToken, lastfmAuthUrl, lastfmGetSession, lastfmGetUserInfo, LastfmUserInfo } from '../api/lastfm';
@@ -13,6 +13,8 @@ import CustomSelect from '../components/CustomSelect';
import ThemePicker from '../components/ThemePicker';
import { useAuthStore, ServerProfile } from '../store/authStore';
import { useThemeStore } from '../store/themeStore';
+import { useFontStore, FontId } from '../store/fontStore';
+import { useKeybindingsStore, KeyAction, formatKeyCode, DEFAULT_BINDINGS } from '../store/keybindingsStore';
import { pingWithCredentials } from '../api/subsonic';
import { open as openDialog } from '@tauri-apps/plugin-dialog';
import { useTranslation } from 'react-i18next';
@@ -20,7 +22,7 @@ import Equalizer from '../components/Equalizer';
const AUDIOBOOK_GENRES_DISPLAY = ['Hörbuch', 'Hoerbuch', 'Hörspiel', 'Hoerspiel', 'Audiobook', 'Audio Book', 'Spoken Word', 'Spokenword', 'Podcast', 'Kapitel', 'Thriller', 'Krimi', 'Speech', 'Fantasy', 'Comedy', 'Literature'];
-type Tab = 'playback' | 'library' | 'appearance' | 'server' | 'about';
+type Tab = 'playback' | 'library' | 'appearance' | 'shortcuts' | 'server' | 'about';
function AddServerForm({ onSave, onCancel }: { onSave: (data: Omit
) => void; onCancel: () => void }) {
const { t } = useTranslation();
@@ -83,6 +85,9 @@ function AddServerForm({ onSave, onCancel }: { onSave: (data: Omit(null);
const navigate = useNavigate();
const { state: routeState } = useLocation();
const { t, i18n } = useTranslation();
@@ -210,6 +215,7 @@ export default function Settings() {
{ id: 'playback', label: t('settings.tabPlayback'), icon: },
{ id: 'library', label: t('settings.tabLibrary'), icon: },
{ id: 'appearance', label: t('settings.tabAppearance'), icon: },
+ { id: 'shortcuts', label: t('settings.tabShortcuts'), icon: },
{ id: 'server', label: t('settings.tabServer'), icon: },
{ id: 'about', label: t('settings.tabAbout'), icon: },
];
@@ -492,9 +498,139 @@ export default function Settings() {