diff --git a/.github/workflows/android-build.yml b/.github/workflows/android-build.yml
index 56b2e83e..fbfa3eae 100644
--- a/.github/workflows/android-build.yml
+++ b/.github/workflows/android-build.yml
@@ -24,7 +24,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
- flutter-version: "3.19.3"
+ flutter-version: "3.22.2"
channel: 'stable'
cache: true
diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml
index 2f5a3d9b..6116d902 100644
--- a/.github/workflows/android-release.yml
+++ b/.github/workflows/android-release.yml
@@ -25,7 +25,7 @@ jobs:
- uses: subosito/flutter-action@v2
with:
- flutter-version: "3.19.3"
+ flutter-version: "3.22.2"
channel: 'stable'
cache: false
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index b4a9535b..673baac5 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -5,16 +5,19 @@
android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
-
+
+
+
+
+
+
-
-
-
+
+
+
Display > Reliability > Show Foreground Notification
+* Added current lap card for stopwatch. Now you can view the current lap time in real time.
+* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm.
+* Changed timer and stopwatch notification so time appears in title
+* Updated translations
+
+🐛 Fixes
+
+* Fixed range schedule automatically getting disabled
+* Fixed timer dismiss actions being mapped incorrectly
+* Fixed timer add length not working correctly after timer rings
+
diff --git a/fastlane/metadata/android/en-US/changelogs/252.txt b/fastlane/metadata/android/en-US/changelogs/252.txt
new file mode 100644
index 00000000..ecf6e5f3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/252.txt
@@ -0,0 +1,14 @@
+✨ Enhancements
+
+* Added option to turn on foreground notification to keep app alive. Go to General > Display > Reliability > Show Foreground Notification
+* Added current lap card for stopwatch. Now you can view the current lap time in real time.
+* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm.
+* Changed timer and stopwatch notification so time appears in title
+* Updated translations
+
+🐛 Fixes
+
+* Fixed range schedule automatically getting disabled
+* Fixed timer dismiss actions being mapped incorrectly
+* Fixed timer add length not working correctly after timer rings
+
diff --git a/fastlane/metadata/android/en-US/changelogs/253.txt b/fastlane/metadata/android/en-US/changelogs/253.txt
new file mode 100644
index 00000000..ecf6e5f3
--- /dev/null
+++ b/fastlane/metadata/android/en-US/changelogs/253.txt
@@ -0,0 +1,14 @@
+✨ Enhancements
+
+* Added option to turn on foreground notification to keep app alive. Go to General > Display > Reliability > Show Foreground Notification
+* Added current lap card for stopwatch. Now you can view the current lap time in real time.
+* Added option to show next alarm in filters. Go to Settings > Alarms > Filters > Show Next Alarm.
+* Changed timer and stopwatch notification so time appears in title
+* Updated translations
+
+🐛 Fixes
+
+* Fixed range schedule automatically getting disabled
+* Fixed timer dismiss actions being mapped incorrectly
+* Fixed timer add length not working correctly after timer rings
+
diff --git a/lib/alarm/data/alarm_settings_schema.dart b/lib/alarm/data/alarm_settings_schema.dart
index 46928399..d690d7d2 100644
--- a/lib/alarm/data/alarm_settings_schema.dart
+++ b/lib/alarm/data/alarm_settings_schema.dart
@@ -25,6 +25,7 @@ import 'package:clock_app/settings/types/setting.dart';
import 'package:clock_app/settings/types/setting_enable_condition.dart';
import 'package:clock_app/settings/types/setting_group.dart';
import 'package:clock_app/timer/types/time_duration.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -209,6 +210,15 @@ SettingGroup alarmSettingsSchema = SettingGroup(
100,
100,
unit: "%"),
+ SliderSetting(
+ "task_volume",
+ (context) => AppLocalizations.of(context)!.volumeWhileTasks,
+ 0,
+ 100,
+ 50,
+ unit: "%",
+ getDescription: (context) => "Percentage of base volume",
+ ),
SwitchSetting(
"Rising Volume",
(context) => AppLocalizations.of(context)!.risingVolumeSetting,
@@ -294,7 +304,9 @@ SettingGroup alarmSettingsSchema = SettingGroup(
ListSetting(
"Tasks",
(context) => AppLocalizations.of(context)!.tasksSetting,
- [],
+ kDebugMode
+ ? [AlarmTask(AlarmTaskType.math), AlarmTask(AlarmTaskType.sequence)]
+ : [],
alarmTaskSchemasMap.keys.map((key) => AlarmTask(key)).toList(),
addCardBuilder: (item) => AlarmTaskCard(task: item, isAddCard: true),
cardBuilder: (item, [onDelete, onDuplicate]) => AlarmTaskCard(
diff --git a/lib/alarm/logic/alarm_isolate.dart b/lib/alarm/logic/alarm_isolate.dart
index 7823a6ed..c9e1699a 100644
--- a/lib/alarm/logic/alarm_isolate.dart
+++ b/lib/alarm/logic/alarm_isolate.dart
@@ -4,8 +4,8 @@ import 'dart:ui';
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/notification_type.dart';
import 'package:clock_app/common/utils/list_storage.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:clock_app/system/logic/initialize_isolate.dart';
-import 'package:clock_app/timer/types/time_duration.dart';
import 'package:clock_app/timer/types/timer.dart';
import 'package:flutter/foundation.dart';
import 'package:clock_app/alarm/logic/schedule_alarm.dart';
@@ -21,18 +21,23 @@ import 'package:clock_app/timer/utils/timer_id.dart';
const String stopAlarmPortName = "stopAlarmPort";
const String updatePortName = "updatePort";
+const String setAlarmVolumePortName = "setAlarmVolumePort";
@pragma('vm:entry-point')
void triggerScheduledNotification(int scheduleId, Json params) async {
- debugPrint("Alarm triggered: $scheduleId");
+ FlutterError.onError = (FlutterErrorDetails details) {
+ logger.f(details.exception.toString());
+ };
+
+ logger.i("Alarm isolate triggered $scheduleId");
// print("Alarm Trigger Isolate: ${Service.getIsolateID(Isolate.current)}");
if (params == null) {
- debugPrint("Params was null when triggering alarm");
+ logger.e("Params was null when triggering alarm");
return;
}
if (params['type'] == null) {
- debugPrint("Params Type was null when triggering alarm");
+ logger.e("Params Type was null when triggering alarm");
return;
}
@@ -73,10 +78,9 @@ void stopScheduledNotification(List message) {
}
void triggerAlarm(int scheduleId, Json params) async {
+ logger.i("Alarm triggered $scheduleId");
if (params == null) {
- if (kDebugMode) {
- print("Params was null when triggering alarm");
- }
+ logger.e("Params was null when triggering alarm");
return;
}
@@ -100,6 +104,7 @@ void triggerAlarm(int scheduleId, Json params) async {
now.millisecondsSinceEpoch >
alarm.currentScheduleDateTime!.millisecondsSinceEpoch +
1000 * 60 * 60) {
+ logger.i("Skipping alarm $scheduleId");
return;
}
@@ -118,6 +123,14 @@ void triggerAlarm(int scheduleId, Json params) async {
RingtonePlayer.playAlarm(alarm);
RingingManager.ringAlarm(scheduleId);
+ ReceivePort receivePort = ReceivePort();
+ IsolateNameServer.removePortNameMapping(setAlarmVolumePortName);
+ IsolateNameServer.registerPortWithName(
+ receivePort.sendPort, setAlarmVolumePortName);
+ receivePort.listen((message) {
+ setVolume(message[0]);
+ });
+
String timeFormatString = await loadTextFile("time_format_string");
String title = alarm.label.isEmpty ? "Alarm Ringing..." : alarm.label;
@@ -137,10 +150,11 @@ void triggerAlarm(int scheduleId, Json params) async {
}
void setVolume(double volume) {
- RingtonePlayer.setVolume(volume);
+ RingtonePlayer.setVolume(volume / 100);
}
void stopAlarm(int scheduleId, AlarmStopAction action) async {
+ logger.i("Stopping alarm $scheduleId with action: ${action.name}");
if (action == AlarmStopAction.snooze) {
await updateAlarmById(scheduleId, (alarm) async => await alarm.snooze());
// await createSnoozeNotification(scheduleId);
@@ -158,6 +172,7 @@ void stopAlarm(int scheduleId, AlarmStopAction action) async {
}
void triggerTimer(int scheduleId, Json params) async {
+ logger.i("Timer triggered $scheduleId");
ClockTimer? timer = getTimerById(scheduleId);
if (timer == null || !timer.isRunning) {
@@ -194,18 +209,12 @@ void triggerTimer(int scheduleId, Json params) async {
}
void stopTimer(int scheduleId, AlarmStopAction action) async {
+ logger.i("Stopping timer $scheduleId with action: ${action.name}");
ClockTimer? timer = getTimerById(scheduleId);
if (timer == null) return;
if (action == AlarmStopAction.snooze) {
- await scheduleSnoozeAlarm(
- scheduleId,
- Duration(minutes: timer.addLength.floor()),
- ScheduledNotificationType.timer,
- "stopTimer(): ${timer.addLength.floor()} added to timer",
- );
updateTimerById(scheduleId, (timer) async {
- timer.setTime(const TimeDuration(minutes: 1));
- await timer.start();
+ await timer.snooze();
});
} else if (action == AlarmStopAction.dismiss) {
// If there was an alarm already ringing when the timer was triggered, we
diff --git a/lib/alarm/logic/alarm_time.dart b/lib/alarm/logic/alarm_time.dart
index 568141b4..94061c5b 100644
--- a/lib/alarm/logic/alarm_time.dart
+++ b/lib/alarm/logic/alarm_time.dart
@@ -4,26 +4,34 @@ import 'package:clock_app/common/utils/date_time.dart';
// Calculates the DateTime when the provided `time` will next occur
DateTime getDailyAlarmDate(
Time time, {
- DateTime? scheduledDate,
+ DateTime? scheduleStartDate,
+ int interval = 1,
}) {
- if (scheduledDate != null && scheduledDate.isAfter(DateTime.now())) {
- return DateTime(scheduledDate.year, scheduledDate.month, scheduledDate.day,
- time.hour, time.minute, time.second);
+ if (scheduleStartDate != null && scheduleStartDate.isAfter(DateTime.now())) {
+ return DateTime(scheduleStartDate.year, scheduleStartDate.month,
+ scheduleStartDate.day, time.hour, time.minute, time.second);
}
// If a date has not been provided, assume it to be today
- scheduledDate = DateTime.now();
+ DateTime scheduleDate = DateTime.now();
DateTime alarmTime;
- if (time.toHours() > scheduledDate.toHours()) {
+ if (time.toHours() > scheduleDate.toHours()) {
// If the time is in the future, set the alarm for today
- alarmTime = DateTime(scheduledDate.year, scheduledDate.month,
- scheduledDate.day, time.hour, time.minute, time.second);
+ alarmTime = DateTime(scheduleDate.year, scheduleDate.month,
+ scheduleDate.day, time.hour, time.minute, time.second);
} else {
- // If the time has already passed, set the alarm for tomorrow
- DateTime nextDateTime = scheduledDate.add(const Duration(days: 1));
- alarmTime = DateTime(nextDateTime.year, nextDateTime.month,
- nextDateTime.day, time.hour, time.minute, time.second);
+ // If the time has already passed, set the alarm for next occurence
+ if (scheduleStartDate != null) {
+ scheduleDate = scheduleStartDate;
+ }
+
+ while (scheduleDate.isBefore(DateTime.now())) {
+ scheduleDate = scheduleDate.add(Duration(days: interval));
+ }
+
+ alarmTime = DateTime(scheduleDate.year, scheduleDate.month,
+ scheduleDate.day, time.hour, time.minute, time.second);
}
return alarmTime;
diff --git a/lib/alarm/logic/schedule_alarm.dart b/lib/alarm/logic/schedule_alarm.dart
index 43e2916c..c2a41fc7 100644
--- a/lib/alarm/logic/schedule_alarm.dart
+++ b/lib/alarm/logic/schedule_alarm.dart
@@ -8,6 +8,7 @@ import 'package:clock_app/common/types/schedule_id.dart';
import 'package:clock_app/common/utils/date_time.dart';
import 'package:clock_app/common/utils/list_storage.dart';
import 'package:clock_app/common/utils/time_of_day.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:clock_app/settings/data/settings_schema.dart';
Future scheduleAlarm(
@@ -68,7 +69,7 @@ Future scheduleAlarm(
scheduleIds.add(ScheduleId(id: scheduleId));
await saveList(name, scheduleIds);
- //
+ //
// if (type == ScheduledNotificationType.alarm && !snooze) {
// }
//
@@ -88,8 +89,10 @@ Future scheduleAlarm(
'type': type.name,
},
);
+
+ logger.i('Scheduled alarm $scheduleId for $startDate of type ${type.name}: $description');
}
- }
+}
Future cancelAlarm(int scheduleId, ScheduledNotificationType type) async {
if (!Platform.environment.containsKey('FLUTTER_TEST')) {
@@ -113,6 +116,8 @@ Future cancelAlarm(int scheduleId, ScheduledNotificationType type) async {
}
AndroidAlarmManager.cancel(scheduleId);
+
+ logger.i('Canceled alarm $scheduleId of type ${type.name}');
}
}
@@ -128,4 +133,6 @@ Future scheduleSnoozeAlarm(int scheduleId, Duration delay,
if (!Platform.environment.containsKey('FLUTTER_TEST')) {
await createSnoozeNotification(scheduleId, DateTime.now().add(delay));
}
+
+ logger.i('Scheduled snooze alarm $scheduleId for ${DateTime.now().add(delay)} with type ${type.name}: $description');
}
diff --git a/lib/alarm/screens/alarm_notification_screen.dart b/lib/alarm/screens/alarm_notification_screen.dart
index 89ad197b..3e3e3030 100644
--- a/lib/alarm/screens/alarm_notification_screen.dart
+++ b/lib/alarm/screens/alarm_notification_screen.dart
@@ -1,8 +1,12 @@
+import 'dart:ui';
+
+import 'package:clock_app/alarm/logic/alarm_isolate.dart';
import 'package:clock_app/alarm/logic/update_alarms.dart';
import 'package:clock_app/alarm/utils/alarm_id.dart';
import 'package:clock_app/alarm/types/alarm.dart';
import 'package:clock_app/common/types/notification_type.dart';
import 'package:clock_app/common/widgets/clock/clock.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:clock_app/navigation/types/routes.dart';
import 'package:clock_app/notifications/types/fullscreen_notification_manager.dart';
import 'package:clock_app/navigation/types/alignment.dart';
@@ -37,6 +41,8 @@ class _AlarmNotificationScreenState extends State {
void _setNextWidget() {
setState(() {
if (_currentIndex < 0) {
+ IsolateNameServer.lookupPortByName(setAlarmVolumePortName)
+ ?.send([alarm.volume]);
_currentWidget = actionWidget;
} else if (_currentIndex >= alarm.tasks.length) {
if (widget.onPop != null) {
@@ -47,6 +53,8 @@ class _AlarmNotificationScreenState extends State {
widget.dismissType, ScheduledNotificationType.alarm);
}
} else {
+ IsolateNameServer.lookupPortByName(setAlarmVolumePortName)
+ ?.send([alarm.volume * alarm.volumeDuringTasks / 100]);
// RingtonePlayer.setVolume(0);
_currentWidget = alarm.tasks[_currentIndex].builder(_setNextWidget);
}
@@ -81,7 +89,7 @@ class _AlarmNotificationScreenState extends State {
snoozeLabel: "Snooze",
);
- debugPrint(e.toString());
+ logger.e(e.toString());
}
_setNextWidget();
diff --git a/lib/alarm/screens/alarm_screen.dart b/lib/alarm/screens/alarm_screen.dart
index 4e7aba13..00168fe7 100644
--- a/lib/alarm/screens/alarm_screen.dart
+++ b/lib/alarm/screens/alarm_screen.dart
@@ -11,13 +11,12 @@ import 'package:clock_app/common/types/list_filter.dart';
import 'package:clock_app/common/types/picker_result.dart';
import 'package:clock_app/common/types/time.dart';
import 'package:clock_app/common/utils/snackbar.dart';
-import 'package:clock_app/common/widgets/card_container.dart';
import 'package:clock_app/common/widgets/fab.dart';
import 'package:clock_app/common/widgets/list/customize_list_item_screen.dart';
import 'package:clock_app/common/widgets/list/persistent_list_view.dart';
import 'package:clock_app/common/widgets/time_picker.dart';
+import 'package:clock_app/navigation/types/quick_action_controller.dart';
import 'package:clock_app/settings/data/settings_schema.dart';
-import 'package:clock_app/settings/types/listener_manager.dart';
import 'package:clock_app/settings/types/setting.dart';
import 'package:flutter/material.dart';
import 'package:great_list_view/great_list_view.dart';
@@ -30,7 +29,9 @@ typedef AlarmCardBuilder = Widget Function(
);
class AlarmScreen extends StatefulWidget {
- const AlarmScreen({super.key});
+ const AlarmScreen({super.key, this.actionController});
+
+ final QuickActionController? actionController;
@override
State createState() => _AlarmScreenState();
@@ -68,10 +69,16 @@ class _AlarmScreenState extends State {
_showNextAlarm.addListener(update);
_showSort.addListener(update);
- ListenerManager.addOnChangeListener("alarms", update);
+ // ListenerManager.addOnChangeListener("alarms", update);
nextAlarm = getNextAlarm();
+ widget.actionController?.setAction((action) {
+ if (action == "add_alarm") {
+ _selectTime();
+ }
+ });
+
// ListenerManager().addListener();
}
@@ -81,7 +88,7 @@ class _AlarmScreenState extends State {
_showFilters.removeListener(update);
_showSort.removeListener(update);
_showNextAlarm.removeListener(update);
- ListenerManager.removeOnChangeListener("alarms", update);
+ // ListenerManager.removeOnChangeListener("alarms", update);
super.dispose();
}
@@ -187,7 +194,13 @@ class _AlarmScreenState extends State {
_listController.changeItems((alarms) {});
}
- List> getListFilterItems() {
+ void handleAddAlarmActon(){
+ ScaffoldMessenger.of(context).removeCurrentSnackBar();
+ _selectTime();
+
+ }
+
+ List> _getListFilterItems() {
List> listFilterItems =
_showFilters.value ? [...alarmListFilters] : [];
@@ -205,31 +218,31 @@ class _AlarmScreenState extends State {
return listFilterItems;
}
- @override
- Widget build(BuildContext context) {
- Future selectTime() async {
- final PickerResult? timePickerResult =
- await showTimePickerDialog(
- context: context,
- initialTime: TimeOfDay.now(),
- title: AppLocalizations.of(context)!.selectTime,
- cancelText: AppLocalizations.of(context)!.cancelButton,
- confirmText: AppLocalizations.of(context)!.saveButton,
- useSimple: false,
- );
-
- if (timePickerResult != null) {
- Alarm alarm = Alarm.fromTimeOfDay(timePickerResult.value);
- if (timePickerResult.isCustomize) {
- await _openCustomizeAlarmScreen(alarm, onSave: (newAlarm) async {
- _listController.addItem(newAlarm);
- }, isNewAlarm: true);
- } else {
- _listController.addItem(alarm);
- }
+ Future _selectTime() async {
+ final PickerResult? timePickerResult =
+ await showTimePickerDialog(
+ context: context,
+ initialTime: TimeOfDay.now(),
+ title: AppLocalizations.of(context)!.selectTime,
+ cancelText: AppLocalizations.of(context)!.cancelButton,
+ confirmText: AppLocalizations.of(context)!.saveButton,
+ useSimple: false,
+ );
+
+ if (timePickerResult != null) {
+ Alarm alarm = Alarm.fromTimeOfDay(timePickerResult.value);
+ if (timePickerResult.isCustomize) {
+ await _openCustomizeAlarmScreen(alarm, onSave: (newAlarm) async {
+ _listController.addItem(newAlarm);
+ }, isNewAlarm: true);
+ } else {
+ _listController.addItem(alarm);
}
}
+ }
+ @override
+ Widget build(BuildContext context) {
return Stack(
children: [
PersistentListView(
@@ -257,8 +270,9 @@ class _AlarmScreenState extends State {
nextAlarm = getNextAlarm();
setState(() {});
},
+ isSelectable: true,
// header: getNextAlarmWidget(),
- listFilters: getListFilterItems(),
+ listFilters: _getListFilterItems(),
customActions: _showFilters.value
? [
ListFilterCustomAction(
@@ -294,11 +308,8 @@ class _AlarmScreenState extends State {
sortOptions: _showSort.value ? alarmSortOptions : [],
),
FAB(
- onPressed: () {
- ScaffoldMessenger.of(context).removeCurrentSnackBar();
- selectTime();
- },
- ),
+ onPressed: handleAddAlarmActon,
+ ),
if (_showInstantAlarmButton.value)
FAB(
onPressed: () {
diff --git a/lib/alarm/types/alarm.dart b/lib/alarm/types/alarm.dart
index 971b80ad..6848cce5 100644
--- a/lib/alarm/types/alarm.dart
+++ b/lib/alarm/types/alarm.dart
@@ -81,6 +81,7 @@ class Alarm extends CustomizableListItem {
FileItem get ringtone => _settings.getSetting("Melody").value;
bool get vibrate => _settings.getSetting("Vibration").value;
double get volume => _settings.getSetting("Volume").value;
+ double get volumeDuringTasks => _settings.getSetting("task_volume").value;
double get snoozeLength => _settings.getSetting("Length").value;
List get tasks => _settings.getSetting("Tasks").value;
List get tags => _settings.getSetting("Tags").value;
@@ -135,7 +136,6 @@ class Alarm extends CustomizableListItem {
Alarm.fromAlarm(Alarm alarm)
: _isEnabled = alarm._isEnabled,
- // _isFinished = alarm._isFinished,
_time = alarm._time,
_snoozeCount = alarm._snoozeCount,
_snoozeTime = alarm._snoozeTime,
@@ -148,7 +148,6 @@ class Alarm extends CustomizableListItem {
@override
void copyFrom(dynamic other) {
_isEnabled = other._isEnabled;
- // _isFinished = other._isFinished;
_time = other._time;
_snoozeCount = other._snoozeCount;
_snoozeTime = other._snoozeTime;
diff --git a/lib/alarm/types/alarm_event.dart b/lib/alarm/types/alarm_event.dart
index db66c538..fc65ef7c 100644
--- a/lib/alarm/types/alarm_event.dart
+++ b/lib/alarm/types/alarm_event.dart
@@ -1,7 +1,7 @@
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/list_item.dart';
import 'package:clock_app/common/types/notification_type.dart';
-import 'package:flutter/foundation.dart';
+import 'package:clock_app/common/utils/id.dart';
// enum AlarmEventType{
// schedule,
@@ -26,7 +26,7 @@ class AlarmEvent extends ListItem {
required this.scheduleId,
required this.startDate,
required this.isActive,
- }) : id = UniqueKey().hashCode;
+ }) : id = getId();
AlarmEvent.fromJson(Json json) {
if (json == null) {
diff --git a/lib/alarm/types/alarm_runner.dart b/lib/alarm/types/alarm_runner.dart
index 2d7e0b20..5043ba2d 100644
--- a/lib/alarm/types/alarm_runner.dart
+++ b/lib/alarm/types/alarm_runner.dart
@@ -1,17 +1,17 @@
import 'package:clock_app/alarm/logic/schedule_alarm.dart';
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/notification_type.dart';
-import 'package:flutter/material.dart';
+import 'package:clock_app/common/utils/id.dart';
class AlarmRunner extends JsonSerializable {
late int _id;
DateTime? _currentScheduleDateTime;
- int get id => _id;
+ get id => _id;
DateTime? get currentScheduleDateTime => _currentScheduleDateTime;
AlarmRunner() {
- _id = UniqueKey().hashCode;
+ _id = getId();
}
Future schedule(DateTime dateTime, String description) async {
@@ -27,10 +27,10 @@ class AlarmRunner extends JsonSerializable {
AlarmRunner.fromJson(Json? json) {
if (json == null) {
- _id = UniqueKey().hashCode;
+ _id = getId();
return;
}
- _id = json['id'] ?? UniqueKey().hashCode;
+ _id = json['id'] ?? getId();
int millisecondsSinceEpoch = json['currentScheduleDateTime'] ?? 0;
_currentScheduleDateTime = millisecondsSinceEpoch == 0
? null
diff --git a/lib/alarm/types/alarm_task.dart b/lib/alarm/types/alarm_task.dart
index a1c83c02..24daea1f 100644
--- a/lib/alarm/types/alarm_task.dart
+++ b/lib/alarm/types/alarm_task.dart
@@ -1,6 +1,7 @@
import 'package:clock_app/alarm/data/alarm_task_schemas.dart';
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/list_item.dart';
+import 'package:clock_app/common/utils/id.dart';
import 'package:clock_app/settings/types/setting_group.dart';
import 'package:flutter/material.dart';
@@ -55,21 +56,21 @@ class AlarmTask extends CustomizableListItem {
AlarmTask(this.type)
: _schema = alarmTaskSchemasMap[type]!.copy(),
- _id = UniqueKey().hashCode;
+ _id = getId();
AlarmTask.from(AlarmTask task)
: type = task.type,
- _id = UniqueKey().hashCode,
+ _id = getId(),
_schema = task._schema.copy();
AlarmTask.fromJson(Json json) {
if (json == null) {
- _id = UniqueKey().hashCode;
+ _id = getId();
type = AlarmTaskType.math;
_schema = alarmTaskSchemasMap[type]!.copy();
return;
}
- _id = json['id'] ?? UniqueKey().hashCode;
+ _id = json['id'] ?? getId();
type = AlarmTaskType.values.byName(json['type']);
_schema = alarmTaskSchemasMap[type]!.copy();
_schema.loadFromJson(json['schema']);
diff --git a/lib/alarm/types/schedules/range_alarm_schedule.dart b/lib/alarm/types/schedules/range_alarm_schedule.dart
index e5b00105..b17dcb62 100644
--- a/lib/alarm/types/schedules/range_alarm_schedule.dart
+++ b/lib/alarm/types/schedules/range_alarm_schedule.dart
@@ -43,15 +43,14 @@ class RangeAlarmSchedule extends AlarmSchedule {
@override
Future schedule(Time time, String description) async {
+ int intervalDays = interval == RangeInterval.daily ? 1 : 7;
// All the dates are not scheduled at once
// Instead we schedule the next date after the current one is finished
-
- DateTime alarmDate = getDailyAlarmDate(time, scheduledDate: startDate);
- print('$alarmDate $startDate $endDate');
+ DateTime alarmDate = getDailyAlarmDate(time,
+ scheduleStartDate: startDate, interval: intervalDays);
if (alarmDate.isAfter(endDate)) {
_isFinished = true;
} else {
- print("_____________");
await _alarmRunner.schedule(alarmDate, description);
_isFinished = false;
}
diff --git a/lib/alarm/types/schedules/weekly_alarm_schedule.dart b/lib/alarm/types/schedules/weekly_alarm_schedule.dart
index e12c504a..222d11e1 100644
--- a/lib/alarm/types/schedules/weekly_alarm_schedule.dart
+++ b/lib/alarm/types/schedules/weekly_alarm_schedule.dart
@@ -89,6 +89,9 @@ class WeeklyAlarmSchedule extends AlarmSchedule {
weekdaySchedule.alarmRunner.cancel();
}
+ // We schedule the next occurence for each weekday.
+ // Subsequent occurences will be scheduled after the first one passes.
+
List weekdays = _weekdaySetting.selected.toList();
List existingWeekdays =
_weekdaySchedules.map((schedule) => schedule.weekday).toList();
diff --git a/lib/alarm/utils/next_alarm.dart b/lib/alarm/utils/next_alarm.dart
index ebb8eaf3..ee0658c6 100644
--- a/lib/alarm/utils/next_alarm.dart
+++ b/lib/alarm/utils/next_alarm.dart
@@ -1,10 +1,7 @@
-
-
import 'package:clock_app/alarm/types/alarm.dart';
-import 'package:clock_app/common/utils/json_serialize.dart';
import 'package:clock_app/common/utils/list_storage.dart';
-Alarm? getNextAlarm () {
+Alarm? getNextAlarm() {
List alarms = loadListSync('alarms');
if (alarms.isEmpty) return null;
alarms.sort((a, b) {
diff --git a/lib/app.dart b/lib/app.dart
index c191902c..3c6da52f 100644
--- a/lib/app.dart
+++ b/lib/app.dart
@@ -22,6 +22,7 @@ import 'package:clock_app/widgets/logic/update_widgets.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
+import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get_storage/get_storage.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -68,7 +69,7 @@ class _AppState extends State {
_styleSettings = _appearanceSettings.getGroup("Style");
_generalSettings = appSettings.getGroup("General");
_animationSpeedSetting =
- _generalSettings.getGroup("Animations").getSetting("Animation Speed");
+ _appearanceSettings.getGroup("Animations").getSetting("Animation Speed");
_animationSpeedSetting.addListener(setAnimationSpeed);
setAnimationSpeed(_animationSpeedSetting.value);
diff --git a/lib/audio/types/ringtone_player.dart b/lib/audio/types/ringtone_player.dart
index d5742ad2..091c201e 100644
--- a/lib/audio/types/ringtone_player.dart
+++ b/lib/audio/types/ringtone_player.dart
@@ -1,6 +1,7 @@
import 'package:audio_session/audio_session.dart';
import 'package:clock_app/alarm/types/alarm.dart';
import 'package:clock_app/audio/types/ringtone_manager.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:clock_app/timer/types/timer.dart';
import 'package:just_audio/just_audio.dart';
import 'package:vibration/vibration.dart';
@@ -98,6 +99,7 @@ class RingtonePlayer {
}
static Future setVolume(double volume) async {
+ logger.t("Setting volume to $volume");
await activePlayer?.setVolume(volume);
}
@@ -117,15 +119,15 @@ class RingtonePlayer {
await activePlayer?.stop();
await activePlayer?.setLoopMode(loopMode);
await activePlayer?.setAudioSource(AudioSource.uri(Uri.parse(ringtoneUri)));
- await activePlayer?.setVolume(volume);
- // activePlayer.setMode
+ await setVolume(volume);
+ // Gradually increase the volume
if (secondsToMaxVolume > 0) {
for (int i = 0; i <= 10; i++) {
Future.delayed(
Duration(milliseconds: i * (secondsToMaxVolume * 100)),
() {
- activePlayer?.setVolume((i / 10) * volume);
+ setVolume((i / 10) * volume);
},
);
}
diff --git a/lib/clock/logic/timezone_database.dart b/lib/clock/logic/timezone_database.dart
index c6435da2..ab490e1a 100644
--- a/lib/clock/logic/timezone_database.dart
+++ b/lib/clock/logic/timezone_database.dart
@@ -1,8 +1,12 @@
+import 'dart:convert';
import 'dart:io';
+import 'package:clock_app/debug/logic/logger.dart';
+import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:clock_app/common/data/paths.dart';
+import 'package:path/path.dart';
// Database? database;
Future initializeDatabases() async {
@@ -12,10 +16,11 @@ Future initializeDatabases() async {
if (FileSystemEntity.typeSync(timezonesDatabasePath) ==
FileSystemEntityType.notFound) {
// Load database from asset and copy
- ByteData data = await rootBundle.load('assets/timezones.db');
+ ByteData data = await rootBundle.load(join('assets', 'timezones.db'));
List bytes =
data.buffer.asUint8List(data.offsetInBytes, data.lengthInBytes);
+ logger.i('Copying timzones.db to $timezonesDatabasePath');
// Save copied asset to documents
await File(timezonesDatabasePath).writeAsBytes(bytes);
}
diff --git a/lib/clock/screens/clock_screen.dart b/lib/clock/screens/clock_screen.dart
index b6a28a26..38668467 100644
--- a/lib/clock/screens/clock_screen.dart
+++ b/lib/clock/screens/clock_screen.dart
@@ -73,6 +73,7 @@ class _ClockScreenState extends State {
onDelete: () => _listController.deleteItem(city)),
placeholderText: "No cities added",
isDuplicateEnabled: false,
+ isSelectable: true,
),
),
]),
diff --git a/lib/clock/types/city.dart b/lib/clock/types/city.dart
index 58898031..63d688a5 100644
--- a/lib/clock/types/city.dart
+++ b/lib/clock/types/city.dart
@@ -1,6 +1,6 @@
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/list_item.dart';
-import 'package:flutter/material.dart';
+import 'package:clock_app/common/utils/id.dart';
class City extends ListItem {
late String _name = "Unknown";
@@ -17,7 +17,7 @@ class City extends ListItem {
@override
bool get isDeletable => true;
- City(this._name, this._country, this._timezone) : _id = UniqueKey().hashCode;
+ City(this._name, this._country, this._timezone) : _id = getId();
@override
copy() {
@@ -26,13 +26,13 @@ class City extends ListItem {
City.fromJson(Json json) {
if (json == null) {
- _id = UniqueKey().hashCode;
+ _id = getId();
return;
}
_name = json['name'] ?? 'Unknown';
_country = json['country'] ?? 'Unknown';
_timezone = json['timezone'] ?? 'America/Detroit';
- _id = json['id'] ?? UniqueKey().hashCode;
+ _id = json['id'] ?? getId();
}
@override
diff --git a/lib/common/data/animations.dart b/lib/common/data/animations.dart
new file mode 100644
index 00000000..43484c46
--- /dev/null
+++ b/lib/common/data/animations.dart
@@ -0,0 +1,18 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_animate/flutter_animate.dart';
+
+extension AnimateWidgetExtensions on Widget {
+ Animate animateCard(dynamic key) => animate(delay: 50.ms, key: key)
+ .slideY(begin: 0.15, end: 0, duration: 150.ms, curve: Curves.easeOut)
+ .fade(duration: 150.ms, curve: Curves.easeOut);
+
+}
+
+extension AnimateListExtensions on List {
+ /// Wraps the target `List` in an [AnimateList] instance, and returns
+ /// the instance for chaining calls.
+ /// Ex. `[foo, bar].animate()` is equivalent to `AnimateList(children: [foo, bar])`.
+ AnimateList animateCardList() => animate(interval: 100.ms)
+ .slideY(begin: 0.15, end: 0, duration: 150.ms, curve: Curves.easeOut)
+ .fade(duration: 150.ms, curve: Curves.easeOut);
+}
diff --git a/lib/common/data/paths.dart b/lib/common/data/paths.dart
index b908ad93..d670eb47 100644
--- a/lib/common/data/paths.dart
+++ b/lib/common/data/paths.dart
@@ -41,3 +41,7 @@ String getRingtonesDirectoryPathSync() {
Future getTimezonesDatabasePath() async {
return path.join(await getAppDataDirectoryPath(), 'timezones.db');
}
+
+Future getLogsFilePath() async {
+ return path.join(await getAppDataDirectoryPath(), "logs.txt");
+}
diff --git a/lib/common/logic/card_decoration.dart b/lib/common/logic/card_decoration.dart
index 28246179..71e84180 100644
--- a/lib/common/logic/card_decoration.dart
+++ b/lib/common/logic/card_decoration.dart
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
BoxDecoration getCardDecoration(BuildContext context,
{Color? color,
bool showLightBorder = false,
+ bool isSelected = false,
showShadow = true,
elevationMultiplier = 1,
blurStyle = BlurStyle.normal}) {
@@ -12,7 +13,12 @@ BoxDecoration getCardDecoration(BuildContext context,
ThemeStyleExtension? themeStyle = theme.extension();
return BoxDecoration(
- border: showLightBorder
+ border: isSelected ? Border.all(
+ color: colorScheme.primary,
+ width: 1,
+ strokeAlign: BorderSide.strokeAlignOutside
+
+ ) : showLightBorder
? Border.all(
color: colorScheme.outline.withOpacity(0.2),
width: 0.5,
diff --git a/lib/common/logic/show_select.dart b/lib/common/logic/show_select.dart
index b060d6c8..c8ddcb5b 100644
--- a/lib/common/logic/show_select.dart
+++ b/lib/common/logic/show_select.dart
@@ -1,6 +1,7 @@
import 'package:clock_app/common/types/popup_action.dart';
import 'package:clock_app/common/types/select_choice.dart';
import 'package:clock_app/common/widgets/fields/select_field/select_bottom_sheet.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:flutter/material.dart';
Future showSelectBottomSheet(
@@ -40,7 +41,7 @@ Future showSelectBottomSheet(
if (indices.length == 1) {
currentSelectedIndices = [indices[0]];
} else {
- debugPrint("Too many indices");
+ logger.e("Too many indices in select bottom sheet");
}
}
});
diff --git a/lib/common/types/file_item.dart b/lib/common/types/file_item.dart
index a8bec429..d95a7e1d 100644
--- a/lib/common/types/file_item.dart
+++ b/lib/common/types/file_item.dart
@@ -2,8 +2,7 @@ import 'dart:convert';
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/list_item.dart';
-import 'package:clock_app/common/types/timer_state.dart';
-import 'package:flutter/material.dart';
+import 'package:clock_app/common/utils/id.dart';
enum FileItemType {
audio,
@@ -33,14 +32,14 @@ class FileItem extends ListItem {
bool get isDeletable => _isDeletable;
FileItem(this.name, this._uri, this._type, {isDeletable = true})
- : _id = UniqueKey().hashCode,
+ : _id = getId(),
_isDeletable = isDeletable;
@override
FileItem.fromJson(Json json)
: _id = json != null
- ? json['id'] ?? UniqueKey().hashCode
- : UniqueKey().hashCode,
+ ? json['id'] ?? getId()
+ : getId(),
_type = json != null
? json['type'] != null
? FileItemType.values
diff --git a/lib/common/types/list_filter.dart b/lib/common/types/list_filter.dart
index 7b7abe59..aefdafa4 100644
--- a/lib/common/types/list_filter.dart
+++ b/lib/common/types/list_filter.dart
@@ -1,5 +1,6 @@
import 'package:clock_app/common/types/list_item.dart';
-import 'package:clock_app/common/utils/debug.dart';
+import 'package:clock_app/common/utils/id.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:flutter/material.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
@@ -37,7 +38,7 @@ class ListFilter- extends ListFilterItem
- {
ListFilter(this.getLocalizedName, bool Function(Item) filterFunction,
{int? id})
- : _id = id ?? UniqueKey().hashCode,
+ : _id = id ?? getId(),
_filterFunction = filterFunction;
int get id => _id;
@@ -198,7 +199,7 @@ abstract class FilterSelect
-
try {
return selectedFilter.filterFunction;
} catch (e) {
- printDebug("Error in getting filter function($displayName): $e");
+ logger.d("Error in getting filter function($displayName): $e");
return (Item item) => true;
}
}
diff --git a/lib/common/types/tag.dart b/lib/common/types/tag.dart
index 9a921aeb..d665fbfe 100644
--- a/lib/common/types/tag.dart
+++ b/lib/common/types/tag.dart
@@ -1,5 +1,6 @@
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/types/list_item.dart';
+import 'package:clock_app/common/utils/id.dart';
import 'package:flutter/material.dart';
class Tag extends ListItem {
@@ -8,16 +9,16 @@ class Tag extends ListItem {
String description;
Color color;
Tag(this.name, {this.description = "", this.color = Colors.blue})
- : _id = UniqueKey().hashCode;
+ : _id = getId();
Tag.fromJson(Json json)
- : _id = json?['id'] ?? UniqueKey().hashCode,
+ : _id = json?['id'] ?? getId(),
name = json?['name'] ?? "Unknown",
description = json?['description'] ?? "",
color = Color(json?['color'] ?? 0);
Tag.from(Tag tag)
- : _id = UniqueKey().hashCode,
+ : _id = getId(),
name = tag.name,
description = tag.description,
color = tag.color;
diff --git a/lib/common/utils/debug.dart b/lib/common/utils/debug.dart
deleted file mode 100644
index f47f7237..00000000
--- a/lib/common/utils/debug.dart
+++ /dev/null
@@ -1,14 +0,0 @@
-import 'dart:isolate';
-
-import 'package:flutter/foundation.dart';
-
-void printIsolateInfo() {
- printDebug(
- "Isolate: ${Isolate.current.debugName}, id: ${Isolate.current.hashCode}");
-}
-
-void printDebug(String message) {
- if (kDebugMode) {
- print(message);
- }
-}
diff --git a/lib/common/utils/id.dart b/lib/common/utils/id.dart
new file mode 100644
index 00000000..36c50288
--- /dev/null
+++ b/lib/common/utils/id.dart
@@ -0,0 +1,5 @@
+import 'package:flutter/material.dart';
+
+int getId() {
+ return UniqueKey().hashCode;
+}
diff --git a/lib/common/utils/json_serialize.dart b/lib/common/utils/json_serialize.dart
index c052c95f..562335aa 100644
--- a/lib/common/utils/json_serialize.dart
+++ b/lib/common/utils/json_serialize.dart
@@ -9,6 +9,7 @@ import 'package:clock_app/common/types/tag.dart';
import 'package:clock_app/common/types/time.dart';
import 'package:clock_app/clock/types/city.dart';
import 'package:clock_app/common/types/json.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:clock_app/stopwatch/types/lap.dart';
import 'package:clock_app/stopwatch/types/stopwatch.dart';
import 'package:clock_app/theme/types/color_scheme.dart';
@@ -51,7 +52,7 @@ List listFromString(String encodedItems) {
List list = rawList.map((json) => fromJson(json)).toList();
return list;
} catch (e) {
- debugPrint("Error decoding string: ${e.toString()}");
+ logger.e("Error decoding string: ${e.toString()}");
rethrow;
}
}
diff --git a/lib/common/utils/list_storage.dart b/lib/common/utils/list_storage.dart
index 71383cf5..0743ff40 100644
--- a/lib/common/utils/list_storage.dart
+++ b/lib/common/utils/list_storage.dart
@@ -4,6 +4,7 @@ import 'dart:io';
import 'package:clock_app/common/data/paths.dart';
import 'package:clock_app/common/types/json.dart';
import 'package:clock_app/common/utils/json_serialize.dart';
+import 'package:clock_app/debug/logic/logger.dart';
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:path/path.dart' as path;
@@ -50,7 +51,7 @@ List loadListSync(String key) {
try{
return listFromString(loadTextFileSync(key));
}catch(e){
- debugPrint("Error loading list ($key): $e");
+ logger.e("Error loading list ($key): $e");
return [];
}
}
@@ -74,7 +75,7 @@ Future initTextFile(String key, String value) async {
if (GetStorage().read('init_$key') == null) {
GetStorage().write('init_$key', true);
if(!textFileExistsSync(key)){
- debugPrint("Initializing $key");
+ logger.i("Initializing $key");
await saveTextFile(key, value);
}
}
diff --git a/lib/common/utils/snackbar.dart b/lib/common/utils/snackbar.dart
index 267c2dfd..f99b2a6a 100644
--- a/lib/common/utils/snackbar.dart
+++ b/lib/common/utils/snackbar.dart
@@ -2,13 +2,17 @@ import 'package:clock_app/settings/data/settings_schema.dart';
import 'package:flutter/material.dart';
void showSnackBar(BuildContext context, String text,
- {bool fab = false, bool navBar = false}) {
+ {bool fab = false, bool navBar = false, bool error = false}) {
+ ThemeData theme = Theme.of(context);
+ ColorScheme colorScheme = theme.colorScheme;
+ Color? color = error ? colorScheme.error : null;
ScaffoldMessenger.of(context).removeCurrentSnackBar();
ScaffoldMessenger.of(context)
- .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar));
+ .showSnackBar(getSnackbar(text, fab: fab, navBar: navBar, color: color));
}
-SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) {
+SnackBar getSnackbar(String text,
+ {bool fab = false, bool navBar = false, Color? color}) {
double left = 20;
double right = 20;
double bottom = 12;
@@ -43,7 +47,9 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) {
content: ConstrainedBox(
constraints: const BoxConstraints(minHeight: 28),
child: Container(
+ padding: const EdgeInsets.all(16),
alignment: Alignment.centerLeft,
+ color: color,
// height: 28,
child: Text(text),
),
@@ -53,6 +59,7 @@ SnackBar getSnackbar(String text, {bool fab = false, bool navBar = false}) {
right: right,
bottom: bottom,
),
+ padding: EdgeInsets.zero,
elevation: 2,
dismissDirection: DismissDirection.none,
);
diff --git a/lib/common/widgets/animated_show_hide.dart b/lib/common/widgets/animated_show_hide.dart
new file mode 100644
index 00000000..2a78adbe
--- /dev/null
+++ b/lib/common/widgets/animated_show_hide.dart
@@ -0,0 +1,297 @@
+import 'package:flutter/material.dart';
+
+/// A typedef for a custom animation transition builder used by
+/// [AnimatedShowHide] and [AnimatedShowHideChild].
+///
+/// The [AnimatedShowHideTransitionBuilder] typedef represents a function that
+/// takes the current build context, an animation object, and the child widget as
+/// arguments and returns a widget. This function allows for custom animation
+/// transitions when showing or hiding a child widget.
+///
+/// The animation object provides information about the current state of the
+/// animation, including the value, which ranges from 0.0 to 1.0. You can use
+/// this information to control the appearance and behavior of the child widget
+/// during the transition.
+///
+/// {@tool snippet}
+/// This example shows how to use a custom animation transition builder to
+/// create a fade-in/fade-out animation.
+///
+/// ```dart
+/// AnimatedShowHide(
+/// child: const Text('Hello World!'),
+/// transitionBuilder: (context, animation, child) {
+/// return FadeTransition(
+/// opacity: animation,
+/// child: child,
+/// );
+/// },
+/// )
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+/// * [AnimatedShowHide]
+/// * [AnimatedShowHideChild]
+/// * [FadeTransition]
+typedef AnimatedShowHideTransitionBuilder = Widget Function(
+ BuildContext context,
+ Animation animation,
+ Widget? child,
+);
+
+/// A widget that manages the showing and hiding of a child widget based on
+/// animation.
+///
+/// The [AnimatedShowHide] widget uses an [AnimationController] to animate the
+/// showing and hiding of its child widget. The animation is controlled by the
+/// [animate] property, which determines whether the child widget should be shown
+/// or hidden.
+///
+/// The animation can be customized using the [duration], [curve], [axis], and
+/// [axisAlignment] properties. The [transitionBuilder] property can be used to
+/// provide a custom animation transition.
+///
+/// {@tool snippet}
+/// This example shows how to use the [AnimatedShowHide] widget to animate the
+/// showing and hiding of a child widget.
+///
+/// ```dart
+/// AnimatedShowHide(
+/// child: const Text('Hello World!'),
+/// animate: true,
+/// duration: const Duration(seconds: 1),
+/// curve: Curves.bounceInOut,
+/// axis: Axis.horizontal,
+/// axisAlignment: 0.5,
+/// )
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+/// * [AnimatedShowHideChild]
+/// * [AnimatedShowHideTransitionBuilder]
+/// * [SizeTransition]
+/// * [FadeTransition]
+class AnimatedShowHide extends StatelessWidget {
+ /// Creates a new [AnimatedShowHide] widget.
+ ///
+ /// The [child] property is the widget to be shown or hidden. The [animate]
+ /// property determines whether the child widget should be shown or hidden. The
+ /// [duration], [curve], [axis], and [axisAlignment] properties can be used to
+ /// customize the animation. The [transitionBuilder] property can be used to
+ /// provide a custom animation transition.
+ const AnimatedShowHide({
+ this.child,
+ this.animate = true,
+ this.duration = const Duration(milliseconds: 180),
+ this.curve = Curves.ease,
+ this.axis = Axis.vertical,
+ this.axisAlignment = -1,
+ this.transitionBuilder,
+ super.key,
+ });
+
+ /// The widget to be shown or hidden.
+ final Widget? child;
+
+ /// Whether to animate the showing and hiding of the child widget.
+ final bool animate;
+
+ /// The duration of the animation.
+ final Duration duration;
+
+ /// The curve of the animation.
+ final Curve curve;
+
+ /// The axis of the animation.
+ final Axis axis;
+
+ /// The axis alignment of the animation.
+ final double axisAlignment;
+
+ /// A custom animation transition builder.
+ final AnimatedShowHideTransitionBuilder? transitionBuilder;
+
+ Widget buildAnimationWidget(BuildContext context) {
+ return AnimatedShowHideChild(
+ transitionBuilder: transitionBuilder,
+ duration: duration,
+ curve: curve,
+ axis: axis,
+ axisAlignment: axisAlignment,
+ child: child,
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (animate) {
+ return buildAnimationWidget(context);
+ }
+ return child ?? const SizedBox();
+ }
+}
+
+/// A widget that manages the showing and hiding of a child widget based on animation.
+///
+/// The [AnimatedShowHideChild] widget uses an [AnimationController] to animate the
+/// showing and hiding of its child widget.
+///
+/// The animation can be customized using the [duration], [curve], [axis], and
+/// [axisAlignment] properties. The [transitionBuilder] property can be used to
+/// provide a custom animation transition.
+///
+/// {@tool snippet}
+/// This example shows how to use the [AnimatedShowHideChild] widget to animate the
+/// showing and hiding of a child widget.
+///
+/// ```dart
+/// AnimatedShowHideChild(
+/// child: show ? const Text('Hello World!') : null,
+/// animate: true,
+/// duration: const Duration(seconds: 1),
+/// curve: Curves.bounceInOut,
+/// axis: Axis.horizontal,
+/// axisAlignment: 0.5,
+/// )
+/// ```
+/// {@end-tool}
+///
+/// See also:
+///
+/// * [AnimatedShowHide]
+/// * [AnimatedShowHideTransitionBuilder]
+/// * [SizeTransition]
+/// * [FadeTransition]
+class AnimatedShowHideChild extends StatefulWidget {
+ /// Creates a new [AnimatedShowHideChild] widget.
+ ///
+ /// The [child] property is the widget to be shown or hidden. The
+ /// [duration], [curve], [axis], and [axisAlignment] properties can be used to
+ /// customize the animation. The [transitionBuilder] property can be used to
+ /// provide a custom animation transition.
+ const AnimatedShowHideChild({
+ this.child,
+ this.duration = const Duration(milliseconds: 180),
+ this.curve = Curves.ease,
+ this.axis = Axis.vertical,
+ this.axisAlignment = -1,
+ this.transitionBuilder,
+ super.key,
+ });
+
+ /// The widget to be shown or hidden.
+ final Widget? child;
+
+ /// The duration of the animation.
+ final Duration duration;
+
+ /// The curve of the animation.
+ final Curve curve;
+
+ /// The axis of the animation.
+ final Axis axis;
+
+ /// The axis alignment of the animation.
+ final double axisAlignment;
+
+ /// A custom animation transition builder.
+ final AnimatedShowHideTransitionBuilder? transitionBuilder;
+
+ @override
+ State createState() => _AnimatedShowHideChildState();
+}
+
+class _AnimatedShowHideChildState extends State
+ with SingleTickerProviderStateMixin {
+ AnimationController? controller;
+ late Animation animation;
+
+ void _listener() {
+ if (controller?.isDismissed ?? false) {
+ setState(() {
+ outGoingChild = const SizedBox();
+ });
+ }
+ }
+
+ Widget outGoingChild = const SizedBox();
+
+ @override
+ void initState() {
+ controller ??= AnimationController(vsync: this, duration: widget.duration);
+ controller!.addListener(_listener);
+ animation = CurvedAnimation(
+ parent: controller!.drive(Tween(begin: 0, end: 1)),
+ curve: widget.curve,
+ );
+ controller!.forward();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ controller?.removeListener(_listener);
+ controller?.dispose();
+ super.dispose();
+ }
+
+ @override
+ void didUpdateWidget(covariant AnimatedShowHideChild oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ animatedOnChanges(oldWidget);
+ }
+
+ // This method manages the changes in the animated widget and ensures the appropriate actions are taken based on the properties of the current and previous widgets.
+ //
+ // If the `transitionBuilder` property of the current widget is null, it checks the `child` property of the previous widget. If the previous child is not null, it sets `_outGoingChild` to the previous child; otherwise, it sets it to `SizedBox()`.
+ //
+ // If the `child` property of the current widget is null, it calls `reverse()` on `_controller`; otherwise, it calls `forward()`.
+ //
+ // If the `transitionBuilder` property is not null, it checks and sets `_outGoingChild` based on the transition builder's call with the context, animation, and child properties. It then decides whether to call `reverse()` or `forward()` on `_controller` based on the transition builder's call result.
+ void animatedOnChanges(covariant AnimatedShowHideChild oldWidget) {
+ if (widget.transitionBuilder == null) {
+ if (oldWidget.child != null) {
+ outGoingChild = oldWidget.child ?? const SizedBox();
+ }
+ if (widget.child == null) {
+ controller?.reverse();
+ } else {
+ controller?.forward();
+ }
+ } else {
+ if (oldWidget.transitionBuilder?.call(context, animation, widget.child) !=
+ null) {
+ outGoingChild = oldWidget.transitionBuilder
+ ?.call(context, animation, widget.child) ??
+ const SizedBox();
+ }
+ if (widget.transitionBuilder?.call(context, animation, widget.child) ==
+ null) {
+ controller?.reverse();
+ } else {
+ controller?.forward();
+ }
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ if (widget.transitionBuilder != null) {
+ return widget.transitionBuilder!(
+ context,
+ animation,
+ widget.child,
+ );
+ }
+ return SizeTransition(
+ sizeFactor: animation,
+ axisAlignment: widget.axisAlignment,
+ axis: widget.axis,
+ child: widget.child ?? outGoingChild,
+ );
+ }
+}
diff --git a/lib/common/widgets/card_container.dart b/lib/common/widgets/card_container.dart
index b430f80c..0cf3bea9 100644
--- a/lib/common/widgets/card_container.dart
+++ b/lib/common/widgets/card_container.dart
@@ -5,13 +5,12 @@ import 'package:clock_app/common/utils/color.dart';
import 'package:material_color_utilities/hct/hct.dart';
import 'package:material_color_utilities/palettes/tonal_palette.dart';
-
TonalPalette toTonalPalette(int value) {
final color = Hct.fromInt(value);
return TonalPalette.of(color.hue, color.chroma);
}
-Color getCardColor(BuildContext context, [Color? color]){
+Color getCardColor(BuildContext context, [Color? color]) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
bool useMaterialYou = appSettings
.getGroup("Appearance")
@@ -22,10 +21,10 @@ Color getCardColor(BuildContext context, [Color? color]){
TonalPalette tonalPalette = toTonalPalette(colorScheme.surface.value);
return color ??
- (useMaterialYou
- ? Color(tonalPalette.get(
- Theme.of(context).brightness == Brightness.light ? 96 : 15))
- : colorScheme.surface);
+ (useMaterialYou
+ ? Color(tonalPalette
+ .get(Theme.of(context).brightness == Brightness.light ? 96 : 15))
+ : colorScheme.surface);
}
class CardContainer extends StatelessWidget {
@@ -38,9 +37,9 @@ class CardContainer extends StatelessWidget {
this.onTap,
this.alignment,
this.showShadow = true,
-
+ this.isSelected = false,
this.showLightBorder = false,
- this.blurStyle = BlurStyle.normal,
+ this.blurStyle = BlurStyle.normal, this.onLongPress,
});
final Widget child;
@@ -48,10 +47,12 @@ class CardContainer extends StatelessWidget {
final Color? color;
final EdgeInsetsGeometry? margin;
final VoidCallback? onTap;
+ final VoidCallback? onLongPress;
final Alignment? alignment;
final bool showShadow;
final BlurStyle blurStyle;
final bool showLightBorder;
+ final bool isSelected;
// TonalPalette primaryTonalP = toTonalPalette(_primaryColor);
// primaryTonalP.get(50); // Getting the specific color
@@ -68,12 +69,14 @@ class CardContainer extends StatelessWidget {
ThemeData theme = Theme.of(context);
ColorScheme colorScheme = theme.colorScheme;
return Container(
+ // duration: const Duration(milliseconds: 100),
alignment: alignment,
margin: margin ?? const EdgeInsets.all(4),
clipBehavior: Clip.hardEdge,
decoration: getCardDecoration(
context,
color: cardColor,
+ isSelected: isSelected,
showLightBorder: showLightBorder,
showShadow: showShadow,
elevationMultiplier: elevationMultiplier,
@@ -84,6 +87,7 @@ class CardContainer extends StatelessWidget {
: Material(
color: Colors.transparent,
child: InkWell(
+ onLongPress: onLongPress,
onTap: onTap,
splashColor: cardColor.darken(0.075),
borderRadius: Theme.of(context).toggleButtonsTheme.borderRadius,
diff --git a/lib/common/widgets/fields/select_field/select_bottom_sheet.dart b/lib/common/widgets/fields/select_field/select_bottom_sheet.dart
index a19cdf19..c916c4e6 100644
--- a/lib/common/widgets/fields/select_field/select_bottom_sheet.dart
+++ b/lib/common/widgets/fields/select_field/select_bottom_sheet.dart
@@ -155,7 +155,7 @@ class SelectBottomSheet extends StatelessWidget {
//
// ],
// ),
- const SizedBox(height: 12.0),
+ // const SizedBox(height: 12.0),
Flexible(
child: _getOptionCard(),
),
diff --git a/lib/common/widgets/fields/slider_field.dart b/lib/common/widgets/fields/slider_field.dart
index ba242740..636d7170 100644
--- a/lib/common/widgets/fields/slider_field.dart
+++ b/lib/common/widgets/fields/slider_field.dart
@@ -11,9 +11,11 @@ class SliderField extends StatefulWidget {
required this.max,
required this.title,
this.unit = '',
- this.snapLength});
+ this.snapLength,
+ this.description = ''});
final String title;
+ final String description;
final double value;
final double min;
final double max;
@@ -89,6 +91,10 @@ class _SliderFieldState extends State {
widget.title,
style: textTheme.headlineMedium,
),
+ if (widget.description.isNotEmpty) ...[
+ const SizedBox(height: 2),
+ Text(widget.description, style: textTheme.bodyMedium)
+ ],
const SizedBox(height: 8.0),
Row(
children: [
@@ -97,7 +103,7 @@ class _SliderFieldState extends State {
// height: textSize.height,
// width: 50,
child: Row(
- // crossAxisAlignment: CrossAxisAlignment.end,
+ // crossAxisAlignment: CrossAxisAlignment.end,
children: [
IntrinsicWidth(
child: TextField(
diff --git a/lib/common/widgets/fields/switch_field.dart b/lib/common/widgets/fields/switch_field.dart
index f3d76b2c..89729337 100644
--- a/lib/common/widgets/fields/switch_field.dart
+++ b/lib/common/widgets/fields/switch_field.dart
@@ -2,13 +2,14 @@ import 'package:flutter/material.dart';
class SwitchField extends StatefulWidget {
const SwitchField(
- {Key? key,
+ {super.key,
required this.value,
required this.onChanged,
- required this.name})
- : super(key: key);
+ required this.name,
+ this.description = ""});
final String name;
+ final String description;
final bool value;
final void Function(bool value)? onChanged;
@@ -19,6 +20,9 @@ class SwitchField extends StatefulWidget {
class _SwitchFieldState extends State {
@override
Widget build(BuildContext context) {
+ ThemeData theme = Theme.of(context);
+ TextTheme textTheme = theme.textTheme;
+
return Material(
color: Colors.transparent,
child: InkWell(
@@ -29,9 +33,21 @@ class _SwitchFieldState extends State {
children: [
Expanded(
flex: 100,
- child: Text(
- widget.name,
- style: Theme.of(context).textTheme.headlineMedium,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 12.0),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ widget.name,
+ style: textTheme.headlineMedium,
+ ),
+ if (widget.description.isNotEmpty) ...[
+ const SizedBox(height: 4),
+ Text(widget.description, style: textTheme.bodyMedium)
+ ],
+ ],
+ ),
),
),
const Spacer(),
diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart
new file mode 100644
index 00000000..9c4c1ac1
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animated_gridview.dart
@@ -0,0 +1,207 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/gestures.dart';
+import 'builder/motion_list_base.dart';
+import 'builder/motion_list_impl.dart';
+
+/// enterTransition: [FadeEffect(), ScaleEffect()],
+///
+/// Effects are always run in parallel (ie. the fade and scale effects in the
+/// example above would be run simultaneously), but you can apply delays to
+/// offset them or run them in sequence.
+///
+/// A Flutter AnimatedGridView that animates insertion and removal of the item.
+class AnimatedGridView extends StatelessWidget {
+ /// The current list of items that this[MotionGridViewBuilder] should represent.
+ final List items;
+
+ ///Called, as needed, to build list item widget
+ final ItemBuilder itemBuilder;
+
+ /// Controls the layout of tiles in a grid.
+ /// Given the current constraints on the grid,
+ /// a SliverGridDelegate computes the layout for the tiles in the grid.
+ /// The tiles can be placed arbitrarily,
+ /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children.
+ final SliverGridDelegate sliverGridDelegate;
+
+ ///List of [AnimationEffect] used for the appearing animation when item is added in the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? enterTransition;
+
+ ///List of [AnimationEffect] used for the disappearing animation when item is removed from list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? exitTransition;
+
+ /// The duration of the animation when an item was inserted into the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration].
+ final Duration? insertDuration;
+
+ /// The duration of the animation when an item was removed from the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration].
+ final Duration? removeDuration;
+
+ /// The axis along which the scroll view scrolls.
+ ///
+ /// Defaults to [Axis.vertical].
+ final Axis scrollDirection;
+
+ /// {@template flutter.widgets.reorderable_list.padding}
+ /// The amount of space by which to inset the list contents.
+ ///
+ /// It defaults to `EdgeInsets.all(0)`.
+ /// {@endtemplate}
+ final EdgeInsetsGeometry? padding;
+
+ /// {@template flutter.widgets.scroll_view.reverse}
+ /// Whether the scroll view scrolls in the reading direction.
+ ///
+ /// For example, if the reading direction is left-to-right and
+ /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
+ /// left to right when [reverse] is false and from right to left when
+ /// [reverse] is true.
+ ///
+ /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
+ /// scrolls from top to bottom when [reverse] is false and from bottom to top
+ /// when [reverse] is true.
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ final bool reverse;
+
+ /// [ScrollController] to get the current scroll position.
+ ///
+ /// Must be null if [primary] is true.
+ ///
+ /// It can be used to read the current
+ // scroll position (see [ScrollController.offset]), or change it (see
+ // [ScrollController.animateTo]).
+ final ScrollController? controller;
+
+ /// When this is true, the scroll view is scrollable even if it does not have
+ /// sufficient content to actually scroll. Otherwise, by default the user can
+ /// only scroll the view if it has sufficient content. See [physics].
+ ///
+ /// Cannot be true while a [ScrollController] is provided to `controller`,
+ /// only one ScrollController can be associated with a ScrollView.
+ ///
+ /// Defaults to null.
+ final bool? primary;
+
+ /// How the scroll view should respond to user input.
+ ///
+ /// For example, determines how the scroll view continues to animate after the
+ /// user stops dragging the scroll view.
+ ///
+ /// Defaults to matching platform conventions. Furthermore, if [primary] is
+ /// false, then the user cannot scroll if there is insufficient content to
+ /// scroll, while if [primary] is true, they can always attempt to scroll.
+ final ScrollPhysics? physics;
+
+ /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
+ /// [ScrollPhysics] is provided in [physics], it will take precedence,
+ /// followed by [scrollBehavior], and then the inherited ancestor
+ /// [ScrollBehavior].
+ final ScrollBehavior? scrollBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final String? restorationId;
+
+ /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
+ /// dismiss the keyboard automatically.
+ final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
+
+ /// Defaults to [Clip.hardEdge].
+ ///
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final Clip clipBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final DragStartBehavior dragStartBehavior;
+
+ /// A custom builder that is for adding items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? insertItemBuilder;
+
+ /// A custom builder that is for removing items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? removeItemBuilder;
+
+ /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed.
+ final bool shrinkWrap;
+
+ /// A function that compares two items to determine whether they are the same.
+ final bool Function(E a, E b)? isSameItem;
+
+ const AnimatedGridView(
+ {Key? key,
+ required this.items,
+ required this.itemBuilder,
+ required this.sliverGridDelegate,
+ this.enterTransition,
+ this.exitTransition,
+ this.insertDuration,
+ this.removeDuration,
+ this.padding,
+ this.scrollDirection = Axis.vertical,
+ this.reverse = false,
+ this.controller,
+ this.primary,
+ this.physics,
+ this.scrollBehavior,
+ this.restorationId,
+ this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
+ this.dragStartBehavior = DragStartBehavior.start,
+ this.clipBehavior = Clip.hardEdge,
+ this.insertItemBuilder,
+ this.removeItemBuilder,
+ this.shrinkWrap = false,
+ this.isSameItem})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CustomScrollView(
+ scrollDirection: scrollDirection,
+ reverse: reverse,
+ controller: controller,
+ primary: primary,
+ physics: physics,
+ scrollBehavior: scrollBehavior,
+ restorationId: restorationId,
+ keyboardDismissBehavior: keyboardDismissBehavior,
+ dragStartBehavior: dragStartBehavior,
+ clipBehavior: clipBehavior,
+ shrinkWrap: shrinkWrap,
+ slivers: [
+ SliverPadding(
+ padding: padding ?? EdgeInsets.zero,
+ sliver: MotionListImpl.grid(
+ items: items,
+ itemBuilder: itemBuilder,
+ sliverGridDelegate: sliverGridDelegate,
+ insertDuration: insertDuration,
+ removeDuration: removeDuration,
+ enterTransition: enterTransition,
+ exitTransition: exitTransition,
+ scrollDirection: scrollDirection,
+ insertItemBuilder: insertItemBuilder,
+ removeItemBuilder: removeItemBuilder,
+ isSameItem: isSameItem),
+ ),
+ ]);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart
new file mode 100644
index 00000000..d304220e
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animated_listview.dart
@@ -0,0 +1,200 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/gestures.dart';
+import 'builder/motion_list_base.dart';
+import 'builder/motion_list_impl.dart';
+
+/// enterTransition: [FadeEffect(), ScaleEffect()],
+///
+/// Effects are always run in parallel (ie. the fade and scale effects in the
+/// example above would be run simultaneously), but you can apply delays to
+/// offset them or run them in sequence.
+/// A Flutter AnimatedGridView that animates insertion and removal of the item.
+class AnimatedListView extends StatelessWidget {
+ /// The current list of items that this[MotionListViewBuilder] should represent.
+ final List items;
+
+ ///Called, as needed, to build list item widget
+ final ItemBuilder itemBuilder;
+
+ ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? enterTransition;
+
+ ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? exitTransition;
+
+ /// The duration of the animation when an item was inserted into the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration].
+ final Duration? insertDuration;
+
+ /// The duration of the animation when an item was removed from the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration].
+ final Duration? removeDuration;
+
+ /// The axis along which the scroll view scrolls.
+ ///
+ /// Defaults to [Axis.vertical].
+ final Axis scrollDirection;
+
+ /// {@template flutter.widgets.scroll_view.reverse}
+ /// Whether the scroll view scrolls in the reading direction.
+ ///
+ /// For example, if the reading direction is left-to-right and
+ /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
+ /// left to right when [reverse] is false and from right to left when
+ /// [reverse] is true.
+ ///
+ /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
+ /// scrolls from top to bottom when [reverse] is false and from bottom to top
+ /// when [reverse] is true.
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ final bool reverse;
+
+ /// [ScrollController] to get the current scroll position.
+ ///
+ /// Must be null if [primary] is true.
+ ///
+ /// It can be used to read the current
+ // scroll position (see [ScrollController.offset]), or change it (see
+ // [ScrollController.animateTo]).
+ final ScrollController? controller;
+
+ /// When this is true, the scroll view is scrollable even if it does not have
+ /// sufficient content to actually scroll. Otherwise, by default the user can
+ /// only scroll the view if it has sufficient content. See [physics].
+ ///
+ /// Cannot be true while a [ScrollController] is provided to `controller`,
+ /// only one ScrollController can be associated with a ScrollView.
+ ///
+ /// Defaults to null.
+ final bool? primary;
+
+ /// {@template flutter.widgets.reorderable_list.padding}
+ /// The amount of space by which to inset the list contents.
+ ///
+ /// It defaults to `EdgeInsets.all(0)`.
+ /// {@endtemplate}
+ final EdgeInsetsGeometry? padding;
+
+ /// How the scroll view should respond to user input.
+ ///
+ /// For example, determines how the scroll view continues to animate after the
+ /// user stops dragging the scroll view.
+ ///
+ /// Defaults to matching platform conventions. Furthermore, if [primary] is
+ /// false, then the user cannot scroll if there is insufficient content to
+ /// scroll, while if [primary] is true, they can always attempt to scroll.
+ final ScrollPhysics? physics;
+
+ /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
+ /// [ScrollPhysics] is provided in [physics], it will take precedence,
+ /// followed by [scrollBehavior], and then the inherited ancestor
+ /// [ScrollBehavior].
+ final ScrollBehavior? scrollBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final String? restorationId;
+
+ /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
+ /// dismiss the keyboard automatically.
+ final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
+
+ /// Defaults to [Clip.hardEdge].
+ ///
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final Clip clipBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final DragStartBehavior dragStartBehavior;
+
+ /// A custom builder that is for adding items with animations.
+ ///
+ /// The `context` argument is the build context where the widget will be
+ /// created, the `index` is the index of the item to be built, and the
+ /// `animation` is an [Animation] that should be used to animate an entry
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? insertItemBuilder;
+
+ /// A custom builder that is for removing items with animations.
+ ///
+ /// The `context` argument is the build context where the widget will be
+ /// created, the `index` is the index of the item to be built, and the
+ /// `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? removeItemBuilder;
+
+ /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed.
+ final bool shrinkWrap;
+
+ /// A function that compares two items to determine whether they are the same.
+ final bool Function(E a, E b)? isSameItem;
+
+ const AnimatedListView({
+ Key? key,
+ required this.items,
+ required this.itemBuilder,
+ this.enterTransition,
+ this.exitTransition,
+ this.insertDuration,
+ this.removeDuration,
+ this.scrollDirection = Axis.vertical,
+ this.padding,
+ this.reverse = false,
+ this.controller,
+ this.primary,
+ this.physics,
+ this.scrollBehavior,
+ this.restorationId,
+ this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
+ this.dragStartBehavior = DragStartBehavior.start,
+ this.clipBehavior = Clip.hardEdge,
+ this.insertItemBuilder,
+ this.removeItemBuilder,
+ this.shrinkWrap = false,
+ this.isSameItem,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CustomScrollView(
+ scrollDirection: scrollDirection,
+ reverse: reverse,
+ controller: controller,
+ primary: primary,
+ physics: physics,
+ scrollBehavior: scrollBehavior,
+ restorationId: restorationId,
+ keyboardDismissBehavior: keyboardDismissBehavior,
+ dragStartBehavior: dragStartBehavior,
+ clipBehavior: clipBehavior,
+ shrinkWrap: shrinkWrap,
+ slivers: [
+ SliverPadding(
+ padding: padding ?? EdgeInsets.zero,
+ sliver: MotionListImpl(
+ items: items,
+ itemBuilder: itemBuilder,
+ enterTransition: enterTransition,
+ exitTransition: exitTransition,
+ insertDuration: insertDuration,
+ removeDuration: removeDuration,
+ scrollDirection: scrollDirection,
+ insertItemBuilder: insertItemBuilder,
+ removeItemBuilder: removeItemBuilder,
+ isSameItem: isSameItem,
+ ),
+ ),
+ ]);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart
new file mode 100644
index 00000000..98ebc732
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_gridview.dart
@@ -0,0 +1,257 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/gestures.dart';
+
+import 'builder/motion_list_base.dart';
+import 'builder/motion_list_impl.dart';
+
+///A GridView that enables users to interactively reorder items through dragging, with animated insertion and removal of items.
+///
+/// enterTransition: [FadeEffect(), ScaleEffect()],
+///
+/// Effects are always run in parallel (ie. the fade and scale effects in the
+/// example above would be run simultaneously), but you can apply delays to
+/// offset them or run them in sequence.
+///
+/// The [onReorder] parameter is required and will be called when a child
+/// widget is dragged to a new position.
+///
+///
+/// All list items must have a key.
+///
+/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator]
+/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is
+/// created with the original list item as its child.
+class AnimatedReorderableGridView extends StatelessWidget {
+ /// The current list of items that this[AnimatedReorderableGridView] should represent.
+ final List items;
+
+ ///Called, as needed, to build list item widget
+ final ItemBuilder itemBuilder;
+
+ /// Controls the layout of tiles in a grid.
+ /// Given the current constraints on the grid,
+ /// a SliverGridDelegate computes the layout for the tiles in the grid.
+ /// The tiles can be placed arbitrarily,
+ /// but it is more efficient to place tiles in roughly in order by scroll offset because grids reify a contiguous sequence of children.
+ final SliverGridDelegate sliverGridDelegate;
+
+ ///List of [AnimationEffect](s) used for the appearing animation when item is added in the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? enterTransition;
+
+ ///List of [AnimationEffect](s) used for the disappearing animation when item is removed from list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? exitTransition;
+
+ /// The duration of the animation when an item was inserted into the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration].
+ final Duration? insertDuration;
+
+ /// The duration of the animation when an item was removed from the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration].
+ final Duration? removeDuration;
+
+ /// A callback used by [ReorderableList] to report that a list item has moved
+ /// to a new position in the list.
+ ///
+ /// Implementations should remove the corresponding list item at [oldIndex]
+ /// and reinsert it at [newIndex].
+ final ReorderCallback onReorder;
+
+ /// A callback that is called when an item drag has started.
+ ///
+ /// The index parameter of the callback is the index of the selected item.
+ final void Function(int)? onReorderStart;
+
+ /// A callback that is called when the dragged item is dropped.
+ ///
+ /// The index parameter of the callback is the index where the item is
+ /// dropped. Unlike [onReorder], this is called even when the list item is
+ /// dropped in the same location.
+ final void Function(int)? onReorderEnd;
+
+ /// {@template flutter.widgets.reorderable_list.proxyDecorator}
+ /// A callback that allows the app to add an animated decoration around
+ /// an item when it is being dragged.
+ /// {@endtemplate}
+ final ReorderItemProxyDecorator? proxyDecorator;
+
+ /// The axis along which the scroll view scrolls.
+ ///
+ /// Defaults to [Axis.vertical].
+ final Axis scrollDirection;
+
+ /// {@template flutter.widgets.reorderable_list.padding}
+ /// The amount of space by which to inset the list contents.
+ ///
+ /// It defaults to `EdgeInsets.all(0)`.
+ /// {@endtemplate}
+ final EdgeInsetsGeometry? padding;
+
+ /// {@template flutter.widgets.scroll_view.reverse}
+ /// Whether the scroll view scrolls in the reading direction.
+ ///
+ /// For example, if the reading direction is left-to-right and
+ /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
+ /// left to right when [reverse] is false and from right to left when
+ /// [reverse] is true.
+ ///
+ /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
+ /// scrolls from top to bottom when [reverse] is false and from bottom to top
+ /// when [reverse] is true.
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ final bool reverse;
+
+ /// [ScrollController] to get the current scroll position.
+ ///
+ /// Must be null if [primary] is true.
+ ///
+ /// It can be used to read the current
+ // scroll position (see [ScrollController.offset]), or change it (see
+ // [ScrollController.animateTo]).
+ final ScrollController? controller;
+
+ /// When this is true, the scroll view is scrollable even if it does not have
+ /// sufficient content to actually scroll. Otherwise, by default the user can
+ /// only scroll the view if it has sufficient content. See [physics].
+ ///
+ /// Cannot be true while a [ScrollController] is provided to `controller`,
+ /// only one ScrollController can be associated with a ScrollView.
+ ///
+ /// Defaults to null.
+ final bool? primary;
+
+ /// How the scroll view should respond to user input.
+ ///
+ /// For example, determines how the scroll view continues to animate after the
+ /// user stops dragging the scroll view.
+ ///
+ /// Defaults to matching platform conventions. Furthermore, if [primary] is
+ /// false, then the user cannot scroll if there is insufficient content to
+ /// scroll, while if [primary] is true, they can always attempt to scroll.
+ final ScrollPhysics? physics;
+
+ /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
+ /// [ScrollPhysics] is provided in [physics], it will take precedence,
+ /// followed by [scrollBehavior], and then the inherited ancestor
+ /// [ScrollBehavior].
+ final ScrollBehavior? scrollBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final String? restorationId;
+
+ /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
+ /// dismiss the keyboard automatically.
+ final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
+
+ /// Defaults to [Clip.hardEdge].
+ ///
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final Clip clipBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final DragStartBehavior dragStartBehavior;
+
+ /// A custom builder that is for adding items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? insertItemBuilder;
+
+ /// A custom builder that is for removing items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? removeItemBuilder;
+
+ /// Whether the items can be dragged by long pressing on them.
+ final bool longPressDraggable;
+
+ /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed.
+ final bool shrinkWrap;
+
+ /// A function that compares two items to determine whether they are the same.
+ final bool Function(E a, E b)? isSameItem;
+
+ const AnimatedReorderableGridView(
+ {Key? key,
+ required this.items,
+ required this.itemBuilder,
+ required this.sliverGridDelegate,
+ required this.onReorder,
+ this.enterTransition,
+ this.exitTransition,
+ this.insertDuration,
+ this.removeDuration,
+ this.onReorderStart,
+ this.onReorderEnd,
+ this.proxyDecorator,
+ this.padding,
+ this.scrollDirection = Axis.vertical,
+ this.reverse = false,
+ this.controller,
+ this.primary,
+ this.physics,
+ this.scrollBehavior,
+ this.restorationId,
+ this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
+ this.dragStartBehavior = DragStartBehavior.start,
+ this.clipBehavior = Clip.hardEdge,
+ this.longPressDraggable = true,
+ this.shrinkWrap = false,
+ this.insertItemBuilder,
+ this.removeItemBuilder,
+ this.isSameItem})
+ : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CustomScrollView(
+ scrollDirection: scrollDirection,
+ reverse: reverse,
+ controller: controller,
+ primary: primary,
+ physics: physics,
+ scrollBehavior: scrollBehavior,
+ restorationId: restorationId,
+ keyboardDismissBehavior: keyboardDismissBehavior,
+ dragStartBehavior: dragStartBehavior,
+ clipBehavior: clipBehavior,
+ shrinkWrap: shrinkWrap,
+ slivers: [
+ SliverPadding(
+ padding: padding ?? EdgeInsets.zero,
+ sliver: MotionListImpl.grid(
+ items: items,
+ itemBuilder: itemBuilder,
+ sliverGridDelegate: sliverGridDelegate,
+ insertDuration: insertDuration,
+ removeDuration: removeDuration,
+ enterTransition: enterTransition,
+ exitTransition: exitTransition,
+ onReorder: onReorder,
+ onReorderStart: onReorderStart,
+ onReorderEnd: onReorderEnd,
+ proxyDecorator: proxyDecorator,
+ scrollDirection: scrollDirection,
+ insertItemBuilder: insertItemBuilder,
+ removeItemBuilder: removeItemBuilder,
+ longPressDraggable: longPressDraggable,
+ isSameItem: isSameItem,
+ ),
+ ),
+ ]);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart
new file mode 100644
index 00000000..483b5c82
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart
@@ -0,0 +1,273 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'builder/motion_list_base.dart';
+import 'builder/motion_list_impl.dart';
+
+///A [ListView] that enables users to interactively reorder items through dragging, with animated insertion and removal of items.
+///
+/// enterTransition: [FadeEffect(), ScaleEffect()],
+///
+/// Effects are always run in parallel (ie. the fade and scale effects in the
+/// example above would be run simultaneously), but you can apply delays to
+/// offset them or run them in sequence.
+///
+/// The [onReorder] parameter is required and will be called when a child
+/// widget is dragged to a new position.
+///
+/// By default, on [TargetPlatformVariant.desktop] platforms each item will
+/// have a drag handle added on top of it that will allow the user to grab it
+/// to move the item. On [TargetPlatformVariant.mobile], no drag handle will be
+/// added, but when the user long presses anywhere on the item it will start
+/// moving the item.Displaying drag handles can be controlled with [AnimatedReorderableListView.buildDefaultDragHandles].
+///
+/// All list items must have a key.
+///
+/// While a drag is underway, the widget returned by the [AnimatedReorderableGridView.proxyDecorator]
+/// callback serves as a "proxy" (a substitute) for the item in the list. The proxy is
+/// created with the original list item as its child.
+
+class AnimatedReorderableListView extends StatelessWidget {
+ /// The current list of items that this[AnimatedReorderableListView] should represent.
+ final List items;
+
+ ///Called, as needed, to build list item widget
+ final ItemBuilder itemBuilder;
+
+ ///List of [AnimationEffect](s) used for the appearing animation when an item was inserted into the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? enterTransition;
+
+ ///List of [AnimationEffect](s) used for the disappearing animation when an item was removed from the list.
+ ///
+ ///Defaults to [FadeAnimation()]
+ final List? exitTransition;
+
+ /// The duration of the animation when an item was inserted into the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [insertDuration].
+ final Duration? insertDuration;
+
+ /// The duration of the animation when an item was removed from the list.
+ ///
+ /// If you provide a specific duration for each AnimationEffect, it will override this [removeDuration].
+ final Duration? removeDuration;
+
+ /// A callback used by [ReorderableList] to report that a list item has moved
+ /// to a new position in the list.
+ ///
+ /// Implementations should remove the corresponding list item at [oldIndex]
+ /// and reinsert it at [newIndex].
+ final ReorderCallback onReorder;
+
+ /// A callback that is called when an item drag has started.
+ ///
+ /// The index parameter of the callback is the index of the selected item.
+ final void Function(int)? onReorderStart;
+
+ /// A callback that is called when the dragged item is dropped.
+ ///
+ /// The index parameter of the callback is the index where the item is
+ /// dropped. Unlike [onReorder], this is called even when the list item is
+ /// dropped in the same location.
+ final void Function(int)? onReorderEnd;
+
+ /// The axis along which the scroll view scrolls.
+ ///
+ /// Defaults to [Axis.vertical].
+ final Axis scrollDirection;
+
+ /// {@template flutter.widgets.reorderable_list.proxyDecorator}
+ /// A callback that allows the app to add an animated decoration around
+ /// an item when it is being dragged.
+ /// {@endtemplate}
+ final ReorderItemProxyDecorator? proxyDecorator;
+
+ /// If true, on desktop platforms, a drag handle is stacked over the center of each item's trailing edge;
+ /// on mobile platforms, a long press anywhere on the item starts a drag.
+ ///
+ /// The default desktop drag handle is just an [Icons.drag_handle] wrapped by [ReorderableDragStartListener].
+ /// On mobile platforms, the entire item is wrapped with a [ReorderableDragStartListener].
+ ///
+ /// To change the appearance or the layout of the drag handles, make this parameter false
+ /// and wrap each list item, or a widget within each list item, with [ReorderableDragStartListener]or
+ /// a subclass of [ReorderableDragStartListener].
+ ///
+ /// To get the idea [Flutter Example](https://api.flutter.dev/flutter/material/ReorderableListView/buildDefaultDragHandles.html)
+
+ final bool buildDefaultDragHandles;
+
+ /// {@template flutter.widgets.scroll_view.reverse}
+ /// Whether the scroll view scrolls in the reading direction.
+ ///
+ /// For example, if the reading direction is left-to-right and
+ /// [scrollDirection] is [Axis.horizontal], then the scroll view scrolls from
+ /// left to right when [reverse] is false and from right to left when
+ /// [reverse] is true.
+ ///
+ /// Similarly, if [scrollDirection] is [Axis.vertical], then the scroll view
+ /// scrolls from top to bottom when [reverse] is false and from bottom to top
+ /// when [reverse] is true.
+ ///
+ /// Defaults to false.
+ /// {@endtemplate}
+ final bool reverse;
+
+ /// [ScrollController] to get the current scroll position.
+ ///
+ /// Must be null if [primary] is true.
+ ///
+ /// It can be used to read the current
+ // scroll position (see [ScrollController.offset]), or change it (see
+ // [ScrollController.animateTo]).
+ final ScrollController? controller;
+
+ /// When this is true, the scroll view is scrollable even if it does not have
+ /// sufficient content to actually scroll. Otherwise, by default the user can
+ /// only scroll the view if it has sufficient content. See [physics].
+ ///
+ /// Cannot be true while a [ScrollController] is provided to `controller`,
+ /// only one ScrollController can be associated with a ScrollView.
+ ///
+ /// Defaults to null.
+ final bool? primary;
+
+ /// {@template flutter.widgets.reorderable_list.padding}
+ /// The amount of space by which to inset the list contents.
+ ///
+ /// It defaults to `EdgeInsets.all(0)`.
+ /// {@endtemplate}
+ final EdgeInsetsGeometry? padding;
+
+ /// How the scroll view should respond to user input.
+ ///
+ /// For example, determines how the scroll view continues to animate after the
+ /// user stops dragging the scroll view.
+ ///
+ /// Defaults to matching platform conventions. Furthermore, if [primary] is
+ /// false, then the user cannot scroll if there is insufficient content to
+ /// scroll, while if [primary] is true, they can always attempt to scroll.
+ final ScrollPhysics? physics;
+
+ /// [ScrollBehavior]s also provide [ScrollPhysics]. If an explicit
+ /// [ScrollPhysics] is provided in [physics], it will take precedence,
+ /// followed by [scrollBehavior], and then the inherited ancestor
+ /// [ScrollBehavior].
+ final ScrollBehavior? scrollBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final String? restorationId;
+
+ /// [ScrollViewKeyboardDismissBehavior] the defines how this [ScrollView] will
+ /// dismiss the keyboard automatically.
+ final ScrollViewKeyboardDismissBehavior keyboardDismissBehavior;
+
+ /// Defaults to [Clip.hardEdge].
+ ///
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final Clip clipBehavior;
+
+ /// Creates a ScrollView that creates custom scroll effects using slivers.
+ /// See the ScrollView constructor for more details on these arguments.
+ final DragStartBehavior dragStartBehavior;
+
+ /// A custom builder that is for adding items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? insertItemBuilder;
+
+ /// A custom builder that is for removing items with animations.
+ ///
+ /// The child argument is the widget that is returned by [itemBuilder],
+ /// and the `animation` is an [Animation] that should be used to animate an exit
+ /// transition for the widget that is built.
+ final AnimatedWidgetBuilder? removeItemBuilder;
+
+ /// Whether the items can be dragged by long pressing on them.
+ final bool longPressDraggable;
+
+ final bool useDefaultDragListeners;
+
+ /// Whether the extent of the scroll view in the scrollDirection should be determined by the contents being viewed.
+ final bool shrinkWrap;
+
+ /// A function that compares two items to determine whether they are the same.
+ final bool Function(E a, E b)? isSameItem;
+
+ const AnimatedReorderableListView({
+ Key? key,
+ required this.items,
+ required this.itemBuilder,
+ required this.onReorder,
+ this.enterTransition,
+ this.exitTransition,
+ this.insertDuration,
+ this.removeDuration,
+ this.onReorderStart,
+ this.onReorderEnd,
+ this.proxyDecorator,
+ this.scrollDirection = Axis.vertical,
+ this.padding,
+ this.reverse = false,
+ this.controller,
+ this.primary,
+ this.physics,
+ this.scrollBehavior,
+ this.restorationId,
+ this.buildDefaultDragHandles = true,
+ this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
+ this.dragStartBehavior = DragStartBehavior.start,
+ this.clipBehavior = Clip.hardEdge,
+ this.insertItemBuilder,
+ this.removeItemBuilder,
+ this.longPressDraggable = true,
+ this.useDefaultDragListeners = true,
+ this.shrinkWrap = false,
+ this.isSameItem,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) {
+ return CustomScrollView(
+ scrollDirection: scrollDirection,
+ reverse: reverse,
+ controller: controller,
+ primary: primary,
+ physics: physics,
+ scrollBehavior: scrollBehavior,
+ restorationId: restorationId,
+ keyboardDismissBehavior: keyboardDismissBehavior,
+ dragStartBehavior: dragStartBehavior,
+ clipBehavior: clipBehavior,
+ shrinkWrap: shrinkWrap,
+ slivers: [
+ SliverPadding(
+ padding: padding ?? EdgeInsets.zero,
+ sliver: MotionListImpl(
+ items: items,
+ itemBuilder: itemBuilder,
+ enterTransition: enterTransition,
+ exitTransition: exitTransition,
+ insertDuration: insertDuration,
+ removeDuration: removeDuration,
+ onReorder: onReorder,
+ onReorderStart: onReorderStart,
+ onReorderEnd: onReorderEnd,
+ proxyDecorator: proxyDecorator,
+ buildDefaultDragHandles: buildDefaultDragHandles,
+ scrollDirection: scrollDirection,
+ insertItemBuilder: insertItemBuilder,
+ removeItemBuilder: removeItemBuilder,
+ longPressDraggable: longPressDraggable,
+ useDefaultDragListeners: useDefaultDragListeners,
+ isSameItem: isSameItem,
+ ),
+ ),
+ ]);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart
new file mode 100644
index 00000000..5c50b12f
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/animation.dart
@@ -0,0 +1,14 @@
+export 'fade_in.dart';
+export 'flipin_x.dart';
+export 'flipin_y.dart';
+export 'landing.dart';
+export 'scale_in.dart';
+export 'scale_in_bottom.dart';
+export 'scale_in_left.dart';
+export 'scale_in_right.dart';
+export 'scale_in_top.dart';
+export 'slide_in_down.dart';
+export 'slide_in_left.dart';
+export 'slide_in_right.dart';
+export 'slide_in_up.dart';
+export 'size_animation.dart';
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart
new file mode 100644
index 00000000..d295db44
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/fade_in.dart
@@ -0,0 +1,23 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class FadeIn extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ FadeIn({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation opacity = buildAnimation(
+ entry,
+ begin: begin ?? beginValue,
+ end: end ?? endValue,
+ totalDuration)
+ .animate(animation);
+ return FadeTransition(opacity: opacity, child: child);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart
new file mode 100644
index 00000000..51361c08
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_x.dart
@@ -0,0 +1,31 @@
+import 'dart:math';
+
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class FlipInX extends AnimationEffect {
+ static const double beginValue = pi / 2;
+ static const double endValue = 0.0;
+ final double? begin;
+ final double? end;
+
+ FlipInX({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation rotation = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform(
+ transform: Matrix4.rotationX(rotation.value),
+ alignment: Alignment.center,
+ child: child,
+ );
+ },
+ child: child);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart
new file mode 100644
index 00000000..42844956
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/flipin_y.dart
@@ -0,0 +1,32 @@
+import 'dart:math';
+
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class FlipInY extends AnimationEffect {
+ static const double beginValue = pi / 2;
+ static const double endValue = 0.0;
+ final double? begin;
+ final double? end;
+
+ FlipInY({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation rotation = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return AnimatedBuilder(
+ animation: rotation,
+ builder: (BuildContext context, Widget? child) {
+ return Transform(
+ transform: Matrix4.rotationY(rotation.value),
+ alignment: Alignment.center,
+ child: child,
+ );
+ },
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart
new file mode 100644
index 00000000..d2f2c777
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/landing.dart
@@ -0,0 +1,26 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class Landing extends AnimationEffect {
+ static const double beginValue = 1.5;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ Landing({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: end ?? endValue)
+ .animate(animation);
+ return FadeTransition(
+ opacity: animation,
+ child: ScaleTransition(
+ scale: scale,
+ child: child,
+ ),
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart
new file mode 100644
index 00000000..0e177938
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/cupertino.dart';
+
+abstract class AnimationEffect {
+ /// The delay for this specific [AnimationEffect].
+ final Duration? delay;
+
+ /// The duration for the specific [AnimationEffect].
+ final Duration? duration;
+
+ /// The curve for the specific [AnimationEffect].
+ final Curve? curve;
+
+ AnimationEffect({
+ this.delay = Duration.zero,
+ this.duration = const Duration(milliseconds: 300),
+ this.curve,
+ });
+
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ return child;
+ }
+
+ Animatable buildAnimation(EffectEntry entry, Duration totalDuration,
+ {required T begin, required T end}) {
+ return Tween(begin: begin, end: end)
+ .chain(entry.buildAnimation(totalDuration: totalDuration));
+ }
+}
+
+@immutable
+class EffectEntry {
+ const EffectEntry({
+ required this.animationEffect,
+ required this.delay,
+ required this.duration,
+ required this.curve,
+ });
+
+ /// The delay for this entry.
+ final Duration delay;
+
+ /// The duration for this entry.
+ final Duration duration;
+
+ /// The curve used by this entry.
+ final Curve curve;
+
+ /// The effect associated with this entry.
+ final AnimationEffect animationEffect;
+
+ /// The begin time for this entry.
+ Duration get begin => delay;
+
+ /// The end time for this entry.
+ Duration get end => begin + duration;
+
+ /// Builds a sub-animation based on the properties of this entry.
+ CurveTween buildAnimation({
+ required Duration totalDuration,
+ Curve? curve,
+ }) {
+ int beginT = begin.inMicroseconds, endT = end.inMicroseconds;
+ return CurveTween(
+ curve: Interval(beginT / totalDuration.inMicroseconds,
+ endT / totalDuration.inMicroseconds,
+ curve: curve ?? this.curve),
+ );
+ }
+
+ @override
+ String toString() {
+ return "delay: $delay, Duration: $duration, curve: $curve, begin: $begin, end: $end, Effect: $animationEffect";
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart
new file mode 100644
index 00000000..339f93cb
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/provider/animation_type.dart
@@ -0,0 +1,16 @@
+enum AnimationType {
+ fadeIn,
+ flipInY,
+ flipInX,
+ landing,
+ size,
+ scaleIn,
+ scaleInTop,
+ scaleInBottom,
+ scaleInLeft,
+ scaleInRight,
+ slideInLeft,
+ slideInRight,
+ slideInDown,
+ slideInUp,
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart
new file mode 100644
index 00000000..7a158ac2
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in.dart
@@ -0,0 +1,23 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class ScaleIn extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ ScaleIn({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ScaleTransition(
+ scale: scale,
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart
new file mode 100644
index 00000000..844a2bcf
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_bottom.dart
@@ -0,0 +1,25 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class ScaleInBottom extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ ScaleInBottom(
+ {super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ScaleTransition(
+ alignment: Alignment.bottomCenter,
+ scale: scale,
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart
new file mode 100644
index 00000000..ed8c35d1
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_left.dart
@@ -0,0 +1,24 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class ScaleInLeft extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ ScaleInLeft({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ScaleTransition(
+ alignment: Alignment.centerLeft,
+ scale: scale,
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart
new file mode 100644
index 00000000..ea322822
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_right.dart
@@ -0,0 +1,25 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class ScaleInRight extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ ScaleInRight(
+ {super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ScaleTransition(
+ alignment: Alignment.centerRight,
+ scale: scale,
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart
new file mode 100644
index 00000000..77588d9a
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/scale_in_top.dart
@@ -0,0 +1,25 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+
+class ScaleInTop extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ final double? begin;
+ final double? end;
+
+ ScaleInTop({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation scale = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ScaleTransition(
+ alignment: Alignment.topCenter,
+ scale: scale,
+ child: child,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart
new file mode 100644
index 00000000..62a3df0e
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/size_animation.dart
@@ -0,0 +1,37 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class SizeAnimation extends AnimationEffect {
+ static const double beginValue = 0.0;
+ static const double endValue = 1.0;
+ static const double alignmentValue = 0.0;
+ final double? begin;
+ final double? end;
+ final Axis? axis;
+ final double? axisAlignment;
+
+ SizeAnimation(
+ {super.delay,
+ super.duration,
+ super.curve,
+ this.begin,
+ this.end,
+ this.axis,
+ this.axisAlignment});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation sizeFactor = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: end ?? endValue)
+ .animate(animation);
+ return Align(
+ child: SizeTransition(
+ sizeFactor: sizeFactor,
+ axis: axis ?? Axis.horizontal,
+ axisAlignment: axisAlignment ?? alignmentValue,
+ child: child,
+ ),
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart
new file mode 100644
index 00000000..3ebe8636
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_down.dart
@@ -0,0 +1,26 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class SlideInDown extends AnimationEffect {
+ static const Offset beginValue = Offset(0, 1);
+ static const Offset endValue = Offset(0, 0);
+ final Offset? begin;
+ final Offset? end;
+
+ SlideInDown({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation position = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: endValue)
+ .animate(animation);
+ return ClipRect(
+ clipBehavior: Clip.hardEdge,
+ child: SlideTransition(
+ position: position,
+ child: child,
+ ),
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart
new file mode 100644
index 00000000..7f21192a
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_left.dart
@@ -0,0 +1,22 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class SlideInLeft extends AnimationEffect {
+ static const Offset beginValue = Offset(-1, 0);
+ static const Offset endValue = Offset(0, 0);
+ final Offset? begin;
+ final Offset? end;
+
+ SlideInLeft({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation position = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: end ?? endValue)
+ .animate(animation);
+ return ClipRect(
+ clipBehavior: Clip.hardEdge,
+ child: SlideTransition(position: position, child: child));
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart
new file mode 100644
index 00000000..a6af2f25
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_right.dart
@@ -0,0 +1,23 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class SlideInRight extends AnimationEffect {
+ static const Offset beginValue = Offset(1, 0);
+ static const Offset endValue = Offset(0, 0);
+ final Offset? begin;
+ final Offset? end;
+
+ SlideInRight(
+ {super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation position = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: end ?? endValue)
+ .animate(animation);
+ return ClipRect(
+ clipBehavior: Clip.hardEdge,
+ child: SlideTransition(position: position, child: child));
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart
new file mode 100644
index 00000000..cafb2d93
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/animation/slide_in_up.dart
@@ -0,0 +1,22 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+
+class SlideInUp extends AnimationEffect {
+ static const Offset beginValue = Offset(0, -1);
+ static const Offset endValue = Offset(0, 0);
+ final Offset? begin;
+ final Offset? end;
+
+ SlideInUp({super.delay, super.duration, super.curve, this.begin, this.end});
+
+ @override
+ Widget build(BuildContext context, Widget child, Animation animation,
+ EffectEntry entry, Duration totalDuration) {
+ final Animation position = buildAnimation(entry, totalDuration,
+ begin: begin ?? beginValue, end: end ?? endValue)
+ .animate(animation);
+ return ClipRect(
+ clipBehavior: Clip.hardEdge,
+ child: SlideTransition(position: position, child: child));
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart
new file mode 100644
index 00000000..65bd1d39
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_animated_builder.dart
@@ -0,0 +1,903 @@
+import 'dart:math';
+
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart';
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/rendering.dart';
+import '../component/drag_listener.dart';
+import '../model/motion_data.dart';
+import 'motion_list_base.dart';
+
+part '../component/drag_item.dart';
+
+part '../component/motion_animated_content.dart';
+
+typedef CustomAnimatedWidgetBuilder = Widget Function(
+ BuildContext context, Widget child, Animation animation);
+
+class MotionBuilder extends StatefulWidget {
+ final CustomAnimatedWidgetBuilder insertAnimationBuilder;
+ final CustomAnimatedWidgetBuilder removeAnimationBuilder;
+ final ReorderCallback? onReorder;
+ final void Function(int index)? onReorderStart;
+ final void Function(int index)? onReorderEnd;
+
+ final ReorderItemProxyDecorator? proxyDecorator;
+ final ItemBuilder itemBuilder;
+ final int initialCount;
+ final Axis scrollDirection;
+ final SliverGridDelegate? delegateBuilder;
+ final bool buildDefaultDragHandles;
+ final bool longPressDraggable;
+ final bool useDefaultDragListeners;
+
+ const MotionBuilder(
+ {Key? key,
+ required this.itemBuilder,
+ required this.insertAnimationBuilder,
+ required this.removeAnimationBuilder,
+ this.onReorder,
+ this.onReorderEnd,
+ this.onReorderStart,
+ this.proxyDecorator,
+ this.initialCount = 0,
+ this.delegateBuilder,
+ this.scrollDirection = Axis.vertical,
+ required this.buildDefaultDragHandles,
+ required this.useDefaultDragListeners,
+ this.longPressDraggable = false})
+ : assert(initialCount >= 0),
+ super(key: key);
+
+ @override
+ State createState() => MotionBuilderState();
+
+ static MotionBuilderState of(BuildContext context) {
+ final MotionBuilderState? result =
+ context.findAncestorStateOfType();
+ assert(() {
+ if (result == null) {
+ throw FlutterError(
+ 'MotionBuilderState.of() called with a context that does not contain a MotionBuilderState.\n'
+ 'No MotionBuilderState ancestor could be found starting from the '
+ 'context that was passed to MotionBuilderState.of(). This can '
+ 'happen when the context provided is from the same StatefulWidget that '
+ 'built the AnimatedList.'
+ 'The context used was:\n'
+ ' $context',
+ );
+ }
+ return true;
+ }());
+ return result!;
+ }
+
+ static MotionBuilderState? maybeOf(BuildContext context) {
+ return context.findAncestorStateOfType();
+ }
+}
+
+class MotionBuilderState extends State
+ with TickerProviderStateMixin, AutomaticKeepAliveClientMixin {
+ final List<_ActiveItem> _incomingItems = <_ActiveItem>[];
+ final List<_ActiveItem> _outgoingItems = <_ActiveItem>[];
+ int _itemsCount = 0;
+
+ Map childrenMap = {};
+ final Map _items =
+ {};
+
+ OverlayEntry? _overlayEntry;
+ int? _dragIndex;
+ _DragInfo? _dragInfo;
+ int? _insertIndex;
+ Offset? _finalDropPosition;
+ MultiDragGestureRecognizer? _recognizer;
+ int? _recognizerPointer;
+ EdgeDraggingAutoScroller? _autoScroller;
+ late ScrollableState _scrollable;
+
+ bool autoScrolling = false;
+
+ Axis get scrollDirection => axisDirectionToAxis(_scrollable.axisDirection);
+
+ bool get _reverse =>
+ _scrollable.axisDirection == AxisDirection.up ||
+ _scrollable.axisDirection == AxisDirection.left;
+
+ bool get isGrid => widget.delegateBuilder != null;
+
+ @override
+ bool get wantKeepAlive => false;
+
+ @override
+ void initState() {
+ _itemsCount = widget.initialCount;
+ for (int i = 0; i < widget.initialCount; i++) {
+ childrenMap[i] = MotionData();
+ }
+
+ super.initState();
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ _scrollable = Scrollable.of(context);
+ }
+
+ @override
+ void didUpdateWidget(covariant MotionBuilder oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.initialCount != oldWidget.initialCount) {
+ cancelReorder();
+ }
+ }
+
+ void startItemDragReorder(
+ {required int index,
+ required PointerDownEvent event,
+ required MultiDragGestureRecognizer recognizer}) {
+ assert(0 <= index && index < _itemsCount);
+ setState(() {
+ if (_dragInfo != null) {
+ cancelReorder();
+ } else if (_recognizer != null && _recognizerPointer != event.pointer) {
+ _recognizer!.dispose();
+ _recognizer = null;
+ _recognizerPointer = null;
+ }
+ if (_items.containsKey(index)) {
+ _dragIndex = index;
+ _recognizer = recognizer
+ ..onStart = _dragStart
+ ..addPointer(event);
+ _recognizerPointer = event.pointer;
+ } else {
+ throw Exception("Attempting ro start drag on a non-visible item");
+ }
+ });
+ }
+
+ Drag? _dragStart(Offset position) {
+ assert(_dragInfo == null);
+ final MotionAnimatedContentState item = _items[_dragIndex]!;
+ item.dragging = true;
+ widget.onReorderStart?.call(_dragIndex!);
+ item.rebuild();
+ _insertIndex = item.index;
+ _dragInfo = _DragInfo(
+ item: item,
+ initialPosition: position,
+ scrollDirection: scrollDirection,
+ gridView: isGrid,
+ onUpdate: _dragUpdate,
+ onCancel: _dragCancel,
+ onEnd: _dragEnd,
+ onDragCompleted: _dropCompleted,
+ proxyDecorator: widget.proxyDecorator,
+ tickerProvider: this);
+
+ _dragInfo!.startDrag();
+ item.dragSize = _dragInfo!.itemSize;
+
+ final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
+ assert(_overlayEntry == null);
+ _overlayEntry = OverlayEntry(builder: _dragInfo!.createProxy);
+ overlay.insert(_overlayEntry!);
+
+ for (final MotionAnimatedContentState childItem in _items.values) {
+ if (childItem == item || !childItem.mounted) {
+ continue;
+ }
+ item.updateForGap(false);
+ }
+ return _dragInfo;
+ }
+
+ void _dragUpdate(_DragInfo item, Offset position, Offset delta) {
+ setState(() {
+ _overlayEntry?.markNeedsBuild();
+ _dragUpdateItems();
+ _autoScrollIfNecessary();
+ });
+ }
+
+ void _dragCancel(_DragInfo item) {
+ setState(() {
+ _dragReset();
+ });
+ }
+
+ Future _autoScrollIfNecessary() async {
+ if (autoScrolling || _dragInfo == null || _dragInfo!.scrollable == null) {
+ return;
+ }
+
+ final position = _dragInfo!.scrollable!.position;
+ double? newOffset;
+
+ const duration = Duration(milliseconds: 14);
+ const step = 1.0;
+ const overDragMax = 20.0;
+ const overDragCoef = 10;
+
+ final isVertical = widget.scrollDirection == Axis.vertical;
+
+ /// get the scroll window position on the screen
+ final scrollRenderBox =
+ _dragInfo!.scrollable!.context.findRenderObject()! as RenderBox;
+ final Offset scrollPosition = scrollRenderBox.localToGlobal(Offset.zero);
+
+ /// calculate the start and end position for the scroll window
+ double scrollWindowStart =
+ isVertical ? scrollPosition.dy : scrollPosition.dx;
+ double scrollWindowEnd = scrollWindowStart +
+ (isVertical ? scrollRenderBox.size.height : scrollRenderBox.size.width);
+
+ /// get the proxy (dragged) object's position on the screen
+ final proxyObjectPosition = _dragInfo!.dragPosition - _dragInfo!.dragOffset;
+
+ /// calculate the start and end position for the proxy object
+ double proxyObjectStart =
+ isVertical ? proxyObjectPosition.dy : proxyObjectPosition.dx;
+ double proxyObjectEnd = proxyObjectStart +
+ (isVertical ? _dragInfo!.itemSize.height : _dragInfo!.itemSize.width);
+
+ if (!_reverse) {
+ /// if start of proxy object is before scroll window
+ if (proxyObjectStart < scrollWindowStart &&
+ position.pixels > position.minScrollExtent) {
+ final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax);
+ newOffset = max(position.minScrollExtent,
+ position.pixels - step * overDrag / overDragCoef);
+ }
+
+ /// if end of proxy object is after scroll window
+ else if (proxyObjectEnd > scrollWindowEnd &&
+ position.pixels < position.maxScrollExtent) {
+ final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax);
+ newOffset = min(position.maxScrollExtent,
+ position.pixels + step * overDrag / overDragCoef);
+ }
+ } else {
+ /// if start of proxy object is before scroll window
+ if (proxyObjectStart < scrollWindowStart &&
+ position.pixels < position.maxScrollExtent) {
+ final overDrag = max(scrollWindowStart - proxyObjectStart, overDragMax);
+ newOffset = max(position.minScrollExtent,
+ position.pixels + step * overDrag / overDragCoef);
+ }
+
+ /// if end of proxy object is after scroll window
+ else if (proxyObjectEnd > scrollWindowEnd &&
+ position.pixels > position.minScrollExtent) {
+ final overDrag = max(proxyObjectEnd - scrollWindowEnd, overDragMax);
+ newOffset = min(position.maxScrollExtent,
+ position.pixels - step * overDrag / overDragCoef);
+ }
+ }
+
+ if (newOffset != null && (newOffset - position.pixels).abs() >= 1.0) {
+ autoScrolling = true;
+ await position.animateTo(
+ newOffset,
+ duration: duration,
+ curve: Curves.linear,
+ );
+ autoScrolling = false;
+ if (_dragInfo != null) {
+ _dragUpdateItems();
+ _autoScrollIfNecessary();
+ }
+ }
+ }
+
+ void _dragEnd(_DragInfo item) {
+ widget.onReorderEnd?.call(_insertIndex!);
+ setState(() => _finalDropPosition = _itemOffsetAt(_insertIndex!));
+ }
+
+ void _dropCompleted() {
+ final int fromIndex = _dragIndex!;
+ final int toIndex = _insertIndex!;
+ if (fromIndex != toIndex) {
+ widget.onReorder?.call(fromIndex, toIndex);
+ }
+ setState(() {
+ _dragReset();
+ });
+ }
+
+ void cancelReorder() {
+ setState(() {
+ _dragReset();
+ });
+ }
+
+ void _dragReset() {
+ if (_dragInfo != null) {
+ if (_dragIndex != null && _items.containsKey(_dragIndex)) {
+ final MotionAnimatedContentState dragItem = _items[_dragIndex]!;
+ dragItem.dragging = false;
+ dragItem.dragSize = Size.zero;
+ dragItem.rebuild();
+ _dragIndex = null;
+ }
+ _dragInfo?.dispose();
+ _dragInfo = null;
+ _autoScroller?.stopAutoScroll();
+ _resetItemGap();
+ _recognizer?.dispose();
+ _recognizer = null;
+ _overlayEntry?.remove();
+ _overlayEntry?.dispose();
+ _overlayEntry = null;
+ _finalDropPosition = null;
+ }
+ }
+
+ void _resetItemGap() {
+ for (final MotionAnimatedContentState item in _items.values) {
+ item.resetGap();
+ }
+ }
+
+ void _dragUpdateItems() {
+ assert(_dragInfo != null);
+
+ int newIndex = _insertIndex!;
+
+ final dragCenter = _dragInfo!.itemSize
+ .center(_dragInfo!.dragPosition - _dragInfo!.dragOffset);
+
+ for (final MotionAnimatedContentState item in _items.values) {
+ if (!item.mounted) continue;
+ final Rect geometry = item.targetGeometryNonOffset();
+
+ if (geometry.contains(dragCenter)) {
+ newIndex = item.index;
+ break;
+ }
+ }
+
+ if (newIndex == _insertIndex) return;
+ _insertIndex = newIndex;
+
+ for (final MotionAnimatedContentState item in _items.values) {
+ if (item.index == _dragIndex) continue;
+ item.updateForGap(true);
+ }
+ }
+
+ Offset calculateNextDragOffset(int index) {
+ int minPos = min(_dragIndex!, _insertIndex!);
+ int maxPos = max(_dragIndex!, _insertIndex!);
+ if (index < minPos || index > maxPos) return Offset.zero;
+
+ final int direction = _insertIndex! > _dragIndex! ? -1 : 1;
+ if (isGrid) {
+ return _itemOffsetAt(index + direction) - _itemOffsetAt(index);
+ } else {
+ final Offset offset =
+ _extentOffset(_dragInfo!.itemExtent, scrollDirection);
+ return _insertIndex! > _dragIndex! ? -offset : offset;
+ }
+ }
+
+ void registerItem(MotionAnimatedContentState item) {
+ _items[item.index] = item;
+ if (item.index == _dragInfo?.index) {
+ item.dragging = true;
+ item.dragSize = _dragInfo!.itemSize;
+ item.rebuild();
+ }
+ }
+
+ void unregisterItem(int index, MotionAnimatedContentState item) {
+ final MotionAnimatedContentState? currentItem = _items[index];
+ if (currentItem == item) {
+ _items.remove(index);
+ }
+ }
+
+ @override
+ void dispose() {
+ for (final _ActiveItem item in _incomingItems.followedBy(_outgoingItems)) {
+ item.controller?.dispose();
+ }
+ _dragReset();
+ super.dispose();
+ }
+
+ _ActiveItem? _removeActiveItemAt(List<_ActiveItem> items, int itemIndex) {
+ final int i = binarySearch(items, _ActiveItem.index(itemIndex));
+ return i == -1 ? null : items.removeAt(i);
+ }
+
+ _ActiveItem? _activeItemAt(List<_ActiveItem> items, int itemIndex) {
+ final int i = binarySearch(items, _ActiveItem.index(itemIndex));
+ return i == -1 ? null : items[i];
+ }
+
+ int _indexToItemIndex(int index) {
+ int itemIndex = index;
+ for (final _ActiveItem item in _outgoingItems) {
+ if (item.itemIndex <= itemIndex) {
+ itemIndex += 1;
+ } else {
+ break;
+ }
+ }
+ return itemIndex;
+ }
+
+ int _itemIndexToIndex(int itemIndex) {
+ int index = itemIndex;
+ for (final _ActiveItem item in _outgoingItems) {
+ assert(item.itemIndex != itemIndex);
+ if (item.itemIndex < itemIndex) {
+ index -= 1;
+ } else {
+ break;
+ }
+ }
+ return index;
+ }
+
+ void insertItem(int index, {required Duration insertDuration}) {
+ assert(index >= 0);
+ final int itemIndex = _indexToItemIndex(index);
+ assert(itemIndex >= 0 && itemIndex <= _itemsCount);
+
+ for (final _ActiveItem item in _incomingItems) {
+ if (item.itemIndex >= itemIndex) item.itemIndex += 1;
+ }
+ for (final _ActiveItem item in _outgoingItems) {
+ if (item.itemIndex >= itemIndex) item.itemIndex += 1;
+ }
+
+ final AnimationController controller = AnimationController(
+ duration: insertDuration,
+ vsync: this,
+ );
+ final AnimationController sizeController = AnimationController(
+ duration: kAnimationDuration,
+ vsync: this,
+ );
+
+ final _ActiveItem incomingItem = _ActiveItem.animation(
+ controller,
+ itemIndex,
+ sizeController,
+ );
+
+ _incomingItems
+ ..add(incomingItem)
+ ..sort();
+
+ final motionData =
+ MotionData(endOffset: Offset.zero, startOffset: Offset.zero);
+
+ final updatedChildrenMap = {};
+
+ if (childrenMap.containsKey(itemIndex)) {
+ for (final entry in childrenMap.entries) {
+ if (entry.key == itemIndex) {
+ updatedChildrenMap[itemIndex] = motionData.copyWith(visible: false);
+ updatedChildrenMap[entry.key + 1] = entry.value.copyWith(
+ startOffset: _itemOffsetAt(entry.key),
+ endOffset: getChildOffset(entry.key));
+ } else if (entry.key > itemIndex) {
+ updatedChildrenMap[entry.key + 1] = entry.value.copyWith(
+ startOffset: _itemOffsetAt(entry.key),
+ endOffset: getChildOffset(entry.key));
+ } else {
+ updatedChildrenMap[entry.key] = entry.value;
+ }
+ }
+ childrenMap.clear();
+ childrenMap.addAll(updatedChildrenMap);
+ sizeController.forward().then((value) {
+ controller.forward().then((_) {
+ _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!
+ .controller!
+ .dispose();
+ });
+ });
+ } else {
+ childrenMap[itemIndex] = motionData;
+ sizeController.value = kAlwaysCompleteAnimation.value;
+ controller.forward().then((_) {
+ _removeActiveItemAt(_incomingItems, incomingItem.itemIndex)!
+ .controller!
+ .dispose();
+ });
+ }
+ setState(() {
+ _itemsCount += 1;
+ });
+ }
+
+ void removeItem(int index, {required Duration removeItemDuration}) {
+ assert(index >= 0);
+ final int itemIndex = _indexToItemIndex(index);
+ if (itemIndex < 0 || itemIndex >= _itemsCount) {
+ return;
+ }
+ assert(itemIndex >= 0 && itemIndex < _itemsCount);
+
+ assert(_activeItemAt(_outgoingItems, itemIndex) == null);
+
+ if (childrenMap.containsKey(itemIndex)) {
+ final _ActiveItem? incomingItem =
+ _removeActiveItemAt(_incomingItems, itemIndex);
+
+ final AnimationController sizeController = incomingItem?.sizeAnimation ??
+ AnimationController(
+ vsync: this, duration: kAnimationDuration, value: 1.0);
+ final AnimationController controller = incomingItem?.controller ??
+ AnimationController(
+ duration: removeItemDuration, value: 1.0, vsync: this)
+ ..addStatusListener((status) => ());
+ final _ActiveItem outgoingItem =
+ _ActiveItem.animation(controller, itemIndex, sizeController);
+ _outgoingItems
+ ..add(outgoingItem)
+ ..sort();
+
+ controller.reverse().then((void value) {
+ if (controller.status == AnimationStatus.dismissed) {
+ if (childrenMap.containsKey(itemIndex)) {
+ childrenMap.update(
+ itemIndex, (value) => value.copyWith(visible: false));
+ }
+ sizeController.reverse(from: 1.0).then((value) {
+ final removedItem =
+ _removeActiveItemAt(_outgoingItems, outgoingItem.itemIndex)!;
+ removedItem.controller!.dispose();
+ removedItem.sizeAnimation!.dispose();
+
+ // Decrement the incoming and outgoing item indices to account
+ // for the removal.
+ for (final _ActiveItem item in _incomingItems) {
+ if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1;
+ }
+ for (final _ActiveItem item in _outgoingItems) {
+ if (item.itemIndex > outgoingItem.itemIndex) item.itemIndex -= 1;
+ }
+ _onItemRemoved(itemIndex, removeItemDuration);
+ });
+ }
+ });
+ }
+ }
+
+ void _onItemRemoved(int itemIndex, Duration removeDuration) {
+ final updatedChildrenMap = {};
+ if (childrenMap.containsKey(itemIndex)) {
+ for (final entry in childrenMap.entries) {
+ if (entry.key < itemIndex) {
+ updatedChildrenMap[entry.key] = childrenMap[entry.key]!;
+ } else if (entry.key == itemIndex) {
+ continue;
+ } else {
+ updatedChildrenMap[entry.key - 1] = childrenMap[entry.key]!.copyWith(
+ startOffset: _itemOffsetAt(entry.key),
+ endOffset: _itemOffsetAt(entry.key - 1));
+ }
+ }
+ }
+ childrenMap.clear();
+ childrenMap.addAll(updatedChildrenMap);
+
+ setState(() => _itemsCount -= 1);
+ }
+
+ Offset getChildOffset(int index) {
+ final currentOffset = _itemOffsetAt(index);
+ if (!isGrid) {
+ return currentOffset;
+ }
+
+ if (widget.delegateBuilder
+ is SliverReorderableGridDelegateWithFixedCrossAxisCount) {
+ final delegateBuilder = widget.delegateBuilder
+ as SliverReorderableGridDelegateWithFixedCrossAxisCount;
+ return delegateBuilder.getOffset(index, currentOffset);
+ } else if (widget.delegateBuilder
+ is SliverReorderableGridWithMaxCrossAxisExtent) {
+ final delegateBuilder =
+ widget.delegateBuilder as SliverReorderableGridWithMaxCrossAxisExtent;
+ final offset = delegateBuilder.getOffset(index, currentOffset);
+ return offset;
+ }
+ return Offset.zero;
+ }
+
+ Offset _itemOffsetAt(int index) {
+ final itemRenderBox =
+ _items[index]?.context.findRenderObject() as RenderBox?;
+ if (itemRenderBox == null) return Offset.zero;
+ return itemRenderBox.localToGlobal(Offset.zero);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+ return widget.delegateBuilder != null
+ ? SliverGrid(
+ gridDelegate: widget.delegateBuilder!, delegate: _createDelegate())
+ : SliverList(delegate: _createDelegate());
+ }
+
+ Widget _itemBuilder(BuildContext context, int index) {
+ final _ActiveItem? outgoingItem = _activeItemAt(_outgoingItems, index);
+ final _ActiveItem? incomingItem = _activeItemAt(_incomingItems, index);
+
+ if (outgoingItem != null) {
+ final child = _items[index]!.widget;
+ return _removeItemBuilder(outgoingItem, child);
+ }
+ if (_dragInfo != null && index >= _itemsCount) {
+ return SizedBox.fromSize(size: _dragInfo!.itemSize);
+ }
+
+ final Widget child = widget.onReorder != null
+ ? reorderableItemBuilder(context, _itemIndexToIndex(index))
+ : widget.itemBuilder(context, _itemIndexToIndex(index));
+
+ assert(() {
+ if (child.key == null) {
+ throw FlutterError(
+ 'Every item of AnimatedReorderableList must have a unique key.',
+ );
+ }
+ return true;
+ }());
+
+ final Key itemGlobalKey = _MotionBuilderItemGlobalKey(child.key!, this);
+ final Widget builder = _insertItemBuilder(incomingItem, child);
+
+ final motionData = childrenMap[index];
+ if (motionData == null) return builder;
+ final OverlayState overlay = Overlay.of(context, debugRequiredFor: widget);
+
+ return MotionAnimatedContent(
+ index: index,
+ key: itemGlobalKey,
+ motionData: motionData,
+ isGrid: isGrid,
+ updateMotionData: (MotionData motionData) {
+ final itemOffset = _itemOffsetAt(index);
+ childrenMap[index] = motionData.copyWith(
+ startOffset: itemOffset, endOffset: itemOffset, visible: true);
+ },
+ capturedThemes:
+ InheritedTheme.capture(from: context, to: overlay.context),
+ child: builder,
+ );
+ }
+
+ SliverChildDelegate _createDelegate() {
+ return SliverChildBuilderDelegate(_itemBuilder, childCount: _itemsCount);
+ }
+
+ Widget reorderableItemBuilder(BuildContext context, int index) {
+ final Widget item = widget.itemBuilder(context, index);
+
+ assert(() {
+ if (item.key == null) {
+ throw FlutterError(
+ 'Every item of AnimatedReorderableList must have a key.',
+ );
+ }
+ return true;
+ }());
+ final Key itemGlobalKey = _MotionBuilderItemGlobalKey(item.key!, this);
+ final Widget itemWithSemantics = _wrapWithSemantics(item, index);
+
+ // if (widget.useDefaultDragListeners) {
+ if (!widget.longPressDraggable) {
+ return _wrapWithSemantics(item, index, itemGlobalKey);
+ }
+ if (widget.buildDefaultDragHandles) {
+ switch (Theme.of(context).platform) {
+ case TargetPlatform.linux:
+ case TargetPlatform.macOS:
+ case TargetPlatform.windows:
+ switch (widget.scrollDirection) {
+ case Axis.horizontal:
+ return Stack(
+ key: itemGlobalKey,
+ children: [
+ itemWithSemantics,
+ Positioned.directional(
+ textDirection: Directionality.of(context),
+ start: 0,
+ end: 0,
+ bottom: 8,
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: ReorderableDragStartListener(
+ index: index,
+ child: const Icon(Icons.drag_handle),
+ ),
+ ))
+ ],
+ );
+ case Axis.vertical:
+ return Stack(
+ key: itemGlobalKey,
+ children: [
+ itemWithSemantics,
+ Positioned.directional(
+ textDirection: Directionality.of(context),
+ top: 0,
+ bottom: 0,
+ end: 8,
+ child: Align(
+ alignment: AlignmentDirectional.centerEnd,
+ child: ReorderableGridDragStartListener(
+ index: index,
+ child: const Icon(Icons.drag_handle),
+ ),
+ ))
+ ],
+ );
+ }
+ case TargetPlatform.android:
+ case TargetPlatform.fuchsia:
+ case TargetPlatform.iOS:
+ return ReorderableGridDelayedDragStartListener(
+ key: itemGlobalKey, index: index, child: item);
+ }
+ }
+
+ const bool enable = true;
+
+ return ReorderableGridDelayedDragStartListener(
+ key: itemGlobalKey,
+ index: index,
+ enabled: enable,
+ child: itemWithSemantics,
+ );
+ }
+
+ Widget _wrapWithSemantics(Widget child, int index, [dynamic key]) {
+ void reorder(int startIndex, int endIndex) {
+ if (startIndex != endIndex) {
+ widget.onReorder?.call(startIndex, endIndex);
+ }
+ }
+
+ // First, determine which semantics actions apply.
+ final Map semanticsActions =
+ {};
+
+ // Create the appropriate semantics actions.
+ void moveToStart() => reorder(index, 0);
+ void moveToEnd() => reorder(index, _itemsCount);
+ void moveBefore() => reorder(index, index - 1);
+ // To move after, we go to index+2 because we are moving it to the space
+ // before index+2, which is after the space at index+1.
+ void moveAfter() => reorder(index, index + 2);
+
+ final WidgetsLocalizations localizations = WidgetsLocalizations.of(context);
+
+ // If the item can move to before its current position in the grid.
+ if (index > 0) {
+ semanticsActions[
+ CustomSemanticsAction(label: localizations.reorderItemToStart)] =
+ moveToStart;
+ String reorderItemBefore = localizations.reorderItemUp;
+ if (widget.scrollDirection == Axis.horizontal) {
+ reorderItemBefore = Directionality.of(context) == TextDirection.ltr
+ ? localizations.reorderItemLeft
+ : localizations.reorderItemRight;
+ }
+ semanticsActions[CustomSemanticsAction(label: reorderItemBefore)] =
+ moveBefore;
+ }
+
+ // If the item can move to after its current position in the grid.
+ if (index < _itemsCount - 1) {
+ String reorderItemAfter = localizations.reorderItemDown;
+ if (widget.scrollDirection == Axis.horizontal) {
+ reorderItemAfter = Directionality.of(context) == TextDirection.ltr
+ ? localizations.reorderItemRight
+ : localizations.reorderItemLeft;
+ }
+ semanticsActions[CustomSemanticsAction(label: reorderItemAfter)] =
+ moveAfter;
+ semanticsActions[
+ CustomSemanticsAction(label: localizations.reorderItemToEnd)] =
+ moveToEnd;
+ }
+
+ // We pass toWrap with a GlobalKey into the item so that when it
+ // gets dragged, the accessibility framework can preserve the selected
+ // state of the dragging item.
+ //
+ // We also apply the relevant custom accessibility actions for moving the item
+ // up, down, to the start, and to the end of the grid.
+ return MergeSemantics(
+ key: key,
+ child: Semantics(
+ customSemanticsActions: semanticsActions,
+ child: child,
+ ),
+ );
+ }
+
+ Widget _removeItemBuilder(_ActiveItem outgoingItem, Widget child) {
+ final Animation animation =
+ outgoingItem.controller ?? kAlwaysCompleteAnimation;
+ final Animation sizeAnimation =
+ outgoingItem.sizeAnimation ?? kAlwaysCompleteAnimation;
+ return SizeTransition(
+ sizeFactor: sizeAnimation,
+ child: widget.removeAnimationBuilder(context, child, animation));
+ }
+
+ Widget _insertItemBuilder(_ActiveItem? incomingItem, Widget child) {
+ final Animation animation =
+ incomingItem?.controller ?? kAlwaysCompleteAnimation;
+ final Animation sizeAnimation =
+ incomingItem?.sizeAnimation ?? kAlwaysCompleteAnimation;
+ return SizeTransition(
+ axis: widget.scrollDirection,
+ sizeFactor: sizeAnimation,
+ child: widget.insertAnimationBuilder(context, child, animation));
+ }
+}
+
+Offset _extentOffset(double extent, Axis scrollDirection) {
+ switch (scrollDirection) {
+ case Axis.horizontal:
+ return Offset(extent, 0.0);
+ case Axis.vertical:
+ return Offset(0.0, extent);
+ }
+}
+
+@optionalTypeArgs
+class _MotionBuilderItemGlobalKey extends GlobalObjectKey {
+ const _MotionBuilderItemGlobalKey(this.subKey, this.state) : super(subKey);
+
+ final Key subKey;
+ final State state;
+
+ @override
+ bool operator ==(Object other) {
+ if (other.runtimeType != runtimeType) {
+ return false;
+ }
+ return other is _MotionBuilderItemGlobalKey &&
+ other.subKey == subKey &&
+ other.state == state;
+ }
+
+ @override
+ int get hashCode => Object.hash(
+ subKey,
+ state,
+ );
+}
+
+class _ActiveItem implements Comparable<_ActiveItem> {
+ _ActiveItem.animation(this.controller, this.itemIndex, this.sizeAnimation);
+
+ _ActiveItem.index(this.itemIndex)
+ : controller = null,
+ sizeAnimation = null;
+
+ final AnimationController? controller;
+ final AnimationController? sizeAnimation;
+ int itemIndex;
+
+ @override
+ int compareTo(_ActiveItem other) => itemIndex - other.itemIndex;
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart
new file mode 100644
index 00000000..843a9e75
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_base.dart
@@ -0,0 +1,253 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart';
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'motion_animated_builder.dart';
+
+typedef ItemBuilder = Widget Function(
+ BuildContext context, int index);
+
+typedef AnimatedWidgetBuilder = Widget Function(
+ Widget child, Animation animation);
+
+typedef EqualityChecker = bool Function(E, E);
+
+const Duration kAnimationDuration = Duration(milliseconds: 300);
+
+abstract class MotionListBase
+ extends StatefulWidget {
+ final ItemBuilder itemBuilder;
+ final List items;
+ final ReorderCallback? onReorder;
+ final void Function(int)? onReorderStart;
+ final void Function(int)? onReorderEnd;
+ final ReorderItemProxyDecorator? proxyDecorator;
+ final List? enterTransition;
+ final List? exitTransition;
+ final Duration? insertDuration;
+ final Duration? removeDuration;
+ final Axis scrollDirection;
+ final SliverGridDelegate? sliverGridDelegate;
+ final AnimatedWidgetBuilder? insertItemBuilder;
+ final AnimatedWidgetBuilder? removeItemBuilder;
+ final bool? buildDefaultDragHandles;
+ final bool useDefaultDragListeners;
+ final bool? longPressDraggable;
+ final bool Function(E a, E b)? isSameItem;
+
+ const MotionListBase(
+ {super.key,
+ required this.items,
+ required this.itemBuilder,
+ this.onReorder,
+ this.onReorderEnd,
+ this.onReorderStart,
+ this.proxyDecorator,
+ this.enterTransition,
+ this.exitTransition,
+ this.insertDuration,
+ this.removeDuration,
+ required this.scrollDirection,
+ this.sliverGridDelegate,
+ this.insertItemBuilder,
+ this.removeItemBuilder,
+ this.buildDefaultDragHandles,
+ this.longPressDraggable,
+ required this.useDefaultDragListeners,
+ this.isSameItem});
+}
+
+abstract class MotionListBaseState<
+ W extends Widget,
+ B extends MotionListBase,
+ E extends Object> extends State with TickerProviderStateMixin {
+ late List oldList;
+
+ Duration _enterDuration = kAnimationDuration;
+ Duration _exitDuration = kAnimationDuration;
+
+ List _enterAnimations = [];
+ List _exitAnimations = [];
+
+ Duration get enterDuration => _enterDuration;
+
+ Duration get exitDuration => _exitDuration;
+
+ @protected
+ GlobalKey listKey = GlobalKey();
+
+ @nonVirtual
+ @protected
+ MotionBuilderState get list => listKey.currentState!;
+
+ @nonVirtual
+ @protected
+ ItemBuilder get itemBuilder => widget.itemBuilder;
+
+ @nonVirtual
+ @protected
+ SliverGridDelegate? get sliverGridDelegate => widget.sliverGridDelegate;
+
+ @nonVirtual
+ @protected
+ ReorderCallback? get onReorder => widget.onReorder;
+
+ @nonVirtual
+ @protected
+ void Function(int)? get onReorderStart => widget.onReorderStart;
+
+ @nonVirtual
+ @protected
+ void Function(int)? get onReorderEnd => widget.onReorderEnd;
+
+ @nonVirtual
+ @protected
+ ReorderItemProxyDecorator? get proxyDecorator => widget.proxyDecorator;
+
+ @nonVirtual
+ @protected
+ Duration get insertDuration => widget.insertDuration ?? enterDuration;
+
+ @nonVirtual
+ @protected
+ Duration get removeDuration => widget.removeDuration ?? exitDuration;
+
+ @protected
+ @nonVirtual
+ Axis get scrollDirection => widget.scrollDirection;
+
+ @nonVirtual
+ @protected
+ List get enterTransition => widget.enterTransition ?? [];
+
+ @nonVirtual
+ @protected
+ List get exitTransition => widget.exitTransition ?? [];
+
+ @nonVirtual
+ @protected
+ bool get buildDefaultDragHandles => widget.buildDefaultDragHandles ?? false;
+
+ @nonVirtual
+ @protected
+ bool get longPressDraggable => widget.longPressDraggable ?? false;
+
+ @nonVirtual
+ @protected
+ bool get useDefaultDragListeners => widget.useDefaultDragListeners ?? true;
+
+ @nonVirtual
+ @protected
+ bool Function(E a, E b) get isSameItem =>
+ widget.isSameItem ?? (a, b) => a == b;
+
+ @override
+ void initState() {
+ super.initState();
+ oldList = List.from(widget.items);
+
+ addEffects(enterTransition, _enterAnimations, enter: true);
+ addEffects(exitTransition, _exitAnimations, enter: false);
+ }
+
+ @override
+ void didUpdateWidget(covariant B oldWidget) {
+ final newList = widget.items;
+ if (!listEquals(oldWidget.enterTransition, enterTransition)) {
+ _enterAnimations = [];
+ addEffects(enterTransition, _enterAnimations, enter: true);
+ }
+ if (!listEquals(oldWidget.exitTransition, exitTransition)) {
+ _exitAnimations = [];
+ addEffects(exitTransition, _exitAnimations, enter: false);
+ }
+ calculateDiff(oldList, newList);
+ oldList = List.from(newList);
+ super.didUpdateWidget(oldWidget);
+ }
+
+ void addEffects(List effects, List enteries,
+ {required bool enter}) {
+ if (effects.isNotEmpty) {
+ for (AnimationEffect effect in effects) {
+ addEffect(effect, enteries, enter: enter);
+ }
+ } else {
+ addEffect(FadeIn(), enteries, enter: enter);
+ }
+ }
+
+ void addEffect(AnimationEffect effect, List enteries,
+ {required bool enter}) {
+ Duration zero = Duration.zero;
+ final timeForAnimation =
+ (effect.delay ?? zero) + (effect.duration ?? kAnimationDuration);
+ if (enter) {
+ _enterDuration =
+ timeForAnimation > _enterDuration ? timeForAnimation : _enterDuration;
+ assert(_enterDuration >= zero, "Duration can not be negative");
+ } else {
+ _exitDuration =
+ timeForAnimation > _exitDuration ? timeForAnimation : _exitDuration;
+ assert(_exitDuration >= zero, "Duration can not be negative");
+ }
+
+ EffectEntry entry = EffectEntry(
+ animationEffect: effect,
+ delay: effect.delay ?? zero,
+ duration: effect.duration ?? kAnimationDuration,
+ curve: effect.curve ?? Curves.linear);
+
+ enteries.add(entry);
+ }
+
+ void calculateDiff(List oldList, List newList) {
+ // Detect removed and updated items
+ for (int i = oldList.length - 1; i >= 0; i--) {
+ if (newList.indexWhere((element) => isSameItem(oldList[i], element)) ==
+ -1) {
+ listKey.currentState!.removeItem(i, removeItemDuration: removeDuration);
+ }
+ }
+ // Detect added items
+ for (int i = 0; i < newList.length; i++) {
+ if (oldList.indexWhere((element) => isSameItem(newList[i], element)) ==
+ -1) {
+ listKey.currentState!.insertItem(i, insertDuration: insertDuration);
+ }
+ }
+ }
+
+ @nonVirtual
+ @protected
+ Widget insertAnimationBuilder(
+ BuildContext context, Widget child, Animation animation) {
+ if (widget.insertItemBuilder != null) {
+ return widget.insertItemBuilder!(child, animation);
+ } else {
+ Widget animatedChild = child;
+ for (EffectEntry entry in _enterAnimations) {
+ animatedChild = entry.animationEffect
+ .build(context, animatedChild, animation, entry, insertDuration);
+ }
+ return animatedChild;
+ }
+ }
+
+ @nonVirtual
+ @protected
+ Widget removeAnimationBuilder(
+ BuildContext context, Widget child, Animation animation) {
+ if (widget.removeItemBuilder != null) {
+ return widget.removeItemBuilder!(child, animation);
+ } else {
+ Widget animatedChild = child;
+ for (EffectEntry entry in _exitAnimations) {
+ animatedChild = entry.animationEffect
+ .build(context, animatedChild, animation, entry, removeDuration);
+ }
+ return animatedChild;
+ }
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart
new file mode 100644
index 00000000..903e9d9d
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/builder/motion_list_impl.dart
@@ -0,0 +1,97 @@
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/provider/animation_effect.dart';
+import 'package:flutter/material.dart';
+
+import 'motion_animated_builder.dart';
+import 'motion_list_base.dart';
+
+class MotionListImpl extends MotionListBase {
+ const MotionListImpl({
+ super.key,
+ required super.items,
+ required super.itemBuilder,
+ super.enterTransition,
+ super.exitTransition,
+ super.insertDuration,
+ super.removeDuration,
+ super.onReorder,
+ super.onReorderStart,
+ super.onReorderEnd,
+ super.proxyDecorator,
+ required super.scrollDirection,
+ super.insertItemBuilder,
+ super.removeItemBuilder,
+ super.buildDefaultDragHandles,
+ super.useDefaultDragListeners = true,
+ super.longPressDraggable,
+ super.isSameItem,
+ });
+
+ const MotionListImpl.grid({
+ Key? key,
+ required List items,
+ required ItemBuilder itemBuilder,
+ required SliverGridDelegate sliverGridDelegate,
+ List? enterTransition,
+ List? exitTransition,
+ ReorderCallback? onReorder,
+ void Function(int)? onReorderStart,
+ void Function(int)? onReorderEnd,
+ ReorderItemProxyDecorator? proxyDecorator,
+ Duration? insertDuration,
+ Duration? removeDuration,
+ required Axis scrollDirection,
+ AnimatedWidgetBuilder? insertItemBuilder,
+ AnimatedWidgetBuilder? removeItemBuilder,
+ bool? buildDefaultDragHandles,
+ bool useDefaultDragListeners = true,
+ bool? longPressDraggable,
+ bool Function(E a, E b)? isSameItem,
+ }) : super(
+ key: key,
+ items: items,
+ itemBuilder: itemBuilder,
+ sliverGridDelegate: sliverGridDelegate,
+ enterTransition: enterTransition,
+ exitTransition: exitTransition,
+ insertDuration: insertDuration,
+ removeDuration: removeDuration,
+ onReorder: onReorder,
+ onReorderStart: onReorderStart,
+ onReorderEnd: onReorderEnd,
+ proxyDecorator: proxyDecorator,
+ scrollDirection: scrollDirection,
+ insertItemBuilder: insertItemBuilder,
+ removeItemBuilder: removeItemBuilder,
+ buildDefaultDragHandles: buildDefaultDragHandles,
+ longPressDraggable: longPressDraggable,
+ useDefaultDragListeners: useDefaultDragListeners,
+ isSameItem: isSameItem);
+
+ @override
+ MotionListImplState createState() => MotionListImplState();
+}
+
+class MotionListImplState
+ extends MotionListBaseState, E> {
+ @override
+ Widget build(BuildContext context) {
+ assert(debugCheckHasMaterialLocalizations(context));
+ assert(debugCheckHasOverlay(context));
+ return MotionBuilder(
+ key: listKey,
+ initialCount: oldList.length,
+ onReorder: onReorder,
+ onReorderStart: onReorderStart,
+ onReorderEnd: onReorderEnd,
+ proxyDecorator: proxyDecorator,
+ insertAnimationBuilder: insertAnimationBuilder,
+ removeAnimationBuilder: removeAnimationBuilder,
+ itemBuilder: itemBuilder,
+ scrollDirection: scrollDirection,
+ delegateBuilder: sliverGridDelegate,
+ buildDefaultDragHandles: buildDefaultDragHandles,
+ longPressDraggable: longPressDraggable,
+ useDefaultDragListeners: useDefaultDragListeners,
+ );
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart
new file mode 100644
index 00000000..7c731edf
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_item.dart
@@ -0,0 +1,181 @@
+part of '../builder/motion_animated_builder.dart';
+
+typedef _DragItemUpdate = void Function(
+ _DragInfo item, Offset position, Offset delta);
+typedef _DragItemCallback = void Function(_DragInfo item);
+
+class _DragInfo extends Drag {
+ final bool gridView;
+ final Axis scrollDirection;
+ final _DragItemUpdate? onUpdate;
+ final _DragItemCallback? onEnd;
+ final _DragItemCallback? onCancel;
+ final VoidCallback? onDragCompleted;
+ final ReorderItemProxyDecorator? proxyDecorator;
+ final TickerProvider tickerProvider;
+
+ late MotionBuilderState listState;
+ late int index;
+ late Widget child;
+ late Offset dragPosition;
+ late Offset dragOffset;
+ late Size itemSize;
+ late double itemExtent;
+ late CapturedThemes capturedThemes;
+ ScrollableState? scrollable;
+ AnimationController? _proxyAnimation;
+
+ _DragInfo({
+ required MotionAnimatedContentState item,
+ Offset initialPosition = Offset.zero,
+ required this.gridView,
+ this.scrollDirection = Axis.vertical,
+ this.onUpdate,
+ this.onEnd,
+ this.onCancel,
+ this.onDragCompleted,
+ this.proxyDecorator,
+ required this.tickerProvider,
+ }) {
+ final RenderBox itemRenderBox =
+ item.context.findRenderObject()! as RenderBox;
+ listState = item.listState;
+ index = item.index;
+ child = item.widget.child;
+ capturedThemes = item.widget.capturedThemes!;
+ dragPosition = initialPosition;
+ dragOffset = itemRenderBox.globalToLocal(initialPosition);
+ itemSize = item.context.size!;
+ itemExtent = _sizeExtent(itemSize, scrollDirection);
+ scrollable = Scrollable.of(item.context);
+ }
+
+ void dispose() {
+ _proxyAnimation?.dispose();
+ }
+
+ void startDrag() {
+ _proxyAnimation = AnimationController(
+ vsync: tickerProvider, duration: const Duration(milliseconds: 250))
+ ..addStatusListener((status) {
+ if (status == AnimationStatus.dismissed) {
+ _dropCompleted();
+ }
+ })
+ ..forward();
+ }
+
+ @override
+ void update(DragUpdateDetails details) {
+ final Offset delta = !gridView
+ ? _restrictAxis(details.delta, scrollDirection)
+ : details.delta;
+ dragPosition += delta;
+ onUpdate?.call(this, dragPosition, details.delta);
+ }
+
+ @override
+ void end(DragEndDetails details) {
+ _proxyAnimation!.reverse();
+ onEnd?.call(this);
+ }
+
+ @override
+ void cancel() {
+ _proxyAnimation?.dispose();
+ _proxyAnimation = null;
+ onCancel?.call(this);
+ }
+
+ void _dropCompleted() {
+ _proxyAnimation?.dispose();
+ _proxyAnimation = null;
+ onDragCompleted?.call();
+ }
+
+ Widget createProxy(BuildContext context) {
+ return capturedThemes.wrap(_DragItemProxy(
+ listState: listState,
+ index: index,
+ position: dragPosition - dragOffset - _overlayOrigin(context),
+ size: itemSize,
+ animation: _proxyAnimation!,
+ proxyDecorator: proxyDecorator,
+ child: child));
+ }
+}
+
+double _sizeExtent(Size size, Axis scrollDirection) {
+ switch (scrollDirection) {
+ case Axis.horizontal:
+ return size.width;
+ case Axis.vertical:
+ return size.height;
+ }
+}
+
+Offset _restrictAxis(Offset offset, Axis scrollDirection) {
+ switch (scrollDirection) {
+ case Axis.horizontal:
+ return Offset(offset.dx, 0.0);
+ case Axis.vertical:
+ return Offset(0.0, offset.dy);
+ }
+}
+
+Offset _overlayOrigin(BuildContext context) {
+ final OverlayState overlay =
+ Overlay.of(context, debugRequiredFor: context.widget);
+ final RenderBox overlayBox = overlay.context.findRenderObject()! as RenderBox;
+ return overlayBox.localToGlobal(Offset.zero);
+}
+
+class _DragItemProxy extends StatelessWidget {
+ final MotionBuilderState listState;
+ final int index;
+ final Widget child;
+ final Offset position;
+ final Size size;
+ final AnimationController animation;
+ final ReorderItemProxyDecorator? proxyDecorator;
+
+ const _DragItemProxy(
+ {required this.listState,
+ required this.index,
+ required this.child,
+ required this.position,
+ required this.size,
+ required this.animation,
+ required this.proxyDecorator});
+
+ @override
+ Widget build(BuildContext context) {
+ final Widget proxyChild =
+ proxyDecorator?.call(child, index, animation.view) ?? child;
+ final Offset overlayOrigin = _overlayOrigin(context);
+ return MediaQuery(
+ data: MediaQuery.of(context).removePadding(removeTop: true),
+ child: AnimatedBuilder(
+ animation: animation,
+ builder: (BuildContext context, Widget? child) {
+ Offset effectivePosition = position;
+ final Offset? dropPosition = listState._finalDropPosition;
+ if (dropPosition != null) {
+ effectivePosition = Offset.lerp(
+ dropPosition - overlayOrigin,
+ effectivePosition,
+ Curves.easeOut.transform(animation.value))!;
+ }
+ return Positioned(
+ left: effectivePosition.dx,
+ top: effectivePosition.dy,
+ child: SizedBox(
+ width: size.width,
+ height: size.height,
+ child: child,
+ ));
+ },
+ child: proxyChild,
+ ));
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart
new file mode 100644
index 00000000..8b6c7516
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/component/drag_listener.dart
@@ -0,0 +1,95 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/gestures.dart';
+
+import '../builder/motion_animated_builder.dart';
+
+class ReorderableGridDragStartListener extends StatelessWidget {
+ /// Creates a listener for a drag immediately following a pointer down
+ /// event over the given child widget.
+ ///
+ /// This is most commonly used to wrap part of a grid item like a drag
+ /// handle.
+ const ReorderableGridDragStartListener({
+ super.key,
+ required this.child,
+ required this.index,
+ this.enabled = true,
+ });
+
+ /// The widget for which the application would like to respond to a tap and
+ /// drag gesture by starting a reordering drag on a reorderable grid.
+ final Widget child;
+
+ /// The index of the associated item that will be dragged in the grid.
+ final int index;
+
+ /// Whether the [child] item can be dragged and moved in the grid.
+ ///
+ /// If true, the item can be moved to another location in the grid when the
+ /// user taps on the child. If false, tapping on the child will be ignored.
+ final bool enabled;
+
+ @override
+ Widget build(BuildContext context) {
+ return Listener(
+ behavior: HitTestBehavior.translucent,
+ onPointerDown: enabled
+ ? (PointerDownEvent event) => _startDragging(context, event)
+ : null,
+ child: child,
+ );
+ }
+
+ /// Provides the gesture recognizer used to indicate the start of a reordering
+ /// drag operation.
+ ///
+ /// By default this returns an [ImmediateMultiDragGestureRecognizer] but
+ /// subclasses can use this to customize the drag start gesture.
+ @protected
+ MultiDragGestureRecognizer createRecognizer() {
+ return DelayedMultiDragGestureRecognizer(debugOwner: this,delay: const Duration(milliseconds: 1));
+ }
+
+ void _startDragging(BuildContext context, PointerDownEvent event) {
+ final MotionBuilderState? list = MotionBuilder.maybeOf(context);
+ list?.startItemDragReorder(
+ index: index,
+ event: event,
+ recognizer: createRecognizer(),
+ );
+ }
+}
+
+/// A wrapper widget that will recognize the start of a drag operation by
+/// looking for a long press event. Once it is recognized, it will start
+/// a drag operation on the wrapped item in the reorderable grid.
+///
+/// See also:
+///
+/// * [ReorderableGridDragStartListener], a similar wrapper that will
+/// recognize the start of the drag immediately after a pointer down event.
+/// * [ReorderableGrid], a widget grid that allows the user to reorder
+/// its items.
+/// * [SliverReorderableGrid], a sliver grid that allows the user to reorder
+/// its items.
+/// * [ReorderableGridView], a material design grid that allows the user to
+/// reorder its items.
+class ReorderableGridDelayedDragStartListener
+ extends ReorderableGridDragStartListener {
+ /// Creates a listener for an drag following a long press event over the
+ /// given child widget.
+ ///
+ /// This is most commonly used to wrap an entire grid item in a reorderable
+ /// grid.
+ const ReorderableGridDelayedDragStartListener({
+ super.key,
+ required super.child,
+ required super.index,
+ super.enabled,
+ });
+
+ @override
+ MultiDragGestureRecognizer createRecognizer() {
+ return DelayedMultiDragGestureRecognizer(debugOwner: this);
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart
new file mode 100644
index 00000000..882aef28
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/component/motion_animated_content.dart
@@ -0,0 +1,221 @@
+part of '../builder/motion_animated_builder.dart';
+
+class MotionAnimatedContent extends StatefulWidget {
+ final int index;
+ final MotionData motionData;
+ final Widget child;
+ final Function(MotionData)? updateMotionData;
+ final CapturedThemes? capturedThemes;
+ final bool isGrid;
+
+ const MotionAnimatedContent({
+ Key? key,
+ required this.index,
+ required this.motionData,
+ required this.child,
+ this.updateMotionData,
+ required this.capturedThemes,
+ required this.isGrid,
+ }) : super(key: key);
+
+ @override
+ State createState() => MotionAnimatedContentState();
+}
+
+class MotionAnimatedContentState extends State
+ with SingleTickerProviderStateMixin {
+ late MotionBuilderState listState;
+
+ Offset _targetOffset = Offset.zero;
+ Offset _startOffset = Offset.zero;
+ AnimationController? _offsetAnimation;
+
+ bool _dragging = false;
+
+ bool get dragging => _dragging;
+
+ set dragging(bool dragging) {
+ if (mounted) {
+ setState(() {
+ _dragging = dragging;
+ });
+ }
+ }
+
+ Size _dragSize = Size.zero;
+
+ set dragSize(Size itemSize) {
+ if (mounted) {
+ setState(() {
+ _dragSize = itemSize;
+ });
+ }
+ }
+
+ int get index => widget.index;
+ bool visible = true;
+
+ @override
+ void initState() {
+ listState = MotionBuilder.of(context);
+ listState.registerItem(this);
+ visible = widget.motionData.visible;
+
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ widget.updateMotionData?.call(widget.motionData);
+ });
+ Future.delayed(kAnimationDuration).then((value) {
+ visible = true;
+ rebuild();
+ });
+ super.initState();
+ }
+
+ @override
+ void didUpdateWidget(covariant MotionAnimatedContent oldWidget) {
+ if (oldWidget.index != widget.index) {
+ listState.unregisterItem(oldWidget.index, this);
+ listState.registerItem(this);
+ }
+ if (oldWidget.index != widget.index && !_dragging && widget.isGrid) {
+ _updateAnimationTranslation();
+ }
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
+ if (mounted) {
+ setState(() {
+ visible = true;
+ });
+ widget.updateMotionData?.call(widget.motionData);
+ }
+ });
+ super.didUpdateWidget(oldWidget);
+ }
+
+ void _updateAnimationTranslation() {
+ Offset offsetDiff =
+ (widget.motionData.startOffset + offset) - widget.motionData.endOffset;
+ _startOffset = offsetDiff;
+ if (offsetDiff.dx != 0 || offsetDiff.dy != 0) {
+ if (_offsetAnimation == null) {
+ _offsetAnimation = AnimationController(
+ vsync: listState,
+ duration: kAnimationDuration,
+ )
+ ..addListener(rebuild)
+ ..addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.completed) {
+ widget.updateMotionData?.call(widget.motionData);
+
+ _startOffset = _targetOffset;
+ _offsetAnimation!.dispose();
+ _offsetAnimation = null;
+ }
+ })
+ ..forward();
+ } else {
+ _startOffset = offsetDiff;
+ _offsetAnimation!.forward(from: 0.0);
+ }
+ }
+ }
+
+ Offset get offset {
+ if (_offsetAnimation != null) {
+ final Offset offset =
+ Offset.lerp(_startOffset, _targetOffset, _offsetAnimation!.value)!;
+ return offset;
+ }
+ return _targetOffset;
+ }
+
+ void updateForGap(bool animate) {
+ if (!mounted) return;
+ final Offset newTargetOffset = listState.calculateNextDragOffset(index);
+ if (newTargetOffset == _targetOffset) return;
+ _targetOffset = newTargetOffset;
+
+ if (animate) {
+ if (_offsetAnimation == null) {
+ _offsetAnimation = AnimationController(
+ vsync: listState,
+ duration: const Duration(milliseconds: 250),
+ )
+ ..addListener(rebuild)
+ ..addStatusListener((AnimationStatus status) {
+ if (status == AnimationStatus.completed) {
+ _startOffset = _targetOffset;
+ _offsetAnimation!.dispose();
+ _offsetAnimation = null;
+ }
+ })
+ ..forward();
+ } else {
+ _startOffset = offset;
+ _offsetAnimation!.forward(from: 0.0);
+ }
+ } else {
+ if (_offsetAnimation != null) {
+ _offsetAnimation!.dispose();
+ _offsetAnimation = null;
+ }
+ _startOffset = _targetOffset;
+ }
+ rebuild();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ listState.registerItem(this);
+ return Visibility(
+ maintainSize: true,
+ maintainAnimation: true,
+ maintainState: true,
+ visible: visible && !_dragging,
+ child: Transform.translate(
+ offset: offset,
+ child:
+ !_dragging ? widget.child : SizedBox.fromSize(size: _dragSize)),
+ );
+ }
+
+ Offset itemOffset() {
+ final box = context.findRenderObject() as RenderBox?;
+ if (box == null) return Offset.zero;
+ return box.localToGlobal(Offset.zero);
+ }
+
+ void resetGap() {
+ if (_offsetAnimation != null) {
+ _offsetAnimation!.dispose();
+ _offsetAnimation = null;
+ }
+ _startOffset = Offset.zero;
+ _targetOffset = Offset.zero;
+ rebuild();
+ }
+
+ Rect targetGeometryNonOffset() {
+ final RenderBox itemRenderBox = context.findRenderObject()! as RenderBox;
+ final Offset itemPosition = itemRenderBox.localToGlobal(Offset.zero);
+ return itemPosition & itemRenderBox.size;
+ }
+
+ void rebuild() {
+ if (mounted) {
+ setState(() {});
+ }
+ }
+
+ @override
+ void dispose() {
+ listState.unregisterItem(widget.index, this);
+ _offsetAnimation?.dispose();
+ super.dispose();
+ }
+
+ @override
+ void deactivate() {
+ listState.unregisterItem(index, this);
+ super.deactivate();
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart
new file mode 100644
index 00000000..188035ed
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_fixed_cross_axis_count.dart
@@ -0,0 +1,157 @@
+import 'dart:math';
+import 'package:flutter/rendering.dart';
+
+class SliverGridWithCustomGeometryLayout extends SliverGridRegularTileLayout {
+ final SliverGridGeometry Function(
+ int index, SliverGridRegularTileLayout layout) geometryBuilder;
+
+ const SliverGridWithCustomGeometryLayout({
+ required this.geometryBuilder,
+ required int crossAxisCount,
+ required double mainAxisStride,
+ required double crossAxisStride,
+ required double childMainAxisExtent,
+ required double childCrossAxisExtent,
+ required bool reverseCrossAxis,
+ }) : assert(crossAxisCount > 0),
+ assert(mainAxisStride >= 0),
+ assert(crossAxisStride >= 0),
+ assert(childMainAxisExtent >= 0),
+ assert(childCrossAxisExtent >= 0),
+ super(
+ crossAxisCount: crossAxisCount,
+ mainAxisStride: mainAxisStride,
+ crossAxisStride: crossAxisStride,
+ childMainAxisExtent: childMainAxisExtent,
+ childCrossAxisExtent: childCrossAxisExtent,
+ reverseCrossAxis: reverseCrossAxis);
+
+ @override
+ SliverGridGeometry getGeometryForChildIndex(int index) {
+ return geometryBuilder(index, this);
+ }
+}
+
+/// Creates grid layouts with a fixed number of tiles in the cross axis.
+///
+/// For example, if the grid is vertical, this delegate will create a layout
+/// with a fixed number of columns. If the grid is horizontal, this delegate
+/// will create a layout with a fixed number of rows.
+///
+/// This delegate creates grids with equally sized and spaced tiles.
+
+class SliverReorderableGridDelegateWithFixedCrossAxisCount
+ extends SliverGridDelegateWithFixedCrossAxisCount {
+ /// The number of children in the cross axis.
+ final int crossAxisCount;
+
+ /// The number of logical pixels between each child along the main axis.
+ final double mainAxisSpacing;
+
+ /// The number of logical pixels between each child along the cross axis.
+ final double crossAxisSpacing;
+
+ /// The ratio of the cross-axis to the main-axis extent of each child.
+ final double childAspectRatio;
+
+ /// The extent of each tile in the main axis. If provided it would define the
+ /// logical pixels taken by each tile in the main-axis.
+ ///
+ /// If null, [childAspectRatio] is used instead.
+ final double? mainAxisExtent;
+
+ double childCrossAxisExtent = 0.0;
+ double childMainAxisExtent = 0.0;
+
+ /// Creates a delegate that makes grid layouts with a fixed number of tiles in
+ /// the cross axis.
+ ///
+ /// The `mainAxisSpacing`, `mainAxisExtent` and `crossAxisSpacing` arguments
+ /// must not be negative. The `crossAxisCount` and `childAspectRatio`
+ /// arguments must be greater than zero.
+
+ SliverReorderableGridDelegateWithFixedCrossAxisCount({
+ required this.crossAxisCount,
+ this.mainAxisSpacing = 0.0,
+ this.crossAxisSpacing = 0.0,
+ this.childAspectRatio = 1.0,
+ this.mainAxisExtent,
+ }) : assert(crossAxisCount > 0),
+ assert(mainAxisSpacing >= 0),
+ assert(crossAxisSpacing >= 0),
+ assert(childAspectRatio > 0),
+ super(
+ crossAxisCount: crossAxisCount,
+ mainAxisSpacing: mainAxisSpacing,
+ crossAxisSpacing: crossAxisSpacing,
+ childAspectRatio: childAspectRatio,
+ );
+
+ bool _debugAssertIsValid() {
+ assert(crossAxisCount > 0);
+ assert(mainAxisSpacing >= 0);
+ assert(crossAxisSpacing >= 0);
+ assert(childAspectRatio > 0);
+ return true;
+ }
+
+ @override
+ SliverGridLayout getLayout(SliverConstraints constraints) {
+ assert(_debugAssertIsValid());
+ final usableCrossAxisCount = max(0.0,
+ constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1));
+
+ childCrossAxisExtent = usableCrossAxisCount / crossAxisCount;
+ childMainAxisExtent = childCrossAxisExtent / childAspectRatio;
+ return SliverGridWithCustomGeometryLayout(
+ geometryBuilder: (index, layout) {
+ return SliverGridGeometry(
+ scrollOffset: (index ~/ crossAxisCount) * layout.mainAxisStride,
+ crossAxisOffset: _getOffsetFromStartInCrossAxis(index, layout),
+ mainAxisExtent: childMainAxisExtent,
+ crossAxisExtent: childCrossAxisExtent);
+ },
+ crossAxisCount: crossAxisCount,
+ mainAxisStride: childMainAxisExtent + mainAxisSpacing,
+ crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
+ childMainAxisExtent: childMainAxisExtent,
+ childCrossAxisExtent: childCrossAxisExtent,
+ reverseCrossAxis:
+ axisDirectionIsReversed(constraints.crossAxisDirection));
+ }
+
+ Offset getOffset(int index, Offset currentOffset) {
+ final int col = index % crossAxisCount;
+ final crossAxisStart = crossAxisSpacing;
+
+ if (col == crossAxisCount - 1) {
+ return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent);
+ } else {
+ return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy);
+ }
+ }
+
+ double _getOffsetFromStartInCrossAxis(
+ int index,
+ SliverGridRegularTileLayout layout,
+ ) {
+ final crossAxisStart = (index % crossAxisCount) * layout.crossAxisStride;
+
+ if (layout.reverseCrossAxis) {
+ return crossAxisCount * layout.crossAxisStride -
+ crossAxisStart -
+ layout.childCrossAxisExtent -
+ (layout.crossAxisStride - layout.childCrossAxisExtent);
+ }
+ return crossAxisStart;
+ }
+
+ @override
+ bool shouldRelayout(SliverGridDelegateWithFixedCrossAxisCount oldDelegate) {
+ return oldDelegate.crossAxisCount != crossAxisCount ||
+ oldDelegate.mainAxisSpacing != mainAxisSpacing ||
+ oldDelegate.crossAxisSpacing != crossAxisSpacing ||
+ oldDelegate.childAspectRatio != childAspectRatio ||
+ oldDelegate.mainAxisExtent != mainAxisExtent;
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart
new file mode 100644
index 00000000..0c06521b
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/component/sliver_grid_with_main_axis_extent.dart
@@ -0,0 +1,128 @@
+import 'package:flutter/rendering.dart';
+
+import 'dart:math' as math;
+
+/// Creates grid layouts with tiles that each have a maximum cross-axis extent.
+///
+/// This delegate will select a cross-axis extent for the tiles that is as
+/// large as possible subject to the following conditions:
+///
+/// - The extent evenly divides the cross-axis extent of the grid.
+/// - The extent is at most [maxCrossAxisExtent].
+///
+/// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
+/// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
+/// columns that are 125.0 pixels wide.
+///
+/// This delegate creates grids with equally sized and spaced tiles.
+
+class SliverReorderableGridWithMaxCrossAxisExtent
+ extends SliverGridDelegateWithMaxCrossAxisExtent {
+ /// The maximum extent of tiles in the cross axis.
+ ///
+ /// This delegate will select a cross-axis extent for the tiles that is as
+ /// large as possible subject to the following conditions:
+ ///
+ /// - The extent evenly divides the cross-axis extent of the grid.
+ /// - The extent is at most [maxCrossAxisExtent].
+ ///
+ /// For example, if the grid is vertical, the grid is 500.0 pixels wide, and
+ /// [maxCrossAxisExtent] is 150.0, this delegate will create a grid with 4
+ /// columns that are 125.0 pixels wide.
+ final double maxCrossAxisExtent;
+
+ /// The number of logical pixels between each child along the main axis.
+ final double mainAxisSpacing;
+
+ /// The number of logical pixels between each child along the cross axis.
+ final double crossAxisSpacing;
+
+ /// The ratio of the cross-axis to the main-axis extent of each child.
+ final double childAspectRatio;
+
+ /// The extent of each tile in the main axis. If provided it would define the
+ /// logical pixels taken by each tile in the main-axis.
+ ///
+ /// If null, [childAspectRatio] is used instead.
+ final double? mainAxisExtent;
+ int crossAxisCount = 0;
+ double childCrossAxisExtent = 0.0;
+ double childMainAxisExtent = 0.0;
+
+ /// Creates a delegate that makes grid layouts with tiles that have a maximum
+ /// cross-axis extent.
+ ///
+ /// The [maxCrossAxisExtent], [mainAxisExtent], [mainAxisSpacing],
+ /// and [crossAxisSpacing] arguments must not be negative.
+ /// The [childAspectRatio] argument must be greater than zero.
+ SliverReorderableGridWithMaxCrossAxisExtent({
+ required this.maxCrossAxisExtent,
+ this.mainAxisSpacing = 0.0,
+ this.crossAxisSpacing = 0.0,
+ this.childAspectRatio = 1.0,
+ this.mainAxisExtent,
+ }) : assert(maxCrossAxisExtent > 0),
+ assert(mainAxisSpacing >= 0),
+ assert(crossAxisSpacing >= 0),
+ assert(childAspectRatio > 0),
+ super(
+ maxCrossAxisExtent: maxCrossAxisExtent,
+ mainAxisSpacing: mainAxisSpacing,
+ crossAxisSpacing: crossAxisSpacing,
+ childAspectRatio: childAspectRatio,
+ mainAxisExtent: mainAxisExtent);
+
+ bool _debugAssertIsValid(double crossAxisExtent) {
+ assert(maxCrossAxisExtent > 0);
+ assert(mainAxisSpacing >= 0);
+ assert(crossAxisSpacing >= 0);
+ assert(childAspectRatio > 0);
+ return true;
+ }
+
+ @override
+ SliverGridLayout getLayout(SliverConstraints constraints) {
+ assert(_debugAssertIsValid(constraints.crossAxisExtent));
+ int childCrossAxisCount =
+ (constraints.crossAxisExtent / (maxCrossAxisExtent + crossAxisSpacing))
+ .ceil();
+ // Ensure a minimum count of 1, can be zero and result in an infinite extent
+ // below when the window size is 0.
+ crossAxisCount = math.max(1, childCrossAxisCount);
+ final double usableCrossAxisExtent = math.max(
+ 0.0,
+ constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1),
+ );
+ childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount;
+ childMainAxisExtent =
+ mainAxisExtent ?? childCrossAxisExtent / childAspectRatio;
+ return SliverGridRegularTileLayout(
+ crossAxisCount: crossAxisCount,
+ mainAxisStride: childMainAxisExtent + mainAxisSpacing,
+ crossAxisStride: childCrossAxisExtent + crossAxisSpacing,
+ childMainAxisExtent: childMainAxisExtent,
+ childCrossAxisExtent: childCrossAxisExtent,
+ reverseCrossAxis: axisDirectionIsReversed(constraints.crossAxisDirection),
+ );
+ }
+
+ Offset getOffset(int index, Offset currentOffset) {
+ final int col = index % crossAxisCount;
+ final crossAxisStart = crossAxisSpacing;
+
+ if (col == crossAxisCount - 1) {
+ return Offset(crossAxisStart, currentOffset.dy + childMainAxisExtent);
+ } else {
+ return Offset(currentOffset.dx + childCrossAxisExtent, currentOffset.dy);
+ }
+ }
+
+ @override
+ bool shouldRelayout(SliverGridDelegateWithMaxCrossAxisExtent oldDelegate) {
+ return oldDelegate.maxCrossAxisExtent != maxCrossAxisExtent ||
+ oldDelegate.mainAxisSpacing != mainAxisSpacing ||
+ oldDelegate.crossAxisSpacing != crossAxisSpacing ||
+ oldDelegate.childAspectRatio != childAspectRatio ||
+ oldDelegate.mainAxisExtent != mainAxisExtent;
+ }
+}
diff --git a/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart
new file mode 100644
index 00000000..06de5a9f
--- /dev/null
+++ b/lib/common/widgets/list/animated_reorderable_list/model/motion_data.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/cupertino.dart';
+
+class MotionData {
+ final Offset startOffset;
+ final Offset endOffset;
+ final bool visible;
+
+ MotionData(
+ {this.startOffset = Offset.zero,
+ this.endOffset = Offset.zero,
+ this.visible = true});
+
+ MotionData copyWith({Offset? startOffset, Offset? endOffset, bool? visible}) {
+ return MotionData(
+ startOffset: startOffset ?? this.startOffset,
+ endOffset: endOffset ?? this.endOffset,
+ visible: visible ?? this.visible);
+ }
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ other is MotionData &&
+ runtimeType == other.runtimeType &&
+ startOffset == other.startOffset &&
+ endOffset == other.endOffset;
+
+ @override
+ int get hashCode => startOffset.hashCode ^ endOffset.hashCode;
+}
diff --git a/lib/common/widgets/list/custom_list_view.dart b/lib/common/widgets/list/custom_list_view.dart
index 429eb7cb..1f978950 100644
--- a/lib/common/widgets/list/custom_list_view.dart
+++ b/lib/common/widgets/list/custom_list_view.dart
@@ -1,21 +1,16 @@
-import 'package:clock_app/common/logic/get_list_filter_chips.dart';
import 'package:clock_app/common/types/list_controller.dart';
import 'package:clock_app/common/types/list_filter.dart';
import 'package:clock_app/common/types/list_item.dart';
import 'package:clock_app/common/utils/reorderable_list_decorator.dart';
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animated_reorderable_listview.dart';
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/animation/fade_in.dart';
import 'package:clock_app/common/widgets/list/delete_alert_dialogue.dart';
-import 'package:clock_app/common/widgets/list/list_filter_chip.dart';
+import 'package:clock_app/common/widgets/list/list_filter_bar.dart';
import 'package:clock_app/common/widgets/list/list_item_card.dart';
+import 'package:clock_app/settings/data/general_settings_schema.dart';
+import 'package:clock_app/settings/data/settings_schema.dart';
+import 'package:clock_app/settings/types/setting.dart';
import 'package:flutter/material.dart';
-import 'package:flutter_slidable/flutter_slidable.dart';
-import 'package:great_list_view/great_list_view.dart';
-import 'package:flutter_gen/gen_l10n/app_localizations.dart';
-
-typedef ItemCardBuilder = Widget Function(
- BuildContext context,
- int index,
- AnimatedWidgetBuilderData data,
-);
class CustomListView
- extends StatefulWidget {
const CustomListView({
@@ -34,13 +29,13 @@ class CustomListView
- extends StatefulWidget {
this.isDeleteEnabled = true,
this.isDuplicateEnabled = true,
this.shouldInsertOnTop = true,
+ this.isSelectable = false,
this.listFilters = const [],
this.customActions = const [],
this.sortOptions = const [],
this.initialSortIndex = 0,
this.onChangeSortIndex,
this.header,
-
});
final List
- items;
@@ -63,6 +58,7 @@ class CustomListView
- extends StatefulWidget {
final List> sortOptions;
final Function(int index)? onChangeSortIndex;
final Widget? header;
+ final bool isSelectable;
@override
State createState() => _CustomListViewState
- ();
@@ -73,12 +69,24 @@ class _CustomListViewState
-
late List
- currentList = List.from(widget.items);
double _itemCardHeight = 0;
final _scrollController = ScrollController();
- final _controller = AnimatedListController();
- late int selectedSortIndex = widget.initialSortIndex;
+ // final _controller = AnimatedListController();
+ late int _selectedSortIndex = widget.initialSortIndex;
+ late Setting _longPressActionSetting;
+ List _selectedIds = [];
+ bool _isSelecting = false;
+ // bool _isReordering = false;
@override
void initState() {
super.initState();
+
+ _longPressActionSetting = appSettings
+ .getGroup("General")
+ .getGroup("Interactions")
+ .getSetting("Long Press Action");
+
+ _longPressActionSetting.addListener(_handleUpdateSettings);
+
widget.listController.setChangeItems(_handleChangeItems);
widget.listController.setAddItem(_handleAddItem);
widget.listController.setDeleteItem(_handleDeleteItem);
@@ -87,31 +95,37 @@ class _CustomListViewState
-
widget.listController.setReloadItems(_handleReloadItems);
widget.listController.setClearItems(_handleClear);
widget.listController.setGetItems(() => widget.items);
- updateCurrentList();
+ _updateCurrentList();
// widget.listController.setChangeItemWithId(_handleChangeItemWithId);
}
+ @override
+ void dispose() {
+ _longPressActionSetting.removeListener(_handleUpdateSettings);
+ super.dispose();
+ }
+
+ void _handleUpdateSettings(dynamic value) {
+ _endSelection();
+ }
+
void _handleReloadItems(List
- items) {
setState(() {
widget.items.clear();
widget.items.addAll(items);
- updateCurrentList();
+ _updateCurrentList();
});
- // TODO: MAN THIS SUCKS, WHY YOU GOTTA DO THIS
- _controller.notifyRemovedRange(
- 0, widget.items.length - 1, _getChangeListBuilder());
- _controller.notifyInsertedRange(0, widget.items.length);
}
- void updateCurrentList() {
- if (selectedSortIndex > widget.sortOptions.length) {
- selectedSortIndex = 0;
+ void _updateCurrentList() {
+ if (_selectedSortIndex > widget.sortOptions.length) {
+ _selectedSortIndex = 0;
}
currentList.clear();
- if (selectedSortIndex != 0) {
+ if (_selectedSortIndex != 0) {
final temp = [...widget.items];
- temp.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction);
+ temp.sort(widget.sortOptions[_selectedSortIndex - 1].sortFunction);
currentList.addAll(temp);
} else {
currentList.addAll(widget.items);
@@ -123,39 +137,17 @@ class _CustomListViewState
-
void _updateItemHeight() {
if (_itemCardHeight == 0) {
- _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0;
+ // _itemCardHeight = _controller.computeItemBox(0)?.height ?? 0;
}
}
- void _notifyChangeList() {
- _controller.notifyChangedRange(
- 0,
- currentList.length,
- _getChangeListBuilder(),
- );
- }
-
- ItemCardBuilder _getChangeWidgetBuilder(Item item) {
- _updateItemHeight();
- return (context, index, data) => data.measuring
- ? SizedBox(height: _itemCardHeight)
- : ListItemCard
- (
- key: ValueKey(item),
- onTap: () {},
- onDelete: () {},
- onDuplicate: () {},
- child: widget.itemBuilder(item),
- );
- }
-
- ItemCardBuilder _getChangeListBuilder() => (context, index, data) =>
- _getChangeWidgetBuilder(widget.items[index])(context, index, data);
-
- bool _handleReorderItems(int oldIndex, int newIndex, Object? slot) {
- if (newIndex >= widget.items.length || selectedSortIndex != 0) return false;
+ bool _handleReorderItems(int oldIndex, int newIndex) {
+ if (newIndex >= widget.items.length || _selectedSortIndex != 0) {
+ return false;
+ }
widget.onReorderItem?.call(widget.items[oldIndex]);
widget.items.insert(newIndex, widget.items.removeAt(oldIndex));
- updateCurrentList();
+ _updateCurrentList();
widget.onModifyList?.call();
return true;
@@ -163,77 +155,32 @@ class _CustomListViewState
-
void _handleChangeItems(
ItemChangerCallback
- callback, bool callOnModifyList) {
- final initialList = List.from(currentList);
-
callback(widget.items);
setState(() {
- updateCurrentList();
+ _updateCurrentList();
});
- final deletedItems = List.from(initialList
- .where(
- (element) => currentList.where((e) => e.id == element.id).isEmpty)
- .toList());
- final addedItems = List.from(currentList
- .where(
- (element) => initialList.where((e) => e.id == element.id).isEmpty)
- .toList());
-
- for (var deletedItem in deletedItems) {
- _controller.notifyRemovedRange(
- initialList.indexWhere((element) => element.id == deletedItem.id),
- 1,
- _getChangeWidgetBuilder(deletedItem),
- );
- }
-
- for (var addedItem in addedItems) {
- _controller.notifyInsertedRange(
- currentList.indexWhere((element) => element.id == addedItem.id),
- 1,
- );
- }
-
- _notifyChangeList();
-
if (callOnModifyList) widget.onModifyList?.call();
}
Future _handleDeleteItem(Item deletedItem,
[bool callOnModifyList = true]) async {
- int index = _getItemIndex(deletedItem);
-
- // print(listToString(widget.items));
-
setState(() {
widget.items.removeWhere((element) => element.id == deletedItem.id);
- updateCurrentList();
+ _updateCurrentList();
});
- _controller.notifyRemovedRange(
- index,
- 1,
- _getChangeWidgetBuilder(deletedItem),
- );
await widget.onDeleteItem?.call(deletedItem);
if (callOnModifyList) widget.onModifyList?.call();
}
Future _handleDeleteItemList(List
- deletedItems) async {
for (var item in deletedItems) {
- int index = _getItemIndex(item);
-
setState(() {
widget.items.removeWhere((element) => element.id == item.id);
- updateCurrentList();
+ _updateCurrentList();
});
-
- _controller.notifyRemovedRange(
- index,
- 1,
- _getChangeWidgetBuilder(deletedItems.first),
- );
}
for (var item in deletedItems) {
await widget.onDeleteItem?.call(item);
@@ -253,16 +200,11 @@ class _CustomListViewState
-
widget.items.insert(index, item);
await widget.onAddItem?.call(item);
setState(() {
- updateCurrentList();
+ _updateCurrentList();
});
int currentListIndex = _getItemIndex(item);
- _controller.notifyInsertedRange(currentListIndex, 1);
- // _scrollToIndex(index);
- // TODO: Remove this delay
- Future.delayed(const Duration(milliseconds: 100), () {
- _scrollToIndex(currentListIndex);
- });
+ _scrollToIndex(currentListIndex);
_updateItemHeight();
widget.onModifyList?.call();
}
@@ -272,152 +214,155 @@ class _CustomListViewState
-
}
void _scrollToIndex(int index) {
- // if (_scrollController.offset == 0) {
- // _scrollController.jumpTo(1);
- // }
if (_itemCardHeight == 0 && index != 0) return;
_scrollController.animateTo(index * _itemCardHeight,
duration: const Duration(milliseconds: 250), curve: Curves.easeIn);
}
- _getItemBuilder() {
- return (BuildContext context, Item item, data) {
- for (var filter in widget.listFilters) {
- // print("${filter.displayName} ${filter.filterFunction}");
- if (!filter.filterFunction(item)) {
- return Container();
- }
+ void _endSelection() {
+ setState(() {
+ _isSelecting = false;
+ // _isReordering = false;
+ _selectedIds.clear();
+ });
+ }
+
+ void _startSelection(Item item) {
+ setState(() {
+ _isSelecting = true;
+ _selectedIds = [item.id];
+ });
+ }
+
+ void _handleSelect(Item item) {
+ setState(() {
+ if (_selectedIds.contains(item.id)) {
+ _selectedIds.remove(item.id);
+ } else {
+ _selectedIds.add(item.id);
}
- return data.measuring
- ? SizedBox(height: _itemCardHeight)
- : ListItemCard
- (
- key: ValueKey(item),
- onTap: () {
- return widget.onTapItem?.call(item, _getItemIndex(item));
- },
- onDelete:
- widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null,
- onDuplicate: () => _handleDuplicateItem(item),
- isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled,
- isDuplicateEnabled: widget.isDuplicateEnabled,
- child: widget.itemBuilder(item),
- );
- };
+ });
+ if (_selectedIds.isEmpty) {
+ _endSelection();
+ }
+ }
+
+ void _handleSortChange(int index) {
+ setState(() {
+ _selectedSortIndex = index;
+ widget.onChangeSortIndex?.call(index);
+ _updateCurrentList();
+ });
+ }
+
+ void _handleFilterChange() {
+ setState(() {});
}
- void onFilterChange() {
+ void _handleSelectAll() {
setState(() {
- _notifyChangeList();
+ _selectedIds = widget.items.map((e) => e.id).toList();
});
}
- List
- getCurrentList() {
- final List
- items = List.from(widget.items);
+ void _handleCustomAction(ListFilterCustomAction
- action) {
+ final list = _getActionableItems();
+ List
- items = list.where((item) =>
+ widget.listFilters.every((filter) => filter.filterFunction(item))).toList();
- if (selectedSortIndex != 0) {
- items.sort(widget.sortOptions[selectedSortIndex - 1].sortFunction);
- }
+ action.action(items);
+ _endSelection();
+ }
+
+ void _handleDeleteAction() async {
+ Navigator.pop(context);
+ final result = await showDeleteAlertDialogue(context);
+ if (result == null || result == false) return;
- return items;
+ final list = _getActionableItems();
+ final itemsToRemove = List
- .from(list.where((item) =>
+ item.isDeletable &&
+ widget.listFilters.every((filter) => filter.filterFunction(item))));
+ _endSelection();
+ await _handleDeleteItemList(itemsToRemove);
+
+ widget.onModifyList?.call();
+ }
+
+ List
- _getActionableItems() {
+ return _isSelecting
+ ? widget.items.where((item) => _selectedIds.contains(item.id)).toList()
+ : widget.items;
+ }
+
+ _getItemBuilder() {
+ return (BuildContext context, int index) {
+ Item item = currentList[index];
+ for (var filter in widget.listFilters) {
+ if (!filter.filterFunction(item)) {
+ return Container(key: ValueKey(item));
+ }
+ }
+ Widget itemWidget = ListItemCard
- (
+ key: ValueKey(item.id),
+ onTap: () {
+ if (_isSelecting) {
+ _handleSelect(item);
+ } else {
+ return widget.onTapItem?.call(item, index);
+ }
+ },
+ onLongPress: () {
+ if (widget.isSelectable &&
+ _longPressActionSetting.value == LongPressAction.multiSelect) {
+ if (!_isSelecting) {
+ _startSelection(item);
+ } else {
+ _handleSelect(item);
+ }
+ }
+ },
+ onDelete: widget.isDeleteEnabled ? () => _handleDeleteItem(item) : null,
+ onDuplicate: () => _handleDuplicateItem(item),
+ isDeleteEnabled: item.isDeletable && widget.isDeleteEnabled,
+ isDuplicateEnabled: widget.isDuplicateEnabled,
+ isSelected: _selectedIds.contains(item.id),
+ showReorderHandle:
+ _isSelecting && widget.isReorderable && _selectedSortIndex == 0,
+ index: index,
+ child: widget.itemBuilder(item),
+ );
+ return itemWidget;
+ };
}
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
ColorScheme colorScheme = theme.colorScheme;
+ TextTheme textTheme = theme.textTheme;
- if (selectedSortIndex > widget.sortOptions.length) {
- updateCurrentList();
+ if (_selectedSortIndex > widget.sortOptions.length) {
+ _updateCurrentList();
}
- List getFilterChips() {
- List widgets = [];
- int activeFilterCount =
- widget.listFilters.where((filter) => filter.isActive).length;
- if (activeFilterCount > 0) {
- widgets.add(ListFilterActionChip(
- actions: [
- ListFilterAction(
- name: AppLocalizations.of(context)!.clearFiltersAction,
- icon: Icons.clear_rounded,
- action: () {
- for (var filter in widget.listFilters) {
- filter.reset();
- }
- onFilterChange();
- },
- ),
- ...widget.customActions.map((action) => ListFilterAction(
- name: action.name,
- icon: action.icon,
- action: () => action.action(widget.items
- .where((item) => widget.listFilters
- .every((filter) => filter.filterFunction(item)))
- .toList()),
- )),
- ListFilterAction(
- name: AppLocalizations.of(context)!.deleteAllFilteredAction,
- icon: Icons.delete_rounded,
- color: colorScheme.error,
- action: () async {
- Navigator.pop(context);
- final result = await showDeleteAlertDialogue(context);
- if (result == null || result == false) return;
-
- final toRemove = List
- .from(widget.items.where((item) =>
- widget.listFilters
- .every((filter) => filter.filterFunction(item))));
- await _handleDeleteItemList(toRemove);
-
- widget.onModifyList?.call();
- },
- )
- ],
- activeFilterCount: activeFilterCount,
- ));
- }
- widgets.addAll(widget.listFilters
- .map((filter) => getListFilterChip(filter, onFilterChange)));
- if (widget.sortOptions.isNotEmpty) {
- widgets.add(
- ListSortChip(
- selectedIndex: selectedSortIndex,
- sortOptions: [
- ListSortOption(
- (context) => AppLocalizations.of(context)!.defaultLabel,
- (a, b) => 0),
- ...widget.sortOptions,
- ],
- onChange: (index) => setState(() {
- selectedSortIndex = index;
- widget.onChangeSortIndex?.call(index);
- updateCurrentList();
- _notifyChangeList();
- }),
- ),
- );
- }
- return widgets;
- }
-
- // timeDilation = 1;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
- Expanded(
- flex: 0,
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0),
- child: SingleChildScrollView(
- scrollDirection: Axis.horizontal,
- child: Row(
- mainAxisAlignment: MainAxisAlignment.start,
- children: getFilterChips(),
- ),
- ),
- ),
- ),
- if(widget.header != null) widget.header!,
+ ListFilterBar(
+ listFilters: widget.listFilters,
+ customActions: widget.customActions,
+ sortOptions: widget.sortOptions,
+ isSelecting: _isSelecting,
+ handleCustomAction: _handleCustomAction,
+ handleEndSelection: _endSelection,
+ handleDeleteAction: _handleDeleteAction,
+ handleSelectAll: _handleSelectAll,
+ selectedIds: _selectedIds,
+ handleFilterChange: _handleFilterChange,
+ selectedSortIndex: _selectedSortIndex,
+ handleSortChange: _handleSortChange),
+ if (widget.header != null) widget.header!,
Expanded(
flex: 1,
child: Stack(children: [
@@ -428,49 +373,30 @@ class _CustomListViewState
-
child: Center(
child: Text(
widget.placeholderText,
- style:
- Theme.of(context).textTheme.displaySmall?.copyWith(
- color: Theme.of(context)
- .colorScheme
- .onBackground
- .withOpacity(0.6),
- ),
+ style: textTheme.displaySmall?.copyWith(
+ color: colorScheme.onBackground.withOpacity(0.6),
+ ),
),
),
)
: Container(),
- SlidableAutoCloseBehavior(
- child: AutomaticAnimatedListView
- (
- list: currentList,
- padding:
- const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
- comparator: AnimatedListDiffListComparator
- (
- sameItem: (a, b) => a.id == b.id,
- sameContent: (a, b) => a.id == b.id,
- ),
- itemBuilder: _getItemBuilder(),
- // animator: DefaultAnimatedListAnimator,
- listController: _controller,
- scrollController: _scrollController,
- addLongPressReorderable: widget.isReorderable,
- reorderModel: widget.isReorderable && selectedSortIndex == 0
- ? AnimatedListReorderModel(
- onReorderStart: (index, dx, dy) => true,
- onReorderFeedback: (int index, int dropIndex,
- double offset, double dx, double dy) =>
- null,
- onReorderMove: (int index, int dropIndex) => true,
- onReorderComplete: _handleReorderItems,
- )
- : null,
- reorderDecorationBuilder:
- widget.isReorderable ? reorderableListDecorator : null,
- footer: const SizedBox(height: 64 + 80),
- // header: widget.header,
-
- // cacheExtent: double.infinity,
- ),
- ),
+ AnimatedReorderableListView(
+ longPressDraggable: false,
+ buildDefaultDragHandles: false,
+ proxyDecorator: (widget, index, animation) =>
+ reorderableListDecorator(context, widget),
+ items: currentList,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ isSameItem: (a, b) => a.id == b.id,
+ scrollDirection: Axis.vertical,
+ itemBuilder: _getItemBuilder(),
+ enterTransition: [FadeIn()],
+ exitTransition: [FadeIn()],
+ controller: _scrollController,
+ insertDuration: const Duration(milliseconds: 300),
+ removeDuration: const Duration(milliseconds: 300),
+ onReorder: _handleReorderItems,
+ )
]),
),
],
diff --git a/lib/common/widgets/list/list_filter_bar.dart b/lib/common/widgets/list/list_filter_bar.dart
new file mode 100644
index 00000000..a6d87910
--- /dev/null
+++ b/lib/common/widgets/list/list_filter_bar.dart
@@ -0,0 +1,136 @@
+import 'package:clock_app/common/logic/get_list_filter_chips.dart';
+import 'package:clock_app/common/types/json.dart';
+import 'package:clock_app/common/types/list_filter.dart';
+import 'package:clock_app/common/types/list_item.dart';
+import 'package:clock_app/common/widgets/list/list_filter_chip.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_animate/flutter_animate.dart';
+import 'package:flutter_gen/gen_l10n/app_localizations.dart';
+
+class ListFilterBar
- extends StatelessWidget {
+ const ListFilterBar(
+ {super.key,
+ required this.listFilters,
+ required this.customActions,
+ required this.sortOptions,
+ required this.isSelecting,
+ required this.handleCustomAction,
+ required this.handleEndSelection,
+ required this.handleDeleteAction,
+ required this.handleSelectAll,
+ required this.selectedIds,
+ required this.handleFilterChange,
+ required this.selectedSortIndex,
+ required this.handleSortChange});
+
+ final List> listFilters;
+ final List> customActions;
+ final List> sortOptions;
+ final bool isSelecting;
+ final Function(ListFilterCustomAction
- ) handleCustomAction;
+ final Function handleEndSelection;
+ final void Function() handleFilterChange;
+ final Function handleSelectAll;
+ final List selectedIds;
+ final int selectedSortIndex;
+ final void Function() handleDeleteAction;
+ final void Function(int) handleSortChange;
+
+ @override
+ Widget build(BuildContext context) {
+ ThemeData theme = Theme.of(context);
+ ColorScheme colorScheme = theme.colorScheme;
+ TextTheme textTheme = theme.textTheme;
+
+ List getFilterChips() {
+ List widgets = [];
+ int activeFilterCount =
+ listFilters.where((filter) => filter.isActive).length;
+ if (activeFilterCount > 0 || isSelecting) {
+ widgets.add(
+ ListFilterActionChip(
+ actions: [
+ ListFilterAction(
+ name: AppLocalizations.of(context)!.clearFiltersAction,
+ icon: Icons.clear_rounded,
+ action: () {
+ for (var filter in listFilters) {
+ filter.reset();
+ }
+ handleEndSelection();
+ },
+ ),
+ ...customActions.map(
+ (action) => ListFilterAction(
+ name: action.name,
+ icon: action.icon,
+ action: () => handleCustomAction(action),
+ ),
+ ),
+ ListFilterAction(
+ name: AppLocalizations.of(context)!.deleteAllFilteredAction,
+ icon: Icons.delete_rounded,
+ color: colorScheme.error,
+ action: handleDeleteAction,
+ )
+ ],
+ activeFilterCount: activeFilterCount + (isSelecting ? 1 : 0),
+ ),
+ );
+ }
+
+ if (isSelecting) {
+ widgets.add(
+ ListButtonChip(
+ label: AppLocalizations.of(context)!
+ .selectionStatus(selectedIds.length),
+ icon: Icons.clear_rounded,
+ onTap: () => handleEndSelection(),
+ ),
+ );
+ widgets.add(
+ ListButtonChip(
+ label: AppLocalizations.of(context)!.selectAll,
+ icon: Icons.select_all_rounded,
+ onTap: () => handleSelectAll(),
+ ),
+ );
+ }
+ widgets.addAll(listFilters
+ .map((filter) => getListFilterChip(filter, handleFilterChange)));
+ if (sortOptions.isNotEmpty) {
+ widgets.add(
+ ListSortChip(
+ selectedIndex: selectedSortIndex,
+ sortOptions: [
+ ListSortOption(
+ (context) => AppLocalizations.of(context)!.defaultLabel,
+ (a, b) => 0),
+ ...sortOptions,
+ ],
+ onChange: handleSortChange,
+ ),
+ );
+ }
+ return widgets;
+ }
+
+ return Expanded(
+ flex: 0,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0),
+ child: AnimatedContainer(
+ duration: 150.ms,
+ height: getFilterChips().isEmpty ? 0 : 40,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.start,
+ children: getFilterChips(),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/lib/common/widgets/list/list_filter_chip.dart b/lib/common/widgets/list/list_filter_chip.dart
index 38bb2890..4a1350ff 100644
--- a/lib/common/widgets/list/list_filter_chip.dart
+++ b/lib/common/widgets/list/list_filter_chip.dart
@@ -2,9 +2,11 @@ import 'package:clock_app/common/logic/show_select.dart';
import 'package:clock_app/common/types/list_filter.dart';
import 'package:clock_app/common/types/list_item.dart';
import 'package:clock_app/common/types/select_choice.dart';
+import 'package:clock_app/common/widgets/animated_show_hide.dart';
import 'package:clock_app/common/widgets/card_container.dart';
import 'package:clock_app/common/widgets/list/action_bottom_sheet.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_gen/gen_l10n/app_localizations.dart';
class ListFilterChip
- extends StatelessWidget {
@@ -23,20 +25,24 @@ class ListFilterChip
- extends StatelessWidget {
ColorScheme colorScheme = theme.colorScheme;
TextTheme textTheme = theme.textTheme;
- return CardContainer(
- color: listFilter.isSelected ? colorScheme.primary : null,
- onTap: () {
- listFilter.isSelected = !listFilter.isSelected;
- onChange();
- },
- child: Padding(
- padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
- child: Text(
- listFilter.displayName(context),
- style: textTheme.headlineSmall?.copyWith(
- color: listFilter.isSelected
- ? colorScheme.onPrimary
- : colorScheme.onSurface,
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ color: listFilter.isSelected ? colorScheme.primary : null,
+ onTap: () {
+ listFilter.isSelected = !listFilter.isSelected;
+ onChange();
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
+ child: Text(
+ listFilter.displayName(context),
+ style: textTheme.headlineSmall?.copyWith(
+ color: listFilter.isSelected
+ ? colorScheme.onPrimary
+ : colorScheme.onSurface,
+ ),
),
),
),
@@ -44,6 +50,64 @@ class ListFilterChip
- extends StatelessWidget {
}
}
+class ListButtonChip
- extends StatelessWidget {
+ const ListButtonChip({
+ super.key,
+ required this.label,
+ this.onTap,
+ required this.icon,
+ this.isActive = false,
+ });
+
+ final String? label;
+ final IconData? icon;
+ final Function()? onTap;
+ final bool isActive;
+
+ @override
+ Widget build(BuildContext context) {
+ ThemeData theme = Theme.of(context);
+ ColorScheme colorScheme = theme.colorScheme;
+ TextTheme textTheme = theme.textTheme;
+
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ onTap: onTap,
+ color: isActive ? colorScheme.primary : null,
+ child: Row(
+ children: [
+ if (icon != null)
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 10.0, right: 6.0, top: 6.0, bottom: 6.0),
+ child: Icon(
+ icon,
+ color:
+ isActive ? colorScheme.onPrimary : colorScheme.onSurface,
+ size: 20,
+ ),
+ ),
+ if (label != null)
+ Padding(
+ padding: const EdgeInsets.only(right: 12.0),
+ child: Text(
+ label!,
+ style: textTheme.headlineSmall?.copyWith(
+ color: isActive
+ ? colorScheme.onPrimary
+ : colorScheme.onSurface,
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
class ListFilterActionChip
- extends StatelessWidget {
const ListFilterActionChip({
super.key,
@@ -75,34 +139,38 @@ class ListFilterActionChip
- extends StatelessWidget {
ColorScheme colorScheme = theme.colorScheme;
TextTheme textTheme = theme.textTheme;
- return CardContainer(
- color: colorScheme.primary,
- onTap: () {
- _showPopupMenu(context);
- // listFilter.isSelected = !listFilter.isSelected;
- // onChange();
- },
- child: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(
- left: 8.0, right: 6.0, top: 6.0, bottom: 6.0),
- child: Icon(
- Icons.filter_list_rounded,
- color: colorScheme.onPrimary,
- size: 20,
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ color: colorScheme.primary,
+ onTap: () {
+ _showPopupMenu(context);
+ // listFilter.isSelected = !listFilter.isSelected;
+ // onChange();
+ },
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ left: 8.0, right: 6.0, top: 6.0, bottom: 6.0),
+ child: Icon(
+ Icons.filter_list_rounded,
+ color: colorScheme.onPrimary,
+ size: 20,
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.only(right: 12.0),
- child: Text(
- activeFilterCount.toString(),
- style: textTheme.headlineSmall?.copyWith(
- color: colorScheme.onPrimary.withOpacity(0.6),
+ Padding(
+ padding: const EdgeInsets.only(right: 12.0),
+ child: Text(
+ activeFilterCount.toString(),
+ style: textTheme.headlineSmall?.copyWith(
+ color: colorScheme.onPrimary.withOpacity(0.6),
+ ),
),
),
- ),
- ],
+ ],
+ ),
),
);
}
@@ -141,34 +209,38 @@ class ListFilterSelectChip
- extends StatelessWidget {
multiSelect: false);
}
- return CardContainer(
- color: isFirstSelected ? null : colorScheme.primary,
- onTap: showSelect,
- child: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
- child: Text(
- isFirstSelected
- ? listFilter.displayName(context)
- : listFilter.selectedFilter.displayName(context),
- style: textTheme.headlineSmall?.copyWith(
- color: isFirstSelected
- ? colorScheme.onSurface
- : colorScheme.onPrimary),
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ color: isFirstSelected ? null : colorScheme.primary,
+ onTap: showSelect,
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
+ child: Text(
+ isFirstSelected
+ ? listFilter.displayName(context)
+ : listFilter.selectedFilter.displayName(context),
+ style: textTheme.headlineSmall?.copyWith(
+ color: isFirstSelected
+ ? colorScheme.onSurface
+ : colorScheme.onPrimary),
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 2.0, right: 8.0),
- child: Icon(
- Icons.keyboard_arrow_down_rounded,
- color: isFirstSelected
- ? colorScheme.onSurface.withOpacity(0.6)
- : colorScheme.onPrimary.withOpacity(0.6),
+ Padding(
+ padding: const EdgeInsets.only(left: 2.0, right: 8.0),
+ child: Icon(
+ Icons.keyboard_arrow_down_rounded,
+ color: isFirstSelected
+ ? colorScheme.onSurface.withOpacity(0.6)
+ : colorScheme.onPrimary.withOpacity(0.6),
+ ),
),
- ),
- ],
+ ],
+ ),
),
);
}
@@ -208,36 +280,40 @@ class ListFilterMultiSelectChip
- extends StatelessWidget {
multiSelect: true);
}
- return CardContainer(
- color: isSelected ? colorScheme.primary : null,
- onTap: showSelect,
- child: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
- child: Text(
- !isSelected
- ? listFilter.displayName(context)
- : listFilter.selectedIndices.length == 1
- ? listFilter.selectedFilters[0].displayName(context)
- : "${listFilter.selectedIndices.length} selected",
- style: textTheme.headlineSmall?.copyWith(
- color: isSelected
- ? colorScheme.onPrimary
- : colorScheme.onSurface),
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ color: isSelected ? colorScheme.primary : null,
+ onTap: showSelect,
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
+ child: Text(
+ !isSelected
+ ? listFilter.displayName(context)
+ : listFilter.selectedIndices.length == 1
+ ? listFilter.selectedFilters[0].displayName(context)
+ : "${listFilter.selectedIndices.length} selected",
+ style: textTheme.headlineSmall?.copyWith(
+ color: isSelected
+ ? colorScheme.onPrimary
+ : colorScheme.onSurface),
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 2.0, right: 8.0),
- child: Icon(
- Icons.keyboard_arrow_down_rounded,
- color: isSelected
- ? colorScheme.onPrimary.withOpacity(0.6)
- : colorScheme.onSurface.withOpacity(0.6),
+ Padding(
+ padding: const EdgeInsets.only(left: 2.0, right: 8.0),
+ child: Icon(
+ Icons.keyboard_arrow_down_rounded,
+ color: isSelected
+ ? colorScheme.onPrimary.withOpacity(0.6)
+ : colorScheme.onSurface.withOpacity(0.6),
+ ),
),
- ),
- ],
+ ],
+ ),
),
);
}
@@ -276,26 +352,30 @@ class ListSortChip
- extends StatelessWidget {
multiSelect: false);
}
- return CardContainer(
- // color: isFirstSelected ? null : colorScheme.primary,
- onTap: showSelect,
- child: Row(
- children: [
- Padding(
- padding: const EdgeInsets.only(
- top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
- child: Text(
- "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}",
- style: textTheme.headlineSmall
- ?.copyWith(color: colorScheme.onSurface),
+ return AnimatedShowHide(
+ duration: 200.ms,
+ axis: Axis.horizontal,
+ child: CardContainer(
+ // color: isFirstSelected ? null : colorScheme.primary,
+ onTap: showSelect,
+ child: Row(
+ children: [
+ Padding(
+ padding: const EdgeInsets.only(
+ top: 8.0, bottom: 8.0, left: 16.0, right: 2.0),
+ child: Text(
+ "${AppLocalizations.of(context)!.sortGroup}${isFirstSelected ? "" : ": ${sortOptions[selectedIndex].displayName(context)}"}",
+ style: textTheme.headlineSmall
+ ?.copyWith(color: colorScheme.onSurface),
+ ),
),
- ),
- Padding(
- padding: const EdgeInsets.only(left: 2.0, right: 8.0),
- child: Icon(Icons.keyboard_arrow_down_rounded,
- color: colorScheme.onSurface.withOpacity(0.6)),
- ),
- ],
+ Padding(
+ padding: const EdgeInsets.only(left: 2.0, right: 8.0),
+ child: Icon(Icons.keyboard_arrow_down_rounded,
+ color: colorScheme.onSurface.withOpacity(0.6)),
+ ),
+ ],
+ ),
),
);
}
diff --git a/lib/common/widgets/list/list_item_card.dart b/lib/common/widgets/list/list_item_card.dart
index 69a87597..eedab955 100644
--- a/lib/common/widgets/list/list_item_card.dart
+++ b/lib/common/widgets/list/list_item_card.dart
@@ -1,9 +1,12 @@
import 'package:clock_app/common/widgets/card_container.dart';
import 'package:clock_app/common/widgets/action_pane.dart';
+import 'package:clock_app/common/widgets/list/animated_reorderable_list/component/drag_listener.dart';
import 'package:clock_app/settings/data/general_settings_schema.dart';
import 'package:clock_app/settings/data/settings_schema.dart';
import 'package:clock_app/settings/types/setting.dart';
+import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_animate/flutter_animate.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
class ListItemCard extends StatefulWidget {
@@ -16,15 +19,23 @@ class ListItemCard extends StatefulWidget {
this.onInit,
this.isDeleteEnabled = true,
this.isDuplicateEnabled = true,
+ this.isSelected = false,
+ this.showReorderHandle = false,
+ required this.index,
+ this.onLongPress,
});
final VoidCallback? onDelete;
final VoidCallback? onDuplicate;
final VoidCallback? onTap;
+ final VoidCallback? onLongPress;
final Widget child;
final VoidCallback? onInit;
final bool isDeleteEnabled;
final bool isDuplicateEnabled;
+ final bool isSelected;
+ final bool showReorderHandle;
+ final int index;
@override
State createState() => _ListItemCardState();
@@ -32,6 +43,7 @@ class ListItemCard extends StatefulWidget {
class _ListItemCardState extends State> {
late Setting swipeActionSetting;
+ late Setting longPressActionSetting;
void update(dynamic value) {
setState(() {});
@@ -41,8 +53,11 @@ class _ListItemCardState extends State> {
void initState() {
super.initState();
widget.onInit?.call();
- swipeActionSetting =
- appSettings.getGroup("General").getSetting("Swipe Action");
+ final interactionSettingsGroup =
+ appSettings.getGroup("General").getGroup("Interactions");
+ swipeActionSetting = interactionSettingsGroup.getSetting("Swipe Action");
+ longPressActionSetting =
+ interactionSettingsGroup.getSetting("Long Press Action");
swipeActionSetting.addListener(update);
}
@@ -55,8 +70,11 @@ class _ListItemCardState