feat: enhance translation functionality with partial segment handling and UI updates
This commit is contained in:
1 parent
cae8b6d774
commit
31adb05ec8
10 files changed
+318
-115
No files matched your search
@@ -1,4 +1,3 @@
|
||||
import 'package:caption_trans/models/subtitle_segment.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../services/translation/translation_provider.dart';
|
||||
import '../../services/translation/translation_service.dart';
|
||||
@@ -21,11 +20,14 @@ class TranslationBloc extends Bloc<TranslationEvent, TranslationState> {
|
||||
StartTranslation event,
|
||||
Emitter<TranslationState> emit,
|
||||
) async {
|
||||
var latestPartials = event.segments;
|
||||
|
||||
try {
|
||||
emit(
|
||||
TranslationInProgress(
|
||||
completed: 0,
|
||||
total: event.segments.length,
|
||||
partialSegments: latestPartials,
|
||||
statusMessage: 'Initializing translation...',
|
||||
),
|
||||
);
|
||||
@@ -38,6 +40,7 @@ class TranslationBloc extends Bloc<TranslationEvent, TranslationState> {
|
||||
segments: event.segments,
|
||||
config: event.config,
|
||||
onProgress: (completed, total, partials) {
|
||||
latestPartials = partials;
|
||||
if (!emit.isDone) {
|
||||
emit(
|
||||
TranslationInProgress(
|
||||
@@ -59,19 +62,24 @@ class TranslationBloc extends Bloc<TranslationEvent, TranslationState> {
|
||||
);
|
||||
} on TranslationAbortedException catch (e) {
|
||||
if (!emit.isDone) {
|
||||
List<SubtitleSegment>? latestPartials;
|
||||
if (state is TranslationInProgress) {
|
||||
latestPartials = (state as TranslationInProgress).partialSegments;
|
||||
}
|
||||
emit(
|
||||
TranslationCancelled(
|
||||
message: e.message,
|
||||
partialSegments: latestPartials ?? event.segments,
|
||||
partialSegments: latestPartials,
|
||||
config: event.config,
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
emit(TranslationError(message: e.toString()));
|
||||
if (!emit.isDone) {
|
||||
emit(
|
||||
TranslationError(
|
||||
message: e.toString(),
|
||||
partialSegments: latestPartials,
|
||||
config: event.config,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,9 +88,7 @@ class TranslationBloc extends Bloc<TranslationEvent, TranslationState> {
|
||||
Emitter<TranslationState> emit,
|
||||
) {
|
||||
if (state is TranslationInProgress) {
|
||||
final s = state as TranslationInProgress;
|
||||
_translationService.cancel();
|
||||
emit(TranslationCancelled(partialSegments: s.partialSegments));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -53,22 +53,30 @@ class TranslationComplete extends TranslationState {
|
||||
class TranslationCancelled extends TranslationState {
|
||||
final String message;
|
||||
final List<SubtitleSegment>? partialSegments;
|
||||
final TranslationConfig? config;
|
||||
|
||||
const TranslationCancelled({
|
||||
this.message = 'Translation cancelled',
|
||||
this.partialSegments,
|
||||
this.config,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message, partialSegments];
|
||||
List<Object?> get props => [message, partialSegments, config];
|
||||
}
|
||||
|
||||
/// Translation failed.
|
||||
class TranslationError extends TranslationState {
|
||||
final String message;
|
||||
final List<SubtitleSegment>? partialSegments;
|
||||
final TranslationConfig? config;
|
||||
|
||||
const TranslationError({required this.message});
|
||||
const TranslationError({
|
||||
required this.message,
|
||||
this.partialSegments,
|
||||
this.config,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, partialSegments, config];
|
||||
}
|
||||
@@ -62,6 +62,8 @@
|
||||
"enterGeminiModel": "e.g., gemini-2.0-flash, gpt-4o",
|
||||
"targetLanguage": "Target Language",
|
||||
"translate": "Translate",
|
||||
"continueTranslation": "Continue Translation",
|
||||
"retranslate": "Retranslate",
|
||||
"initializingTranslation": "Initializing translation...",
|
||||
"translatingProgress": "Translating subtitles ({completed}/{total})...",
|
||||
"@translatingProgress": {
|
||||
|
||||
@@ -410,6 +410,18 @@ abstract class AppLocalizations {
|
||||
/// **'Translate'**
|
||||
String get translate;
|
||||
|
||||
/// No description provided for @continueTranslation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Continue Translation'**
|
||||
String get continueTranslation;
|
||||
|
||||
/// No description provided for @retranslate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Retranslate'**
|
||||
String get retranslate;
|
||||
|
||||
/// No description provided for @initializingTranslation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -170,6 +170,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get translate => 'Translate';
|
||||
|
||||
@override
|
||||
String get continueTranslation => 'Continue Translation';
|
||||
|
||||
@override
|
||||
String get retranslate => 'Retranslate';
|
||||
|
||||
@override
|
||||
String get initializingTranslation => 'Initializing translation...';
|
||||
|
||||
|
||||
@@ -166,6 +166,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get translate => '翻译';
|
||||
|
||||
@override
|
||||
String get continueTranslation => '继续翻译';
|
||||
|
||||
@override
|
||||
String get retranslate => '重新翻译';
|
||||
|
||||
@override
|
||||
String get initializingTranslation => '正在初始化翻译...';
|
||||
|
||||
|
||||
+3
-1
@@ -52,6 +52,8 @@
|
||||
"enterGeminiModel": "例如:gemini-2.0-flash, gpt-4o",
|
||||
"targetLanguage": "目标语言",
|
||||
"translate": "翻译",
|
||||
"continueTranslation": "继续翻译",
|
||||
"retranslate": "重新翻译",
|
||||
"initializingTranslation": "正在初始化翻译...",
|
||||
"translatingProgress": "正在翻译字幕 ({completed}/{total})...",
|
||||
"segmentsTranslated": "已翻译 {count} 条字幕为{lang}",
|
||||
@@ -81,4 +83,4 @@
|
||||
"savedProvidersTitle": "已保存的服务商",
|
||||
"savedProvidersEmpty": "暂无已保存的服务商",
|
||||
"savedProvidersHint": "API验证成功后自会动保存到本地"
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
const Object _translatedTextNotSet = Object();
|
||||
|
||||
/// A single subtitle segment with timing and text content.
|
||||
class SubtitleSegment extends Equatable {
|
||||
final int index;
|
||||
@@ -21,14 +23,16 @@ class SubtitleSegment extends Equatable {
|
||||
Duration? startTime,
|
||||
Duration? endTime,
|
||||
String? text,
|
||||
String? translatedText,
|
||||
Object? translatedText = _translatedTextNotSet,
|
||||
}) {
|
||||
return SubtitleSegment(
|
||||
index: index ?? this.index,
|
||||
startTime: startTime ?? this.startTime,
|
||||
endTime: endTime ?? this.endTime,
|
||||
text: text ?? this.text,
|
||||
translatedText: translatedText ?? this.translatedText,
|
||||
translatedText: identical(translatedText, _translatedTextNotSet)
|
||||
? this.translatedText
|
||||
: translatedText as String?,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+196
-91
@@ -195,6 +195,118 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
TranslationConfig? _buildCurrentTranslationConfig(
|
||||
TranscriptionState transcriptionState,
|
||||
) {
|
||||
if (transcriptionState is! TranscriptionComplete) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TranslationConfig(
|
||||
providerId: _llmProvider,
|
||||
apiKey: _apiKey,
|
||||
baseUrl: _llmBaseUrl,
|
||||
model: _targetModel,
|
||||
sourceLanguage: transcriptionState.result.language,
|
||||
targetLanguage: _targetLanguage,
|
||||
batchSize: _batchSize,
|
||||
);
|
||||
}
|
||||
|
||||
List<SubtitleSegment>? _resolveEffectiveSegments(
|
||||
TranslationState translationState,
|
||||
TranscriptionState transcriptionState,
|
||||
) {
|
||||
if (translationState is TranslationInProgress &&
|
||||
translationState.partialSegments != null) {
|
||||
return translationState.partialSegments;
|
||||
}
|
||||
|
||||
if (translationState is TranslationComplete) {
|
||||
return translationState.translatedSegments;
|
||||
}
|
||||
|
||||
if (translationState is TranslationCancelled &&
|
||||
translationState.partialSegments != null) {
|
||||
return translationState.partialSegments;
|
||||
}
|
||||
|
||||
if (translationState is TranslationError &&
|
||||
translationState.partialSegments != null) {
|
||||
return translationState.partialSegments;
|
||||
}
|
||||
|
||||
if (transcriptionState is TranscriptionComplete) {
|
||||
return transcriptionState.result.segments;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
int _countTranslatedSegments(List<SubtitleSegment>? segments) {
|
||||
if (segments == null) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return segments
|
||||
.where((segment) => segment.translatedText?.trim().isNotEmpty == true)
|
||||
.length;
|
||||
}
|
||||
|
||||
List<SubtitleSegment> _clearTranslatedSegments(
|
||||
List<SubtitleSegment> segments,
|
||||
) {
|
||||
return segments
|
||||
.map((segment) => segment.copyWith(translatedText: null))
|
||||
.toList();
|
||||
}
|
||||
|
||||
void _persistProjectTranslation(
|
||||
BuildContext context, {
|
||||
required List<SubtitleSegment> segments,
|
||||
TranslationConfig? config,
|
||||
}) {
|
||||
if (_activeProject == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final updatedResult = _activeProject!.transcription.copyWith(
|
||||
segments: segments,
|
||||
);
|
||||
final updatedProject = _activeProject!.copyWith(
|
||||
transcription: updatedResult,
|
||||
updatedAt: DateTime.now(),
|
||||
translationConfig: config ?? _activeProject!.translationConfig,
|
||||
);
|
||||
|
||||
_activeProject = updatedProject;
|
||||
context.read<ProjectBloc>().add(UpdateProject(updatedProject));
|
||||
}
|
||||
|
||||
void _startTranslation(
|
||||
BuildContext context, {
|
||||
required TranslationState translationState,
|
||||
required TranscriptionState transcriptionState,
|
||||
bool restart = false,
|
||||
}) {
|
||||
final config = _buildCurrentTranslationConfig(transcriptionState);
|
||||
final segments = _resolveEffectiveSegments(
|
||||
translationState,
|
||||
transcriptionState,
|
||||
);
|
||||
|
||||
if (config == null || segments == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<TranslationBloc>().add(
|
||||
StartTranslation(
|
||||
segments: restart ? _clearTranslatedSegments(segments) : segments,
|
||||
config: config,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -271,6 +383,13 @@ class _HomePageState extends State<HomePage> {
|
||||
const SizedBox(height: 12),
|
||||
BlocConsumer<TranslationBloc, TranslationState>(
|
||||
listener: (context, state) {
|
||||
final transcriptionState = context
|
||||
.read<TranscriptionBloc>()
|
||||
.state;
|
||||
final currentConfig = _buildCurrentTranslationConfig(
|
||||
transcriptionState,
|
||||
);
|
||||
|
||||
if (state is TranslationError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
@@ -278,40 +397,58 @@ class _HomePageState extends State<HomePage> {
|
||||
backgroundColor: Colors.red.shade700,
|
||||
),
|
||||
);
|
||||
if (state.partialSegments != null) {
|
||||
_persistProjectTranslation(
|
||||
context,
|
||||
segments: state.partialSegments!,
|
||||
config: state.config ?? currentConfig,
|
||||
);
|
||||
}
|
||||
} else if (state is TranslationInProgress &&
|
||||
state.partialSegments != null) {
|
||||
if (_activeProject != null) {
|
||||
final updatedResult = _activeProject!.transcription
|
||||
.copyWith(segments: state.partialSegments!);
|
||||
_activeProject = _activeProject!.copyWith(
|
||||
transcription: updatedResult,
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
context.read<ProjectBloc>().add(
|
||||
UpdateProject(_activeProject!),
|
||||
);
|
||||
}
|
||||
_persistProjectTranslation(
|
||||
context,
|
||||
segments: state.partialSegments!,
|
||||
config: currentConfig,
|
||||
);
|
||||
} else if (state is TranslationComplete) {
|
||||
if (_activeProject != null) {
|
||||
final updatedResult = _activeProject!.transcription
|
||||
.copyWith(segments: state.translatedSegments);
|
||||
_activeProject = _activeProject!.copyWith(
|
||||
transcription: updatedResult,
|
||||
updatedAt: DateTime.now(),
|
||||
translationConfig: state.config,
|
||||
);
|
||||
context.read<ProjectBloc>().add(
|
||||
UpdateProject(_activeProject!),
|
||||
);
|
||||
}
|
||||
_persistProjectTranslation(
|
||||
context,
|
||||
segments: state.translatedSegments,
|
||||
config: state.config,
|
||||
);
|
||||
} else if (state is TranslationCancelled &&
|
||||
state.partialSegments != null) {
|
||||
_persistProjectTranslation(
|
||||
context,
|
||||
segments: state.partialSegments!,
|
||||
config: state.config ?? currentConfig,
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, translationState) {
|
||||
return BlocBuilder<TranscriptionBloc, TranscriptionState>(
|
||||
builder: (context, transcriptionState) {
|
||||
final effectiveSegments = _resolveEffectiveSegments(
|
||||
translationState,
|
||||
transcriptionState,
|
||||
);
|
||||
final translatedSegmentCount =
|
||||
_countTranslatedSegments(effectiveSegments);
|
||||
final totalSegmentCount =
|
||||
effectiveSegments?.length ?? 0;
|
||||
final hasPartialTranslation =
|
||||
translatedSegmentCount > 0 &&
|
||||
translatedSegmentCount < totalSegmentCount;
|
||||
final isFullyTranslated =
|
||||
totalSegmentCount > 0 &&
|
||||
translatedSegmentCount == totalSegmentCount;
|
||||
|
||||
return TranslationPanel(
|
||||
transcriptionState: transcriptionState,
|
||||
translationState: translationState,
|
||||
translatedSegmentCount: translatedSegmentCount,
|
||||
totalSegmentCount: totalSegmentCount,
|
||||
llmProvider: _llmProvider,
|
||||
llmBaseUrl: _llmBaseUrl,
|
||||
onLlmProviderChanged: (provider) {
|
||||
@@ -347,25 +484,23 @@ class _HomePageState extends State<HomePage> {
|
||||
widget.settingsService.setBatchSize(size);
|
||||
},
|
||||
onStartTranslation: () {
|
||||
if (transcriptionState is TranscriptionComplete) {
|
||||
context.read<TranslationBloc>().add(
|
||||
StartTranslation(
|
||||
segments:
|
||||
transcriptionState.result.segments,
|
||||
config: TranslationConfig(
|
||||
providerId: _llmProvider,
|
||||
apiKey: _apiKey,
|
||||
baseUrl: _llmBaseUrl,
|
||||
model: _targetModel,
|
||||
sourceLanguage:
|
||||
transcriptionState.result.language,
|
||||
targetLanguage: _targetLanguage,
|
||||
batchSize: _batchSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
_startTranslation(
|
||||
context,
|
||||
translationState: translationState,
|
||||
transcriptionState: transcriptionState,
|
||||
);
|
||||
},
|
||||
onRestartTranslation:
|
||||
hasPartialTranslation || isFullyTranslated
|
||||
? () {
|
||||
_startTranslation(
|
||||
context,
|
||||
translationState: translationState,
|
||||
transcriptionState: transcriptionState,
|
||||
restart: true,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
onCancelTranslation: () {
|
||||
context.read<TranslationBloc>().add(
|
||||
const CancelTranslation(),
|
||||
@@ -511,63 +646,28 @@ class _HomePageState extends State<HomePage> {
|
||||
builder: (context, translationState) {
|
||||
return BlocBuilder<TranscriptionBloc, TranscriptionState>(
|
||||
builder: (context, transcriptionState) {
|
||||
List<SubtitleSegment>? segments;
|
||||
bool hasTranslation = false;
|
||||
|
||||
if (translationState is TranslationInProgress &&
|
||||
translationState.partialSegments != null) {
|
||||
segments = translationState.partialSegments;
|
||||
hasTranslation =
|
||||
segments != null &&
|
||||
segments.any((s) => s.translatedText?.isNotEmpty == true);
|
||||
} else if (translationState is TranslationComplete) {
|
||||
segments = translationState.translatedSegments;
|
||||
hasTranslation = true;
|
||||
} else if (translationState is TranslationCancelled) {
|
||||
segments = translationState.partialSegments;
|
||||
hasTranslation =
|
||||
segments != null &&
|
||||
segments.any((s) => s.translatedText?.isNotEmpty == true);
|
||||
} else if (transcriptionState is TranscriptionComplete) {
|
||||
segments = transcriptionState.result.segments;
|
||||
// Check if the loaded project naturally has translations
|
||||
hasTranslation = segments.any(
|
||||
(s) => s.translatedText?.isNotEmpty == true,
|
||||
);
|
||||
}
|
||||
final segments = _resolveEffectiveSegments(
|
||||
translationState,
|
||||
transcriptionState,
|
||||
);
|
||||
final hasTranslation = _countTranslatedSegments(segments) > 0;
|
||||
|
||||
return SubtitlePreview(
|
||||
segments: segments,
|
||||
hasTranslation:
|
||||
hasTranslation || translationState is TranslationComplete,
|
||||
hasTranslation: hasTranslation,
|
||||
bilingual: _bilingual,
|
||||
onBilingualChanged: (v) {
|
||||
setState(() => _bilingual = v);
|
||||
widget.settingsService.setBilingual(v);
|
||||
},
|
||||
onExportOriginal: segments != null
|
||||
? () => _exportSrt(
|
||||
context,
|
||||
segments!.cast<dynamic>(),
|
||||
false,
|
||||
false,
|
||||
)
|
||||
? () => _exportSrt(context, segments, false, false)
|
||||
: null,
|
||||
onExportTranslated: hasTranslation && segments != null
|
||||
? () => _exportSrt(
|
||||
context,
|
||||
segments!.cast<dynamic>(),
|
||||
true,
|
||||
false,
|
||||
)
|
||||
? () => _exportSrt(context, segments, true, false)
|
||||
: null,
|
||||
onExportBilingual: hasTranslation && segments != null
|
||||
? () => _exportSrt(
|
||||
context,
|
||||
segments!.cast<dynamic>(),
|
||||
false,
|
||||
true,
|
||||
)
|
||||
? () => _exportSrt(context, segments, false, true)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
@@ -604,14 +704,14 @@ class _HomePageState extends State<HomePage> {
|
||||
|
||||
Future<void> _exportSrt(
|
||||
BuildContext context,
|
||||
List<dynamic> segments,
|
||||
List<SubtitleSegment> segments,
|
||||
bool translatedOnly,
|
||||
bool bilingual,
|
||||
) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
try {
|
||||
final srtContent = SrtParser.generate(
|
||||
segments.cast(),
|
||||
segments,
|
||||
useTranslation: translatedOnly,
|
||||
bilingual: bilingual,
|
||||
);
|
||||
@@ -864,10 +964,15 @@ class _HomePageState extends State<HomePage> {
|
||||
_activeProject = project;
|
||||
_sourceVideoLanguage = project.sourceVideoLanguage;
|
||||
if (project.translationConfig != null) {
|
||||
_targetLanguage = project.translationConfig!.targetLanguage;
|
||||
// Restore other config if needed
|
||||
if (project.translationConfig!.model != null) {
|
||||
_targetModel = project.translationConfig!.model!;
|
||||
final config = project.translationConfig!;
|
||||
_llmProvider = config.providerId;
|
||||
_llmBaseUrl = config.baseUrl;
|
||||
_apiKey = config.apiKey;
|
||||
_targetLanguage = config.targetLanguage;
|
||||
_batchSize = config.batchSize;
|
||||
_availableModels = config.model != null ? [config.model!] : [];
|
||||
if (config.model != null) {
|
||||
_targetModel = config.model!;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -23,6 +23,8 @@ const Map<String, String> defaultLlmBaseUrls = {
|
||||
class TranslationPanel extends StatelessWidget {
|
||||
final TranscriptionState transcriptionState;
|
||||
final TranslationState translationState;
|
||||
final int translatedSegmentCount;
|
||||
final int totalSegmentCount;
|
||||
final String targetLanguage;
|
||||
final String llmProvider;
|
||||
final String llmBaseUrl;
|
||||
@@ -39,12 +41,15 @@ class TranslationPanel extends StatelessWidget {
|
||||
final ValueChanged<int> onBatchSizeChanged;
|
||||
final VoidCallback onCheckModels;
|
||||
final VoidCallback onStartTranslation;
|
||||
final VoidCallback? onRestartTranslation;
|
||||
final VoidCallback onCancelTranslation;
|
||||
|
||||
const TranslationPanel({
|
||||
super.key,
|
||||
required this.transcriptionState,
|
||||
required this.translationState,
|
||||
required this.translatedSegmentCount,
|
||||
required this.totalSegmentCount,
|
||||
required this.targetLanguage,
|
||||
required this.llmProvider,
|
||||
required this.llmBaseUrl,
|
||||
@@ -61,6 +66,7 @@ class TranslationPanel extends StatelessWidget {
|
||||
required this.onBatchSizeChanged,
|
||||
required this.onCheckModels,
|
||||
required this.onStartTranslation,
|
||||
this.onRestartTranslation,
|
||||
required this.onCancelTranslation,
|
||||
});
|
||||
|
||||
@@ -318,10 +324,9 @@ class TranslationPanel extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final controls = Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: targetLanguage,
|
||||
decoration: const InputDecoration(
|
||||
@@ -348,10 +353,28 @@ class TranslationPanel extends StatelessWidget {
|
||||
if (v != null) onTargetLanguageChanged(v);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
_buildStartButton(context, l10n),
|
||||
],
|
||||
);
|
||||
final actions = _buildActionButtons(context, l10n);
|
||||
final useStackedLayout =
|
||||
constraints.maxWidth < 720 &&
|
||||
(_hasPartialTranslation || _isFullyTranslated);
|
||||
|
||||
if (useStackedLayout) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(children: [controls]),
|
||||
const SizedBox(height: 12),
|
||||
Align(alignment: Alignment.centerRight, child: actions),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [controls, const SizedBox(width: 16), actions],
|
||||
);
|
||||
},
|
||||
),
|
||||
if (_isTranslating ||
|
||||
translationState is TranslationComplete ||
|
||||
@@ -368,6 +391,11 @@ class TranslationPanel extends StatelessWidget {
|
||||
}
|
||||
|
||||
bool get _isTranslating => translationState is TranslationInProgress;
|
||||
bool get _hasAnyTranslation => translatedSegmentCount > 0;
|
||||
bool get _hasPartialTranslation =>
|
||||
_hasAnyTranslation && translatedSegmentCount < totalSegmentCount;
|
||||
bool get _isFullyTranslated =>
|
||||
totalSegmentCount > 0 && translatedSegmentCount == totalSegmentCount;
|
||||
|
||||
bool get _canStart =>
|
||||
transcriptionState is TranscriptionComplete &&
|
||||
@@ -386,7 +414,7 @@ class TranslationPanel extends StatelessWidget {
|
||||
return '${model.substring(0, head)}...${model.substring(model.length - tail)}';
|
||||
}
|
||||
|
||||
Widget _buildStartButton(BuildContext context, AppLocalizations l10n) {
|
||||
Widget _buildActionButtons(BuildContext context, AppLocalizations l10n) {
|
||||
if (_isTranslating) {
|
||||
final errorColor = Theme.of(context).colorScheme.error;
|
||||
return OutlinedButton.icon(
|
||||
@@ -400,6 +428,30 @@ class TranslationPanel extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
if (_hasPartialTranslation) {
|
||||
return Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
FilledButton(
|
||||
onPressed: _canStart ? onStartTranslation : null,
|
||||
child: Text(l10n.continueTranslation),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: _canStart ? onRestartTranslation : null,
|
||||
child: Text(l10n.retranslate),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (_isFullyTranslated) {
|
||||
return OutlinedButton(
|
||||
onPressed: _canStart ? onRestartTranslation : null,
|
||||
child: Text(l10n.retranslate),
|
||||
);
|
||||
}
|
||||
|
||||
return FilledButton(
|
||||
onPressed: _canStart ? onStartTranslation : null,
|
||||
child: Text(l10n.translate),
|
||||
|
||||
Reference in new issue
Block a user