Resolved merge conflicts

This commit is contained in:
ashilkn 2023-02-09 18:59:43 +05:30
commit 112b80db8a
14 changed files with 647 additions and 166 deletions

View file

@ -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;

View file

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

6
lib/models/typedefs.dart Normal file
View file

@ -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();

View file

@ -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;

View file

@ -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;
},
),
),
],
)
],
),
),
);
}
}

View file

@ -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;

View file

@ -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;

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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 {

View file

@ -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: [

View file

@ -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');
},
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);
}
}

View file

@ -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,
),
),
);
},
);
}

View file

@ -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();
}