Apply i18n to all components, add collapsible sidebar, playback speed control, inline title editing, and ConfirmDialog for deletions

Phase 4: i18n all 15 components from PR #19. Sidebar gains collapse/expand
toggle. Player gets playback speed selector (0.25x-2.0x) fixing bug B1
(hardcoded Chinese '正常'). SongList adds inline title editing and model
version badge. Delete actions now use ConfirmDialog instead of
window.confirm. Volume persisted to localStorage. Training nav item
deferred to Phase 7.
This commit is contained in:
fspecii
2026-02-08 18:35:09 +02:00
parent 1b13f01b3d
commit 325046eaf2
17 changed files with 760 additions and 431 deletions
+10 -8
View File
@@ -5,6 +5,7 @@ import { useAuth } from '../context/AuthContext';
import { SongDropdownMenu } from './SongDropdownMenu';
import { ShareModal } from './ShareModal';
import { AlbumCover } from './AlbumCover';
import { useI18n } from '../context/I18nContext';
interface LibraryViewProps {
allSongs: Song[];
@@ -46,6 +47,7 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
onDeleteSong,
onDeleteReferenceTrack,
}) => {
const { t } = useI18n();
const { user } = useAuth();
const [activeTab, setActiveTab] = useState<'all' | 'playlists' | 'liked' | 'uploads'>('all');
const [shareModalOpen, setShareModalOpen] = useState(false);
@@ -67,13 +69,13 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
<>
<div className="flex-1 bg-white dark:bg-black overflow-y-auto custom-scrollbar p-6 lg:p-10 pb-32 transition-colors duration-300">
<div className="flex items-center justify-between mb-8">
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">Your Library</h1>
<h1 className="text-3xl font-bold text-zinc-900 dark:text-white">{t('yourLibrary')}</h1>
<button
onClick={onCreatePlaylist}
className="flex items-center gap-2 bg-zinc-900 dark:bg-zinc-800 hover:bg-zinc-800 dark:hover:bg-zinc-700 text-white px-4 py-2 rounded-full font-medium transition-colors shadow-lg shadow-zinc-900/10 dark:shadow-none"
>
<Plus size={18} />
<span>New Playlist</span>
<span>{t('newPlaylist')}</span>
</button>
</div>
@@ -90,14 +92,14 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
onClick={() => setActiveTab('liked')}
className={`pb-3 text-sm font-bold transition-colors relative ${activeTab === 'liked' ? 'text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
>
Liked Songs
{t('likedSongs')}
{activeTab === 'liked' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 rounded-full"></div>}
</button>
<button
onClick={() => setActiveTab('playlists')}
className={`pb-3 text-sm font-bold transition-colors relative ${activeTab === 'playlists' ? 'text-zinc-900 dark:text-white' : 'text-zinc-500 hover:text-zinc-900 dark:hover:text-white'}`}
>
Playlists
{t('playlists')}
{activeTab === 'playlists' && <div className="absolute bottom-0 left-0 right-0 h-0.5 bg-green-500 rounded-full"></div>}
</button>
<button
@@ -168,10 +170,10 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
<Heart fill="white" size={64} className="text-white" />
</div>
<div className="mb-2">
<h2 className="text-sm font-bold uppercase text-zinc-500 dark:text-white mb-2">Playlist</h2>
<h1 className="text-5xl font-extrabold text-zinc-900 dark:text-white mb-4">Liked Songs</h1>
<h2 className="text-sm font-bold uppercase text-zinc-500 dark:text-white mb-2">{t('playlist')}</h2>
<h1 className="text-5xl font-extrabold text-zinc-900 dark:text-white mb-4">{t('likedSongs')}</h1>
<div className="text-sm text-zinc-500 dark:text-zinc-300 font-medium">
{likedSongs.length} songs
{likedSongs.length} {t('songs')}
</div>
</div>
<div className="ml-auto mb-2 opacity-0 group-hover:opacity-100 transition-opacity">
@@ -241,7 +243,7 @@ export const LibraryView: React.FC<LibraryViewProps> = ({
)}
</div>
<h3 className="font-bold text-zinc-900 dark:text-white truncate">{playlist.name}</h3>
<p className="text-sm text-zinc-500 dark:text-zinc-400 line-clamp-2">{playlist.description || `By You`}</p>
<p className="text-sm text-zinc-500 dark:text-zinc-400 line-clamp-2">{playlist.description || t('byYou')}</p>
</div>
))}
</div>