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
+129 -44
View File
@@ -1,6 +1,7 @@
import React from 'react';
import { Library, Disc, Search, User, LogIn, LogOut, Sun, Moon } from 'lucide-react';
import { View } from '../types';
import { useI18n } from '../context/I18nContext';
interface SidebarProps {
currentView: View;
@@ -11,6 +12,8 @@ interface SidebarProps {
onLogin?: () => void;
onLogout?: () => void;
onOpenSettings?: () => void;
isOpen?: boolean;
onToggle?: () => void;
}
export const Sidebar: React.FC<SidebarProps> = ({
@@ -22,84 +25,161 @@ export const Sidebar: React.FC<SidebarProps> = ({
onLogin,
onLogout,
onOpenSettings,
isOpen = true,
onToggle,
}) => {
const { t } = useI18n();
return (
<div className="flex flex-col h-full bg-white dark:bg-suno-sidebar border-r border-zinc-200 dark:border-white/5 flex-shrink-0 w-[72px] items-center py-4 z-30 transition-colors duration-300 overflow-y-auto scrollbar-hide">
{/* Logo */}
<div
className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center mb-8 cursor-pointer shadow-lg hover:scale-105 transition-transform"
onClick={() => onNavigate('create')}
title="ACE-Step UI"
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-white">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
<>
{/* Backdrop for mobile - only when expanded */}
{isOpen && onToggle && (
<div
className="fixed inset-0 bg-black/60 backdrop-blur-sm z-40 md:hidden"
onClick={onToggle}
/>
)}
{/* Sidebar */}
<div className={`
flex flex-col h-full bg-white dark:bg-suno-sidebar border-r border-zinc-200 dark:border-white/5 flex-shrink-0 py-4 overflow-y-auto scrollbar-hide transition-all duration-300
fixed left-0 top-0 z-50 md:relative
${isOpen ? 'w-[200px]' : 'w-[72px]'}
`}>
{/* Logo & Brand */}
<div className="px-3 mb-8 flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="w-10 h-10 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center cursor-pointer shadow-lg hover:scale-105 transition-transform flex-shrink-0"
onClick={() => onNavigate('create')}
title={t('aceStepUI')}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" className="w-5 h-5 text-white">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 17L12 22L22 17" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path d="M2 12L12 17L22 12" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
</svg>
</div>
{isOpen && (
<span className="text-lg font-bold text-zinc-900 dark:text-white whitespace-nowrap">ACE Step</span>
)}
</div>
{/* Collapse/Expand Button */}
{onToggle && (
<button
onClick={onToggle}
className="w-8 h-8 rounded-lg hover:bg-zinc-100 dark:hover:bg-white/10 flex items-center justify-center text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white transition-colors flex-shrink-0"
title={isOpen ? t('collapseSidebar') : t('expandSidebar')}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
{isOpen ? (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
) : (
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
)}
</svg>
</button>
)}
</div>
<nav className="flex-1 flex flex-col gap-4 w-full px-3">
<nav className="flex-1 flex flex-col gap-2 w-full px-3">
<NavItem
icon={<Disc size={24} />}
label="Create"
icon={<Disc size={20} />}
label={t('create')}
active={currentView === 'create'}
onClick={() => onNavigate('create')}
isExpanded={isOpen}
/>
<NavItem
icon={<Library size={24} />}
label="Library"
icon={<Library size={20} />}
label={t('library')}
active={currentView === 'library'}
onClick={() => onNavigate('library')}
isExpanded={isOpen}
/>
<NavItem
icon={<Search size={24} />}
label="Search"
icon={<Search size={20} />}
label={t('search')}
active={currentView === 'search'}
onClick={() => onNavigate('search')}
isExpanded={isOpen}
/>
<div className="mt-auto flex flex-col gap-4">
<div className="mt-auto flex flex-col gap-2">
{/* Theme Toggle */}
<button
onClick={onToggleTheme}
className="w-10 h-10 rounded-full hover:bg-zinc-100 dark:hover:bg-white/10 flex items-center justify-center text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white transition-colors mx-auto"
title={theme === 'dark' ? 'Light Mode' : 'Dark Mode'}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={theme === 'dark' ? t('lightMode') : t('darkMode')}
>
{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}
<div className="flex-shrink-0">{theme === 'dark' ? <Sun size={20} /> : <Moon size={20} />}</div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">
{theme === 'dark' ? t('lightMode') : t('darkMode')}
</span>
)}
</button>
{user ? (
<div className="flex flex-col items-center gap-2">
<div
<>
{/* User Settings */}
<button
onClick={onOpenSettings}
className="w-8 h-8 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center text-white text-xs font-bold cursor-pointer border border-white/20 hover:scale-110 transition-transform overflow-hidden"
title={`${user.username} - Settings`}
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={`${user.username} - ${t('settings')}`}
>
{user.avatar_url ? (
<img src={user.avatar_url} alt={user.username} className="w-full h-full object-cover" />
) : (
user.username.charAt(0).toUpperCase()
<div className="w-6 h-6 rounded-full bg-gradient-to-br from-pink-500 to-purple-600 flex items-center justify-center text-white text-xs font-bold border border-white/20 overflow-hidden flex-shrink-0">
{user.avatar_url ? (
<img src={user.avatar_url} alt={user.username} className="w-full h-full object-cover" />
) : (
user.username.charAt(0).toUpperCase()
)}
</div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap truncate flex-1 text-left">
{user.username}
</span>
)}
</div>
</button>
{/* Logout */}
<button
onClick={onLogout}
className="w-8 h-8 rounded-full hover:bg-red-500/20 flex items-center justify-center text-zinc-500 hover:text-red-500 transition-colors"
title="Sign Out"
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 hover:text-red-500 hover:bg-red-500/10
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={t('signOut')}
>
<LogOut size={16} />
<div className="flex-shrink-0"><LogOut size={20} /></div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">{t('signOut')}</span>
)}
</button>
</div>
</>
) : (
<button
onClick={onLogin}
className="w-10 h-10 rounded-full hover:bg-zinc-100 dark:hover:bg-white/10 flex items-center justify-center text-zinc-500 dark:text-zinc-400 hover:text-pink-500 transition-colors mx-auto"
title="Sign In"
className={`
w-full rounded-xl flex items-center gap-3 transition-all duration-200 text-zinc-500 dark:text-zinc-400 hover:text-pink-500 hover:bg-zinc-100 dark:hover:bg-white/5
${isOpen ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
`}
title={t('signIn')}
>
<LogIn size={20} />
<div className="flex-shrink-0"><LogIn size={20} /></div>
{isOpen && (
<span className="text-sm font-medium whitespace-nowrap">{t('signIn')}</span>
)}
</button>
)}
</div>
</nav>
</div>
</div>
</>
);
};
@@ -108,18 +188,23 @@ interface NavItemProps {
label: string;
active?: boolean;
onClick: () => void;
isExpanded?: boolean;
}
const NavItem: React.FC<NavItemProps> = ({ icon, label, active, onClick }) => (
const NavItem: React.FC<NavItemProps> = ({ icon, label, active, onClick, isExpanded }) => (
<button
onClick={onClick}
className={`
w-full aspect-square rounded-xl flex flex-col items-center justify-center gap-1 transition-all duration-200 group relative
w-full rounded-xl flex items-center gap-3 transition-all duration-200 group relative overflow-hidden
${isExpanded ? 'px-3 py-2.5 justify-start' : 'aspect-square justify-center'}
${active ? 'bg-zinc-100 dark:bg-white/10 text-black dark:text-white' : 'text-zinc-500 hover:text-black dark:hover:text-white hover:bg-zinc-100 dark:hover:bg-white/5'}
`}
title={label}
>
{active && <div className="absolute left-0 top-1/2 -translate-y-1/2 h-8 w-1 bg-pink-500 rounded-r-full"></div>}
{icon}
<div className="flex-shrink-0">{icon}</div>
{isExpanded && (
<span className="text-sm font-medium whitespace-nowrap">{label}</span>
)}
</button>
);