diff --git a/lib/core/error-reporting/super_logging.dart b/lib/core/error-reporting/super_logging.dart index e9506ea28..a45c0fa4d 100644 --- a/lib/core/error-reporting/super_logging.dart +++ b/lib/core/error-reporting/super_logging.dart @@ -14,10 +14,9 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:photos/core/error-reporting/tunneled_transport.dart'; +import 'package:photos/models/typedefs.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -typedef FutureOrVoidCallback = FutureOr Function(); - extension SuperString on String { Iterable chunked(int chunkSize) sync* { var start = 0; diff --git a/lib/models/execution_states.dart b/lib/models/execution_states.dart new file mode 100644 index 000000000..764386c6e --- /dev/null +++ b/lib/models/execution_states.dart @@ -0,0 +1,6 @@ +enum ExecutionState { + idle, + inProgress, + error, + successful; +} diff --git a/lib/models/typedefs.dart b/lib/models/typedefs.dart new file mode 100644 index 000000000..5a0ba2bf2 --- /dev/null +++ b/lib/models/typedefs.dart @@ -0,0 +1,6 @@ +import 'dart:async'; + +typedef FutureVoidCallback = Future Function(); +typedef BoolCallBack = bool Function(); +typedef FutureVoidCallbackParamStr = Future Function(String); +typedef FutureOrVoidCallback = FutureOr Function(); diff --git a/lib/ui/components/button_widget.dart b/lib/ui/components/button_widget.dart index 6133359a9..5fc2918cd 100644 --- a/lib/ui/components/button_widget.dart +++ b/lib/ui/components/button_widget.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; +import 'package:photos/models/execution_states.dart'; import "package:photos/models/search/button_result.dart"; +import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/text_style.dart'; @@ -9,13 +11,6 @@ import 'package:photos/ui/components/models/button_type.dart'; import 'package:photos/ui/components/models/custom_button_style.dart'; import 'package:photos/utils/debouncer.dart'; -enum ExecutionState { - idle, - inProgress, - error, - successful; -} - enum ButtonSize { small, large; @@ -30,8 +25,6 @@ enum ButtonAction { error; } -typedef FutureVoidCallback = Future Function(); - class ButtonWidget extends StatelessWidget { final IconData? icon; final String? labelText; diff --git a/lib/ui/components/dialog_widget.dart b/lib/ui/components/dialog_widget.dart index df26b12da..1818ae6c7 100644 --- a/lib/ui/components/dialog_widget.dart +++ b/lib/ui/components/dialog_widget.dart @@ -2,10 +2,13 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:photos/core/constants.dart'; +import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/colors.dart'; import 'package:photos/theme/effects.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/button_widget.dart'; +import 'package:photos/ui/components/models/button_type.dart'; +import 'package:photos/ui/components/text_input_widget.dart'; import 'package:photos/utils/separators_util.dart'; ///Will return null if dismissed by tapping outside @@ -151,3 +154,128 @@ class Actions extends StatelessWidget { ); } } + +class TextInputDialog extends StatefulWidget { + final String title; + final String? body; + final String submitButtonLabel; + final IconData? icon; + final String? label; + final String? message; + final FutureVoidCallbackParamStr onSubmit; + final String? hintText; + final IconData? prefixIcon; + final String? initialValue; + final Alignment? alignMessage; + final int? maxLength; + final bool showOnlyLoadingState; + final TextCapitalization? textCapitalization; + final bool alwaysShowSuccessState; + const TextInputDialog({ + required this.title, + this.body, + required this.submitButtonLabel, + required this.onSubmit, + this.icon, + this.label, + this.message, + this.hintText, + this.prefixIcon, + this.initialValue, + this.alignMessage, + this.maxLength, + this.textCapitalization, + this.showOnlyLoadingState = false, + this.alwaysShowSuccessState = false, + super.key, + }); + + @override + State createState() => _TextInputDialogState(); +} + +class _TextInputDialogState extends State { + //the value of this ValueNotifier has no significance + final _submitNotifier = ValueNotifier(false); + + @override + void dispose() { + _submitNotifier.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final widthOfScreen = MediaQuery.of(context).size.width; + final isMobileSmall = widthOfScreen <= mobileSmallThreshold; + final colorScheme = getEnteColorScheme(context); + return Container( + width: min(widthOfScreen, 320), + padding: isMobileSmall + ? const EdgeInsets.all(0) + : const EdgeInsets.fromLTRB(6, 8, 6, 6), + decoration: BoxDecoration( + color: colorScheme.backgroundElevated, + boxShadow: shadowFloatLight, + borderRadius: const BorderRadius.all(Radius.circular(8)), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ContentContainer( + title: widget.title, + body: widget.body, + icon: widget.icon, + ), + Padding( + padding: const EdgeInsets.only(top: 19), + child: TextInputWidget( + label: widget.label, + message: widget.message, + hintText: widget.hintText, + prefixIcon: widget.prefixIcon, + initialValue: widget.initialValue, + alignMessage: widget.alignMessage, + autoFocus: true, + maxLength: widget.maxLength, + submitNotifier: _submitNotifier, + onSubmit: widget.onSubmit, + popNavAfterSubmission: true, + showOnlyLoadingState: widget.showOnlyLoadingState, + textCapitalization: widget.textCapitalization, + alwaysShowSuccessState: widget.alwaysShowSuccessState, + ), + ), + const SizedBox(height: 36), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Expanded( + child: ButtonWidget( + buttonType: ButtonType.secondary, + buttonSize: ButtonSize.small, + labelText: "Cancel", + isInAlert: true, + ), + ), + const SizedBox(width: 8), + Expanded( + child: ButtonWidget( + buttonSize: ButtonSize.small, + buttonType: ButtonType.neutral, + labelText: widget.submitButtonLabel, + onTap: () async { + _submitNotifier.value = !_submitNotifier.value; + }, + ), + ), + ], + ) + ], + ), + ), + ); + } +} diff --git a/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart b/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart index 59f7f69a5..b380e07c3 100644 --- a/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart +++ b/lib/ui/components/menu_item_widget/menu_item_child_widgets.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:photos/models/execution_states.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/common/loading_widget.dart'; -import 'package:photos/ui/components/menu_item_widget/menu_item_widget.dart'; class TrailingWidget extends StatefulWidget { final ValueNotifier executionStateNotifier; diff --git a/lib/ui/components/menu_item_widget/menu_item_widget.dart b/lib/ui/components/menu_item_widget/menu_item_widget.dart index f89d60a85..112af62e2 100644 --- a/lib/ui/components/menu_item_widget/menu_item_widget.dart +++ b/lib/ui/components/menu_item_widget/menu_item_widget.dart @@ -1,18 +1,11 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/material.dart'; +import 'package:photos/models/execution_states.dart'; +import 'package:photos/models/typedefs.dart'; import 'package:photos/theme/ente_theme.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart'; import 'package:photos/utils/debouncer.dart'; -enum ExecutionState { - idle, - inProgress, - error, - successful; -} - -typedef FutureVoidCallback = Future Function(); - class MenuItemWidget extends StatefulWidget { final Widget captionedTextWidget; final bool isExpandable; diff --git a/lib/ui/components/text_input_widget.dart b/lib/ui/components/text_input_widget.dart new file mode 100644 index 000000000..da91769f4 --- /dev/null +++ b/lib/ui/components/text_input_widget.dart @@ -0,0 +1,312 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:photos/models/execution_states.dart'; +import 'package:photos/models/typedefs.dart'; +import 'package:photos/theme/ente_theme.dart'; +import 'package:photos/ui/common/loading_widget.dart'; +import 'package:photos/utils/debouncer.dart'; +import 'package:photos/utils/separators_util.dart'; + +class TextInputWidget extends StatefulWidget { + final String? label; + final String? message; + final String? hintText; + final IconData? prefixIcon; + final String? initialValue; + final Alignment? alignMessage; + final bool? autoFocus; + final int? maxLength; + + ///TextInputWidget will listen to this notifier and executes onSubmit when + ///notified. + final ValueNotifier? submitNotifier; + final bool alwaysShowSuccessState; + final bool showOnlyLoadingState; + final FutureVoidCallbackParamStr onSubmit; + final bool popNavAfterSubmission; + final bool shouldSurfaceExecutionStates; + final TextCapitalization? textCapitalization; + const TextInputWidget({ + required this.onSubmit, + this.label, + this.message, + this.hintText, + this.prefixIcon, + this.initialValue, + this.alignMessage, + this.autoFocus, + this.maxLength, + this.submitNotifier, + this.alwaysShowSuccessState = false, + this.showOnlyLoadingState = false, + this.popNavAfterSubmission = false, + this.shouldSurfaceExecutionStates = true, + this.textCapitalization = TextCapitalization.none, + super.key, + }); + + @override + State createState() => _TextInputWidgetState(); +} + +class _TextInputWidgetState extends State { + ExecutionState executionState = ExecutionState.idle; + final _textController = TextEditingController(); + final _debouncer = Debouncer(const Duration(milliseconds: 300)); + + ///This is to pass if the TextInputWidget is in a dialog and an error is + ///thrown in executing onSubmit by passing it as arg in Navigator.pop() + Exception? _exception; + + @override + void initState() { + widget.submitNotifier?.addListener(() { + _onSubmit(); + }); + + if (widget.initialValue != null) { + _textController.value = TextEditingValue( + text: widget.initialValue!, + selection: TextSelection.collapsed(offset: widget.initialValue!.length), + ); + } + super.initState(); + } + + @override + void dispose() { + widget.submitNotifier?.dispose(); + _textController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + if (executionState == ExecutionState.successful) { + Future.delayed(Duration(seconds: widget.popNavAfterSubmission ? 1 : 2), + () { + setState(() { + executionState = ExecutionState.idle; + }); + }); + } + final colorScheme = getEnteColorScheme(context); + final textTheme = getEnteTextTheme(context); + var textInputChildren = []; + if (widget.label != null) { + textInputChildren.add(Text(widget.label!)); + } + textInputChildren.add( + ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(8)), + child: Material( + child: TextFormField( + textCapitalization: widget.textCapitalization!, + autofocus: widget.autoFocus ?? false, + controller: _textController, + inputFormatters: widget.maxLength != null + ? [LengthLimitingTextInputFormatter(50)] + : null, + decoration: InputDecoration( + hintText: widget.hintText, + hintStyle: textTheme.body.copyWith(color: colorScheme.textMuted), + filled: true, + fillColor: colorScheme.fillFaint, + contentPadding: const EdgeInsets.fromLTRB( + 12, + 12, + 0, + 12, + ), + border: const UnderlineInputBorder( + borderSide: BorderSide.none, + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide(color: colorScheme.strokeMuted), + borderRadius: BorderRadius.circular(8), + ), + suffixIcon: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: AnimatedSwitcher( + duration: const Duration(milliseconds: 175), + switchInCurve: Curves.easeInExpo, + switchOutCurve: Curves.easeOutExpo, + child: SuffixIconWidget( + key: ValueKey(executionState), + executionState: executionState, + shouldSurfaceExecutionStates: + widget.shouldSurfaceExecutionStates, + ), + ), + ), + prefixIconConstraints: const BoxConstraints( + maxHeight: 44, + maxWidth: 44, + minHeight: 44, + minWidth: 44, + ), + suffixIconConstraints: const BoxConstraints( + maxHeight: 24, + maxWidth: 48, + minHeight: 24, + minWidth: 48, + ), + prefixIcon: widget.prefixIcon != null + ? Icon( + widget.prefixIcon, + color: colorScheme.strokeMuted, + ) + : null, + ), + onEditingComplete: () { + _onSubmit(); + }, + ), + ), + ), + ); + if (widget.message != null) { + textInputChildren.add( + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Align( + alignment: widget.alignMessage ?? Alignment.centerLeft, + child: Text( + widget.message!, + style: textTheme.small.copyWith(color: colorScheme.textMuted), + ), + ), + ), + ); + } + textInputChildren = + addSeparators(textInputChildren, const SizedBox(height: 4)); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: textInputChildren, + ); + } + + void _onSubmit() async { + _debouncer.run( + () => Future(() { + setState(() { + executionState = ExecutionState.inProgress; + }); + }), + ); + try { + await widget.onSubmit.call(_textController.text); + } catch (e) { + executionState = ExecutionState.error; + _debouncer.cancelDebounce(); + _exception = e as Exception; + if (!widget.popNavAfterSubmission) { + rethrow; + } + } + widget.alwaysShowSuccessState && _debouncer.isActive() + ? executionState = ExecutionState.successful + : null; + _debouncer.cancelDebounce(); + if (executionState == ExecutionState.successful) { + setState(() {}); + } + + // when the time taken by widget.onSubmit is approximately equal to the debounce + // time, the callback is getting executed when/after the if condition + // below is executing/executed which results in execution state stuck at + // idle state. This Future is for delaying the execution of the if + // condition so that the calback in the debouncer finishes execution before. + await Future.delayed(const Duration(milliseconds: 5)); + if (executionState == ExecutionState.inProgress || + executionState == ExecutionState.error) { + if (executionState == ExecutionState.inProgress) { + if (mounted) { + if (widget.showOnlyLoadingState) { + setState(() { + executionState = ExecutionState.idle; + }); + _popNavigatorStack(context); + } else { + setState(() { + executionState = ExecutionState.successful; + Future.delayed( + Duration( + seconds: widget.shouldSurfaceExecutionStates + ? (widget.popNavAfterSubmission ? 1 : 2) + : 0, + ), () { + widget.popNavAfterSubmission + ? _popNavigatorStack(context) + : null; + if (mounted) { + setState(() { + executionState = ExecutionState.idle; + }); + } + }); + }); + } + } + } + if (executionState == ExecutionState.error) { + setState(() { + executionState = ExecutionState.idle; + widget.popNavAfterSubmission + ? Future.delayed( + const Duration(seconds: 0), + () => _popNavigatorStack(context, e: _exception), + ) + : null; + }); + } + } else { + if (widget.popNavAfterSubmission) { + Future.delayed( + Duration(seconds: widget.alwaysShowSuccessState ? 1 : 0), + () => _popNavigatorStack(context), + ); + } + } + } + + void _popNavigatorStack(BuildContext context, {Exception? e}) { + Navigator.of(context).canPop() ? Navigator.of(context).pop(e) : null; + } +} + +//todo: Add clear and custom icon for suffic icon +class SuffixIconWidget extends StatelessWidget { + final ExecutionState executionState; + final bool shouldSurfaceExecutionStates; + const SuffixIconWidget({ + required this.executionState, + required this.shouldSurfaceExecutionStates, + super.key, + }); + + @override + Widget build(BuildContext context) { + final Widget trailingWidget; + final colorScheme = getEnteColorScheme(context); + if (executionState == ExecutionState.idle || + !shouldSurfaceExecutionStates) { + trailingWidget = const SizedBox.shrink(); + } else if (executionState == ExecutionState.inProgress) { + trailingWidget = EnteLoadingWidget( + color: colorScheme.strokeMuted, + ); + } else if (executionState == ExecutionState.successful) { + trailingWidget = Icon( + Icons.check_outlined, + size: 22, + color: colorScheme.primary500, + ); + } else { + trailingWidget = const SizedBox.shrink(); + } + return trailingWidget; + } +} diff --git a/lib/ui/components/toggle_switch_widget.dart b/lib/ui/components/toggle_switch_widget.dart index 7b6a82084..c6a9077bf 100644 --- a/lib/ui/components/toggle_switch_widget.dart +++ b/lib/ui/components/toggle_switch_widget.dart @@ -1,17 +1,10 @@ import 'package:flutter/material.dart'; import 'package:photos/ente_theme_data.dart'; +import 'package:photos/models/execution_states.dart'; +import 'package:photos/models/typedefs.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/utils/debouncer.dart'; -enum ExecutionState { - idle, - inProgress, - successful, -} - -typedef FutureVoidCallback = Future Function(); -typedef BoolCallBack = bool Function(); - class ToggleSwitchWidget extends StatefulWidget { final BoolCallBack value; final FutureVoidCallback onChanged; diff --git a/lib/ui/create_collection_sheet.dart b/lib/ui/create_collection_sheet.dart index c2f5d7ab6..8effafe02 100644 --- a/lib/ui/create_collection_sheet.dart +++ b/lib/ui/create_collection_sheet.dart @@ -152,8 +152,27 @@ class _CreateCollectionSheetState extends State { if (index == 0 && widget.showOptionToCreateNewAlbum) { return GestureDetector( - onTap: () { - _showNameAlbumDialog(); + onTap: () async { + final result = + await showTextInputDialog( + context, + title: "Album title", + submitButtonLabel: "OK", + hintText: "Enter album name", + onSubmit: _nameAlbum, + showOnlyLoadingState: true, + textCapitalization: + TextCapitalization.words, + ); + if (result is Exception) { + showGenericErrorDialog( + context: context, + ); + _logger.severe( + "Failed to name album", + result, + ); + } }, behavior: HitTestBehavior.opaque, child: @@ -225,84 +244,44 @@ class _CreateCollectionSheetState extends State { ); } - void _showNameAlbumDialog() async { - String? albumName; - final AlertDialog alert = AlertDialog( - title: const Text("Album title"), - content: TextFormField( - decoration: const InputDecoration( - hintText: "Christmas 2020 / Dinner at Alice's", - contentPadding: EdgeInsets.all(8), - ), - onChanged: (value) { - albumName = value; - }, - autofocus: true, - keyboardType: TextInputType.text, - textCapitalization: TextCapitalization.words, - ), - actions: [ - TextButton( - child: Text( - "Ok", - style: TextStyle( - color: getEnteColorScheme(context).primary500, - ), - ), - onPressed: () async { - if (albumName != null && albumName!.isNotEmpty) { - Navigator.of(context, rootNavigator: true).pop('dialog'); - final collection = await _createAlbum(albumName!); - if (collection != null) { - if (await _runCollectionAction(collection.id)) { - if (widget.actionType == CollectionActionType.restoreFiles) { - showShortToast( - context, - 'Restored files to album ' + albumName!, - ); - } else { - showShortToast( - context, - "Album '" + albumName! + "' created.", - ); - } - _navigateToCollection(collection); - } - } - } - }, - ), - ], - ); - - showDialog( - context: context, - builder: (BuildContext context) { - return alert; - }, - ); + Future _nameAlbum(String albumName) async { + if (albumName.isNotEmpty) { + final collection = await _createAlbum(albumName); + if (collection != null) { + if (await _runCollectionAction( + collectionID: collection.id, + showProgressDialog: false, + )) { + if (widget.actionType == CollectionActionType.restoreFiles) { + showShortToast( + context, + 'Restored files to album ' + albumName, + ); + } else { + showShortToast( + context, + "Album '" + albumName + "' created.", + ); + } + _navigateToCollection(collection); + } + } + } } Future _createAlbum(String albumName) async { Collection? collection; - final dialog = createProgressDialog(context, "Creating album..."); - await dialog.show(); try { collection = await CollectionsService.instance.createAlbum(albumName); } catch (e, s) { - _logger.severe(e, s); - await dialog.hide(); - showGenericErrorDialog(context: context); - } finally { - await dialog.hide(); + _logger.severe("Failed to create album", e, s); + rethrow; } return collection; } Future _albumListItemOnTap(CollectionWithThumbnail item) async { - if (await _runCollectionAction( - item.collection.id, - )) { + if (await _runCollectionAction(collectionID: item.collection.id)) { showShortToast( context, widget.actionType == CollectionActionType.addFiles @@ -347,10 +326,16 @@ class _CreateCollectionSheetState extends State { ); } - Future _runCollectionAction(int collectionID) async { + Future _runCollectionAction({ + required int collectionID, + bool showProgressDialog = true, + }) async { switch (widget.actionType) { case CollectionActionType.addFiles: - return _addToCollection(collectionID); + return _addToCollection( + collectionID: collectionID, + showProgressDialog: showProgressDialog, + ); case CollectionActionType.moveFiles: return _moveFilesToCollection(collectionID); case CollectionActionType.unHide: @@ -360,14 +345,19 @@ class _CreateCollectionSheetState extends State { } } - Future _addToCollection(int collectionID) async { - final dialog = createProgressDialog( - context, - "Uploading files to album" - "...", - isDismissible: true, - ); - await dialog.show(); + Future _addToCollection({ + required int collectionID, + required bool showProgressDialog, + }) async { + final dialog = showProgressDialog + ? createProgressDialog( + context, + "Uploading files to album" + "...", + isDismissible: true, + ) + : null; + await dialog?.show(); try { final List files = []; final List filesPendingUpload = []; @@ -410,7 +400,7 @@ class _CreateCollectionSheetState extends State { CollectionsService.instance.getCollectionByID(collectionID); if (c != null && c.owner!.id != currentUserID) { showToast(context, "Can not upload to albums owned by others"); - await dialog.hide(); + await dialog?.hide(); return false; } else { // filesPendingUpload might be getting ignored during auto-upload @@ -424,15 +414,15 @@ class _CreateCollectionSheetState extends State { await CollectionsService.instance.addToCollection(collectionID, files); } RemoteSyncService.instance.sync(silently: true); - await dialog.hide(); + await dialog?.hide(); widget.selectedFiles?.clearAll(); return true; } catch (e, s) { - _logger.severe("Could not add to album", e, s); - await dialog.hide(); + _logger.severe("Failed to add to album", e, s); + await dialog?.hide(); showGenericErrorDialog(context: context); + rethrow; } - return false; } Future _moveFilesToCollection(int toCollectionID) async { diff --git a/lib/ui/viewer/file/file_info_widget.dart b/lib/ui/viewer/file/file_info_widget.dart index 0edc37986..2a6e7b8f3 100644 --- a/lib/ui/viewer/file/file_info_widget.dart +++ b/lib/ui/viewer/file/file_info_widget.dart @@ -2,6 +2,7 @@ import "package:exif/exif.dart"; import "package:flutter/cupertino.dart"; import "package:flutter/material.dart"; import 'package:flutter_datetime_picker/flutter_datetime_picker.dart'; +import 'package:path/path.dart' as path; import 'package:photo_manager/photo_manager.dart'; import "package:photos/core/configuration.dart"; import 'package:photos/db/files_db.dart'; @@ -154,7 +155,8 @@ class _FileInfoWidgetState extends State { ), ), title: Text( - file.displayName, + path.basenameWithoutExtension(file.displayName) + + path.extension(file.displayName).toUpperCase(), ), subtitle: Row( children: [ diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index 846f3360e..77c4b920a 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -17,7 +17,6 @@ import 'package:photos/services/collections_service.dart'; import 'package:photos/services/sync_service.dart'; import 'package:photos/services/update_service.dart'; import 'package:photos/ui/actions/collection/collection_sharing_actions.dart'; -import 'package:photos/ui/common/rename_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart'; @@ -111,29 +110,32 @@ class _GalleryAppBarWidgetState extends State { if (widget.type != GalleryType.ownedCollection) { return; } - final result = await showDialog( - context: context, - builder: (BuildContext context) { - return RenameDialog(_appBarTitle, 'Album'); - }, - barrierColor: Colors.black.withOpacity(0.85), - ); - // indicates user cancelled the rename request - if (result == null || result.trim() == _appBarTitle!.trim()) { - return; - } + final result = await showTextInputDialog( + context, + title: "Rename album", + submitButtonLabel: "Rename", + hintText: "Enter album name", + alwaysShowSuccessState: true, + textCapitalization: TextCapitalization.words, + onSubmit: (String text) async { + // indicates user cancelled the rename request + if (text == "" || text.trim() == _appBarTitle!.trim()) { + return; + } - final dialog = createProgressDialog(context, "Changing name..."); - await dialog.show(); - try { - await CollectionsService.instance.rename(widget.collection!, result); - await dialog.hide(); - if (mounted) { - _appBarTitle = result; - setState(() {}); - } - } catch (e) { - await dialog.hide(); + try { + await CollectionsService.instance.rename(widget.collection!, text); + if (mounted) { + _appBarTitle = text; + setState(() {}); + } + } catch (e, s) { + _logger.severe("Failed to rename album", e, s); + rethrow; + } + }, + ); + if (result is Exception) { showGenericErrorDialog(context: context); } } diff --git a/lib/utils/dialog_util.dart b/lib/utils/dialog_util.dart index a624b2d73..d3fc4d250 100644 --- a/lib/utils/dialog_util.dart +++ b/lib/utils/dialog_util.dart @@ -4,6 +4,8 @@ import 'package:confetti/confetti.dart'; import 'package:flutter/material.dart'; import 'package:photos/core/constants.dart'; import "package:photos/models/search/button_result.dart"; +import 'package:photos/models/typedefs.dart'; +import 'package:photos/theme/colors.dart'; import 'package:photos/ui/common/loading_widget.dart'; import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/components/action_sheet_widget.dart'; @@ -251,3 +253,53 @@ Future showConfettiDialog({ routeSettings: routeSettings, ); } + +Future showTextInputDialog( + BuildContext context, { + required String title, + String? body, + required String submitButtonLabel, + IconData? icon, + String? label, + String? message, + String? hintText, + required FutureVoidCallbackParamStr onSubmit, + IconData? prefixIcon, + String? initialValue, + Alignment? alignMessage, + int? maxLength, + bool showOnlyLoadingState = false, + TextCapitalization textCapitalization = TextCapitalization.none, + bool alwaysShowSuccessState = false, +}) { + return showDialog( + barrierColor: backdropFaintDark, + context: context, + builder: (context) { + final bottomInset = MediaQuery.of(context).viewInsets.bottom; + final isKeyboardUp = bottomInset > 100; + return Center( + child: Padding( + padding: EdgeInsets.only(bottom: isKeyboardUp ? bottomInset : 0), + child: TextInputDialog( + title: title, + message: message, + label: label, + body: body, + icon: icon, + submitButtonLabel: submitButtonLabel, + onSubmit: onSubmit, + hintText: hintText, + prefixIcon: prefixIcon, + initialValue: initialValue, + alignMessage: alignMessage, + maxLength: maxLength, + showOnlyLoadingState: showOnlyLoadingState, + textCapitalization: textCapitalization, + alwaysShowSuccessState: alwaysShowSuccessState, + ), + ), + ); + }, + ); +} diff --git a/lib/utils/magic_util.dart b/lib/utils/magic_util.dart index e8d2ef1fd..fdd2e76b9 100644 --- a/lib/utils/magic_util.dart +++ b/lib/utils/magic_util.dart @@ -9,7 +9,6 @@ import 'package:photos/models/magic_metadata.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/services/file_magic_service.dart'; import 'package:photos/ui/common/progress_dialog.dart'; -import 'package:photos/ui/common/rename_dialog.dart'; import 'package:photos/utils/dialog_util.dart'; import 'package:photos/utils/toast_util.dart'; @@ -94,36 +93,41 @@ Future editTime( } } -Future editFilename( +Future editFilename( BuildContext context, File file, ) async { - try { - final fileName = file.displayName; - final nameWithoutExt = basenameWithoutExtension(fileName); - final extName = extension(fileName); - var result = await showDialog( - context: context, - builder: (BuildContext context) { - return RenameDialog(nameWithoutExt, 'file', maxLength: 50); - }, - barrierColor: Colors.black.withOpacity(0.85), - ); - - if (result == null || result.trim() == nameWithoutExt.trim()) { - return true; - } - result = result + extName; - await _updatePublicMetadata( - context, - List.of([file]), - pubMagicKeyEditedName, - result, - ); - return true; - } catch (e) { - showShortToast(context, 'Something went wrong'); - return false; + final fileName = file.displayName; + final nameWithoutExt = basenameWithoutExtension(fileName); + final extName = extension(fileName); + final result = await showTextInputDialog( + context, + title: "Rename file", + submitButtonLabel: "Rename", + initialValue: nameWithoutExt, + message: extName.toUpperCase(), + alignMessage: Alignment.centerRight, + hintText: "Enter file name", + maxLength: 50, + alwaysShowSuccessState: true, + onSubmit: (String text) async { + if (text.isEmpty || text.trim() == nameWithoutExt.trim()) { + return; + } + final newName = text + extName; + await _updatePublicMetadata( + context, + List.of([file]), + pubMagicKeyEditedName, + newName, + showProgressDialogs: false, + showDoneToast: false, + ); + }, + ); + if (result is Exception) { + _logger.severe("Failed to rename file"); + showGenericErrorDialog(context: context); } } @@ -155,12 +159,13 @@ Future _updatePublicMetadata( String key, dynamic value, { bool showDoneToast = true, + bool showProgressDialogs = true, }) async { if (files.isEmpty) { return; } ProgressDialog? dialog; - if (context != null) { + if (context != null && showProgressDialogs) { dialog = createProgressDialog(context, 'Please wait...'); await dialog.show(); }