feat(apps): 重构 UI 架构为 presentation 层并新增 l10n 国际化支持
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import 'package:social_app/core/api/i_api_client.dart';
|
||||
import 'package:social_app/core/network/i_api_client.dart';
|
||||
|
||||
import 'models/schedule_item_model.dart';
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ class ScheduleItemModel {
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
|
||||
static const int PERMISSION_VIEW = 1;
|
||||
static const int PERMISSION_INVITE = 2;
|
||||
static const int PERMISSION_EDIT = 4;
|
||||
static const int permissionView = 1;
|
||||
static const int permissionInvite = 2;
|
||||
static const int permissionEdit = 4;
|
||||
|
||||
bool get canEdit => isOwner || (permission & PERMISSION_EDIT) != 0;
|
||||
bool get canInvite => isOwner || (permission & PERMISSION_INVITE) != 0;
|
||||
bool get canEdit => isOwner || (permission & permissionEdit) != 0;
|
||||
bool get canInvite => isOwner || (permission & permissionInvite) != 0;
|
||||
bool get canDelete => isOwner;
|
||||
|
||||
ScheduleItemModel({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:social_app/core/api/i_api_client.dart';
|
||||
import 'package:social_app/core/network/i_api_client.dart';
|
||||
import 'package:social_app/core/cache/cache_invalidator.dart';
|
||||
import 'package:social_app/core/di/injection.dart';
|
||||
import 'package:social_app/app/di/injection.dart';
|
||||
|
||||
import '../calendar_api.dart';
|
||||
import '../models/schedule_item_model.dart';
|
||||
|
||||
+17
-9
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../home/ui/navigation/home_return_policy.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../home/presentation/navigation/home_return_policy.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../data/models/schedule_item_model.dart';
|
||||
@@ -36,7 +37,6 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
static const double _dayItemWidth = 44;
|
||||
static const double _dayItemGap = 12;
|
||||
static const double _minEventTapHeight = 32;
|
||||
static const List<String> _dayNames = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
|
||||
final DayEventLayoutEngine _layoutEngine = const DayEventLayoutEngine();
|
||||
final Map<int, Offset> _activePointers = {};
|
||||
@@ -224,7 +224,10 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final monthLabel = '${_selectedDate.year}年${_selectedDate.month}月';
|
||||
final monthLabel = context.l10n.calendarDayWeekMonthYearLabel(
|
||||
_selectedDate.year,
|
||||
_selectedDate.month,
|
||||
);
|
||||
final isNotToday = !isSameDay(_selectedDate, DateTime.now());
|
||||
|
||||
return SizedBox(
|
||||
@@ -298,10 +301,10 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.messageBtnBorder),
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'今天',
|
||||
style: TextStyle(
|
||||
context.l10n.calendarToday,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate700,
|
||||
@@ -448,7 +451,7 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_dayNames[date.weekday % 7],
|
||||
_weekdayLabel(date),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: isWeekend ? AppColors.slate400 : AppColors.slate600,
|
||||
@@ -481,6 +484,11 @@ class _CalendarDayWeekScreenState extends State<CalendarDayWeekScreen>
|
||||
);
|
||||
}
|
||||
|
||||
String _weekdayLabel(DateTime date) {
|
||||
const labels = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
return labels[date.weekday % 7];
|
||||
}
|
||||
|
||||
Widget _buildTimelineBoard() {
|
||||
final now = DateTime.now();
|
||||
final showCurrent = shouldShowCurrentMarker(_selectedDate, now);
|
||||
+80
-46
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import '../../../../core/notifications/local_notification_service.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../../features/notification/data/services/local_notification_service.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
@@ -72,7 +73,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const BackTitlePageHeader(title: '日程详情'),
|
||||
BackTitlePageHeader(title: context.l10n.calendarDetailTitle),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
@@ -83,8 +84,8 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
'未找到该日程',
|
||||
Text(
|
||||
context.l10n.calendarDetailNotFoundTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -92,8 +93,8 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
const Text(
|
||||
'可能已被删除,或你没有访问权限。',
|
||||
Text(
|
||||
context.l10n.calendarDetailNotFoundDesc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
@@ -156,7 +157,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
|
||||
Widget _buildHeader(ScheduleItemModel event) {
|
||||
return BackTitlePageHeader(
|
||||
title: '日程详情',
|
||||
title: context.l10n.calendarDetailTitle,
|
||||
onBack: () => context.pop(),
|
||||
trailing: _buildHeaderActions(event),
|
||||
);
|
||||
@@ -168,7 +169,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.edit,
|
||||
label: '编辑',
|
||||
label: context.l10n.commonEdit,
|
||||
icon: LucideIcons.pencil,
|
||||
),
|
||||
);
|
||||
@@ -177,7 +178,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.delete,
|
||||
label: '删除',
|
||||
label: context.l10n.commonDelete,
|
||||
icon: LucideIcons.trash2,
|
||||
isDestructive: true,
|
||||
),
|
||||
@@ -187,7 +188,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.share,
|
||||
label: '分享',
|
||||
label: context.l10n.commonShare,
|
||||
icon: LucideIcons.share2,
|
||||
),
|
||||
);
|
||||
@@ -197,7 +198,7 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
items.add(
|
||||
DetailHeaderActionItem<_CalendarHeaderAction>(
|
||||
value: _CalendarHeaderAction.archive,
|
||||
label: '归档',
|
||||
label: context.l10n.commonArchive,
|
||||
icon: LucideIcons.archive,
|
||||
enabled: !isExpired,
|
||||
),
|
||||
@@ -313,8 +314,8 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'时间安排',
|
||||
Text(
|
||||
context.l10n.calendarDetailTimeArrangement,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -341,8 +342,12 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
|
||||
Widget _buildMetaSurface(ScheduleItemModel event) {
|
||||
final startAt = event.startAt;
|
||||
final dateStr =
|
||||
'${startAt.year}年${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)}';
|
||||
final dateStr = context.l10n.calendarDetailDateLabel(
|
||||
startAt.year,
|
||||
startAt.month,
|
||||
startAt.day,
|
||||
_getWeekday(startAt.weekday),
|
||||
);
|
||||
final color = resolveEventColor(
|
||||
status: event.status,
|
||||
colorHex: event.metadata?.color,
|
||||
@@ -358,8 +363,8 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'基础信息',
|
||||
Text(
|
||||
context.l10n.calendarDetailBasicInfo,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -367,21 +372,21 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('日期', dateStr),
|
||||
_buildDetailRow(context.l10n.calendarDetailDate, dateStr),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
'提醒',
|
||||
context.l10n.calendarDetailReminder,
|
||||
_formatReminderText(event.metadata?.reminderMinutes),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(
|
||||
SizedBox(
|
||||
width: AppSpacing.xxl * 3,
|
||||
child: Text(
|
||||
'颜色',
|
||||
style: TextStyle(
|
||||
context.l10n.calendarDetailColor,
|
||||
style: const TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.slate500,
|
||||
@@ -421,8 +426,8 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'补充信息',
|
||||
Text(
|
||||
context.l10n.calendarDetailExtraInfo,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -431,16 +436,22 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
),
|
||||
if (event.metadata?.location?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('地点', event.metadata!.location!.trim()),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailLocation,
|
||||
event.metadata!.location!.trim(),
|
||||
),
|
||||
],
|
||||
if (event.description?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow('描述', event.description!.trim()),
|
||||
_buildDetailRow(
|
||||
context.l10n.calendarDetailDescription,
|
||||
event.description!.trim(),
|
||||
),
|
||||
],
|
||||
if (event.metadata?.notes?.trim().isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDetailRow(
|
||||
'备注',
|
||||
context.l10n.calendarDetailNotes,
|
||||
event.metadata!.notes!.trim(),
|
||||
multiline: true,
|
||||
),
|
||||
@@ -452,16 +463,25 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
|
||||
String _formatReminderText(int? reminderMinutes) {
|
||||
if (reminderMinutes == null) {
|
||||
return '无';
|
||||
return context.l10n.calendarDetailReminderNone;
|
||||
}
|
||||
if (reminderMinutes == 0) {
|
||||
return '准时提醒';
|
||||
return context.l10n.calendarDetailReminderOnTime;
|
||||
}
|
||||
return '开始前$reminderMinutes分钟';
|
||||
return context.l10n.calendarDetailReminderBeforeMinutes(reminderMinutes);
|
||||
}
|
||||
|
||||
String _getWeekday(int weekday) {
|
||||
const weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日'];
|
||||
final l10n = context.l10n;
|
||||
final weekdays = [
|
||||
l10n.calendarWeekdayMon,
|
||||
l10n.calendarWeekdayTue,
|
||||
l10n.calendarWeekdayWed,
|
||||
l10n.calendarWeekdayThu,
|
||||
l10n.calendarWeekdayFri,
|
||||
l10n.calendarWeekdaySat,
|
||||
l10n.calendarWeekdaySun,
|
||||
];
|
||||
return weekdays[weekday - 1];
|
||||
}
|
||||
|
||||
@@ -472,9 +492,9 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
Future<void> _showDeleteConfirmation() async {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: '删除日程',
|
||||
message: '确定要删除这个日程吗?',
|
||||
confirmText: '确认删除',
|
||||
title: context.l10n.calendarDetailDeleteTitle,
|
||||
message: context.l10n.calendarDetailDeleteMessage,
|
||||
confirmText: context.l10n.calendarDetailDeleteConfirm,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@@ -492,9 +512,9 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
Future<void> _archiveEvent() async {
|
||||
final confirmed = await showDestructiveActionSheet(
|
||||
context,
|
||||
title: '归档日程',
|
||||
message: '归档后此日程将标记为过期,确定要归档吗?',
|
||||
confirmText: '确认归档',
|
||||
title: context.l10n.calendarDetailArchiveTitle,
|
||||
message: context.l10n.calendarDetailArchiveMessage,
|
||||
confirmText: context.l10n.calendarDetailArchiveConfirm,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
@@ -506,14 +526,22 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Toast.show(context, '归档失败', type: ToastType.error);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.calendarDetailArchiveFailed,
|
||||
type: ToastType.error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatRangeLabel(DateTime startAt, DateTime? endAt) {
|
||||
final startLabel =
|
||||
'${startAt.month}月${startAt.day}日 ${_getWeekday(startAt.weekday)} ${_formatTime(startAt)}';
|
||||
final startLabel = context.l10n.calendarDetailDateTimeShort(
|
||||
startAt.month,
|
||||
startAt.day,
|
||||
_getWeekday(startAt.weekday),
|
||||
_formatTime(startAt),
|
||||
);
|
||||
if (endAt == null) {
|
||||
return startLabel;
|
||||
}
|
||||
@@ -524,9 +552,13 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
if (isSameDay) {
|
||||
return '$startLabel - ${_formatTime(endAt)}';
|
||||
}
|
||||
final endLabel =
|
||||
'${endAt.month}月${endAt.day}日 ${_getWeekday(endAt.weekday)} ${_formatTime(endAt)}';
|
||||
return '开始: $startLabel\n结束: $endLabel';
|
||||
final endLabel = context.l10n.calendarDetailDateTimeShort(
|
||||
endAt.month,
|
||||
endAt.day,
|
||||
_getWeekday(endAt.weekday),
|
||||
_formatTime(endAt),
|
||||
);
|
||||
return context.l10n.calendarDetailRangeWithStartEnd(startLabel, endLabel);
|
||||
}
|
||||
|
||||
Widget _buildStatusBadge(ScheduleStatus status) {
|
||||
@@ -548,7 +580,9 @@ class _CalendarEventDetailScreenState extends State<CalendarEventDetailScreen> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
isArchived ? '已过期' : '启用',
|
||||
isArchived
|
||||
? context.l10n.calendarDetailStatusExpired
|
||||
: context.l10n.settingsJobStatusEnabled,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
+8
-3
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../widgets/create_event_sheet.dart';
|
||||
@@ -50,8 +51,12 @@ class _CalendarEventEditScreenState extends State<CalendarEventEditScreen> {
|
||||
}
|
||||
|
||||
if (_event == null) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: Text('日程不存在或无权限'))),
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Text(context.l10n.calendarEventNoAccessOrMissing),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+9
-4
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/l10n/l10n.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/back_title_page_header.dart';
|
||||
@@ -52,8 +53,12 @@ class _CalendarEventShareScreenState extends State<CalendarEventShareScreen> {
|
||||
|
||||
final event = _event;
|
||||
if (event == null) {
|
||||
return const Scaffold(
|
||||
body: SafeArea(child: Center(child: Text('日程不存在或无权限'))),
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: Center(
|
||||
child: Text(context.l10n.calendarEventNoAccessOrMissing),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,7 +68,7 @@ class _CalendarEventShareScreenState extends State<CalendarEventShareScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const BackTitlePageHeader(title: '分享日程'),
|
||||
BackTitlePageHeader(title: context.l10n.calendarShareTitle),
|
||||
Expanded(
|
||||
child: CalendarShareDialog(
|
||||
eventId: event.id,
|
||||
+31
-11
@@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/router/app_routes.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../app/router/app_routes.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_pressable.dart';
|
||||
import '../../../home/ui/navigation/home_return_policy.dart';
|
||||
import '../../../home/presentation/navigation/home_return_policy.dart';
|
||||
import '../calendar_state_manager.dart';
|
||||
import '../calendar_time_utils.dart';
|
||||
import '../utils/event_color_resolver.dart';
|
||||
@@ -107,6 +108,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final l10n = context.l10n;
|
||||
final today = DateTime.now();
|
||||
final isNotToday = !isSameDay(_selectedDate, today);
|
||||
|
||||
@@ -132,7 +134,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
switchInCurve: Curves.easeOut,
|
||||
switchOutCurve: Curves.easeOut,
|
||||
child: Text(
|
||||
'${_currentMonth.month}月',
|
||||
l10n.calendarMonthHeader(_currentMonth.month),
|
||||
key: ValueKey(_currentMonth.month),
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
@@ -163,9 +165,9 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
borderRadius: BorderRadius.circular(AppRadius.xl),
|
||||
border: Border.all(color: AppColors.messageBtnBorder),
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: Text(
|
||||
'今天',
|
||||
l10n.calendarMonthToday,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -234,7 +236,16 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
}
|
||||
|
||||
Widget _buildWeekdayHeader() {
|
||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六'];
|
||||
final l10n = context.l10n;
|
||||
final List<String> weekdays = [
|
||||
l10n.calendarMonthWeekdaySunShort,
|
||||
l10n.calendarMonthWeekdayMonShort,
|
||||
l10n.calendarMonthWeekdayTueShort,
|
||||
l10n.calendarMonthWeekdayWedShort,
|
||||
l10n.calendarMonthWeekdayThuShort,
|
||||
l10n.calendarMonthWeekdayFriShort,
|
||||
l10n.calendarMonthWeekdaySatShort,
|
||||
];
|
||||
return SizedBox(
|
||||
height: 40,
|
||||
child: Padding(
|
||||
@@ -452,6 +463,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
}
|
||||
|
||||
void _showMonthPicker() {
|
||||
final l10n = context.l10n;
|
||||
var selectedYear = _currentMonth.year;
|
||||
var selectedMonth = _currentMonth.month;
|
||||
|
||||
@@ -472,7 +484,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
children: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('取消'),
|
||||
child: Text(l10n.commonCancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
@@ -492,7 +504,7 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
_calendarManager.setSelectedDate(_selectedDate);
|
||||
_loadMonthEvents();
|
||||
},
|
||||
child: const Text('确定'),
|
||||
child: Text(l10n.commonConfirm),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -512,7 +524,11 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
});
|
||||
},
|
||||
children: List.generate(20, (index) {
|
||||
return Center(child: Text('${2020 + index}年'));
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.calendarMonthYearLabel(2020 + index),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
@@ -528,7 +544,11 @@ class _CalendarMonthScreenState extends State<CalendarMonthScreen>
|
||||
});
|
||||
},
|
||||
children: List.generate(12, (index) {
|
||||
return Center(child: Text('${index + 1}月'));
|
||||
return Center(
|
||||
child: Text(
|
||||
l10n.calendarMonthHeader(index + 1),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
+40
-16
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart' hide BackButton;
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../../../shared/widgets/toast/toast.dart';
|
||||
@@ -47,7 +48,7 @@ class CalendarShareDialog extends StatefulWidget {
|
||||
|
||||
class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
final _phoneController = TextEditingController();
|
||||
bool _permissionView = true;
|
||||
final bool _permissionView = true;
|
||||
bool _permissionEdit = false;
|
||||
bool _permissionInvite = false;
|
||||
bool _isLoading = false;
|
||||
@@ -59,9 +60,14 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
}
|
||||
|
||||
Future<void> _handleShare() async {
|
||||
final l10n = context.l10n;
|
||||
final phone = _phoneController.text.trim();
|
||||
if (phone.isEmpty) {
|
||||
Toast.show(context, '请输入手机号', type: ToastType.error);
|
||||
Toast.show(
|
||||
context,
|
||||
l10n.calendarSharePhoneRequired,
|
||||
type: ToastType.error,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -77,12 +83,20 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
invite: _permissionInvite,
|
||||
);
|
||||
if (mounted) {
|
||||
Toast.show(context, '邀请已发送', type: ToastType.success);
|
||||
Toast.show(
|
||||
context,
|
||||
l10n.calendarShareInviteSent,
|
||||
type: ToastType.success,
|
||||
);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Toast.show(context, '发送邀请失败', type: ToastType.error);
|
||||
Toast.show(
|
||||
context,
|
||||
l10n.calendarShareInviteFailed,
|
||||
type: ToastType.error,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
@@ -93,6 +107,8 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
@@ -113,8 +129,8 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
const Text(
|
||||
'分享日历',
|
||||
Text(
|
||||
l10n.calendarShareTitle,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600),
|
||||
),
|
||||
IconButton(
|
||||
@@ -129,8 +145,8 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
TextField(
|
||||
controller: _phoneController,
|
||||
decoration: InputDecoration(
|
||||
labelText: '手机号',
|
||||
hintText: '输入对方的 +86 手机号',
|
||||
labelText: l10n.calendarSharePhoneLabel,
|
||||
hintText: l10n.calendarSharePhoneHint,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||
),
|
||||
@@ -138,20 +154,28 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
const Text('权限设置', style: TextStyle(fontWeight: FontWeight.w600)),
|
||||
Text(
|
||||
l10n.calendarSharePermissionTitle,
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
_buildPermissionSwitch('查看', '可以查看此日历事件(必选)', true, null),
|
||||
_buildPermissionSwitch(
|
||||
'编辑',
|
||||
'可以编辑此日历事件',
|
||||
l10n.calendarSharePermissionView,
|
||||
l10n.calendarSharePermissionViewDesc,
|
||||
true,
|
||||
null,
|
||||
),
|
||||
_buildPermissionSwitch(
|
||||
l10n.calendarSharePermissionEdit,
|
||||
l10n.calendarSharePermissionEditDesc,
|
||||
_permissionEdit,
|
||||
widget.canEdit
|
||||
? (v) => setState(() => _permissionEdit = v)
|
||||
: null,
|
||||
),
|
||||
_buildPermissionSwitch(
|
||||
'邀请',
|
||||
'可以邀请其他人',
|
||||
l10n.calendarSharePermissionInvite,
|
||||
l10n.calendarSharePermissionInviteDesc,
|
||||
_permissionInvite,
|
||||
widget.canInvite
|
||||
? (v) => setState(() => _permissionInvite = v)
|
||||
@@ -159,7 +183,7 @@ class _CalendarShareDialogState extends State<CalendarShareDialog> {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
AppButton(
|
||||
text: '发送邀请',
|
||||
text: l10n.calendarShareSendInvite,
|
||||
onPressed: _isLoading ? null : _handleShare,
|
||||
isLoading: _isLoading,
|
||||
),
|
||||
+95
-54
@@ -1,7 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
import '../../../../core/di/injection.dart';
|
||||
import '../../../../core/notifications/local_notification_service.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
import '../../../../app/di/injection.dart';
|
||||
import '../../../../features/notification/data/services/local_notification_service.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_loading_indicator.dart';
|
||||
import '../../../../shared/widgets/app_selection_sheet.dart';
|
||||
@@ -204,7 +205,9 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
|
||||
Widget _buildPageHeader() {
|
||||
return BackTitlePageHeader(
|
||||
title: _isEditing ? '编辑日程' : '新建日程',
|
||||
title: _isEditing
|
||||
? context.l10n.calendarCreateEditTitle
|
||||
: context.l10n.calendarCreateNewTitle,
|
||||
onBack: () => Navigator.of(context).pop(),
|
||||
trailing: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _titleController,
|
||||
@@ -226,7 +229,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
trackColor: AppColors.blue200,
|
||||
)
|
||||
: Text(
|
||||
'保存',
|
||||
context.l10n.commonSave,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -266,7 +269,9 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_isEditing ? '编辑日程' : '新建日程',
|
||||
_isEditing
|
||||
? context.l10n.calendarCreateEditTitle
|
||||
: context.l10n.calendarCreateNewTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -295,7 +300,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
trackColor: AppColors.blue200,
|
||||
)
|
||||
: Text(
|
||||
'保存',
|
||||
context.l10n.commonSave,
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -323,9 +328,9 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
labelColor: AppColors.blue600,
|
||||
unselectedLabelColor: AppColors.slate600,
|
||||
indicatorColor: AppColors.blue600,
|
||||
tabs: const [
|
||||
Tab(text: '基础'),
|
||||
Tab(text: '进阶'),
|
||||
tabs: [
|
||||
Tab(text: context.l10n.calendarCreateTabBasic),
|
||||
Tab(text: context.l10n.calendarCreateTabAdvanced),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -345,38 +350,47 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextField('标题', _titleController, '请输入日程标题'),
|
||||
const SizedBox(height: 20),
|
||||
_buildDateTimePicker('开始', _startDate, _startTime, (date, time) {
|
||||
setState(() {
|
||||
_startDate = date;
|
||||
_startTime = time;
|
||||
if (_endDate != null && _endTime != null) {
|
||||
final endDateTime = DateTime(
|
||||
_endDate!.year,
|
||||
_endDate!.month,
|
||||
_endDate!.day,
|
||||
_endTime!.hour,
|
||||
_endTime!.minute,
|
||||
);
|
||||
final startDateTime = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
if (endDateTime.isBefore(startDateTime) ||
|
||||
endDateTime.isAtSameMomentAs(startDateTime)) {
|
||||
_endDate = date;
|
||||
_endTime = time.add(const Duration(hours: 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
}),
|
||||
_buildTextField(
|
||||
context.l10n.calendarCreateFieldTitle,
|
||||
_titleController,
|
||||
context.l10n.calendarCreateFieldTitleHint,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildDateTimePicker(
|
||||
'结束',
|
||||
context.l10n.calendarCreateFieldStart,
|
||||
_startDate,
|
||||
_startTime,
|
||||
(date, time) {
|
||||
setState(() {
|
||||
_startDate = date;
|
||||
_startTime = time;
|
||||
if (_endDate != null && _endTime != null) {
|
||||
final endDateTime = DateTime(
|
||||
_endDate!.year,
|
||||
_endDate!.month,
|
||||
_endDate!.day,
|
||||
_endTime!.hour,
|
||||
_endTime!.minute,
|
||||
);
|
||||
final startDateTime = DateTime(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour,
|
||||
time.minute,
|
||||
);
|
||||
if (endDateTime.isBefore(startDateTime) ||
|
||||
endDateTime.isAtSameMomentAs(startDateTime)) {
|
||||
_endDate = date;
|
||||
_endTime = time.add(const Duration(hours: 1));
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildDateTimePicker(
|
||||
context.l10n.calendarCreateFieldEnd,
|
||||
_endDate ?? _startDate,
|
||||
_endTime ?? _startTime,
|
||||
(date, time) {
|
||||
@@ -426,15 +440,28 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTextField('描述', _descriptionController, '请输入描述'),
|
||||
_buildTextField(
|
||||
context.l10n.calendarCreateFieldDescription,
|
||||
_descriptionController,
|
||||
context.l10n.calendarCreateFieldDescriptionHint,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField('地点', _locationController, '请输入地点'),
|
||||
_buildTextField(
|
||||
context.l10n.calendarCreateFieldLocation,
|
||||
_locationController,
|
||||
context.l10n.calendarCreateFieldLocationHint,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildReminderPicker(),
|
||||
const SizedBox(height: 20),
|
||||
_buildColorPicker(),
|
||||
const SizedBox(height: 20),
|
||||
_buildTextField('备注', _notesController, '请输入备注', maxLines: 3),
|
||||
_buildTextField(
|
||||
context.l10n.calendarDetailNotes,
|
||||
_notesController,
|
||||
context.l10n.calendarCreateFieldNotesHint,
|
||||
maxLines: 3,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -468,7 +495,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label + (isOptional ? '(可选)' : ''),
|
||||
isOptional ? context.l10n.calendarCreateOptionalField(label) : label,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -525,7 +552,13 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
}
|
||||
|
||||
String _formatDateTimeLabel(DateTime date, DateTime time) {
|
||||
return '${date.year}年${date.month}月${date.day}日 ${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';
|
||||
return context.l10n.calendarCreateDateTimeLabel(
|
||||
date.year,
|
||||
date.month,
|
||||
date.day,
|
||||
time.hour.toString().padLeft(2, '0'),
|
||||
time.minute.toString().padLeft(2, '0'),
|
||||
);
|
||||
}
|
||||
|
||||
Future<(DateTime, DateTime)?> _pickDateTime(
|
||||
@@ -550,8 +583,8 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'颜色',
|
||||
Text(
|
||||
context.l10n.calendarDetailColor,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -591,19 +624,19 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
Widget _buildReminderPicker() {
|
||||
String labelOf(int? value) {
|
||||
if (value == null) {
|
||||
return '无提醒';
|
||||
return context.l10n.calendarCreateReminderNone;
|
||||
}
|
||||
if (value == 0) {
|
||||
return '准时提醒';
|
||||
return context.l10n.calendarDetailReminderOnTime;
|
||||
}
|
||||
return '开始前$value分钟';
|
||||
return context.l10n.calendarDetailReminderBeforeMinutes(value);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'提醒时间',
|
||||
Text(
|
||||
context.l10n.calendarCreateReminderTime,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -616,7 +649,7 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
final options = _buildReminderOptions();
|
||||
final selected = await showAppSelectionSheet<int?>(
|
||||
context,
|
||||
title: '选择提醒时间',
|
||||
title: context.l10n.calendarCreatePickReminderTime,
|
||||
items: options
|
||||
.map((v) => AppSelectionItem(value: v, label: labelOf(v)))
|
||||
.toList(),
|
||||
@@ -745,7 +778,11 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
await notificationService.upsertEventReminder(saved);
|
||||
} catch (_) {
|
||||
if (mounted) {
|
||||
Toast.show(context, '提醒创建失败,请检查通知权限', type: ToastType.warning);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.calendarCreateReminderPermissionFailed,
|
||||
type: ToastType.warning,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -755,7 +792,11 @@ class _CreateEventSheetState extends State<CreateEventSheet>
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
Toast.show(context, '保存失败: $e', type: ToastType.error);
|
||||
Toast.show(
|
||||
context,
|
||||
context.l10n.todoSaveFailed('$e'),
|
||||
type: ToastType.error,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
+21
-16
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:social_app/core/l10n/l10n.dart';
|
||||
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
|
||||
@@ -130,6 +131,8 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Container(
|
||||
height: 420,
|
||||
decoration: const BoxDecoration(
|
||||
@@ -148,7 +151,7 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPickerLabel('日期'),
|
||||
_buildPickerLabel(l10n.calendarDateTimePickerDateLabel),
|
||||
Expanded(
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
@@ -165,8 +168,8 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'年',
|
||||
Text(
|
||||
l10n.calendarDateTimePickerYearUnit,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
@@ -186,8 +189,8 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
});
|
||||
}, (v) => '$v'),
|
||||
),
|
||||
const Text(
|
||||
'月',
|
||||
Text(
|
||||
l10n.calendarDateTimePickerMonthUnit,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
@@ -201,8 +204,8 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
(v) => '$v',
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'日',
|
||||
Text(
|
||||
l10n.calendarDateTimePickerDayUnit,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.slate600,
|
||||
@@ -220,7 +223,7 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPickerLabel('时间'),
|
||||
_buildPickerLabel(l10n.calendarDateTimePickerTimeLabel),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -285,6 +288,8 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
}
|
||||
|
||||
Widget _buildHeader() {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
@@ -297,13 +302,13 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: () => Navigator.pop(context),
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(fontSize: 17, color: AppColors.slate600),
|
||||
child: Text(
|
||||
l10n.commonCancel,
|
||||
style: const TextStyle(fontSize: 17, color: AppColors.slate600),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'选择时间',
|
||||
Text(
|
||||
l10n.calendarDateTimePickerTitle,
|
||||
style: TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -317,9 +322,9 @@ class _DateTimePickerSheetState extends State<DateTimePickerSheet> {
|
||||
DateTime(2000, 1, 1, _selectedHour, _selectedMinute),
|
||||
));
|
||||
},
|
||||
child: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
child: Text(
|
||||
l10n.commonConfirm,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.blue600,
|
||||
@@ -1,23 +0,0 @@
|
||||
enum ReminderAction {
|
||||
archive('archive'),
|
||||
snooze10m('snooze10m');
|
||||
|
||||
const ReminderAction(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
static ReminderAction fromValue(String raw) {
|
||||
switch (raw) {
|
||||
case 'archive':
|
||||
case 'cancel':
|
||||
case 'auto_archive':
|
||||
return ReminderAction.archive;
|
||||
case 'snooze10m':
|
||||
case 'snooze_10m':
|
||||
case 'timeout_30s':
|
||||
return ReminderAction.snooze10m;
|
||||
default:
|
||||
throw ArgumentError.value(raw, 'raw', 'Unsupported reminder action');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
class ReminderPayload {
|
||||
final String eventId;
|
||||
final String title;
|
||||
final DateTime startAt;
|
||||
final DateTime? endAt;
|
||||
final String timezone;
|
||||
final String? location;
|
||||
final String? notes;
|
||||
final String? color;
|
||||
final ReminderPayloadMode mode;
|
||||
final List<String> aggregateIds;
|
||||
final int? fireTimeBucket;
|
||||
final int version;
|
||||
|
||||
const ReminderPayload({
|
||||
required this.eventId,
|
||||
required this.title,
|
||||
required this.startAt,
|
||||
required this.timezone,
|
||||
this.endAt,
|
||||
this.location,
|
||||
this.notes,
|
||||
this.color,
|
||||
this.mode = ReminderPayloadMode.single,
|
||||
this.aggregateIds = const [],
|
||||
this.fireTimeBucket,
|
||||
this.version = 1,
|
||||
});
|
||||
|
||||
ReminderPayload copyWith({
|
||||
String? eventId,
|
||||
String? title,
|
||||
DateTime? startAt,
|
||||
DateTime? endAt,
|
||||
String? timezone,
|
||||
String? location,
|
||||
String? notes,
|
||||
String? color,
|
||||
ReminderPayloadMode? mode,
|
||||
List<String>? aggregateIds,
|
||||
int? fireTimeBucket,
|
||||
int? version,
|
||||
}) {
|
||||
return ReminderPayload(
|
||||
eventId: eventId ?? this.eventId,
|
||||
title: title ?? this.title,
|
||||
startAt: startAt ?? this.startAt,
|
||||
endAt: endAt ?? this.endAt,
|
||||
timezone: timezone ?? this.timezone,
|
||||
location: location ?? this.location,
|
||||
notes: notes ?? this.notes,
|
||||
color: color ?? this.color,
|
||||
mode: mode ?? this.mode,
|
||||
aggregateIds: aggregateIds ?? this.aggregateIds,
|
||||
fireTimeBucket: fireTimeBucket ?? this.fireTimeBucket,
|
||||
version: version ?? this.version,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'eventId': eventId,
|
||||
'title': title,
|
||||
'startAt': startAt.toIso8601String(),
|
||||
'endAt': endAt?.toIso8601String(),
|
||||
'timezone': timezone,
|
||||
'location': location,
|
||||
'notes': notes,
|
||||
'color': color,
|
||||
'mode': mode.value,
|
||||
'aggregateIds': aggregateIds,
|
||||
'fireTimeBucket': fireTimeBucket,
|
||||
'version': version,
|
||||
};
|
||||
}
|
||||
|
||||
factory ReminderPayload.fromJson(Map<String, dynamic> json) {
|
||||
final eventId = (json['eventId'] as String?) ?? '';
|
||||
if (eventId.isEmpty) {
|
||||
throw const FormatException('eventId is required');
|
||||
}
|
||||
|
||||
final startAtRaw = json['startAt'] as String?;
|
||||
if (startAtRaw == null || startAtRaw.isEmpty) {
|
||||
throw const FormatException('startAt is required');
|
||||
}
|
||||
final parsedStartAt = DateTime.parse(startAtRaw);
|
||||
|
||||
final mode = ReminderPayloadMode.fromValue(
|
||||
(json['mode'] as String?) ?? 'single',
|
||||
);
|
||||
final aggregateIds = (json['aggregateIds'] as List<dynamic>? ?? const [])
|
||||
.map((item) => item.toString())
|
||||
.toList();
|
||||
if (mode == ReminderPayloadMode.aggregate && aggregateIds.length < 2) {
|
||||
throw const FormatException('aggregateIds must contain at least 2 items');
|
||||
}
|
||||
|
||||
return ReminderPayload(
|
||||
eventId: eventId,
|
||||
title: (json['title'] as String?) ?? '',
|
||||
startAt: parsedStartAt,
|
||||
endAt: json['endAt'] != null
|
||||
? DateTime.parse(json['endAt'] as String)
|
||||
: null,
|
||||
timezone: (json['timezone'] as String?) ?? 'UTC',
|
||||
location: json['location'] as String?,
|
||||
notes: json['notes'] as String?,
|
||||
color: json['color'] as String?,
|
||||
mode: mode,
|
||||
aggregateIds: aggregateIds,
|
||||
fireTimeBucket: json['fireTimeBucket'] as int?,
|
||||
version: (json['version'] as int?) ?? 1,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return other is ReminderPayload &&
|
||||
other.eventId == eventId &&
|
||||
other.title == title &&
|
||||
other.startAt == startAt &&
|
||||
other.endAt == endAt &&
|
||||
other.timezone == timezone &&
|
||||
other.location == location &&
|
||||
other.notes == notes &&
|
||||
other.color == color &&
|
||||
other.mode == mode &&
|
||||
_listEquals(other.aggregateIds, aggregateIds) &&
|
||||
other.fireTimeBucket == fireTimeBucket &&
|
||||
other.version == version;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
eventId,
|
||||
title,
|
||||
startAt,
|
||||
endAt,
|
||||
timezone,
|
||||
location,
|
||||
notes,
|
||||
color,
|
||||
mode,
|
||||
Object.hashAll(aggregateIds),
|
||||
fireTimeBucket,
|
||||
version,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ReminderPayloadMode {
|
||||
single('single'),
|
||||
aggregate('aggregate');
|
||||
|
||||
const ReminderPayloadMode(this.value);
|
||||
|
||||
final String value;
|
||||
|
||||
static ReminderPayloadMode fromValue(String raw) {
|
||||
return ReminderPayloadMode.values.firstWhere(
|
||||
(item) => item.value == raw,
|
||||
orElse: () => ReminderPayloadMode.single,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _listEquals(List<String> left, List<String> right) {
|
||||
if (left.length != right.length) {
|
||||
return false;
|
||||
}
|
||||
for (var i = 0; i < left.length; i++) {
|
||||
if (left[i] != right[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import '../data/services/calendar_service.dart';
|
||||
import '../../../core/notifications/local_notification_service.dart';
|
||||
import 'models/reminder_action.dart';
|
||||
import 'models/reminder_payload.dart';
|
||||
|
||||
class ReminderActionExecutor {
|
||||
final CalendarService _calendarService;
|
||||
final LocalNotificationService _notificationService;
|
||||
|
||||
ReminderActionExecutor({
|
||||
required CalendarService calendarService,
|
||||
required LocalNotificationService notificationService,
|
||||
}) : _calendarService = calendarService,
|
||||
_notificationService = notificationService;
|
||||
|
||||
Future<void> handleAction({
|
||||
required ReminderAction action,
|
||||
required ReminderPayload payload,
|
||||
}) async {
|
||||
final ids = payload.mode == ReminderPayloadMode.aggregate
|
||||
? (payload.aggregateIds.isNotEmpty
|
||||
? payload.aggregateIds
|
||||
: <String>[payload.eventId])
|
||||
: <String>[payload.eventId];
|
||||
|
||||
if (action == ReminderAction.archive) {
|
||||
for (final id in ids) {
|
||||
await _notificationService.cancelEventReminder(id);
|
||||
await _archiveEvent(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (action == ReminderAction.snooze10m) {
|
||||
for (final id in ids) {
|
||||
await _snoozeEvent(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _snoozeEvent(String eventId) async {
|
||||
final event = await _calendarService.getEventById(eventId);
|
||||
if (event == null) {
|
||||
return;
|
||||
}
|
||||
final now = DateTime.now();
|
||||
final endAt = event.endAt;
|
||||
if (endAt != null && !now.isBefore(endAt)) {
|
||||
await _notificationService.cancelEventReminder(eventId);
|
||||
await _archiveEvent(eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
final nextAt = now.add(const Duration(minutes: 10));
|
||||
if (endAt != null && !nextAt.isBefore(endAt)) {
|
||||
await _notificationService.cancelEventReminder(eventId);
|
||||
await _archiveEvent(eventId);
|
||||
return;
|
||||
}
|
||||
|
||||
await _notificationService.scheduleReminderAt(event, nextAt);
|
||||
}
|
||||
|
||||
Future<void> _archiveEvent(String eventId) async {
|
||||
await _calendarService.archiveEvent(eventId);
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import 'models/reminder_payload.dart';
|
||||
|
||||
class ReminderQueueManager {
|
||||
ReminderPayload? _currentPayload;
|
||||
final List<ReminderPayload> _pending = [];
|
||||
|
||||
void enqueueFromClick(ReminderPayload payload) {
|
||||
_currentPayload = payload;
|
||||
}
|
||||
|
||||
void enqueuePending(List<ReminderPayload> payloads) {
|
||||
payloads.sort((a, b) => a.startAt.compareTo(b.startAt));
|
||||
_pending.addAll(payloads);
|
||||
}
|
||||
|
||||
ReminderPayload? get currentPayload => _currentPayload;
|
||||
|
||||
bool get isEmpty => _currentPayload == null && _pending.isEmpty;
|
||||
|
||||
void dequeueCurrent() {
|
||||
_currentPayload = null;
|
||||
if (_pending.isNotEmpty) {
|
||||
_currentPayload = _pending.removeAt(0);
|
||||
}
|
||||
}
|
||||
|
||||
void clear() {
|
||||
_currentPayload = null;
|
||||
_pending.clear();
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../../core/theme/design_tokens.dart';
|
||||
import '../../../../shared/widgets/app_button.dart';
|
||||
import '../../reminders/reminder_queue_manager.dart';
|
||||
import '../../reminders/models/reminder_payload.dart';
|
||||
|
||||
class ReminderOverlay extends StatefulWidget {
|
||||
const ReminderOverlay({
|
||||
super.key,
|
||||
required this.queueManager,
|
||||
required this.onComplete,
|
||||
required this.onSnooze,
|
||||
required this.onArchive,
|
||||
});
|
||||
|
||||
final ReminderQueueManager queueManager;
|
||||
final VoidCallback onComplete;
|
||||
final void Function(int minutes) onSnooze;
|
||||
final VoidCallback onArchive;
|
||||
|
||||
@override
|
||||
State<ReminderOverlay> createState() => _ReminderOverlayState();
|
||||
}
|
||||
|
||||
class _ReminderOverlayState extends State<ReminderOverlay> {
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
ReminderPayload? get _currentPayload => widget.queueManager.currentPayload;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_hideSnoozeOptions();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _hideSnoozeOptions() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
|
||||
void _showSnoozeDropdown() {
|
||||
_hideSnoozeOptions();
|
||||
|
||||
final box = context.findRenderObject() as RenderBox?;
|
||||
if (box == null) return;
|
||||
|
||||
final button = box.localToGlobal(Offset.zero);
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
left: button.dx,
|
||||
top: button.dy + box.size.height + 4,
|
||||
width: 120,
|
||||
child: Material(
|
||||
elevation: 4,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: AppColors.borderSecondary),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_SnoozeOption(
|
||||
label: '5 分钟',
|
||||
onTap: () {
|
||||
_hideSnoozeOptions();
|
||||
_handleSnooze(5);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1, color: AppColors.borderSecondary),
|
||||
_SnoozeOption(
|
||||
label: '15 分钟',
|
||||
onTap: () {
|
||||
_hideSnoozeOptions();
|
||||
_handleSnooze(15);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
void _handleComplete() {
|
||||
widget.onArchive();
|
||||
widget.queueManager.dequeueCurrent();
|
||||
widget.onComplete();
|
||||
}
|
||||
|
||||
void _handleSnooze(int minutes) {
|
||||
widget.onSnooze(minutes);
|
||||
widget.queueManager.dequeueCurrent();
|
||||
widget.onComplete();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final payload = _currentPayload;
|
||||
if (payload == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Container(
|
||||
color: AppColors.white,
|
||||
child: SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
payload.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
color: AppColors.slate900,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Text(
|
||||
DateFormat('HH:mm').format(DateTime.now()),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(color: AppColors.slate500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: AppButton(
|
||||
text: '稍后提醒',
|
||||
isOutlined: true,
|
||||
onPressed: _showSnoozeDropdown,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: AppButton(text: '完成', onPressed: _handleComplete),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SnoozeOption extends StatelessWidget {
|
||||
const _SnoozeOption({required this.label, required this.onTap});
|
||||
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: AppSpacing.md,
|
||||
vertical: AppSpacing.sm,
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: AppColors.slate900),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user