feat: add edit character dialog with localization support
This commit is contained in:
@@ -84,6 +84,7 @@
|
|||||||
"descPlaceholder": "Character description",
|
"descPlaceholder": "Character description",
|
||||||
"voiceDesign": "Voice #{{id}}",
|
"voiceDesign": "Voice #{{id}}",
|
||||||
"noVoice": "Unassigned",
|
"noVoice": "Unassigned",
|
||||||
|
"editTitle": "Edit Character: {{name}}",
|
||||||
"savedSuccess": "Character saved"
|
"savedSuccess": "Character saved"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"descPlaceholder": "キャラクター説明",
|
"descPlaceholder": "キャラクター説明",
|
||||||
"voiceDesign": "音声 #{{id}}",
|
"voiceDesign": "音声 #{{id}}",
|
||||||
"noVoice": "未割り当て",
|
"noVoice": "未割り当て",
|
||||||
|
"editTitle": "キャラクターを編集:{{name}}",
|
||||||
"savedSuccess": "キャラクターを保存しました"
|
"savedSuccess": "キャラクターを保存しました"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"descPlaceholder": "캐릭터 설명",
|
"descPlaceholder": "캐릭터 설명",
|
||||||
"voiceDesign": "음성 #{{id}}",
|
"voiceDesign": "음성 #{{id}}",
|
||||||
"noVoice": "미할당",
|
"noVoice": "미할당",
|
||||||
|
"editTitle": "캐릭터 편집: {{name}}",
|
||||||
"savedSuccess": "캐릭터가 저장되었습니다"
|
"savedSuccess": "캐릭터가 저장되었습니다"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
"descPlaceholder": "角色描述",
|
"descPlaceholder": "角色描述",
|
||||||
"voiceDesign": "音色 #{{id}}",
|
"voiceDesign": "音色 #{{id}}",
|
||||||
"noVoice": "未分配",
|
"noVoice": "未分配",
|
||||||
|
"editTitle": "编辑角色:{{name}}",
|
||||||
"savedSuccess": "角色已保存",
|
"savedSuccess": "角色已保存",
|
||||||
"regeneratingPreview": "重新生成试听中...",
|
"regeneratingPreview": "重新生成试听中...",
|
||||||
"regeneratePreview": "重生试听",
|
"regeneratePreview": "重生试听",
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"descPlaceholder": "角色描述",
|
"descPlaceholder": "角色描述",
|
||||||
"voiceDesign": "音色 #{{id}}",
|
"voiceDesign": "音色 #{{id}}",
|
||||||
"noVoice": "未分配",
|
"noVoice": "未分配",
|
||||||
|
"editTitle": "編輯角色:{{name}}",
|
||||||
"savedSuccess": "角色已儲存"
|
"savedSuccess": "角色已儲存"
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from '@/components/ui/textarea'
|
||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Progress } from '@/components/ui/progress'
|
import { Progress } from '@/components/ui/progress'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'
|
||||||
import { Navbar } from '@/components/Navbar'
|
import { Navbar } from '@/components/Navbar'
|
||||||
import { AudioPlayer } from '@/components/AudioPlayer'
|
import { AudioPlayer } from '@/components/AudioPlayer'
|
||||||
import { ChapterPlayer } from '@/components/ChapterPlayer'
|
import { ChapterPlayer } from '@/components/ChapterPlayer'
|
||||||
@@ -1245,6 +1245,65 @@ function CharactersPanel({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Dialog open={editingCharId !== null} onOpenChange={open => { if (!open) setEditingCharId(null) }}>
|
||||||
|
<DialogContent className="sm:max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t('projectCard.characters.editTitle', { name: detail?.characters.find(c => c.id === editingCharId)?.name ?? '' })}</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-3 py-2">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">{t('projectCard.characters.namePlaceholder')}</label>
|
||||||
|
<Input
|
||||||
|
value={editFields.name}
|
||||||
|
onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))}
|
||||||
|
placeholder={t('projectCard.characters.namePlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">{t('projectCard.characters.genderPlaceholder')}</label>
|
||||||
|
<select
|
||||||
|
className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
|
||||||
|
value={editFields.gender}
|
||||||
|
onChange={e => setEditFields(f => ({ ...f, gender: e.target.value }))}
|
||||||
|
>
|
||||||
|
<option value="">{t('projectCard.characters.genderPlaceholder')}</option>
|
||||||
|
<option value="男">{t('projectCard.characters.genderMale')}</option>
|
||||||
|
<option value="女">{t('projectCard.characters.genderFemale')}</option>
|
||||||
|
<option value="未知">{t('projectCard.characters.genderUnknown')}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">{t('projectCard.characters.instructPlaceholder')}</label>
|
||||||
|
<Textarea
|
||||||
|
value={editFields.instruct}
|
||||||
|
onChange={e => setEditFields(f => ({ ...f, instruct: e.target.value }))}
|
||||||
|
placeholder={t('projectCard.characters.instructPlaceholder')}
|
||||||
|
rows={4}
|
||||||
|
className="resize-none"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<label className="text-sm font-medium">{t('projectCard.characters.descPlaceholder')}</label>
|
||||||
|
<Input
|
||||||
|
value={editFields.description}
|
||||||
|
onChange={e => setEditFields(f => ({ ...f, description: e.target.value }))}
|
||||||
|
placeholder={t('projectCard.characters.descPlaceholder')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="ghost" onClick={() => setEditingCharId(null)}>{t('common:cancel')}</Button>
|
||||||
|
{editingCharId !== null && detail && (() => {
|
||||||
|
const editingChar = detail.characters.find(c => c.id === editingCharId)
|
||||||
|
return editingChar ? (
|
||||||
|
<Button onClick={() => saveEditChar(editingChar)}>
|
||||||
|
<Check className="h-3 w-3 mr-1" />{t('common:save')}
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
})()}
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
{(!detail || charCount === 0) ? (
|
{(!detail || charCount === 0) ? (
|
||||||
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-3 text-center">
|
<div className="flex-1 flex items-center justify-center text-xs text-muted-foreground px-3 text-center">
|
||||||
{status === 'pending' ? t('stepHints.pending') : t('projectCard.characters.title', { count: 0 })}
|
{status === 'pending' ? t('stepHints.pending') : t('projectCard.characters.title', { count: 0 })}
|
||||||
@@ -1253,65 +1312,13 @@ function CharactersPanel({
|
|||||||
<div className="flex-1 overflow-y-auto px-2 py-2 space-y-1.5">
|
<div className="flex-1 overflow-y-auto px-2 py-2 space-y-1.5">
|
||||||
{detail.characters.map(char => (
|
{detail.characters.map(char => (
|
||||||
<div key={char.id} className="border rounded-lg px-3 py-2 bg-muted/30 shadow-sm">
|
<div key={char.id} className="border rounded-lg px-3 py-2 bg-muted/30 shadow-sm">
|
||||||
{editingCharId === char.id ? (
|
<div className="flex flex-col gap-1 text-sm">
|
||||||
<div className="space-y-2">
|
|
||||||
<Input
|
|
||||||
value={editFields.name}
|
|
||||||
onChange={e => setEditFields(f => ({ ...f, name: e.target.value }))}
|
|
||||||
placeholder={t('projectCard.characters.namePlaceholder')}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="w-full h-9 rounded-md border border-input bg-background px-3 py-1 text-sm"
|
|
||||||
value={editFields.gender}
|
|
||||||
onChange={e => setEditFields(f => ({ ...f, gender: e.target.value }))}
|
|
||||||
>
|
|
||||||
<option value="">{t('projectCard.characters.genderPlaceholder')}</option>
|
|
||||||
<option value="男">{t('projectCard.characters.genderMale')}</option>
|
|
||||||
<option value="女">{t('projectCard.characters.genderFemale')}</option>
|
|
||||||
<option value="未知">{t('projectCard.characters.genderUnknown')}</option>
|
|
||||||
</select>
|
|
||||||
<Input
|
|
||||||
value={editFields.instruct}
|
|
||||||
onChange={e => setEditFields(f => ({ ...f, instruct: e.target.value }))}
|
|
||||||
placeholder={t('projectCard.characters.instructPlaceholder')}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
value={editFields.description}
|
|
||||||
onChange={e => setEditFields(f => ({ ...f, description: e.target.value }))}
|
|
||||||
placeholder={t('projectCard.characters.descPlaceholder')}
|
|
||||||
/>
|
|
||||||
<label className="flex items-center gap-2 text-sm cursor-pointer select-none">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={editFields.use_indextts2}
|
|
||||||
onChange={e => setEditFields(f => ({ ...f, use_indextts2: e.target.checked }))}
|
|
||||||
className="w-4 h-4"
|
|
||||||
/>
|
|
||||||
<Zap className="h-3 w-3 text-amber-400" />
|
|
||||||
<span>使用 IndexTTS2(需要音色克隆参考音频)</span>
|
|
||||||
</label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button size="sm" onClick={() => saveEditChar(char)}>
|
|
||||||
<Check className="h-3 w-3 mr-1" />{t('common:save')}
|
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="ghost" onClick={() => setEditingCharId(null)}>
|
|
||||||
<X className="h-3 w-3 mr-1" />{t('common:cancel')}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="flex flex-col gap-1 text-sm">
|
|
||||||
<div className="flex items-center justify-between gap-1">
|
<div className="flex items-center justify-between gap-1">
|
||||||
<div className="flex items-center gap-1.5 min-w-0">
|
<div className="flex items-center gap-1.5 min-w-0">
|
||||||
<span className={`font-medium truncate ${char.gender === '男' ? 'text-blue-400' : char.gender === '女' ? 'text-pink-400' : ''}`}>{char.name}</span>
|
<span className={`font-medium truncate ${char.gender === '男' ? 'text-blue-400' : char.gender === '女' ? 'text-pink-400' : ''}`}>{char.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-1 shrink-0">
|
||||||
{char.use_indextts2 && (
|
{status === 'characters_ready' && (
|
||||||
<Badge variant="outline" className="text-xs border-amber-400/50 text-amber-400">
|
|
||||||
<Zap className="h-2.5 w-2.5 mr-0.5" />IndexTTS2
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
{status === 'characters_ready' && (
|
|
||||||
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEditChar(char)}>
|
<Button size="icon" variant="ghost" className="h-6 w-6" onClick={() => startEditChar(char)}>
|
||||||
<Pencil className="h-3 w-3" />
|
<Pencil className="h-3 w-3" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1320,13 +1327,10 @@ function CharactersPanel({
|
|||||||
</div>
|
</div>
|
||||||
{char.description && <span className="text-xs text-muted-foreground">{char.description}</span>}
|
{char.description && <span className="text-xs text-muted-foreground">{char.description}</span>}
|
||||||
{char.instruct && <span className="text-xs text-muted-foreground/70">{char.instruct}</span>}
|
{char.instruct && <span className="text-xs text-muted-foreground/70">{char.instruct}</span>}
|
||||||
<div className="text-xs text-muted-foreground/60">
|
{!char.voice_design_id && (
|
||||||
{char.voice_design_id
|
<div className="text-xs text-muted-foreground/60">{t('projectCard.characters.noVoice')}</div>
|
||||||
? (char.voice_design_name || `#${char.voice_design_id}`)
|
)}
|
||||||
: t('projectCard.characters.noVoice')}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{!editingCharId && char.voice_design_id && (
|
{!editingCharId && char.voice_design_id && (
|
||||||
<div className="mt-2 flex items-center gap-2">
|
<div className="mt-2 flex items-center gap-2">
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -1363,7 +1367,7 @@ function CharactersPanel({
|
|||||||
<Button
|
<Button
|
||||||
className="w-full"
|
className="w-full"
|
||||||
onClick={onConfirm}
|
onClick={onConfirm}
|
||||||
disabled={loadingAction || editingCharId !== null}
|
disabled={loadingAction}
|
||||||
>
|
>
|
||||||
{loadingAction
|
{loadingAction
|
||||||
? t('projectCard.confirm.loading')
|
? t('projectCard.confirm.loading')
|
||||||
|
|||||||
Reference in New Issue
Block a user