mirror of
https://github.com/immich-app/immich.git
synced 2025-03-04 02:11:44 -05:00
fix(mobile): improve timezone picker (#15615)
- Fix missing timezones - Remove the UTC prefix from timezone display text to align with web app - Remove unnecessary layout builder - Created a custom `DropdownSearchMenu` widget Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
parent
d12b1c907d
commit
19f2f888ee
3 changed files with 239 additions and 74 deletions
|
@ -248,6 +248,7 @@
|
|||
"download_waiting_to_retry": "Waiting to retry",
|
||||
"edit_date_time_dialog_date_time": "Date and Time",
|
||||
"edit_date_time_dialog_timezone": "Timezone",
|
||||
"edit_date_time_dialog_search_timezone": "Search timezone...",
|
||||
"edit_image_title": "Edit",
|
||||
"edit_location_dialog_title": "Location",
|
||||
"end_date": "End date",
|
||||
|
|
|
@ -4,6 +4,7 @@ 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';
|
||||
|
||||
|
@ -24,7 +25,7 @@ Future<String?> showDateTimePicker({
|
|||
}
|
||||
|
||||
String _getFormattedOffset(int offsetInMilli, tz.Location location) {
|
||||
return "${location.name} (UTC${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||
return "${location.name} (${Duration(milliseconds: offsetInMilli).formatAsOffset()})";
|
||||
}
|
||||
|
||||
class _DateTimePicker extends HookWidget {
|
||||
|
@ -73,7 +74,6 @@ class _DateTimePicker extends HookWidget {
|
|||
// 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();
|
||||
|
@ -133,83 +133,78 @@ class _DateTimePicker extends HookWidget {
|
|||
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,
|
||||
return 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,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: popWithDateTime,
|
||||
child: Text(
|
||||
"action_common_update",
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: context.primaryColor,
|
||||
),
|
||||
).tr(),
|
||||
const SizedBox(height: 24),
|
||||
DropdownSearchMenu(
|
||||
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,
|
||||
),
|
||||
],
|
||||
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
169
mobile/lib/widgets/common/dropdown_search_menu.dart
Normal file
169
mobile/lib/widgets/common/dropdown_search_menu.dart
Normal file
|
@ -0,0 +1,169 @@
|
|||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
|
||||
class DropdownSearchMenu<T> extends HookWidget {
|
||||
const DropdownSearchMenu({
|
||||
super.key,
|
||||
required this.dropdownMenuEntries,
|
||||
this.initialSelection,
|
||||
this.onSelected,
|
||||
this.trailingIcon,
|
||||
this.hintText,
|
||||
this.label,
|
||||
this.textStyle,
|
||||
this.menuConstraints,
|
||||
});
|
||||
|
||||
final List<DropdownMenuEntry<T>> dropdownMenuEntries;
|
||||
final T? initialSelection;
|
||||
final ValueChanged<T>? onSelected;
|
||||
final Widget? trailingIcon;
|
||||
final String? hintText;
|
||||
final Widget? label;
|
||||
final TextStyle? textStyle;
|
||||
final BoxConstraints? menuConstraints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selectedItem = useState<DropdownMenuEntry<T>?>(
|
||||
dropdownMenuEntries
|
||||
.firstWhereOrNull((item) => item.value == initialSelection),
|
||||
);
|
||||
final showTimeZoneDropdown = useState<bool>(false);
|
||||
|
||||
final effectiveConstraints = menuConstraints ??
|
||||
const BoxConstraints(
|
||||
minWidth: 280,
|
||||
maxWidth: 280,
|
||||
minHeight: 0,
|
||||
maxHeight: 280,
|
||||
);
|
||||
|
||||
final inputDecoration = InputDecoration(
|
||||
contentPadding: const EdgeInsets.fromLTRB(12, 4, 12, 4),
|
||||
border: const OutlineInputBorder(),
|
||||
suffixIcon: trailingIcon,
|
||||
label: label,
|
||||
hintText: hintText,
|
||||
).applyDefaults(context.themeData.inputDecorationTheme);
|
||||
|
||||
if (!showTimeZoneDropdown.value) {
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: GestureDetector(
|
||||
onTap: () => showTimeZoneDropdown.value = true,
|
||||
child: InputDecorator(
|
||||
decoration: inputDecoration,
|
||||
child: selectedItem.value != null
|
||||
? Text(
|
||||
selectedItem.value!.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textStyle,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Autocomplete<DropdownMenuEntry<T>>(
|
||||
displayStringForOption: (option) => option.label,
|
||||
optionsBuilder: (textEditingValue) {
|
||||
return dropdownMenuEntries.where(
|
||||
(item) => item.label
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.contains(textEditingValue.text.toLowerCase().trim()),
|
||||
);
|
||||
},
|
||||
onSelected: (option) {
|
||||
selectedItem.value = option;
|
||||
showTimeZoneDropdown.value = false;
|
||||
onSelected?.call(option.value);
|
||||
},
|
||||
fieldViewBuilder: (context, textEditingController, focusNode, _) {
|
||||
return TextField(
|
||||
autofocus: true,
|
||||
focusNode: focusNode,
|
||||
controller: textEditingController,
|
||||
decoration: inputDecoration.copyWith(
|
||||
hintText: "edit_date_time_dialog_search_timezone".tr(),
|
||||
),
|
||||
maxLines: 1,
|
||||
style: context.textTheme.bodyMedium,
|
||||
expands: false,
|
||||
onTapOutside: (event) {
|
||||
showTimeZoneDropdown.value = false;
|
||||
focusNode.unfocus();
|
||||
},
|
||||
onSubmitted: (_) {
|
||||
showTimeZoneDropdown.value = false;
|
||||
},
|
||||
);
|
||||
},
|
||||
optionsViewBuilder: (context, onSelected, options) {
|
||||
// This widget is a copy of the default implementation.
|
||||
// We have only changed the `constraints` parameter.
|
||||
return Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: ConstrainedBox(
|
||||
constraints: effectiveConstraints,
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: ListView.builder(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: options.length,
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final option = options.elementAt(index);
|
||||
return InkWell(
|
||||
onTap: () => onSelected(option),
|
||||
child: Builder(
|
||||
builder: (BuildContext context) {
|
||||
final bool highlight =
|
||||
AutocompleteHighlightedOption.of(context) ==
|
||||
index;
|
||||
if (highlight) {
|
||||
SchedulerBinding.instance.addPostFrameCallback(
|
||||
(Duration timeStamp) {
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
alignment: 0.5,
|
||||
);
|
||||
},
|
||||
debugLabel: 'AutocompleteOptions.ensureVisible',
|
||||
);
|
||||
}
|
||||
return Container(
|
||||
color: highlight
|
||||
? Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withOpacity(0.12)
|
||||
: null,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
option.label,
|
||||
style: textStyle,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue