feat: update button sizes and styles for improved UI consistency in Audiobook and button components

This commit is contained in:
2026-03-12 18:33:44 +08:00
parent afb6830a6d
commit fa54208b96
3 changed files with 50 additions and 36 deletions

View File

@@ -457,7 +457,7 @@ class IndexTTS2Backend:
vec = [0.0] * 8 vec = [0.0] * 8
score = 0.8 if len(matched) == 1 else 0.5 score = 0.8 if len(matched) == 1 else 0.5
for idx in matched: for idx in matched:
vec[idx] = score vec[idx] = 0.2 if idx == 1 else score
return vec return vec
async def generate( async def generate(

View File

@@ -22,6 +22,7 @@ const buttonVariants = cva(
size: { size: {
default: "h-10 px-4 py-2", default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3", sm: "h-9 rounded-md px-3",
xs: "h-6 rounded-md px-2 text-xs",
lg: "h-11 rounded-md px-8", lg: "h-11 rounded-md px-8",
icon: "h-10 w-10", icon: "h-10 w-10",
}, },

View File

@@ -409,7 +409,7 @@ function ProjectListSidebar({
<button <button
key={p.id} key={p.id}
onClick={() => onSelect(p.id)} onClick={() => onSelect(p.id)}
className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 ${ className={`w-full text-left rounded-lg border p-3 flex flex-col gap-1.5 transition-colors hover:bg-muted/60 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 ${
selectedId === p.id selectedId === p.id
? 'bg-background border-border shadow-sm' ? 'bg-background border-border shadow-sm'
: 'bg-background/50 border-border/40' : 'bg-background/50 border-border/40'
@@ -541,7 +541,7 @@ function CharactersPanel({
return ( return (
<button <button
key={ch.id} key={ch.id}
className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/60 border-b border-border/30 transition-colors" className="w-full flex items-center gap-1.5 px-2 py-1.5 text-left hover:bg-muted/60 border-b border-border/30 transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
onClick={() => onScrollToChapter(ch.id)} onClick={() => onScrollToChapter(ch.id)}
> >
<span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dotClass}`} /> <span className={`w-1.5 h-1.5 rounded-full shrink-0 ${dotClass}`} />
@@ -562,7 +562,7 @@ function CharactersPanel({
</span> </span>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
{!isActive && status !== 'pending' && charCount > 0 && ( {!isActive && status !== 'pending' && charCount > 0 && (
<Button size="sm" variant="ghost" className="h-6 text-xs px-2 text-muted-foreground" onClick={onAnalyze} disabled={loadingAction}> <Button size="xs" variant="ghost" className="text-muted-foreground" onClick={onAnalyze} disabled={loadingAction}>
{t('projectCard.reanalyze')} {t('projectCard.reanalyze')}
</Button> </Button>
)} )}
@@ -758,6 +758,7 @@ function ChaptersPanel({
const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set()) const [regeneratingSegs, setRegeneratingSegs] = useState<Set<number>>(new Set())
const [audioVersions, setAudioVersions] = useState<Record<number, number>>({}) const [audioVersions, setAudioVersions] = useState<Record<number, number>>({})
const prevSegStatusRef = useRef<Record<number, string>>({}) const prevSegStatusRef = useRef<Record<number, string>>({})
const initialExpandDoneRef = useRef(false)
useEffect(() => { useEffect(() => {
if (!scrollToChapterId) return if (!scrollToChapterId) return
@@ -823,12 +824,23 @@ function ChaptersPanel({
const generatingChapterIds = detail.chapters const generatingChapterIds = detail.chapters
.filter(ch => segments.some(s => s.chapter_index === ch.chapter_index && s.status === 'generating')) .filter(ch => segments.some(s => s.chapter_index === ch.chapter_index && s.status === 'generating'))
.map(ch => ch.id) .map(ch => ch.id)
if (generatingChapterIds.length === 0) return if (generatingChapterIds.length > 0) {
setExpandedChapters(prev => { setExpandedChapters(prev => {
const next = new Set(prev) const next = new Set(prev)
generatingChapterIds.forEach(id => next.add(id)) generatingChapterIds.forEach(id => next.add(id))
return next.size === prev.size ? prev : next return next.size === prev.size ? prev : next
}) })
return
}
if (!initialExpandDoneRef.current) {
initialExpandDoneRef.current = true
const chapterIdsWithSegs = detail.chapters
.filter(ch => segments.some(s => s.chapter_index === ch.chapter_index))
.map(ch => ch.id)
if (chapterIdsWithSegs.length > 0) {
setExpandedChapters(new Set(chapterIdsWithSegs))
}
}
}, [segments, detail]) }, [segments, detail])
const hasChapters = detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status) const hasChapters = detail && detail.chapters.length > 0 && ['ready', 'generating', 'done'].includes(status)
@@ -842,17 +854,17 @@ function ChaptersPanel({
{hasChapters && ( {hasChapters && (
<div className="flex items-center gap-1 flex-wrap"> <div className="flex items-center gap-1 flex-wrap">
{detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && ( {detail!.chapters.some(c => ['pending', 'error', 'ready'].includes(c.status)) && (
<Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onParseAll}> <Button size="xs" variant="outline" disabled={loadingAction} onClick={onParseAll}>
{t('projectCard.chapters.parseAll')} {t('projectCard.chapters.parseAll')}
</Button> </Button>
)} )}
{detail!.chapters.some(c => c.status === 'ready') && ( {detail!.chapters.some(c => c.status === 'ready') && (
<Button size="sm" variant="outline" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onGenerateAll}> <Button size="xs" variant="outline" disabled={loadingAction} onClick={onGenerateAll}>
{t('projectCard.chapters.generateAll')} {t('projectCard.chapters.generateAll')}
</Button> </Button>
)} )}
{detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && ( {detail!.chapters.some(c => ['pending', 'error'].includes(c.status)) && detail!.chapters.some(c => c.status === 'ready') && (
<Button size="sm" className="h-6 text-xs px-2" disabled={loadingAction} onClick={onProcessAll}> <Button size="xs" disabled={loadingAction} onClick={onProcessAll}>
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')} {loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : t('projectCard.chapters.processAll')}
</Button> </Button>
)} )}
@@ -882,7 +894,7 @@ function ChaptersPanel({
<div key={ch.id} id={`ch-${ch.id}`}> <div key={ch.id} id={`ch-${ch.id}`}>
{/* Chapter header — flat, full-width, click to expand */} {/* Chapter header — flat, full-width, click to expand */}
<button <button
className="w-full flex items-center gap-2 px-3 py-2.5 bg-muted/40 hover:bg-muted/70 border-b text-left transition-colors" className="w-full flex items-center gap-2 px-3 py-2.5 bg-muted/40 hover:bg-muted/70 border-b text-left transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1"
onClick={toggleChExpand} onClick={toggleChExpand}
> >
<span className="shrink-0 text-muted-foreground"> <span className="shrink-0 text-muted-foreground">
@@ -891,7 +903,7 @@ function ChaptersPanel({
<span className="text-xs font-medium flex-1 truncate">{chTitle}</span> <span className="text-xs font-medium flex-1 truncate">{chTitle}</span>
<span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}> <span className="shrink-0 flex items-center gap-1.5" onClick={e => e.stopPropagation()}>
{ch.status === 'pending' && ( {ch.status === 'pending' && (
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2" onClick={() => onParseChapter(ch.id, ch.title)}> <Button size="xs" variant="outline" onClick={() => onParseChapter(ch.id, ch.title)}>
{t('projectCard.chapters.parse')} {t('projectCard.chapters.parse')}
</Button> </Button>
)} )}
@@ -902,13 +914,13 @@ function ChaptersPanel({
)} )}
{ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && ( {ch.status === 'ready' && !chGenerating && !chAllDone && !generatingChapterIndices.has(ch.chapter_index) && (
<> <>
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2" disabled={loadingAction} onClick={() => { <Button size="xs" variant="outline" disabled={loadingAction} onClick={() => {
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n }) setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
onGenerate(ch.chapter_index) onGenerate(ch.chapter_index)
}}> }}>
{t('projectCard.chapters.generate')} {t('projectCard.chapters.generate')}
</Button> </Button>
<Button size="sm" variant="ghost" className="h-5 text-[11px] px-1.5 text-muted-foreground" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}> <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => onParseChapter(ch.id, ch.title)}>
{t('projectCard.chapters.reparse')} {t('projectCard.chapters.reparse')}
</Button> </Button>
</> </>
@@ -918,22 +930,22 @@ function ChaptersPanel({
<Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.chapters.segmentProgress', { done: chDone, total: chTotal })} <Loader2 className="h-3 w-3 animate-spin" />{t('projectCard.chapters.segmentProgress', { done: chDone, total: chTotal })}
</span> </span>
)} )}
{ch.status === 'ready' && chAllDone && ( {(ch.status === 'done' || (ch.status === 'ready' && chAllDone)) && (
<> <>
<span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span> <span className="text-[11px] text-muted-foreground">{t('projectCard.chapters.doneBadge', { count: chDone })}</span>
<Button size="sm" variant="ghost" className="h-5 text-[11px] px-1.5 text-muted-foreground" disabled={loadingAction} onClick={() => { <Button size="xs" variant="ghost" className="text-muted-foreground" disabled={loadingAction} onClick={() => {
setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n }) setExpandedChapters(prev => { const n = new Set(prev); n.add(ch.id); return n })
onGenerate(ch.chapter_index, true) onGenerate(ch.chapter_index, true)
}}> }}>
<RefreshCw className="h-3 w-3 mr-1" />{t('projectCard.chapters.generate')} <RefreshCw className="h-3 w-3" />{t('projectCard.chapters.generate')}
</Button> </Button>
<Button size="sm" variant="ghost" className="h-5 w-5 p-0" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}> <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => onDownload(ch.chapter_index)} title={t('projectCard.downloadAll')}>
<Download className="h-3 w-3" /> <Download className="h-3 w-3" />
</Button> </Button>
</> </>
)} )}
{ch.status === 'error' && ( {ch.status === 'error' && (
<Button size="sm" variant="outline" className="h-5 text-[11px] px-2 text-destructive border-destructive/40" onClick={() => onParseChapter(ch.id, ch.title)}> <Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={() => onParseChapter(ch.id, ch.title)}>
{t('projectCard.chapters.reparse')} {t('projectCard.chapters.reparse')}
</Button> </Button>
)} )}
@@ -979,19 +991,19 @@ function ChaptersPanel({
<div className="ml-auto flex items-center gap-0.5 shrink-0"> <div className="ml-auto flex items-center gap-0.5 shrink-0">
{!isEditing ? ( {!isEditing ? (
<> <>
<Button size="icon" variant="ghost" className="h-5 w-5" onClick={() => startEdit(seg)} disabled={isRegenerating} title={t('projectCard.segments.edit')}> <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEdit(seg)} disabled={isRegenerating} title={t('projectCard.segments.edit')}>
<Pencil className="h-3 w-3" /> <Pencil className="h-3 w-3" />
</Button> </Button>
<Button size="icon" variant="ghost" className="h-5 w-5" onClick={() => handleRegenerate(seg.id)} disabled={isRegenerating} title={t('projectCard.segments.regenerate')}> <Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => handleRegenerate(seg.id)} disabled={isRegenerating} title={t('projectCard.segments.regenerate')}>
<RefreshCw className="h-3 w-3" /> <RefreshCw className="h-3 w-3" />
</Button> </Button>
</> </>
) : ( ) : (
<> <>
<Button size="icon" variant="ghost" className="h-5 w-5 text-destructive" onClick={cancelEdit} title={t('projectCard.segments.cancel')}> <Button size="icon" variant="ghost" className="h-6 w-6 text-destructive" onClick={cancelEdit} title={t('projectCard.segments.cancel')}>
<X className="h-3 w-3" /> <X className="h-3 w-3" />
</Button> </Button>
<Button size="icon" variant="ghost" className="h-5 w-5 text-primary" onClick={() => saveEdit(seg.id)} disabled={isSaving} title={t('projectCard.segments.save')}> <Button size="icon" variant="ghost" className="h-6 w-6 text-primary" onClick={() => saveEdit(seg.id)} disabled={isSaving} title={t('projectCard.segments.save')}>
{isSaving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />} {isSaving ? <Loader2 className="h-3 w-3 animate-spin" /> : <Check className="h-3 w-3" />}
</Button> </Button>
</> </>
@@ -1484,22 +1496,22 @@ export default function Audiobook() {
{t(`status.${displayStatus}`, { defaultValue: displayStatus })} {t(`status.${displayStatus}`, { defaultValue: displayStatus })}
</Badge> </Badge>
{status === 'pending' && ( {status === 'pending' && (
<Button size="sm" className="h-7 text-xs px-2" onClick={handleAnalyze} disabled={loadingAction}> <Button size="sm" onClick={handleAnalyze} disabled={loadingAction}>
{t('projectCard.analyze')} {t('projectCard.analyze')}
</Button> </Button>
)} )}
{status === 'ready' && ( {status === 'ready' && (
<Button size="sm" className="h-7 text-xs px-2" onClick={handleProcessAll} disabled={loadingAction}> <Button size="sm" onClick={handleProcessAll} disabled={loadingAction}>
{loadingAction ? <Loader2 className="h-3 w-3 animate-spin mr-1" /> : null} {loadingAction ? <Loader2 className="h-3 w-3 animate-spin" /> : null}
{t('projectCard.processAll')} {t('projectCard.processAll')}
</Button> </Button>
)} )}
{status === 'done' && ( {status === 'done' && (
<Button size="sm" variant="outline" className="h-7 text-xs px-2" onClick={() => handleDownload()} disabled={loadingAction}> <Button size="sm" variant="outline" onClick={() => handleDownload()} disabled={loadingAction}>
<Download className="h-3 w-3 mr-1" />{t('projectCard.downloadAll')} <Download className="h-3 w-3" />{t('projectCard.downloadAll')}
</Button> </Button>
)} )}
<Button size="icon" variant="ghost" className="h-7 w-7 shrink-0" onClick={handleDelete}> <Button size="icon" variant="ghost" className="h-8 w-8 shrink-0" onClick={handleDelete}>
<Trash2 className="h-4 w-4 text-destructive" /> <Trash2 className="h-4 w-4 text-destructive" />
</Button> </Button>
</div> </div>
@@ -1537,14 +1549,14 @@ export default function Audiobook() {
{chaptersError > 0 && ( {chaptersError > 0 && (
<> <>
<span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span> <span className="text-destructive">({t('projectCard.chaptersError', { count: chaptersError })})</span>
<Button size="sm" variant="outline" className="h-5 text-[10px] px-1.5 text-destructive border-destructive/40" onClick={handleRetryFailed}> <Button size="xs" variant="outline" className="text-destructive border-destructive/40" onClick={handleRetryFailed}>
{t('projectCard.retryFailed')} {t('projectCard.retryFailed')}
</Button> </Button>
</> </>
)} )}
</div> </div>
{chaptersParsing > 0 && totalCount > 0 && ( {chaptersParsing > 0 && totalCount > 0 && (
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}> <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
{t('projectCard.cancelParsing')} {t('projectCard.cancelParsing')}
</Button> </Button>
)} )}
@@ -1560,7 +1572,7 @@ export default function Audiobook() {
<span>{t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })}</span> <span>{t('projectCard.segmentsProgress', { done: doneCount, total: totalCount })}</span>
</div> </div>
{!chaptersParsing && hasGenerating && ( {!chaptersParsing && hasGenerating && (
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}> <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
{t('projectCard.cancelGenerating')} {t('projectCard.cancelGenerating')}
</Button> </Button>
)} )}
@@ -1570,7 +1582,7 @@ export default function Audiobook() {
)} )}
{chaptersParsing > 0 && !totalCount && ( {chaptersParsing > 0 && !totalCount && (
<div className="flex justify-end"> <div className="flex justify-end">
<Button size="sm" variant="ghost" className="h-5 text-[10px] px-1.5 text-destructive" onClick={handleCancelBatch}> <Button size="xs" variant="ghost" className="text-destructive" onClick={handleCancelBatch}>
{t('projectCard.cancelParsing')} {t('projectCard.cancelParsing')}
</Button> </Button>
</div> </div>
@@ -1591,6 +1603,7 @@ export default function Audiobook() {
onScrollToChapter={(id) => setScrollToChapterId(id)} onScrollToChapter={(id) => setScrollToChapterId(id)}
/> />
<ChaptersPanel <ChaptersPanel
key={selectedProject.id}
project={selectedProject} project={selectedProject}
detail={detail} detail={detail}
segments={segments} segments={segments}