mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05:00 
			
		
		
		
	* date time component * rename to info_sheet * simplify map info * Edit datetime sheet * fix janking when scroll on info sheet * Location refactor * refactor name * Update date time after editing * localize rebuild to smaller component * restore advanced bottom sheet * reassign EXIF back to local database * remove print statements
		
			
				
	
	
		
			265 lines
		
	
	
		
			7.9 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			265 lines
		
	
	
		
			7.9 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 []);
 | 
						|
    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 LayoutBuilder(
 | 
						|
      builder: (context, constraint) => AlertDialog(
 | 
						|
        contentPadding:
 | 
						|
            const EdgeInsets.symmetric(vertical: 32, horizontal: 18),
 | 
						|
        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(),
 | 
						|
          ),
 | 
						|
        ],
 | 
						|
        content: Column(
 | 
						|
          mainAxisSize: MainAxisSize.min,
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            const Text(
 | 
						|
              "edit_date_time_dialog_date_time",
 | 
						|
              style: TextStyle(
 | 
						|
                fontSize: 16,
 | 
						|
                fontWeight: FontWeight.w600,
 | 
						|
              ),
 | 
						|
            ).tr(),
 | 
						|
            const SizedBox(height: 32),
 | 
						|
            ListTile(
 | 
						|
              tileColor: context.colorScheme.surfaceContainerHighest,
 | 
						|
              shape: ShapeBorder.lerp(
 | 
						|
                RoundedRectangleBorder(
 | 
						|
                  borderRadius: BorderRadius.circular(10),
 | 
						|
                ),
 | 
						|
                RoundedRectangleBorder(
 | 
						|
                  borderRadius: BorderRadius.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),
 | 
						|
            DropdownMenu(
 | 
						|
              width: 275,
 | 
						|
              menuHeight: 300,
 | 
						|
              trailingIcon: Icon(
 | 
						|
                Icons.arrow_drop_down,
 | 
						|
                color: context.primaryColor,
 | 
						|
              ),
 | 
						|
              hintText: "edit_date_time_dialog_timezone".tr(),
 | 
						|
              label: const Text('edit_date_time_dialog_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;
 | 
						|
}
 |