소스 검색

Resolved merge conflicts

ashilkn 2 년 전
부모
커밋
112b80db8a

+ 1 - 2
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<void> Function();
-
 extension SuperString on String {
   Iterable<String> chunked(int chunkSize) sync* {
     var start = 0;

+ 6 - 0
lib/models/execution_states.dart

@@ -0,0 +1,6 @@
+enum ExecutionState {
+  idle,
+  inProgress,
+  error,
+  successful;
+}

+ 6 - 0
lib/models/typedefs.dart

@@ -0,0 +1,6 @@
+import 'dart:async';
+
+typedef FutureVoidCallback = Future<void> Function();
+typedef BoolCallBack = bool Function();
+typedef FutureVoidCallbackParamStr = Future<void> Function(String);
+typedef FutureOrVoidCallback = FutureOr<void> Function();

+ 2 - 9
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<void> Function();
-
 class ButtonWidget extends StatelessWidget {
   final IconData? icon;
   final String? labelText;

+ 128 - 0
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<TextInputDialog> createState() => _TextInputDialogState();
+}
+
+class _TextInputDialogState extends State<TextInputDialog> {
+  //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;
+                    },
+                  ),
+                ),
+              ],
+            )
+          ],
+        ),
+      ),
+    );
+  }
+}

+ 1 - 1
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;

+ 2 - 9
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<void> Function();
-
 class MenuItemWidget extends StatefulWidget {
   final Widget captionedTextWidget;
   final bool isExpandable;

+ 312 - 0
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<TextInputWidget> createState() => _TextInputWidgetState();
+}
+
+class _TextInputWidgetState extends State<TextInputWidget> {
+  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 = <Widget>[];
+    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;
+  }
+}

+ 2 - 9
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<void> Function();
-typedef BoolCallBack = bool Function();
-
 class ToggleSwitchWidget extends StatefulWidget {
   final BoolCallBack value;
   final FutureVoidCallback onChanged;

+ 73 - 83
lib/ui/create_collection_sheet.dart

@@ -152,8 +152,27 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
                                         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<CreateCollectionSheet> {
     );
   }
 
-  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<void> _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<Collection?> _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<void> _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<CreateCollectionSheet> {
     );
   }
 
-  Future<bool> _runCollectionAction(int collectionID) async {
+  Future<bool> _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<CreateCollectionSheet> {
     }
   }
 
-  Future<bool> _addToCollection(int collectionID) async {
-    final dialog = createProgressDialog(
-      context,
-      "Uploading files to album"
-      "...",
-      isDismissible: true,
-    );
-    await dialog.show();
+  Future<bool> _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<File> files = [];
       final List<File> filesPendingUpload = [];
@@ -410,7 +400,7 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
             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<CreateCollectionSheet> {
         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<bool> _moveFilesToCollection(int toCollectionID) async {

+ 3 - 1
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<FileInfoWidget> {
                 ),
               ),
         title: Text(
-          file.displayName,
+          path.basenameWithoutExtension(file.displayName) +
+              path.extension(file.displayName).toUpperCase(),
         ),
         subtitle: Row(
           children: [

+ 24 - 22
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<GalleryAppBarWidget> {
     if (widget.type != GalleryType.ownedCollection) {
       return;
     }
-    final result = await showDialog<String>(
-      context: context,
-      builder: (BuildContext context) {
-        return RenameDialog(_appBarTitle, 'Album');
+    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;
+        }
+
+        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;
+        }
       },
-      barrierColor: Colors.black.withOpacity(0.85),
     );
-    // indicates user cancelled the rename request
-    if (result == null || result.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();
+    if (result is Exception) {
       showGenericErrorDialog(context: context);
     }
   }

+ 52 - 0
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<ButtonAction?> showConfettiDialog<T>({
     routeSettings: routeSettings,
   );
 }
+
+Future<Exception?> 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,
+          ),
+        ),
+      );
+    },
+  );
+}

+ 34 - 29
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<bool> editTime(
   }
 }
 
-Future<bool> editFilename(
+Future<void> editFilename(
   BuildContext context,
   File file,
 ) async {
-  try {
-    final fileName = file.displayName;
-    final nameWithoutExt = basenameWithoutExtension(fileName);
-    final extName = extension(fileName);
-    var result = await showDialog<String>(
-      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<void> _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();
   }