mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00: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
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
Add a link
Reference in a new issue