feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -3,6 +3,8 @@ import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:record/record.dart';
|
||||
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
|
||||
abstract class VoiceRecorder {
|
||||
Future<void> start();
|
||||
Future<String?> stop();
|
||||
@@ -22,10 +24,10 @@ class RecordVoiceRecorder implements VoiceRecorder {
|
||||
try {
|
||||
hasPermission = await _recorder.hasPermission();
|
||||
} on MissingPluginException catch (_) {
|
||||
throw StateError('录音组件未加载,请完全重启 App 后重试');
|
||||
throw StateError(L10n.current.homeRecorderPluginUnavailable);
|
||||
}
|
||||
if (!hasPermission) {
|
||||
throw StateError('录音权限未授权');
|
||||
throw StateError(L10n.current.homeRecorderPermissionDenied);
|
||||
}
|
||||
|
||||
final fileName =
|
||||
@@ -42,7 +44,7 @@ class RecordVoiceRecorder implements VoiceRecorder {
|
||||
path: path,
|
||||
);
|
||||
} on MissingPluginException catch (_) {
|
||||
throw StateError('录音组件未加载,请完全重启 App 后重试');
|
||||
throw StateError(L10n.current.homeRecorderPluginUnavailable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +54,7 @@ class RecordVoiceRecorder implements VoiceRecorder {
|
||||
try {
|
||||
stoppedPath = await _recorder.stop();
|
||||
} on MissingPluginException catch (_) {
|
||||
throw StateError('录音组件未加载,请完全重启 App 后重试');
|
||||
throw StateError(L10n.current.homeRecorderPluginUnavailable);
|
||||
}
|
||||
return stoppedPath ?? _currentPath;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
|
||||
enum HomeReturnAction { pop, goHome, goHomeForDock }
|
||||
|
||||
+13
-8
@@ -5,10 +5,11 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import '../../../../core/api/api_exception.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/router/app_route_observer.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../core/network/api_exception.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../app/router/app_route_observer.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../chat/presentation/bloc/agent_stage.dart';
|
||||
import '../../../chat/presentation/bloc/chat_bloc.dart';
|
||||
@@ -32,7 +33,7 @@ import '../widgets/home_unread_badge.dart';
|
||||
|
||||
part 'home_screen_interactions.dart';
|
||||
|
||||
/// 布局常量
|
||||
/// Layout constants.
|
||||
const _defaultPadding = 20.0;
|
||||
const _itemSpacing = 16.0;
|
||||
const _cancelThreshold = -(AppSpacing.xxl + AppSpacing.xxl);
|
||||
@@ -46,7 +47,7 @@ const homeConversationStageKey = ValueKey('home_conversation_stage');
|
||||
const homeBottomInputStackKey = ValueKey('home_bottom_input_stack');
|
||||
const homeEmptyStateAmbientKey = ValueKey('home_empty_state_ambient');
|
||||
|
||||
/// 颜色常量
|
||||
/// Color constants.
|
||||
const _chatBgColor = AppColors.slate50;
|
||||
|
||||
class HomeScreen extends StatefulWidget {
|
||||
@@ -331,7 +332,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
padding: const EdgeInsets.only(
|
||||
bottom: _itemSpacing,
|
||||
),
|
||||
child: HomeChatItemRenderer.build(item),
|
||||
child: HomeChatItemRenderer.build(context, item),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -394,7 +395,11 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (hasEarlierHistory) {
|
||||
await _loadMoreHistoryPreservingViewport(chatBloc);
|
||||
} else {
|
||||
Toast.show(context, '没有更早的历史记录了', type: ToastType.info);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.homeNoEarlierHistory,
|
||||
type: ToastType.info,
|
||||
);
|
||||
}
|
||||
_applyViewportDecision(
|
||||
_dispatchViewportEvent(
|
||||
+13
-5
@@ -23,7 +23,11 @@ extension _HomeScreenInteractions on _HomeScreenState {
|
||||
_isCancelGestureActive = false;
|
||||
});
|
||||
if (showToast) {
|
||||
Toast.show(context, '已取消', type: ToastType.info);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.homeRecordingCanceled,
|
||||
type: ToastType.info,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +79,7 @@ extension _HomeScreenInteractions on _HomeScreenState {
|
||||
return;
|
||||
}
|
||||
if (canceled) {
|
||||
Toast.show(context, '已请求停止', type: ToastType.info);
|
||||
Toast.show(context, context.l10n.homeStopRequested, type: ToastType.info);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,7 +149,7 @@ extension _HomeScreenInteractions on _HomeScreenState {
|
||||
_isCancelGestureActive = false;
|
||||
});
|
||||
if (audioPath == null || audioPath.isEmpty) {
|
||||
throw StateError('录音失败,请重试');
|
||||
throw StateError(context.l10n.errorGenericSafe);
|
||||
}
|
||||
final transcript = await _transcribeAudio(audioPath);
|
||||
if (!mounted) {
|
||||
@@ -153,7 +157,11 @@ extension _HomeScreenInteractions on _HomeScreenState {
|
||||
}
|
||||
final normalizedTranscript = transcript.trim();
|
||||
if (normalizedTranscript.isEmpty) {
|
||||
Toast.show(context, '未识别到有效语音,请靠近麦克风并连续说话后重试', type: ToastType.error);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.homeNoValidSpeech,
|
||||
type: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
_messageController.text = normalizedTranscript;
|
||||
@@ -196,7 +204,7 @@ extension _HomeScreenInteractions on _HomeScreenState {
|
||||
}
|
||||
final raw = error.toString();
|
||||
if (raw.startsWith('Instance of')) {
|
||||
return '请求失败,请稍后重试';
|
||||
return context.l10n.errorGenericSafe;
|
||||
}
|
||||
return raw.replaceFirst('Bad state: ', '');
|
||||
}
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
class HomeSheet extends StatelessWidget {
|
||||
@@ -57,14 +58,14 @@ class HomeSheet extends StatelessWidget {
|
||||
_buildOptionCard(
|
||||
context: context,
|
||||
icon: LucideIcons.camera,
|
||||
label: '拍照',
|
||||
label: context.l10n.homeSheetTakePhoto,
|
||||
onTap: () => _handleCameraTap(context),
|
||||
),
|
||||
const SizedBox(width: 24),
|
||||
_buildOptionCard(
|
||||
context: context,
|
||||
icon: LucideIcons.image,
|
||||
label: '相册',
|
||||
label: context.l10n.homeSheetPhotoLibrary,
|
||||
onTap: () => _handlePhotoTap(context),
|
||||
),
|
||||
],
|
||||
+11
-9
@@ -3,11 +3,12 @@ import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/utils/tool_name_localizer.dart';
|
||||
import '../../../../core/utils/tool_name_localizer.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../chat/data/models/chat_list_item.dart';
|
||||
import '../../../chat/ui/widgets/ui_schema_renderer.dart';
|
||||
import '../../../ui_schema/presentation/widgets/ui_schema_renderer.dart';
|
||||
|
||||
const _messagePaddingH = 13.0;
|
||||
const _messagePaddingV = 9.0;
|
||||
@@ -19,12 +20,12 @@ const _toolResultWidthFactor = 0.9;
|
||||
const _iconSize = 24.0;
|
||||
|
||||
class HomeChatItemRenderer {
|
||||
static Widget build(ChatListItem item) {
|
||||
static Widget build(BuildContext context, ChatListItem item) {
|
||||
switch (item.type) {
|
||||
case ChatItemType.message:
|
||||
return _buildMessageItem(item as TextMessageItem);
|
||||
case ChatItemType.toolCall:
|
||||
return _buildToolCallItem(item as ToolCallItem);
|
||||
return _buildToolCallItem(context, item as ToolCallItem);
|
||||
case ChatItemType.toolResult:
|
||||
return _buildToolResultItem(item as ToolResultItem);
|
||||
}
|
||||
@@ -198,25 +199,26 @@ class HomeChatItemRenderer {
|
||||
);
|
||||
}
|
||||
|
||||
static Widget _buildToolCallItem(ToolCallItem item) {
|
||||
static Widget _buildToolCallItem(BuildContext context, ToolCallItem item) {
|
||||
final l10n = context.l10n;
|
||||
final (statusText, statusColor, statusIcon) = switch (item.status) {
|
||||
ToolCallStatus.pending => (
|
||||
'工具准备中',
|
||||
l10n.homeToolPreparing,
|
||||
AppColors.slate500,
|
||||
LucideIcons.clock,
|
||||
),
|
||||
ToolCallStatus.executing => (
|
||||
'任务执行中',
|
||||
l10n.homeToolExecuting,
|
||||
AppColors.blue600,
|
||||
LucideIcons.loader,
|
||||
),
|
||||
ToolCallStatus.error => (
|
||||
item.errorMessage ?? '执行失败',
|
||||
item.errorMessage ?? l10n.homeToolExecutionFailed,
|
||||
AppColors.red600,
|
||||
LucideIcons.alertCircle,
|
||||
),
|
||||
ToolCallStatus.completed => (
|
||||
'已完成',
|
||||
l10n.homeToolCompleted,
|
||||
AppColors.emerald600,
|
||||
LucideIcons.checkCircle,
|
||||
),
|
||||
+15
-12
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/message_composer.dart';
|
||||
@@ -94,12 +95,14 @@ class HomeComposerStack extends StatelessWidget {
|
||||
onHoldToSpeakEnd: onHoldToSpeakEnd,
|
||||
onHoldToSpeakMoveUpdate: onHoldToSpeakMoveUpdate,
|
||||
onHoldToSpeakCancel: onHoldToSpeakCancel,
|
||||
textInputChild: _buildTextInputContent(),
|
||||
textInputChild: _buildTextInputContent(context),
|
||||
recordingAnimation: const SizedBox.shrink(),
|
||||
recordingText: isCancelGestureActive ? '松手取消' : '松手发送',
|
||||
recordingText: isCancelGestureActive
|
||||
? context.l10n.homeRecordingReleaseCancel
|
||||
: context.l10n.homeRecordingReleaseSend,
|
||||
recordingHintText: isCancelGestureActive
|
||||
? '松开取消'
|
||||
: '松开发送,上滑取消',
|
||||
? context.l10n.homeRecordingHintReleaseCancel
|
||||
: context.l10n.homeRecordingHintReleaseSend,
|
||||
showRecordingInlineFeedback: false,
|
||||
);
|
||||
},
|
||||
@@ -111,9 +114,9 @@ class HomeComposerStack extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextInputContent() {
|
||||
Widget _buildTextInputContent(BuildContext context) {
|
||||
if (isTranscribing) {
|
||||
return _buildTranscribingIndicator();
|
||||
return _buildTranscribingIndicator(context);
|
||||
}
|
||||
return SizedBox.expand(
|
||||
child: Align(
|
||||
@@ -129,8 +132,8 @@ class HomeComposerStack extends StatelessWidget {
|
||||
color: AppColors.slate900,
|
||||
),
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: const InputDecoration(
|
||||
hintText: '输入消息...',
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.homeInputHint,
|
||||
hintStyle: TextStyle(
|
||||
fontSize: AppSpacing.lg,
|
||||
height: 1,
|
||||
@@ -152,7 +155,7 @@ class HomeComposerStack extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranscribingIndicator() {
|
||||
Widget _buildTranscribingIndicator(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
@@ -170,10 +173,10 @@ class HomeComposerStack extends StatelessWidget {
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
_buildWaveDots(),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
const Expanded(
|
||||
Expanded(
|
||||
child: Text(
|
||||
'语音识别中...',
|
||||
style: TextStyle(
|
||||
context.l10n.homeTranscribing,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.blue600,
|
||||
fontWeight: FontWeight.w600,
|
||||
+12
-6
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
|
||||
@@ -45,11 +46,16 @@ class HomeDateDivider extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final now = DateTime.now();
|
||||
final weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
const weekdays = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||
final weekday = weekdays[date.weekday - 1];
|
||||
final label = date.year == now.year
|
||||
? '${date.month}月${date.day}日 $weekday'
|
||||
: '${date.year}年${date.month}月${date.day}日 $weekday';
|
||||
? context.l10n.homeDateLabelNoYear(date.month, date.day, weekday)
|
||||
: context.l10n.homeDateLabelWithYear(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
weekday,
|
||||
);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
@@ -87,9 +93,9 @@ class HomeLoadMoreButton extends StatelessWidget {
|
||||
color: AppColors.slate400,
|
||||
trackColor: AppColors.slate200,
|
||||
)
|
||||
: const Text(
|
||||
'查看历史',
|
||||
style: TextStyle(fontSize: 12, color: AppColors.slate400),
|
||||
: Text(
|
||||
context.l10n.homeViewHistory,
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.slate400),
|
||||
),
|
||||
),
|
||||
);
|
||||
+4
-1
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
const _recordingCancelTopColor = AppColors.warningBackground;
|
||||
@@ -30,7 +31,9 @@ class HomeRecordingOverlay extends StatelessWidget {
|
||||
final labelColor = isCancel
|
||||
? _recordingCancelLabelColor
|
||||
: _recordingActiveLabelColor;
|
||||
final label = isCancel ? '松手取消' : '松手发送,上移取消';
|
||||
final label = isCancel
|
||||
? context.l10n.homeRecordingReleaseCancel
|
||||
: context.l10n.homeRecordingHintReleaseSend;
|
||||
|
||||
return IgnorePointer(
|
||||
child: Align(
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
|
||||
@@ -30,7 +31,7 @@ class HomeUnreadBadge extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
'有$count条新消息',
|
||||
context.l10n.homeUnreadMessages(count),
|
||||
style: const TextStyle(
|
||||
color: AppColors.white,
|
||||
fontSize: 12,
|
||||
Reference in New Issue
Block a user