diff --git a/assets/icons/app_icon.ico b/assets/icons/app_icon.ico new file mode 100644 index 0000000..fe61b28 Binary files /dev/null and b/assets/icons/app_icon.ico differ diff --git a/assets/icons/task.ico b/assets/icons/task.ico deleted file mode 100644 index 3d4ede2..0000000 Binary files a/assets/icons/task.ico and /dev/null differ diff --git a/assets/icons/task.png b/assets/icons/task.png deleted file mode 100644 index 3084879..0000000 Binary files a/assets/icons/task.png and /dev/null differ diff --git a/lib/main.dart b/lib/main.dart index f9f48ee..fd0b25a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,13 +1,28 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:hive_flutter/adapters.dart'; +import 'package:task_hub/models/task.dart'; import 'package:task_hub/pages/home_page.dart'; import 'package:task_hub/pages/schedule_page.dart'; import 'package:task_hub/pages/settings_page.dart'; import 'package:task_hub/pages/task_page.dart'; +import 'package:task_hub/utils/notice.dart'; +import 'package:timezone/data/latest.dart' as tz; import 'package:window_size/window_size.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); setWindowMinSize(const Size(800, 600)); + + tz.initializeTimeZones(); + + await Hive.initFlutter(); + Hive.registerAdapter(TaskAdapter()); + await Hive.openBox('taskBox'); + + await initNotify(); + + checkScheduleTime(); + runApp(const TaskHubApp()); } @@ -21,7 +36,7 @@ class TaskHubApp extends StatelessWidget { themeMode: ThemeMode.system, theme: FluentThemeData( fontFamily: 'CustomFont', - accentColor: Colors.purple + accentColor: Colors.purple, ), debugShowCheckedModeBanner: false, home: const MainLayout(), diff --git a/lib/models/schedule.dart b/lib/models/schedule.dart index bc4d26f..021ae45 100644 --- a/lib/models/schedule.dart +++ b/lib/models/schedule.dart @@ -13,6 +13,6 @@ class Schedule { required this.endTime, this.scheduleTime, DateTime? createTime, - }) : id = id ?? DateTime.timestamp().microsecond, - createTime = DateTime.now(); -} \ No newline at end of file + }) : id = id ?? DateTime.now().millisecondsSinceEpoch ~/ 1000, + createTime = DateTime.now(); +} diff --git a/lib/models/task.dart b/lib/models/task.dart index de46c3f..80b497e 100644 --- a/lib/models/task.dart +++ b/lib/models/task.dart @@ -1,7 +1,7 @@ import 'package:hive/hive.dart'; // flutter packages pub run build_runner build -// part 'todo.g.dart'; +part 'task.g.dart'; @HiveType(typeId: 0) class Task extends HiveObject { @@ -45,10 +45,11 @@ class Task extends HiveObject { this.isCompleted = false, Priority priority = Priority.medium, DateTime? createTime, - }) : id = id ?? DateTime.timestamp().microsecond, + DateTime? updateTime, + }) : id = id ?? DateTime.now().millisecondsSinceEpoch ~/ 1000, this.priorityIndex = priority.index, - createTime = DateTime.now(), - updateTime = DateTime.now(); + createTime = createTime ?? DateTime.now(), + updateTime = updateTime ?? DateTime.now(); } enum Priority { low, medium, high } diff --git a/lib/models/task.g.dart b/lib/models/task.g.dart new file mode 100644 index 0000000..1c1927a --- /dev/null +++ b/lib/models/task.g.dart @@ -0,0 +1,65 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'task.dart'; + +// ************************************************************************** +// TypeAdapterGenerator +// ************************************************************************** + +class TaskAdapter extends TypeAdapter { + @override + final int typeId = 0; + + @override + Task read(BinaryReader reader) { + final numOfFields = reader.readByte(); + final fields = { + for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(), + }; + return Task( + id: fields[0] as int?, + title: fields[1] as String, + desc: fields[2] as String?, + dueDate: fields[3] as DateTime?, + scheduleTime: fields[4] as DateTime?, + isCompleted: fields[5] as bool, + createTime: fields[7] as DateTime?, + ) + ..priorityIndex = fields[6] as int + ..updateTime = fields[8] as DateTime; + } + + @override + void write(BinaryWriter writer, Task obj) { + writer + ..writeByte(9) + ..writeByte(0) + ..write(obj.id) + ..writeByte(1) + ..write(obj.title) + ..writeByte(2) + ..write(obj.desc) + ..writeByte(3) + ..write(obj.dueDate) + ..writeByte(4) + ..write(obj.scheduleTime) + ..writeByte(5) + ..write(obj.isCompleted) + ..writeByte(6) + ..write(obj.priorityIndex) + ..writeByte(7) + ..write(obj.createTime) + ..writeByte(8) + ..write(obj.updateTime); + } + + @override + int get hashCode => typeId.hashCode; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TaskAdapter && + runtimeType == other.runtimeType && + typeId == other.typeId; +} diff --git a/lib/notice.dart b/lib/notice.dart deleted file mode 100644 index 09766d4..0000000 --- a/lib/notice.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; - -// 初始化通知 -Future _initNotify() async { - final notify = FlutterLocalNotificationsPlugin(); - await notify.initialize( - const InitializationSettings( - windows: WindowsInitializationSettings( - appName: 'TaskHub', - appUserModelId: 'com.cxx.task', - guid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', - iconPath: 'assets/icons/task.png' - ), - ), - ); -} - -// 发送通知 -Future _showNotify(String title, String text) async { - final notify = FlutterLocalNotificationsPlugin(); - final id = DateTime.now().millisecondsSinceEpoch.remainder(10000); - - await notify.show( - id, - title, - text, - const NotificationDetails( - windows: WindowsNotificationDetails(), - ), - ); -} \ No newline at end of file diff --git a/lib/pages/task_page.dart b/lib/pages/task_page.dart index 000f1d2..bc8c4c6 100644 --- a/lib/pages/task_page.dart +++ b/lib/pages/task_page.dart @@ -2,7 +2,9 @@ import 'dart:ui'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart' show SegmentedButton, ButtonSegment; +import 'package:hive/hive.dart'; import 'package:task_hub/models/task.dart'; +import 'package:task_hub/utils/notice.dart'; import 'package:task_hub/widgets/task_dialog.dart'; import 'package:task_hub/widgets/task_item.dart'; @@ -15,10 +17,24 @@ class TaskPage extends StatefulWidget { } class _TaskPageState extends State { - final List _tasks = []; + late Box _taskBox; + List _tasks = []; String _filter = 'all'; String _sortBy = 'created'; + @override + void initState() { + super.initState(); + _loadTasks(); + } + + void _loadTasks() { + _taskBox = Hive.box('taskBox'); + setState(() { + _tasks = _taskBox.values.toList(); + }); + } + List get _filteredTasks { List tasks = _tasks.where((task) { if (_filter == 'active') return !task.isCompleted; @@ -48,10 +64,12 @@ class _TaskPageState extends State { showDialog( context: context, builder: (context) => TaskDialog( - onSave: (task) { + onSave: (task) async { + await _taskBox.put(task.id, task); setState(() { - _tasks.add(task); + _tasks = _taskBox.values.toList(); }); + checkScheduleTime(); }, ), ); @@ -62,22 +80,26 @@ class _TaskPageState extends State { context: context, builder: (context) => TaskDialog( task: task, - onSave: (editedTask) { + onSave: (editedTask) async { + editedTask.updateTime = DateTime.now(); + await _taskBox.put(editedTask.id, editedTask); setState(() { - final index = _tasks.indexWhere((t) => t.id == task.id); - if (index != -1) { - _tasks[index] = editedTask; - } + _tasks = _taskBox.values.toList(); }); + checkScheduleTime(); }, ), ); } - void _deleteTask(int taskId) { + void _deleteTask(int taskId) async { + final task = _tasks.firstWhere((t) => t.id == taskId); + await task.delete(); + setState(() { - _tasks.removeWhere((task) => task.id == taskId); + _tasks = _taskBox.values.toList(); }); + checkScheduleTime(); } void _toggleTaskComplete(Task task) { @@ -91,7 +113,7 @@ class _TaskPageState extends State { createTime: task.createTime, dueDate: task.dueDate, isCompleted: !task.isCompleted, - priority: task.priority + priority: task.priority, ); } }); diff --git a/lib/utils/common.dart b/lib/utils/common.dart new file mode 100644 index 0000000..818244a --- /dev/null +++ b/lib/utils/common.dart @@ -0,0 +1,3 @@ +bool isSameDate(DateTime a, DateTime b) { + return a.year == b.year && a.month == b.month && a.day == b.day; +} \ No newline at end of file diff --git a/lib/utils/notice.dart b/lib/utils/notice.dart new file mode 100644 index 0000000..ac57226 --- /dev/null +++ b/lib/utils/notice.dart @@ -0,0 +1,82 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:hive/hive.dart'; +import 'package:timezone/timezone.dart' as tz; + +import '../models/task.dart'; +import 'common.dart'; + +// 初始化通知 +Future initNotify() async { + final notify = FlutterLocalNotificationsPlugin(); + await notify.initialize( + const InitializationSettings( + windows: WindowsInitializationSettings( + appName: 'TaskHub', + appUserModelId: 'com.cxx.task', + guid: 'f47ac10b-58cc-4372-a567-0e02b2c3d479', + iconPath: 'assets/icons/app_icon.ico', + ), + ), + ); +} + +Future showNotify(String title, String body) async { + final notify = FlutterLocalNotificationsPlugin(); + final id = DateTime.now().millisecondsSinceEpoch.remainder(10000); + + await notify.show( + id, + title, + body, + const NotificationDetails(windows: WindowsNotificationDetails()), + ); +} + +// 发送通知 +Future scheduleNotification( + int id, + String title, + String body, + DateTime scheduleTime, +) async { + final notify = FlutterLocalNotificationsPlugin(); + + final local = tz.getLocation('Asia/Shanghai'); + final tzTime = tz.TZDateTime.from(scheduleTime, local); + + await notify.zonedSchedule( + id, + title, + body, + tzTime, + const NotificationDetails(windows: WindowsNotificationDetails()), + androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, + ); +} + +// 取消通知 +Future cancelAllSchedule() async { + final notify = FlutterLocalNotificationsPlugin(); + + await notify.cancelAll(); +} + +void checkScheduleTime() { + final taskBox = Hive.box('taskBox'); + final now = DateTime.now(); + + cancelAllSchedule(); + for (var task in taskBox.values) { + if (task.scheduleTime != null && + !task.isCompleted && + isSameDate(task.scheduleTime!, DateTime.now()) && + task.scheduleTime!.isAfter(now)) { + scheduleNotification( + task.id, + task.title, + task.desc ?? '', + task.scheduleTime!, + ); + } + } +} diff --git a/lib/widgets/common.dart b/lib/widgets/common.dart index 479c345..9020b54 100644 --- a/lib/widgets/common.dart +++ b/lib/widgets/common.dart @@ -79,6 +79,9 @@ Widget buildOmniDateButton({ onPressed: () async { final DateTime? picked = await showOmniDateTimePicker( context: context, + constraints: const BoxConstraints( + maxWidth: 400, + ), initialDate: selectedDate ?? DateTime.now(), firstDate: DateTime( DateTime.now().year, diff --git a/lib/widgets/task_dialog.dart b/lib/widgets/task_dialog.dart index 72405dd..18094ad 100644 --- a/lib/widgets/task_dialog.dart +++ b/lib/widgets/task_dialog.dart @@ -19,6 +19,7 @@ class _TaskDialogState extends State { late Priority _selectedPriority; DateTime? _selectedDueDate; DateTime? _selectedScheduleTime; + DateTime? _createTime; final _formKey = GlobalKey(); @override @@ -29,6 +30,7 @@ class _TaskDialogState extends State { _selectedPriority = widget.task?.priority ?? Priority.medium; _selectedDueDate = widget.task?.dueDate; _selectedScheduleTime = widget.task?.scheduleTime; + _createTime = widget.task?.createTime; } @override @@ -38,8 +40,22 @@ class _TaskDialogState extends State { super.dispose(); } - void onConfirmClick() { + void onConfirmClick() async { if (_formKey.currentState!.validate()) { + if (_selectedScheduleTime != null && + _selectedScheduleTime!.isBefore(DateTime.now())) { + await displayInfoBar( + context, + builder: (context, close) { + return InfoBar( + title: const Text('提醒时间不能早于当前时间'), + severity: InfoBarSeverity.error, + ); + }, + ); + return; + } + final task = Task( id: widget.task?.id, title: _titleController.text, @@ -48,6 +64,7 @@ class _TaskDialogState extends State { scheduleTime: _selectedScheduleTime, priority: _selectedPriority, isCompleted: widget.task?.isCompleted ?? false, + createTime: _createTime, ); widget.onSave(task); Navigator.pop(context); diff --git a/lib/widgets/task_item.dart b/lib/widgets/task_item.dart index df530fb..4ad5dc9 100644 --- a/lib/widgets/task_item.dart +++ b/lib/widgets/task_item.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:intl/intl.dart'; import 'package:task_hub/models/task.dart'; class TaskItem extends StatelessWidget { @@ -39,11 +40,14 @@ class TaskItem extends StatelessWidget { Expanded(child: _buildTitle(context)), ], ), - const SizedBox(height: 4), - if (task.desc != null && task.desc!.isNotEmpty) _buildDesc(), - const SizedBox(height: 4), - if (task.dueDate != null || task.scheduleTime != null) + if (task.desc != null && task.desc!.isNotEmpty) ...[ + const SizedBox(height: 4), + _buildDesc(), + ], + if (task.dueDate != null || task.scheduleTime != null) ...[ + const SizedBox(height: 4), _buildDate(), + ] ], ), ), @@ -92,7 +96,7 @@ class TaskItem extends StatelessWidget { Icon(FluentIcons.calendar, size: 12, color: Colors.grey), const SizedBox(width: 4), Text( - '截止日期: ${_formatDate(task.dueDate!)}', + '截止日期: ${DateFormat('yyyy-MM-dd').format(task.dueDate!)}', style: TextStyle(color: Colors.grey, fontSize: 12), ), const SizedBox(width: 4), @@ -101,7 +105,7 @@ class TaskItem extends StatelessWidget { Icon(FluentIcons.clock, size: 12, color: Colors.grey), const SizedBox(width: 4), Text( - '提醒日期: ${_formatDate(task.scheduleTime!)}', + '提醒日期: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(task.scheduleTime!)}', style: TextStyle(color: Colors.grey, fontSize: 12), ), ], @@ -181,8 +185,4 @@ class TaskItem extends StatelessWidget { ), ); } - - String _formatDate(DateTime date) { - return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}'; - } } diff --git a/pubspec.lock b/pubspec.lock index 7c081bc..2f27014 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -17,6 +17,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "6.4.1" + archive: + dependency: transitive + description: + name: archive + sha256: a96e8b390886ee8abb49b7bd3ac8df6f451c621619f52a26e815fdcf568959ff + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.0.9" args: dependency: transitive description: @@ -129,6 +137,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.4" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.flutter-io.cn" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -153,6 +169,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.19.1" + console: + dependency: transitive + description: + name: console + sha256: e04e7824384c5b39389acdd6dc7d33f3efe6b232f6f16d7626f194f6a01ad69a + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.1.0" convert: dependency: transitive description: @@ -312,6 +336,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.0.0" + get_it: + dependency: transitive + description: + name: get_it + sha256: "568d62f0e68666fb5d95519743b3c24a34c7f19d834b0658c46e26d778461f66" + url: "https://pub.flutter-io.cn" + source: hosted + version: "9.2.1" glob: dependency: transitive description: @@ -376,6 +408,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "4.1.2" + image: + dependency: transitive + description: + name: image + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce + url: "https://pub.flutter-io.cn" + source: hosted + version: "4.8.0" intl: dependency: "direct main" description: @@ -504,6 +544,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "2.0.0" + msix: + dependency: "direct dev" + description: + name: msix + sha256: b6b08e7a7b5d1845f2b1d31216d5b1fb558e98251efefe54eb79ed00d27bc2ac + url: "https://pub.flutter-io.cn" + source: hosted + version: "3.16.13" nested: dependency: transitive description: @@ -616,6 +664,14 @@ packages: url: "https://pub.flutter-io.cn" source: hosted version: "1.5.2" + posix: + dependency: transitive + description: + name: posix + sha256: "185ef7606574f789b40f289c233efa52e96dead518aed988e040a10737febb07" + url: "https://pub.flutter-io.cn" + source: hosted + version: "6.5.0" provider: dependency: transitive description: @@ -790,7 +846,7 @@ packages: source: hosted version: "0.7.7" timezone: - dependency: transitive + dependency: "direct main" description: name: timezone sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 diff --git a/pubspec.yaml b/pubspec.yaml index 1c2afee..197bbd3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1 +1 @@ -name: task_hub description: "待办事项、日程安排" publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ^3.10.0 dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_local_notifications: ^19.5.0 syncfusion_flutter_calendar: ^30.1.37 fluent_ui: ^4.15.0 omni_datetime_picker: ^2.3.1 intl: ^0.20.2 window_size: ^0.1.0 hive: ^2.2.3 hive_flutter: ^1.1.0 dev_dependencies: flutter_test: sdk: flutter hive_generator: ^2.0.1 build_runner: ^2.4.6 flutter_lints: ^5.0.0 flutter: uses-material-design: true assets: - assets/icons/ fonts: - family: CustomFont fonts: - asset: fonts/custom.ttf \ No newline at end of file +name: task_hub description: "待办事项、日程安排" publish_to: 'none' # Remove this line if you wish to publish to pub.dev version: 1.0.0+1 environment: sdk: ^3.10.0 dependencies: flutter: sdk: flutter # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 flutter_local_notifications: ^19.5.0 syncfusion_flutter_calendar: ^30.1.37 fluent_ui: ^4.15.0 omni_datetime_picker: ^2.3.1 intl: ^0.20.2 window_size: ^0.1.0 hive: ^2.2.3 hive_flutter: ^1.1.0 timezone: ^0.10.1 dev_dependencies: flutter_test: sdk: flutter hive_generator: ^2.0.1 build_runner: ^2.4.6 flutter_lints: ^5.0.0 msix: ^3.16.13 flutter: uses-material-design: true assets: - assets/icons/ fonts: - family: CustomFont fonts: - asset: fonts/custom.ttf msix_config: display_name: TaskHub publisher_display_name: Cxx identity_name: com.cxx.task msix_version: 1.0.0.0 logo_path: assets/icons/app_icon.ico capabilities: internetClient \ No newline at end of file