mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			261 lines
		
	
	
		
			7.7 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			261 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: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} (UTC${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
 | 
						|
        .where((l) => !l.currentTimeZone.abbreviation.contains("0"))
 | 
						|
        .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 []);
 | 
						|
 | 
						|
    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.all(30),
 | 
						|
      alignment: Alignment.center,
 | 
						|
      content: Column(
 | 
						|
        mainAxisSize: MainAxisSize.min,
 | 
						|
        children: [
 | 
						|
          const Text(
 | 
						|
            "edit_date_time_dialog_date_time",
 | 
						|
            textAlign: TextAlign.center,
 | 
						|
          ).tr(),
 | 
						|
          TextButton.icon(
 | 
						|
            onPressed: pickDate,
 | 
						|
            icon: Text(
 | 
						|
              DateFormat("dd-MM-yyyy hh:mm a").format(date.value),
 | 
						|
              style: context.textTheme.bodyLarge
 | 
						|
                  ?.copyWith(color: context.primaryColor),
 | 
						|
            ),
 | 
						|
            label: const Icon(
 | 
						|
              Icons.edit_outlined,
 | 
						|
              size: 18,
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          const Text(
 | 
						|
            "edit_date_time_dialog_timezone",
 | 
						|
            textAlign: TextAlign.center,
 | 
						|
          ).tr(),
 | 
						|
          DropdownMenu(
 | 
						|
            menuHeight: 300,
 | 
						|
            width: 280,
 | 
						|
            inputDecorationTheme: const InputDecorationTheme(
 | 
						|
              border: InputBorder.none,
 | 
						|
              contentPadding: EdgeInsets.zero,
 | 
						|
            ),
 | 
						|
            trailingIcon: Padding(
 | 
						|
              padding: const EdgeInsets.only(right: 10),
 | 
						|
              child: Icon(
 | 
						|
                Icons.arrow_drop_down,
 | 
						|
                color: context.primaryColor,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            textStyle: context.textTheme.bodyLarge?.copyWith(
 | 
						|
              color: context.primaryColor,
 | 
						|
            ),
 | 
						|
            menuStyle: const MenuStyle(
 | 
						|
              fixedSize: MaterialStatePropertyAll(Size.fromWidth(350)),
 | 
						|
              alignment: Alignment(-1.25, 0.5),
 | 
						|
            ),
 | 
						|
            onSelected: (value) => tzOffset.value = value!,
 | 
						|
            initialSelection: tzOffset.value,
 | 
						|
            dropdownMenuEntries: timeZones
 | 
						|
                .map(
 | 
						|
                  (t) => DropdownMenuEntry<_TimeZoneOffset>(
 | 
						|
                    value: t,
 | 
						|
                    label: t.display,
 | 
						|
                    style: ButtonStyle(
 | 
						|
                      textStyle: MaterialStatePropertyAll(
 | 
						|
                        context.textTheme.bodyMedium,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                )
 | 
						|
                .toList(),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
      actions: [
 | 
						|
        TextButton(
 | 
						|
          onPressed: () => context.pop(),
 | 
						|
          child: Text(
 | 
						|
            "action_common_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(),
 | 
						|
        ),
 | 
						|
      ],
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
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;
 | 
						|
}
 |