mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-26 08:12:33 -04:00 
			
		
		
		
	* enable border radius, switch exp, const constructor * regenerate provider * more formatting --------- Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
		
			
				
	
	
		
			264 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			264 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
| import 'package:collection/collection.dart';
 | |
| import 'package:easy_localization/easy_localization.dart';
 | |
| import 'package:flutter/material.dart';
 | |
| import 'package:flutter_hooks/flutter_hooks.dart';
 | |
| import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | |
| import 'package:immich_mobile/extensions/duration_extensions.dart';
 | |
| import 'package:immich_mobile/widgets/common/dropdown_search_menu.dart';
 | |
| import 'package:timezone/timezone.dart' as tz;
 | |
| import 'package:timezone/timezone.dart';
 | |
| 
 | |
| Future<String?> showDateTimePicker({
 | |
|   required BuildContext context,
 | |
|   DateTime? initialDateTime,
 | |
|   String? initialTZ,
 | |
|   Duration? initialTZOffset,
 | |
| }) {
 | |
|   return showDialog<String?>(
 | |
|     context: context,
 | |
|     builder: (context) => _DateTimePicker(
 | |
|       initialDateTime: initialDateTime,
 | |
|       initialTZ: initialTZ,
 | |
|       initialTZOffset: initialTZOffset,
 | |
|     ),
 | |
|   );
 | |
| }
 | |
| 
 | |
| String _getFormattedOffset(int offsetInMilli, tz.Location location) {
 | |
|   return "${location.name} (${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
 | |
| }
 | |
| 
 | |
| class _DateTimePicker extends HookWidget {
 | |
|   final DateTime? initialDateTime;
 | |
|   final String? initialTZ;
 | |
|   final Duration? initialTZOffset;
 | |
| 
 | |
|   const _DateTimePicker({
 | |
|     this.initialDateTime,
 | |
|     this.initialTZ,
 | |
|     this.initialTZOffset,
 | |
|   });
 | |
| 
 | |
|   _TimeZoneOffset _getInitiationLocation() {
 | |
|     if (initialTZ != null) {
 | |
|       try {
 | |
|         return _TimeZoneOffset.fromLocation(
 | |
|           tz.timeZoneDatabase.get(initialTZ!),
 | |
|         );
 | |
|       } on LocationNotFoundException {
 | |
|         // no-op
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     Duration? tzOffset = initialTZOffset ?? initialDateTime?.timeZoneOffset;
 | |
| 
 | |
|     if (tzOffset != null) {
 | |
|       final offsetInMilli = tzOffset.inMilliseconds;
 | |
|       // get all locations with matching offset
 | |
|       final locations = tz.timeZoneDatabase.locations.values.where(
 | |
|         (location) => location.currentTimeZone.offset == offsetInMilli,
 | |
|       );
 | |
|       // Prefer locations with abbreviation first
 | |
|       final location = locations.firstWhereOrNull(
 | |
|             (e) => !e.currentTimeZone.abbreviation.contains("0"),
 | |
|           ) ??
 | |
|           locations.firstOrNull;
 | |
|       if (location != null) {
 | |
|         return _TimeZoneOffset.fromLocation(location);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return _TimeZoneOffset.fromLocation(tz.getLocation("UTC"));
 | |
|   }
 | |
| 
 | |
|   // returns a list of location<name> along with it's offset in duration
 | |
|   List<_TimeZoneOffset> getAllTimeZones() {
 | |
|     return tz.timeZoneDatabase.locations.values
 | |
|         .map(_TimeZoneOffset.fromLocation)
 | |
|         .sorted()
 | |
|         .toList();
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   Widget build(BuildContext context) {
 | |
|     final date = useState<DateTime>(initialDateTime ?? DateTime.now());
 | |
|     final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation());
 | |
|     final timeZones = useMemoized(() => getAllTimeZones(), const []);
 | |
|     final menuEntries = timeZones
 | |
|         .map(
 | |
|           (timezone) => DropdownMenuEntry<_TimeZoneOffset>(
 | |
|             value: timezone,
 | |
|             label: timezone.display,
 | |
|             style: ButtonStyle(
 | |
|               textStyle: WidgetStatePropertyAll(
 | |
|                 context.textTheme.bodyMedium,
 | |
|               ),
 | |
|             ),
 | |
|           ),
 | |
|         )
 | |
|         .toList();
 | |
| 
 | |
|     void pickDate() async {
 | |
|       final now = DateTime.now();
 | |
|       // Handles cases where the date from the asset is far off in the future
 | |
|       final initialDate = date.value.isAfter(now) ? now : date.value;
 | |
|       final newDate = await showDatePicker(
 | |
|         context: context,
 | |
|         initialDate: initialDate,
 | |
|         firstDate: DateTime(1800),
 | |
|         lastDate: now,
 | |
|       );
 | |
|       if (newDate == null) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       final newTime = await showTimePicker(
 | |
|         context: context,
 | |
|         initialTime: TimeOfDay.fromDateTime(date.value),
 | |
|       );
 | |
| 
 | |
|       if (newTime == null) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       date.value = newDate.copyWith(hour: newTime.hour, minute: newTime.minute);
 | |
|     }
 | |
| 
 | |
|     void popWithDateTime() {
 | |
|       final formattedDateTime =
 | |
|           DateFormat("yyyy-MM-dd'T'HH:mm:ss").format(date.value);
 | |
|       final dtWithOffset = formattedDateTime +
 | |
|           Duration(milliseconds: tzOffset.value.offsetInMilliseconds)
 | |
|               .formatAsOffset();
 | |
|       context.pop(dtWithOffset);
 | |
|     }
 | |
| 
 | |
|     return AlertDialog(
 | |
|       contentPadding: const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
 | |
|       actions: [
 | |
|         TextButton(
 | |
|           onPressed: () => context.pop(),
 | |
|           child: Text(
 | |
|             "cancel",
 | |
|             style: context.textTheme.bodyMedium?.copyWith(
 | |
|               fontWeight: FontWeight.w600,
 | |
|               color: context.colorScheme.error,
 | |
|             ),
 | |
|           ).tr(),
 | |
|         ),
 | |
|         TextButton(
 | |
|           onPressed: popWithDateTime,
 | |
|           child: Text(
 | |
|             "action_common_update",
 | |
|             style: context.textTheme.bodyMedium?.copyWith(
 | |
|               fontWeight: FontWeight.w600,
 | |
|               color: context.primaryColor,
 | |
|             ),
 | |
|           ).tr(),
 | |
|         ),
 | |
|       ],
 | |
|       content: Column(
 | |
|         mainAxisSize: MainAxisSize.min,
 | |
|         crossAxisAlignment: CrossAxisAlignment.start,
 | |
|         children: [
 | |
|           const Text(
 | |
|             "date_and_time",
 | |
|             style: TextStyle(
 | |
|               fontSize: 16,
 | |
|               fontWeight: FontWeight.w600,
 | |
|             ),
 | |
|           ).tr(),
 | |
|           const SizedBox(height: 32),
 | |
|           ListTile(
 | |
|             tileColor: context.colorScheme.surfaceContainerHighest,
 | |
|             shape: ShapeBorder.lerp(
 | |
|               const RoundedRectangleBorder(
 | |
|                 borderRadius: BorderRadius.all(
 | |
|                   Radius.circular(10),
 | |
|                 ),
 | |
|               ),
 | |
|               const RoundedRectangleBorder(
 | |
|                 borderRadius: BorderRadius.all(
 | |
|                   Radius.circular(10),
 | |
|                 ),
 | |
|               ),
 | |
|               1,
 | |
|             ),
 | |
|             trailing: Icon(
 | |
|               Icons.edit_outlined,
 | |
|               size: 18,
 | |
|               color: context.primaryColor,
 | |
|             ),
 | |
|             title: Text(
 | |
|               DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
 | |
|               style: context.textTheme.bodyMedium,
 | |
|             ).tr(),
 | |
|             onTap: pickDate,
 | |
|           ),
 | |
|           const SizedBox(height: 24),
 | |
|           DropdownSearchMenu(
 | |
|             trailingIcon: Icon(
 | |
|               Icons.arrow_drop_down,
 | |
|               color: context.primaryColor,
 | |
|             ),
 | |
|             hintText: "timezone".tr(),
 | |
|             label: const Text('timezone').tr(),
 | |
|             textStyle: context.textTheme.bodyMedium,
 | |
|             onSelected: (value) => tzOffset.value = value,
 | |
|             initialSelection: tzOffset.value,
 | |
|             dropdownMenuEntries: menuEntries,
 | |
|           ),
 | |
|         ],
 | |
|       ),
 | |
|     );
 | |
|   }
 | |
| }
 | |
| 
 | |
| class _TimeZoneOffset implements Comparable<_TimeZoneOffset> {
 | |
|   final String display;
 | |
|   final Location location;
 | |
| 
 | |
|   const _TimeZoneOffset({
 | |
|     required this.display,
 | |
|     required this.location,
 | |
|   });
 | |
| 
 | |
|   _TimeZoneOffset copyWith({
 | |
|     String? display,
 | |
|     Location? location,
 | |
|   }) {
 | |
|     return _TimeZoneOffset(
 | |
|       display: display ?? this.display,
 | |
|       location: location ?? this.location,
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   int get offsetInMilliseconds => location.currentTimeZone.offset;
 | |
| 
 | |
|   _TimeZoneOffset.fromLocation(tz.Location l)
 | |
|       : display = _getFormattedOffset(l.currentTimeZone.offset, l),
 | |
|         location = l;
 | |
| 
 | |
|   @override
 | |
|   int compareTo(_TimeZoneOffset other) {
 | |
|     return offsetInMilliseconds.compareTo(other.offsetInMilliseconds);
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   String toString() =>
 | |
|       '_TimeZoneOffset(display: $display, location: $location)';
 | |
| 
 | |
|   @override
 | |
|   bool operator ==(Object other) {
 | |
|     if (identical(this, other)) return true;
 | |
| 
 | |
|     return other is _TimeZoneOffset &&
 | |
|         other.display == display &&
 | |
|         other.offsetInMilliseconds == offsetInMilliseconds;
 | |
|   }
 | |
| 
 | |
|   @override
 | |
|   int get hashCode =>
 | |
|       display.hashCode ^ offsetInMilliseconds.hashCode ^ location.hashCode;
 | |
| }
 |