feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_loading_indicator.dart';
|
||||
|
||||
@@ -7,16 +8,17 @@ class AppPullRefreshFeedback extends StatelessWidget {
|
||||
const AppPullRefreshFeedback({
|
||||
super.key,
|
||||
required this.visible,
|
||||
this.label = '正在刷新',
|
||||
this.label,
|
||||
this.margin = const EdgeInsets.only(top: AppSpacing.sm),
|
||||
});
|
||||
|
||||
final bool visible;
|
||||
final String label;
|
||||
final String? label;
|
||||
final EdgeInsetsGeometry margin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final resolvedLabel = label ?? context.l10n.commonRefreshing;
|
||||
return IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: visible ? 1 : 0,
|
||||
@@ -44,7 +46,7 @@ class AppPullRefreshFeedback extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
Text(
|
||||
label,
|
||||
resolvedLabel,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: AppColors.slate600,
|
||||
fontWeight: FontWeight.w500,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_button.dart';
|
||||
|
||||
@@ -69,7 +70,7 @@ Future<T?> showAppSelectionSheet<T>(
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: AppButton(
|
||||
text: '取消',
|
||||
text: context.l10n.commonCancel,
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.of(sheetContext).pop(),
|
||||
),
|
||||
|
||||
@@ -21,7 +21,7 @@ class AppBanner extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
if (!visible) return const SizedBox.shrink();
|
||||
|
||||
final config = ToastTypeConfig.fromType(type);
|
||||
final config = ToastTypeConfig.fromType(context, type);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/theme/design_tokens.dart';
|
||||
|
||||
enum MessageSender { user, ai }
|
||||
@@ -77,11 +78,11 @@ class ChatBubble extends StatelessWidget {
|
||||
|
||||
String dateStr;
|
||||
if (msgDate == today) {
|
||||
dateStr = '今天';
|
||||
dateStr = L10n.current.chatTimestampToday;
|
||||
} else if (msgDate == today.subtract(const Duration(days: 1))) {
|
||||
dateStr = '昨天';
|
||||
dateStr = L10n.current.chatTimestampYesterday;
|
||||
} else {
|
||||
dateStr = '${time.month}月${time.day}日';
|
||||
dateStr = L10n.current.chatTimestampMonthDay(time.month, time.day);
|
||||
}
|
||||
|
||||
final timeStr =
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_button.dart';
|
||||
|
||||
@@ -7,10 +8,13 @@ Future<bool> showConfirmSheet(
|
||||
BuildContext context, {
|
||||
required String title,
|
||||
required String message,
|
||||
String confirmText = '确认',
|
||||
String cancelText = '取消',
|
||||
String? confirmText,
|
||||
String? cancelText,
|
||||
bool isDestructive = false,
|
||||
}) async {
|
||||
final l10n = context.l10n;
|
||||
final resolvedConfirmText = confirmText ?? l10n.commonConfirm;
|
||||
final resolvedCancelText = cancelText ?? l10n.commonCancel;
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@@ -64,7 +68,7 @@ Future<bool> showConfirmSheet(
|
||||
borderRadius: BorderRadius.circular(AppRadius.full),
|
||||
),
|
||||
child: Text(
|
||||
confirmText,
|
||||
resolvedConfirmText,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -78,7 +82,7 @@ Future<bool> showConfirmSheet(
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: cancelText,
|
||||
text: resolvedCancelText,
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.of(sheetContext).pop(false),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_button.dart';
|
||||
|
||||
@@ -74,7 +75,7 @@ Future<bool> showDestructiveActionSheet(
|
||||
SizedBox(
|
||||
height: 52,
|
||||
child: AppButton(
|
||||
text: '取消',
|
||||
text: context.l10n.commonCancel,
|
||||
isOutlined: true,
|
||||
onPressed: () => Navigator.of(sheetContext).pop(false),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_button.dart';
|
||||
|
||||
@@ -30,7 +31,7 @@ class ErrorRetrySurface extends StatelessWidget {
|
||||
style: const TextStyle(color: AppColors.red500),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
AppButton(text: '重试', onPressed: onRetry),
|
||||
AppButton(text: context.l10n.commonRetry, onPressed: onRetry),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
import '../../core/l10n/l10n.dart';
|
||||
import '../../core/theme/design_tokens.dart';
|
||||
import 'app_loading_indicator.dart';
|
||||
|
||||
@@ -37,10 +38,10 @@ class MessageComposer extends StatelessWidget {
|
||||
required this.onHoldToSpeakCancel,
|
||||
required this.textInputChild,
|
||||
required this.recordingAnimation,
|
||||
this.holdToSpeakText = '按住说话',
|
||||
this.recordingText = '松开发送',
|
||||
this.transcribingText = '语音识别中...',
|
||||
this.recordingHintText = '松开发送,上滑取消',
|
||||
this.holdToSpeakText,
|
||||
this.recordingText,
|
||||
this.transcribingText,
|
||||
this.recordingHintText,
|
||||
this.showRecordingInlineFeedback = true,
|
||||
});
|
||||
|
||||
@@ -58,10 +59,10 @@ class MessageComposer extends StatelessWidget {
|
||||
final VoidCallback onHoldToSpeakCancel;
|
||||
final Widget textInputChild;
|
||||
final Widget recordingAnimation;
|
||||
final String holdToSpeakText;
|
||||
final String recordingText;
|
||||
final String transcribingText;
|
||||
final String recordingHintText;
|
||||
final String? holdToSpeakText;
|
||||
final String? recordingText;
|
||||
final String? transcribingText;
|
||||
final String? recordingHintText;
|
||||
final bool showRecordingInlineFeedback;
|
||||
|
||||
bool get _isHoldMode => mode == MessageComposerMode.holdToSpeak;
|
||||
@@ -193,12 +194,20 @@ class MessageComposer extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildHoldToSpeakContent() {
|
||||
final l10n = L10n.current;
|
||||
final resolvedRecordingText =
|
||||
recordingText ?? l10n.homeRecordingReleaseSend;
|
||||
final resolvedRecordingHintText =
|
||||
recordingHintText ?? l10n.homeRecordingHintReleaseSend;
|
||||
final resolvedTranscribingText = transcribingText ?? l10n.homeTranscribing;
|
||||
final resolvedHoldToSpeakText = holdToSpeakText ?? l10n.homeHoldToSpeakText;
|
||||
|
||||
if (_isRecording) {
|
||||
if (!showRecordingInlineFeedback) {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
recordingText,
|
||||
resolvedRecordingText,
|
||||
style: const TextStyle(color: AppColors.slate700),
|
||||
),
|
||||
);
|
||||
@@ -210,12 +219,12 @@ class MessageComposer extends StatelessWidget {
|
||||
recordingAnimation,
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
recordingText,
|
||||
resolvedRecordingText,
|
||||
style: const TextStyle(color: AppColors.slate700),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
recordingHintText,
|
||||
resolvedRecordingHintText,
|
||||
key: messageComposerRecordingHintKey,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
),
|
||||
@@ -227,7 +236,7 @@ class MessageComposer extends StatelessWidget {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
transcribingText,
|
||||
resolvedTranscribingText,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
),
|
||||
);
|
||||
@@ -236,7 +245,7 @@ class MessageComposer extends StatelessWidget {
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
holdToSpeakText,
|
||||
resolvedHoldToSpeakText,
|
||||
style: const TextStyle(color: AppColors.slate500),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -91,7 +91,7 @@ class _ToastWidgetState extends State<_ToastWidget>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final config = ToastTypeConfig.fromType(widget.type);
|
||||
final config = ToastTypeConfig.fromType(context, widget.type);
|
||||
|
||||
return Positioned(
|
||||
top: MediaQuery.of(context).padding.top + 12,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import 'toast_type.dart';
|
||||
import '../../../core/theme/design_tokens.dart';
|
||||
|
||||
@@ -19,38 +20,41 @@ class ToastTypeConfig {
|
||||
required this.icon,
|
||||
});
|
||||
|
||||
static ToastTypeConfig fromType(ToastType type) => switch (type) {
|
||||
ToastType.success => const ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackSuccessSurface,
|
||||
borderColor: AppColors.feedbackSuccessBorder,
|
||||
iconColor: AppColors.feedbackSuccessIcon,
|
||||
textColor: AppColors.feedbackSuccessText,
|
||||
label: '成功',
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
ToastType.warning => const ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackWarningSurface,
|
||||
borderColor: AppColors.feedbackWarningBorder,
|
||||
iconColor: AppColors.feedbackWarningIcon,
|
||||
textColor: AppColors.feedbackWarningText,
|
||||
label: '提醒',
|
||||
icon: Icons.warning_amber_rounded,
|
||||
),
|
||||
ToastType.error => const ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackErrorSurface,
|
||||
borderColor: AppColors.feedbackErrorBorder,
|
||||
iconColor: AppColors.feedbackErrorIcon,
|
||||
textColor: AppColors.feedbackErrorText,
|
||||
label: '错误',
|
||||
icon: Icons.error_outline,
|
||||
),
|
||||
ToastType.info => const ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackInfoSurface,
|
||||
borderColor: AppColors.feedbackInfoBorder,
|
||||
iconColor: AppColors.feedbackInfoIcon,
|
||||
textColor: AppColors.feedbackInfoText,
|
||||
label: '提示',
|
||||
icon: Icons.info_outline,
|
||||
),
|
||||
};
|
||||
static ToastTypeConfig fromType(BuildContext context, ToastType type) {
|
||||
final l10n = context.l10n;
|
||||
return switch (type) {
|
||||
ToastType.success => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackSuccessSurface,
|
||||
borderColor: AppColors.feedbackSuccessBorder,
|
||||
iconColor: AppColors.feedbackSuccessIcon,
|
||||
textColor: AppColors.feedbackSuccessText,
|
||||
label: l10n.toastLabelSuccess,
|
||||
icon: Icons.check_circle_outline,
|
||||
),
|
||||
ToastType.warning => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackWarningSurface,
|
||||
borderColor: AppColors.feedbackWarningBorder,
|
||||
iconColor: AppColors.feedbackWarningIcon,
|
||||
textColor: AppColors.feedbackWarningText,
|
||||
label: l10n.toastLabelWarning,
|
||||
icon: Icons.warning_amber_rounded,
|
||||
),
|
||||
ToastType.error => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackErrorSurface,
|
||||
borderColor: AppColors.feedbackErrorBorder,
|
||||
iconColor: AppColors.feedbackErrorIcon,
|
||||
textColor: AppColors.feedbackErrorText,
|
||||
label: l10n.toastLabelError,
|
||||
icon: Icons.error_outline,
|
||||
),
|
||||
ToastType.info => ToastTypeConfig(
|
||||
surfaceColor: AppColors.feedbackInfoSurface,
|
||||
borderColor: AppColors.feedbackInfoBorder,
|
||||
iconColor: AppColors.feedbackInfoIcon,
|
||||
textColor: AppColors.feedbackInfoText,
|
||||
label: l10n.toastLabelInfo,
|
||||
icon: Icons.info_outline,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user