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/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:photos/core/error-reporting/tunneled_transport.dart'; import 'package:photos/core/error-reporting/tunneled_transport.dart';
import 'package:photos/models/typedefs.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
typedef FutureOrVoidCallback = FutureOr<void> Function();
extension SuperString on String { extension SuperString on String {
Iterable<String> chunked(int chunkSize) sync* { Iterable<String> chunked(int chunkSize) sync* {
var start = 0; 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/material.dart';
import 'package:flutter/scheduler.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/search/button_result.dart";
import 'package:photos/models/typedefs.dart';
import 'package:photos/theme/colors.dart'; import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/ente_theme.dart';
import 'package:photos/theme/text_style.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/ui/components/models/custom_button_style.dart';
import 'package:photos/utils/debouncer.dart'; import 'package:photos/utils/debouncer.dart';
enum ExecutionState {
idle,
inProgress,
error,
successful;
}
enum ButtonSize { enum ButtonSize {
small, small,
large; large;
@ -30,8 +25,6 @@ enum ButtonAction {
error; error;
} }
typedef FutureVoidCallback = Future<void> Function();
class ButtonWidget extends StatelessWidget { class ButtonWidget extends StatelessWidget {
final IconData? icon; final IconData? icon;
final String? labelText; final String? labelText;

View file

@ -2,10 +2,13 @@ import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart'; import 'package:photos/core/constants.dart';
import 'package:photos/models/typedefs.dart';
import 'package:photos/theme/colors.dart'; import 'package:photos/theme/colors.dart';
import 'package:photos/theme/effects.dart'; import 'package:photos/theme/effects.dart';
import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/button_widget.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'; import 'package:photos/utils/separators_util.dart';
///Will return null if dismissed by tapping outside ///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:flutter/material.dart';
import 'package:photos/models/execution_states.dart';
import 'package:photos/theme/ente_theme.dart'; import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.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 { class TrailingWidget extends StatefulWidget {
final ValueNotifier executionStateNotifier; final ValueNotifier executionStateNotifier;

View file

@ -1,18 +1,11 @@
import 'package:expandable/expandable.dart'; import 'package:expandable/expandable.dart';
import 'package:flutter/material.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/theme/ente_theme.dart';
import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart'; import 'package:photos/ui/components/menu_item_widget/menu_item_child_widgets.dart';
import 'package:photos/utils/debouncer.dart'; import 'package:photos/utils/debouncer.dart';
enum ExecutionState {
idle,
inProgress,
error,
successful;
}
typedef FutureVoidCallback = Future<void> Function();
class MenuItemWidget extends StatefulWidget { class MenuItemWidget extends StatefulWidget {
final Widget captionedTextWidget; final Widget captionedTextWidget;
final bool isExpandable; 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:flutter/material.dart';
import 'package:photos/ente_theme_data.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/ui/common/loading_widget.dart';
import 'package:photos/utils/debouncer.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 { class ToggleSwitchWidget extends StatefulWidget {
final BoolCallBack value; final BoolCallBack value;
final FutureVoidCallback onChanged; final FutureVoidCallback onChanged;

View file

@ -152,8 +152,27 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
if (index == 0 && if (index == 0 &&
widget.showOptionToCreateNewAlbum) { widget.showOptionToCreateNewAlbum) {
return GestureDetector( return GestureDetector(
onTap: () { onTap: () async {
_showNameAlbumDialog(); 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, behavior: HitTestBehavior.opaque,
child: child:
@ -225,84 +244,44 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
); );
} }
void _showNameAlbumDialog() async { Future<void> _nameAlbum(String albumName) async {
String? albumName; if (albumName.isNotEmpty) {
final AlertDialog alert = AlertDialog( final collection = await _createAlbum(albumName);
title: const Text("Album title"), if (collection != null) {
content: TextFormField( if (await _runCollectionAction(
decoration: const InputDecoration( collectionID: collection.id,
hintText: "Christmas 2020 / Dinner at Alice's", showProgressDialog: false,
contentPadding: EdgeInsets.all(8), )) {
), if (widget.actionType == CollectionActionType.restoreFiles) {
onChanged: (value) { showShortToast(
albumName = value; context,
}, 'Restored files to album ' + albumName,
autofocus: true, );
keyboardType: TextInputType.text, } else {
textCapitalization: TextCapitalization.words, showShortToast(
), context,
actions: [ "Album '" + albumName + "' created.",
TextButton( );
child: Text( }
"Ok", _navigateToCollection(collection);
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<Collection?> _createAlbum(String albumName) async { Future<Collection?> _createAlbum(String albumName) async {
Collection? collection; Collection? collection;
final dialog = createProgressDialog(context, "Creating album...");
await dialog.show();
try { try {
collection = await CollectionsService.instance.createAlbum(albumName); collection = await CollectionsService.instance.createAlbum(albumName);
} catch (e, s) { } catch (e, s) {
_logger.severe(e, s); _logger.severe("Failed to create album", e, s);
await dialog.hide(); rethrow;
showGenericErrorDialog(context: context);
} finally {
await dialog.hide();
} }
return collection; return collection;
} }
Future<void> _albumListItemOnTap(CollectionWithThumbnail item) async { Future<void> _albumListItemOnTap(CollectionWithThumbnail item) async {
if (await _runCollectionAction( if (await _runCollectionAction(collectionID: item.collection.id)) {
item.collection.id,
)) {
showShortToast( showShortToast(
context, context,
widget.actionType == CollectionActionType.addFiles 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) { switch (widget.actionType) {
case CollectionActionType.addFiles: case CollectionActionType.addFiles:
return _addToCollection(collectionID); return _addToCollection(
collectionID: collectionID,
showProgressDialog: showProgressDialog,
);
case CollectionActionType.moveFiles: case CollectionActionType.moveFiles:
return _moveFilesToCollection(collectionID); return _moveFilesToCollection(collectionID);
case CollectionActionType.unHide: case CollectionActionType.unHide:
@ -360,14 +345,19 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
} }
} }
Future<bool> _addToCollection(int collectionID) async { Future<bool> _addToCollection({
final dialog = createProgressDialog( required int collectionID,
context, required bool showProgressDialog,
"Uploading files to album" }) async {
"...", final dialog = showProgressDialog
isDismissible: true, ? createProgressDialog(
); context,
await dialog.show(); "Uploading files to album"
"...",
isDismissible: true,
)
: null;
await dialog?.show();
try { try {
final List<File> files = []; final List<File> files = [];
final List<File> filesPendingUpload = []; final List<File> filesPendingUpload = [];
@ -410,7 +400,7 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
CollectionsService.instance.getCollectionByID(collectionID); CollectionsService.instance.getCollectionByID(collectionID);
if (c != null && c.owner!.id != currentUserID) { if (c != null && c.owner!.id != currentUserID) {
showToast(context, "Can not upload to albums owned by others"); showToast(context, "Can not upload to albums owned by others");
await dialog.hide(); await dialog?.hide();
return false; return false;
} else { } else {
// filesPendingUpload might be getting ignored during auto-upload // filesPendingUpload might be getting ignored during auto-upload
@ -424,15 +414,15 @@ class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
await CollectionsService.instance.addToCollection(collectionID, files); await CollectionsService.instance.addToCollection(collectionID, files);
} }
RemoteSyncService.instance.sync(silently: true); RemoteSyncService.instance.sync(silently: true);
await dialog.hide(); await dialog?.hide();
widget.selectedFiles?.clearAll(); widget.selectedFiles?.clearAll();
return true; return true;
} catch (e, s) { } catch (e, s) {
_logger.severe("Could not add to album", e, s); _logger.severe("Failed to add to album", e, s);
await dialog.hide(); await dialog?.hide();
showGenericErrorDialog(context: context); showGenericErrorDialog(context: context);
rethrow;
} }
return false;
} }
Future<bool> _moveFilesToCollection(int toCollectionID) async { 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/cupertino.dart";
import "package:flutter/material.dart"; import "package:flutter/material.dart";
import 'package:flutter_datetime_picker/flutter_datetime_picker.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:photo_manager/photo_manager.dart';
import "package:photos/core/configuration.dart"; import "package:photos/core/configuration.dart";
import 'package:photos/db/files_db.dart'; import 'package:photos/db/files_db.dart';
@ -154,7 +155,8 @@ class _FileInfoWidgetState extends State<FileInfoWidget> {
), ),
), ),
title: Text( title: Text(
file.displayName, path.basenameWithoutExtension(file.displayName) +
path.extension(file.displayName).toUpperCase(),
), ),
subtitle: Row( subtitle: Row(
children: [ 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/sync_service.dart';
import 'package:photos/services/update_service.dart'; import 'package:photos/services/update_service.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.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/action_sheet_widget.dart';
import 'package:photos/ui/components/button_widget.dart'; import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/dialog_widget.dart'; import 'package:photos/ui/components/dialog_widget.dart';
@ -111,29 +110,32 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
if (widget.type != GalleryType.ownedCollection) { if (widget.type != GalleryType.ownedCollection) {
return; return;
} }
final result = await showDialog<String>( final result = await showTextInputDialog(
context: context, context,
builder: (BuildContext context) { title: "Rename album",
return RenameDialog(_appBarTitle, 'Album'); submitButtonLabel: "Rename",
}, hintText: "Enter album name",
barrierColor: Colors.black.withOpacity(0.85), alwaysShowSuccessState: true,
); textCapitalization: TextCapitalization.words,
// indicates user cancelled the rename request onSubmit: (String text) async {
if (result == null || result.trim() == _appBarTitle!.trim()) { // indicates user cancelled the rename request
return; if (text == "" || text.trim() == _appBarTitle!.trim()) {
} return;
}
final dialog = createProgressDialog(context, "Changing name..."); try {
await dialog.show(); await CollectionsService.instance.rename(widget.collection!, text);
try { if (mounted) {
await CollectionsService.instance.rename(widget.collection!, result); _appBarTitle = text;
await dialog.hide(); setState(() {});
if (mounted) { }
_appBarTitle = result; } catch (e, s) {
setState(() {}); _logger.severe("Failed to rename album", e, s);
} rethrow;
} catch (e) { }
await dialog.hide(); },
);
if (result is Exception) {
showGenericErrorDialog(context: context); showGenericErrorDialog(context: context);
} }
} }

View file

@ -4,6 +4,8 @@ import 'package:confetti/confetti.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:photos/core/constants.dart'; import 'package:photos/core/constants.dart';
import "package:photos/models/search/button_result.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/loading_widget.dart';
import 'package:photos/ui/common/progress_dialog.dart'; import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/components/action_sheet_widget.dart'; import 'package:photos/ui/components/action_sheet_widget.dart';
@ -251,3 +253,53 @@ Future<ButtonAction?> showConfettiDialog<T>({
routeSettings: routeSettings, 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/collections_service.dart';
import 'package:photos/services/file_magic_service.dart'; import 'package:photos/services/file_magic_service.dart';
import 'package:photos/ui/common/progress_dialog.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/dialog_util.dart';
import 'package:photos/utils/toast_util.dart'; import 'package:photos/utils/toast_util.dart';
@ -94,36 +93,41 @@ Future<bool> editTime(
} }
} }
Future<bool> editFilename( Future<void> editFilename(
BuildContext context, BuildContext context,
File file, File file,
) async { ) async {
try { final fileName = file.displayName;
final fileName = file.displayName; final nameWithoutExt = basenameWithoutExtension(fileName);
final nameWithoutExt = basenameWithoutExtension(fileName); final extName = extension(fileName);
final extName = extension(fileName); final result = await showTextInputDialog(
var result = await showDialog<String>( context,
context: context, title: "Rename file",
builder: (BuildContext context) { submitButtonLabel: "Rename",
return RenameDialog(nameWithoutExt, 'file', maxLength: 50); initialValue: nameWithoutExt,
}, message: extName.toUpperCase(),
barrierColor: Colors.black.withOpacity(0.85), alignMessage: Alignment.centerRight,
); hintText: "Enter file name",
maxLength: 50,
if (result == null || result.trim() == nameWithoutExt.trim()) { alwaysShowSuccessState: true,
return true; onSubmit: (String text) async {
} if (text.isEmpty || text.trim() == nameWithoutExt.trim()) {
result = result + extName; return;
await _updatePublicMetadata( }
context, final newName = text + extName;
List.of([file]), await _updatePublicMetadata(
pubMagicKeyEditedName, context,
result, List.of([file]),
); pubMagicKeyEditedName,
return true; newName,
} catch (e) { showProgressDialogs: false,
showShortToast(context, 'Something went wrong'); showDoneToast: false,
return false; );
},
);
if (result is Exception) {
_logger.severe("Failed to rename file");
showGenericErrorDialog(context: context);
} }
} }
@ -155,12 +159,13 @@ Future<void> _updatePublicMetadata(
String key, String key,
dynamic value, { dynamic value, {
bool showDoneToast = true, bool showDoneToast = true,
bool showProgressDialogs = true,
}) async { }) async {
if (files.isEmpty) { if (files.isEmpty) {
return; return;
} }
ProgressDialog? dialog; ProgressDialog? dialog;
if (context != null) { if (context != null && showProgressDialogs) {
dialog = createProgressDialog(context, 'Please wait...'); dialog = createProgressDialog(context, 'Please wait...');
await dialog.show(); await dialog.show();
} }