import 'package:flutter/material.dart'; import "package:modal_bottom_sheet/modal_bottom_sheet.dart"; import "package:photos/core/constants.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/location/location.dart"; import "package:photos/services/location_service.dart"; import 'package:photos/states/location_state.dart'; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/common/loading_widget.dart"; import "package:photos/ui/components/bottom_of_title_bar_widget.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import "package:photos/ui/components/divider_widget.dart"; import "package:photos/ui/components/keyboard/keybiard_oveylay.dart"; import "package:photos/ui/components/keyboard/keyboard_top_button.dart"; import "package:photos/ui/components/models/button_type.dart"; import "package:photos/ui/components/text_input_widget.dart"; import "package:photos/ui/components/title_bar_title_widget.dart"; import 'package:photos/ui/viewer/location/dynamic_location_gallery_widget.dart'; import "package:photos/ui/viewer/location/radius_picker_widget.dart"; showAddLocationSheet( BuildContext context, Location coordinates, ) { showBarModalBottomSheet( context: context, builder: (context) { return LocationTagStateProvider( centerPoint: coordinates, const AddLocationSheet(), ); }, shape: const RoundedRectangleBorder( side: BorderSide(width: 0), borderRadius: BorderRadius.vertical( top: Radius.circular(5), ), ), topControl: const SizedBox.shrink(), backgroundColor: getEnteColorScheme(context).backgroundElevated, barrierColor: backdropFaintDark, ); } class AddLocationSheet extends StatefulWidget { const AddLocationSheet({super.key}); @override State createState() => _AddLocationSheetState(); } class _AddLocationSheetState extends State { //The value of these notifiers has no significance. //When memoriesCountNotifier is null, we show the loading widget in the //memories count section which also means the gallery is loading. final ValueNotifier _memoriesCountNotifier = ValueNotifier(null); final ValueNotifier _submitNotifer = ValueNotifier(false); final ValueNotifier _cancelNotifier = ValueNotifier(false); final ValueNotifier _selectedRadiusIndexNotifier = ValueNotifier(defaultRadiusValueIndex); final _focusNode = FocusNode(); final _textEditingController = TextEditingController(); final _isEmptyNotifier = ValueNotifier(true); Widget? _keyboardTopButtons; @override void initState() { _focusNode.addListener(_focusNodeListener); _selectedRadiusIndexNotifier.addListener(_selectedRadiusIndexListener); super.initState(); } @override void dispose() { _focusNode.removeListener(_focusNodeListener); _submitNotifer.dispose(); _cancelNotifier.dispose(); _selectedRadiusIndexNotifier.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final textTheme = getEnteTextTheme(context); final colorScheme = getEnteColorScheme(context); return Padding( padding: const EdgeInsets.fromLTRB(0, 32, 0, 8), child: Column( children: [ Padding( padding: const EdgeInsets.only(bottom: 16), child: BottomOfTitleBarWidget( title: TitleBarTitleWidget(title: S.of(context).addLocation), ), ), Expanded( child: SingleChildScrollView( physics: const BouncingScrollPhysics( decelerationRate: ScrollDecelerationRate.fast, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ Row( children: [ Expanded( child: TextInputWidget( hintText: S.of(context).locationName, borderRadius: 2, focusNode: _focusNode, submitNotifier: _submitNotifer, cancelNotifier: _cancelNotifier, popNavAfterSubmission: false, shouldUnfocusOnClearOrSubmit: true, alwaysShowSuccessState: true, textCapitalization: TextCapitalization.words, textEditingController: _textEditingController, isEmptyNotifier: _isEmptyNotifier, ), ), const SizedBox(width: 8), ValueListenableBuilder( valueListenable: _isEmptyNotifier, builder: (context, bool value, _) { return AnimatedSwitcher( duration: const Duration(milliseconds: 250), switchInCurve: Curves.easeInOut, switchOutCurve: Curves.easeInOut, child: ButtonWidget( key: ValueKey(value), buttonType: ButtonType.secondary, buttonSize: ButtonSize.small, labelText: S.of(context).addLocationButton, isDisabled: value, onTap: () async { _focusNode.unfocus(); await _addLocationTag(); }, ), ); }, ) ], ), const SizedBox(height: 24), RadiusPickerWidget( _selectedRadiusIndexNotifier, ), const SizedBox(height: 16), Text( S.of(context).locationTagFeatureDescription, style: textTheme.smallMuted, ), ], ), ), const DividerWidget( dividerType: DividerType.solid, padding: EdgeInsets.only(top: 24, bottom: 20), ), SizedBox( width: double.infinity, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16), child: ValueListenableBuilder( valueListenable: _memoriesCountNotifier, builder: (context, int? value, _) { Widget widget; if (value == null) { widget = RepaintBoundary( child: EnteLoadingWidget( size: 14, color: colorScheme.strokeMuted, alignment: Alignment.centerLeft, padding: 3, ), ); } else { widget = Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( S.of(context).memoryCount(value), style: textTheme.body, ), if (value > 1000) Padding( padding: const EdgeInsets.only(top: 2), child: Text( S.of(context).galleryMemoryLimitInfo, style: textTheme.miniMuted, ), ), ], ); } return Align( alignment: Alignment.centerLeft, child: AnimatedSwitcher( duration: const Duration(milliseconds: 250), switchInCurve: Curves.easeInOutExpo, switchOutCurve: Curves.easeInOutExpo, child: widget, ), ); }, ), ), ), const SizedBox(height: 24), DynamicLocationGalleryWidget( _memoriesCountNotifier, "Add_location", ), ], ), ), ), ], ), ); } Future _addLocationTag() async { final locationData = InheritedLocationTagData.of(context); final coordinates = locationData.centerPoint; final radiusValues = locationData.radiusValues; final radius = radiusValues[locationData.selectedRadiusIndex]; await LocationService.instance.addLocation( _textEditingController.text.trim(), coordinates, radius, ); Navigator.pop(context); } void _focusNodeListener() { final bool hasFocus = _focusNode.hasFocus; _keyboardTopButtons ??= KeyboardTopButton( onDoneTap: () { _submitNotifer.value = !_submitNotifer.value; }, onCancelTap: () { _cancelNotifier.value = !_cancelNotifier.value; }, ); if (hasFocus) { KeyboardOverlay.showOverlay(context, _keyboardTopButtons!); } else { KeyboardOverlay.removeOverlay(); } } void _selectedRadiusIndexListener() { InheritedLocationTagData.of( context, ).updateSelectedIndex( _selectedRadiusIndexNotifier.value, ); _memoriesCountNotifier.value = null; } }