Merge pull request #817 from ente-io/redesign-add-to-album

Redesign add/move to album
This commit is contained in:
Ashil 2023-01-27 10:41:06 +05:30 committed by GitHub
commit cc23be733c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 749 additions and 550 deletions

View file

@ -81,6 +81,8 @@ class Collection {
return (owner?.id ?? 0) == userID;
}
String get collectionName => name ?? "Unnamed collection";
void updateSharees(List<User> newSharees) {
sharees?.clear();
sharees?.addAll(newSharees);

View file

@ -0,0 +1,104 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7480%3A33462&t=H5AvR79OYDnB9ekw-4
class AlbumListItemWidget extends StatelessWidget {
final CollectionWithThumbnail item;
const AlbumListItemWidget(
this.item, {
super.key,
});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
alignment: Alignment.center,
children: [
Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: item.thumbnail != null
? ThumbnailWidget(
item.thumbnail,
showFavForAlbumOnly: true,
)
: const NoThumbnailWidget(
addBorder: false,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(item.collection.collectionName),
FutureBuilder<int>(
future: FilesDB.instance.collectionFileCount(
item.collection.id,
),
builder: (context, snapshot) {
if (snapshot.hasData) {
final text =
snapshot.data == 1 ? " memory" : " memories";
return Text(
snapshot.data.toString() + text,
style: textTheme.small.copyWith(
color: colorScheme.textMuted,
),
);
} else {
if (snapshot.hasError) {
Logger("AlbumListItemWidget").severe(
"Failed to fetch file count of collection",
snapshot.error,
);
}
return Text(
"",
style: textTheme.small.copyWith(
color: colorScheme.textMuted,
),
);
}
},
)
],
),
),
],
),
IgnorePointer(
child: Container(
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(4)),
border: Border.all(
color: colorScheme.strokeFainter,
),
),
height: sideOfThumbnail,
width: constraints.maxWidth,
),
),
],
);
},
);
}
}

View file

@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=7309%3A29088&t=ReeZ2Big8xSsemZb-4
class BottomOfTitleBarWidget extends StatelessWidget {
final TitleBarTitleWidget? title;
final String? caption;
const BottomOfTitleBarWidget({this.title, this.caption, super.key});
@override
Widget build(BuildContext context) {
return Row(
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
title ?? const SizedBox.shrink(),
caption != null
? Text(
caption!,
style: getEnteTextTheme(context).small.copyWith(
color: getEnteColorScheme(context).textMuted,
),
)
: const SizedBox.shrink(),
],
),
),
),
],
);
}
}

View file

@ -0,0 +1,73 @@
import 'package:dotted_border/dotted_border.dart';
import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
///https://www.figma.com/file/SYtMyLBs5SAOkTbfMMzhqt/ente-Visual-Design?node-id=10854%3A57947&t=H5AvR79OYDnB9ekw-4
class NewAlbumListItemWidget extends StatelessWidget {
const NewAlbumListItemWidget({
super.key,
});
@override
Widget build(BuildContext context) {
final textTheme = getEnteTextTheme(context);
final colorScheme = getEnteColorScheme(context);
const sideOfThumbnail = 60.0;
return LayoutBuilder(
builder: (context, constraints) {
return Stack(
alignment: Alignment.center,
children: [
Row(
children: [
ClipRRect(
borderRadius: const BorderRadius.horizontal(
left: Radius.circular(4),
),
child: SizedBox(
height: sideOfThumbnail,
width: sideOfThumbnail,
child: Icon(
Icons.add_outlined,
color: colorScheme.strokeMuted,
),
),
),
Padding(
padding: const EdgeInsets.only(left: 12),
child: Text(
"New album",
style:
textTheme.body.copyWith(color: colorScheme.textMuted),
),
),
],
),
IgnorePointer(
child: DottedBorder(
dashPattern: const [4],
color: colorScheme.strokeFainter,
strokeWidth: 1,
padding: const EdgeInsets.all(0),
borderType: BorderType.RRect,
radius: const Radius.circular(4),
child: SizedBox(
//Have to decrease the height and width by 1 pt as the stroke
//dotted border gives is of strokeAlign.center, so 0.5 inside and
// outside. Here for the row, stroke should be inside so we
//decrease the size of this sizedBox by 1 (so it shrinks 0.5 from
//every side) so that the strokeAlign.center of this sizedBox
//looks like a strokeAlign.inside in the row.
height: sideOfThumbnail - 1,
//This width will work for this only if the row widget takes up the
//full size it's parent (stack).
width: constraints.maxWidth - 1,
),
),
),
],
);
},
);
}
}

View file

@ -1,416 +0,0 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/ui/common/gradient_button.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/viewer/file/no_thumbnail_widget.dart';
import 'package:photos/ui/viewer/file/thumbnail_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
String _actionName(CollectionActionType type, bool plural) {
final titleSuffix = (plural ? "s" : "");
String text = "";
switch (type) {
case CollectionActionType.addFiles:
text = "Add file";
break;
case CollectionActionType.moveFiles:
text = "Move file";
break;
case CollectionActionType.restoreFiles:
text = "Restore file";
break;
case CollectionActionType.unHide:
text = "Unhide file";
break;
}
return text + titleSuffix;
}
class CreateCollectionPage extends StatefulWidget {
final SelectedFiles? selectedFiles;
final List<SharedMediaFile>? sharedFiles;
final CollectionActionType actionType;
const CreateCollectionPage(
this.selectedFiles,
this.sharedFiles, {
Key? key,
this.actionType = CollectionActionType.addFiles,
}) : super(key: key);
@override
State<CreateCollectionPage> createState() => _CreateCollectionPageState();
}
class _CreateCollectionPageState extends State<CreateCollectionPage> {
final _logger = Logger((_CreateCollectionPageState).toString());
late String _albumName;
@override
Widget build(BuildContext context) {
final filesCount = widget.sharedFiles != null
? widget.sharedFiles!.length
: widget.selectedFiles!.files.length;
return Scaffold(
appBar: AppBar(
title: Text(_actionName(widget.actionType, filesCount > 1)),
),
body: _getBody(context),
);
}
Widget _getBody(BuildContext context) {
return SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Expanded(
child: Padding(
padding: const EdgeInsets.only(
top: 30,
bottom: 12,
left: 40,
right: 40,
),
child: GradientButton(
onTap: () async {
_showNameAlbumDialog();
},
iconData: Icons.create_new_folder_outlined,
text: "To a new album",
),
),
),
],
),
const Padding(
padding: EdgeInsets.fromLTRB(40, 24, 40, 20),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
"To an existing album",
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(20, 4, 20, 0),
child: _getExistingCollectionsWidget(),
),
],
),
);
}
Widget _getExistingCollectionsWidget() {
return FutureBuilder<List<CollectionWithThumbnail>>(
future: _getCollectionsWithThumbnail(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text(snapshot.error.toString());
} else if (snapshot.hasData) {
return ListView.builder(
itemBuilder: (context, index) {
return _buildCollectionItem(snapshot.data![index]);
},
itemCount: snapshot.data!.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
);
} else {
return const EnteLoadingWidget();
}
},
);
}
Widget _buildCollectionItem(CollectionWithThumbnail item) {
return Container(
padding: const EdgeInsets.only(left: 24, bottom: 16),
child: GestureDetector(
behavior: HitTestBehavior.translucent,
child: Row(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(2.0),
child: SizedBox(
height: 64,
width: 64,
key: Key("collection_item:" + (item.thumbnail?.tag ?? "")),
child: item.thumbnail != null
? ThumbnailWidget(
item.thumbnail,
showFavForAlbumOnly: true,
)
: const NoThumbnailWidget(),
),
),
const Padding(padding: EdgeInsets.all(8)),
Expanded(
child: Text(
item.collection.name!,
style: const TextStyle(
fontSize: 16,
),
),
),
],
),
onTap: () async {
if (await _runCollectionAction(item.collection.id)) {
showShortToast(
context,
widget.actionType == CollectionActionType.addFiles
? "Added successfully to " + item.collection.name!
: "Moved successfully to " + item.collection.name!,
);
_navigateToCollection(item.collection);
}
},
),
);
}
Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails(
// in collections where user is a collaborator, only addTo and remove
// action can to be performed
includeCollabCollections:
widget.actionType == CollectionActionType.addFiles,
);
collectionsWithThumbnail.removeWhere(
(element) => (element.collection.type == CollectionType.favorites ||
element.collection.type == CollectionType.uncategorized ||
element.collection.isSharedFilesCollection()),
);
collectionsWithThumbnail.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.collection.name ?? "",
second.collection.name ?? "",
);
});
return collectionsWithThumbnail;
}
void _showNameAlbumDialog() async {
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) {
setState(() {
_albumName = value;
});
},
autofocus: true,
keyboardType: TextInputType.text,
textCapitalization: TextCapitalization.words,
),
actions: [
TextButton(
child: Text(
"Ok",
style: TextStyle(
color: Theme.of(context).colorScheme.greenAlternative,
),
),
onPressed: () async {
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;
},
);
}
void _navigateToCollection(Collection collection) {
Navigator.pop(context);
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(collection, null),
),
);
}
Future<bool> _runCollectionAction(int collectionID) async {
switch (widget.actionType) {
case CollectionActionType.addFiles:
return _addToCollection(collectionID);
case CollectionActionType.moveFiles:
return _moveFilesToCollection(collectionID);
case CollectionActionType.unHide:
return _moveFilesToCollection(collectionID);
case CollectionActionType.restoreFiles:
return _restoreFilesToCollection(collectionID);
}
}
Future<bool> _moveFilesToCollection(int toCollectionID) async {
final String message = widget.actionType == CollectionActionType.moveFiles
? "Moving files to album..."
: "Unhiding files to album";
final dialog = createProgressDialog(context, message);
await dialog.show();
try {
final int fromCollectionID =
widget.selectedFiles!.files.first.collectionID!;
await CollectionsService.instance.move(
toCollectionID,
fromCollectionID,
widget.selectedFiles!.files.toList(),
);
await dialog.hide();
RemoteSyncService.instance.sync(silently: true);
widget.selectedFiles?.clearAll();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, "Oops", e.message as String?);
return false;
} catch (e, s) {
_logger.severe("Could not move to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
return false;
}
}
Future<bool> _restoreFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Restoring files...");
await dialog.show();
try {
await CollectionsService.instance
.restore(toCollectionID, widget.selectedFiles!.files.toList());
RemoteSyncService.instance.sync(silently: true);
widget.selectedFiles?.clearAll();
await dialog.hide();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, "Oops", e.message as String?);
return false;
} catch (e, s) {
_logger.severe("Could not move to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
return false;
}
}
Future<bool> _addToCollection(int collectionID) async {
final dialog = createProgressDialog(context, "Uploading files to album...");
await dialog.show();
try {
final List<File> files = [];
final List<File> filesPendingUpload = [];
if (widget.sharedFiles != null) {
filesPendingUpload.addAll(
await convertIncomingSharedMediaToFile(
widget.sharedFiles!,
collectionID,
),
);
} else {
for (final file in widget.selectedFiles!.files) {
final File? currentFile =
await (FilesDB.instance.getFile(file.generatedID!));
if (currentFile == null) {
_logger.severe("Failed to find fileBy genID");
continue;
}
if (currentFile.uploadedFileID == null) {
currentFile.collectionID = collectionID;
filesPendingUpload.add(currentFile);
} else {
files.add(currentFile);
}
}
}
if (filesPendingUpload.isNotEmpty) {
// filesPendingUpload might be getting ignored during auto-upload
// because the user deleted these files from ente in the past.
await IgnoredFilesService.instance
.removeIgnoredMappings(filesPendingUpload);
await FilesDB.instance.insertMultiple(filesPendingUpload);
}
if (files.isNotEmpty) {
await CollectionsService.instance.addToCollection(collectionID, files);
}
RemoteSyncService.instance.sync(silently: true);
await dialog.hide();
widget.selectedFiles?.clearAll();
return true;
} catch (e, s) {
_logger.severe("Could not add to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
}
return false;
}
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();
}
return collection;
}
}

View file

@ -0,0 +1,465 @@
import 'dart:math';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/components/album_list_item_widget.dart';
import 'package:photos/ui/components/bottom_of_title_bar_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/components/new_album_list_widget.dart';
import 'package:photos/ui/components/title_bar_title_widget.dart';
import 'package:photos/ui/viewer/gallery/collection_page.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/navigation_util.dart';
import 'package:photos/utils/share_util.dart';
import 'package:photos/utils/toast_util.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
String _actionName(CollectionActionType type, bool plural) {
bool addTitleSuffix = false;
final titleSuffix = (plural ? "s" : "");
String text = "";
switch (type) {
case CollectionActionType.addFiles:
text = "Add item";
addTitleSuffix = true;
break;
case CollectionActionType.moveFiles:
text = "Move item";
addTitleSuffix = true;
break;
case CollectionActionType.restoreFiles:
text = "Restore to album";
break;
case CollectionActionType.unHide:
text = "Unhide to album";
break;
}
return addTitleSuffix ? text + titleSuffix : text;
}
void createCollectionSheet(
SelectedFiles? selectedFiles,
List<SharedMediaFile>? sharedFiles,
BuildContext context, {
CollectionActionType actionType = CollectionActionType.addFiles,
bool showOptionToCreateNewAlbum = true,
}) {
showBarModalBottomSheet(
context: context,
builder: (context) {
return CreateCollectionSheet(
selectedFiles: selectedFiles,
sharedFiles: sharedFiles,
actionType: actionType,
showOptionToCreateNewAlbum: showOptionToCreateNewAlbum,
);
},
shape: const RoundedRectangleBorder(
side: BorderSide(width: 0),
borderRadius: BorderRadius.vertical(
top: Radius.circular(5),
),
),
topControl: const SizedBox.shrink(),
backgroundColor: getEnteColorScheme(context).backgroundElevated,
barrierColor: backdropFaintDark,
enableDrag: false,
);
}
class CreateCollectionSheet extends StatefulWidget {
final SelectedFiles? selectedFiles;
final List<SharedMediaFile>? sharedFiles;
final CollectionActionType actionType;
final bool showOptionToCreateNewAlbum;
const CreateCollectionSheet({
required this.selectedFiles,
required this.sharedFiles,
required this.actionType,
required this.showOptionToCreateNewAlbum,
super.key,
});
@override
State<CreateCollectionSheet> createState() => _CreateCollectionSheetState();
}
class _CreateCollectionSheetState extends State<CreateCollectionSheet> {
final _logger = Logger((_CreateCollectionSheetState).toString());
@override
Widget build(BuildContext context) {
final filesCount = widget.sharedFiles != null
? widget.sharedFiles!.length
: widget.selectedFiles!.files.length;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ConstrainedBox(
constraints: BoxConstraints(
maxWidth: min(428, MediaQuery.of(context).size.width),
),
child: Padding(
padding: const EdgeInsets.fromLTRB(0, 32, 0, 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
BottomOfTitleBarWidget(
title: TitleBarTitleWidget(
title: _actionName(widget.actionType, filesCount > 1),
),
caption: "Create or select album",
),
Flexible(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 4, 0),
child: Scrollbar(
radius: const Radius.circular(2),
child: Padding(
padding: const EdgeInsets.only(right: 12),
child: FutureBuilder(
future: _getCollectionsWithThumbnail(),
builder: (context, snapshot) {
if (snapshot.hasError) {
//Need to show an error on the UI here
return const SizedBox.shrink();
} else if (snapshot.hasData) {
final collectionsWithThumbnail = snapshot
.data as List<CollectionWithThumbnail>;
return ListView.separated(
itemBuilder: (context, index) {
if (index == 0 &&
widget.showOptionToCreateNewAlbum) {
return GestureDetector(
onTap: () {
_showNameAlbumDialog();
},
behavior: HitTestBehavior.opaque,
child:
const NewAlbumListItemWidget(),
);
}
final item = collectionsWithThumbnail[
index -
(widget.showOptionToCreateNewAlbum
? 1
: 0)];
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () =>
_albumListItemOnTap(item),
child: AlbumListItemWidget(
item,
),
);
},
separatorBuilder: (context, index) =>
const SizedBox(
height: 8,
),
itemCount:
collectionsWithThumbnail.length +
(widget.showOptionToCreateNewAlbum
? 1
: 0),
shrinkWrap: true,
physics: const BouncingScrollPhysics(),
);
} else {
return const EnteLoadingWidget();
}
},
),
),
),
),
),
SafeArea(
child: Container(
//inner stroke of 1pt + 15 pts of top padding = 16 pts
padding: const EdgeInsets.fromLTRB(16, 15, 16, 8),
decoration: BoxDecoration(
border: Border(
top: BorderSide(
color: getEnteColorScheme(context).strokeFaint,
),
),
),
child: const ButtonWidget(
buttonType: ButtonType.secondary,
buttonAction: ButtonAction.cancel,
isInAlert: true,
labelText: "Cancel",
),
),
)
],
),
),
],
),
),
),
],
);
}
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<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();
}
return collection;
}
Future<void> _albumListItemOnTap(CollectionWithThumbnail item) async {
if (await _runCollectionAction(
item.collection.id,
)) {
showShortToast(
context,
widget.actionType == CollectionActionType.addFiles
? "Added successfully to " + item.collection.name!
: "Moved successfully to " + item.collection.name!,
);
_navigateToCollection(
item.collection,
);
}
}
Future<List<CollectionWithThumbnail>> _getCollectionsWithThumbnail() async {
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails(
// in collections where user is a collaborator, only addTo and remove
// action can to be performed
includeCollabCollections:
widget.actionType == CollectionActionType.addFiles,
);
collectionsWithThumbnail.removeWhere(
(element) => (element.collection.type == CollectionType.favorites ||
element.collection.type == CollectionType.uncategorized ||
element.collection.isSharedFilesCollection()),
);
collectionsWithThumbnail.sort((first, second) {
return compareAsciiLowerCaseNatural(
first.collection.name ?? "",
second.collection.name ?? "",
);
});
return collectionsWithThumbnail;
}
void _navigateToCollection(Collection collection) {
Navigator.pop(context);
routeToPage(
context,
CollectionPage(
CollectionWithThumbnail(collection, null),
),
);
}
Future<bool> _runCollectionAction(int collectionID) async {
switch (widget.actionType) {
case CollectionActionType.addFiles:
return _addToCollection(collectionID);
case CollectionActionType.moveFiles:
return _moveFilesToCollection(collectionID);
case CollectionActionType.unHide:
return _moveFilesToCollection(collectionID);
case CollectionActionType.restoreFiles:
return _restoreFilesToCollection(collectionID);
}
}
Future<bool> _addToCollection(int collectionID) async {
final dialog = createProgressDialog(context, "Uploading files to album...");
await dialog.show();
try {
final List<File> files = [];
final List<File> filesPendingUpload = [];
if (widget.sharedFiles != null) {
filesPendingUpload.addAll(
await convertIncomingSharedMediaToFile(
widget.sharedFiles!,
collectionID,
),
);
} else {
for (final file in widget.selectedFiles!.files) {
final File? currentFile =
await (FilesDB.instance.getFile(file.generatedID!));
if (currentFile == null) {
_logger.severe("Failed to find fileBy genID");
continue;
}
if (currentFile.uploadedFileID == null) {
currentFile.collectionID = collectionID;
filesPendingUpload.add(currentFile);
} else {
files.add(currentFile);
}
}
}
if (filesPendingUpload.isNotEmpty) {
// filesPendingUpload might be getting ignored during auto-upload
// because the user deleted these files from ente in the past.
await IgnoredFilesService.instance
.removeIgnoredMappings(filesPendingUpload);
await FilesDB.instance.insertMultiple(filesPendingUpload);
}
if (files.isNotEmpty) {
await CollectionsService.instance.addToCollection(collectionID, files);
}
RemoteSyncService.instance.sync(silently: true);
await dialog.hide();
widget.selectedFiles?.clearAll();
return true;
} catch (e, s) {
_logger.severe("Could not add to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
}
return false;
}
Future<bool> _moveFilesToCollection(int toCollectionID) async {
final String message = widget.actionType == CollectionActionType.moveFiles
? "Moving files to album..."
: "Unhiding files to album";
final dialog = createProgressDialog(context, message);
await dialog.show();
try {
final int fromCollectionID =
widget.selectedFiles!.files.first.collectionID!;
await CollectionsService.instance.move(
toCollectionID,
fromCollectionID,
widget.selectedFiles!.files.toList(),
);
await dialog.hide();
RemoteSyncService.instance.sync(silently: true);
widget.selectedFiles?.clearAll();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, "Oops", e.message as String?);
return false;
} catch (e, s) {
_logger.severe("Could not move to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
return false;
}
}
Future<bool> _restoreFilesToCollection(int toCollectionID) async {
final dialog = createProgressDialog(context, "Restoring files...");
await dialog.show();
try {
await CollectionsService.instance
.restore(toCollectionID, widget.selectedFiles!.files.toList());
RemoteSyncService.instance.sync(silently: true);
widget.selectedFiles?.clearAll();
await dialog.hide();
return true;
} on AssertionError catch (e) {
await dialog.hide();
showErrorDialog(context, "Oops", e.message as String?);
return false;
} catch (e, s) {
_logger.severe("Could not move to album", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
return false;
}
}
}

View file

@ -28,7 +28,7 @@ import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/collections_gallery_widget.dart';
import 'package:photos/ui/common/bottom_shadow.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/extents_page_view.dart';
import 'package:photos/ui/home/grant_permissions_widget.dart';
import 'package:photos/ui/home/header_widget.dart';
@ -72,6 +72,7 @@ class _HomeWidgetState extends State<HomeWidget> {
// ignore: unused_field
StreamSubscription? _intentDataStreamSubscription;
List<SharedMediaFile>? _sharedFiles;
bool _shouldRenderCreateCollectionSheet = false;
late StreamSubscription<TabChangedEvent> _tabChangedEventSubscription;
late StreamSubscription<SubscriptionPurchasedEvent>
@ -236,6 +237,7 @@ class _HomeWidgetState extends State<HomeWidget> {
ReceiveSharingIntent.getMediaStream().listen(
(List<SharedMediaFile> value) {
setState(() {
_shouldRenderCreateCollectionSheet = true;
_sharedFiles = value;
});
},
@ -317,9 +319,22 @@ class _HomeWidgetState extends State<HomeWidget> {
return const LoadingPhotosWidget();
}
if (_sharedFiles != null && _sharedFiles!.isNotEmpty) {
if (_sharedFiles != null &&
_sharedFiles!.isNotEmpty &&
_shouldRenderCreateCollectionSheet) {
//The gallery is getting rebuilt for some reason when the keyboard is up.
//So to stop showing multiple CreateCollectionSheets, this flag
//needs to be set to false the first time it is rendered.
_shouldRenderCreateCollectionSheet = false;
ReceiveSharingIntent.reset();
return CreateCollectionPage(null, _sharedFiles);
Future.delayed(const Duration(milliseconds: 10), () {
createCollectionSheet(
null,
_sharedFiles,
context,
actionType: CollectionActionType.addFiles,
);
});
}
final isBottomInsetPresent = MediaQuery.of(context).viewPadding.bottom != 0;

View file

@ -1,7 +1,6 @@
import 'package:fast_base58/fast_base58.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:page_transition/page_transition.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/device_collection.dart';
@ -20,7 +19,7 @@ import 'package:photos/ui/components/blur_menu_item_widget.dart';
import 'package:photos/ui/components/bottom_action_bar/expanded_menu_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/sharing/manage_links_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -255,7 +254,12 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
await _selectionCollectionForAction(CollectionActionType.moveFiles);
createCollectionSheet(
widget.selectedFiles,
null,
context,
actionType: CollectionActionType.moveFiles,
);
}
Future<void> _addToAlbum() async {
@ -263,7 +267,11 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
await _selectionCollectionForAction(CollectionActionType.addFiles);
createCollectionSheet(
widget.selectedFiles,
null,
context,
);
}
Future<void> _onDeleteClick() async {
@ -339,7 +347,12 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
await _selectionCollectionForAction(CollectionActionType.unHide);
createCollectionSheet(
widget.selectedFiles,
null,
context,
actionType: CollectionActionType.unHide,
);
}
Future<void> _onCreatedSharedLinkClicked() async {
@ -407,20 +420,4 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
showShortToast(context, "Link copied to clipboard");
}
}
Future<Object?> _selectionCollectionForAction(
CollectionActionType type,
) async {
return Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
widget.selectedFiles,
null,
actionType: type,
),
),
);
}
}

View file

@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:page_transition/page_transition.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/device_collection.dart';
import 'package:photos/models/gallery_type.dart';
@ -8,7 +7,7 @@ import 'package:photos/models/selected_files.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/components/bottom_action_bar/bottom_action_bar_widget.dart';
import 'package:photos/ui/components/icon_button_widget.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/actions/file_selection_actions_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -85,9 +84,14 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
icon: Icons.visibility_off_outlined,
iconButtonType: IconButtonType.primary,
iconColor: iconColor,
onTap: () => _selectionCollectionForAction(
CollectionActionType.unHide,
),
onTap: () {
createCollectionSheet(
widget.selectedFiles,
null,
context,
actionType: CollectionActionType.unHide,
);
},
),
);
}
@ -143,22 +147,6 @@ class _FileSelectionOverlayBarState extends State<FileSelectionOverlayBar> {
widget.selectedFiles.clearAll();
}
Future<Object?> _selectionCollectionForAction(
CollectionActionType type,
) async {
return Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
widget.selectedFiles,
null,
actionType: type,
),
),
);
}
_selectedFilesListener() {
widget.selectedFiles.files.isNotEmpty
? _bottomPosition.value = 0.0

View file

@ -6,7 +6,6 @@ import 'package:flutter/material.dart';
import 'package:like_button/like_button.dart';
import 'package:logging/logging.dart';
import 'package:media_extension/media_extension.dart';
import 'package:page_transition/page_transition.dart';
import 'package:path/path.dart' as file_path;
import 'package:photo_manager/photo_manager.dart';
import 'package:photos/core/event_bus.dart';
@ -26,7 +25,7 @@ import 'package:photos/ui/common/progress_dialog.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/models/button_type.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/file/custom_app_bar.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
@ -272,16 +271,11 @@ class FadingAppBarState extends State<FadingAppBar> {
Future<void> _handleUnHideRequest(BuildContext context) async {
final s = SelectedFiles();
s.files.add(widget.file);
Navigator.push(
createCollectionSheet(
s,
null,
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
s,
null,
actionType: CollectionActionType.unHide,
),
),
actionType: CollectionActionType.unHide,
);
}

View file

@ -3,7 +3,6 @@ import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
import 'package:page_transition/page_transition.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
@ -13,7 +12,7 @@ import 'package:photos/models/trash_file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/theme/colors.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/ui/viewer/file/file_info_widget.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -238,16 +237,11 @@ class FadingBottomBarState extends State<FadingBottomBar> {
onPressed: () {
final selectedFiles = SelectedFiles();
selectedFiles.toggleSelection(widget.file);
Navigator.push(
createCollectionSheet(
selectedFiles,
null,
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
selectedFiles,
null,
actionType: CollectionActionType.restoreFiles,
),
),
actionType: CollectionActionType.restoreFiles,
);
},
),

View file

@ -2,7 +2,8 @@ import 'package:flutter/material.dart';
import 'package:photos/theme/ente_theme.dart';
class NoThumbnailWidget extends StatelessWidget {
const NoThumbnailWidget({Key? key}) : super(key: key);
final bool addBorder;
const NoThumbnailWidget({this.addBorder = true, Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -10,10 +11,12 @@ class NoThumbnailWidget extends StatelessWidget {
return Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(1),
border: Border.all(
color: enteColorScheme.strokeFaint,
width: 1,
),
border: addBorder
? Border.all(
color: enteColorScheme.strokeFaint,
width: 1,
)
: null,
color: enteColorScheme.fillFaint,
),
child: Center(

View file

@ -5,7 +5,6 @@ import 'dart:ui';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:page_transition/page_transition.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/ente_theme_data.dart';
@ -16,7 +15,7 @@ import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/ui/create_collection_page.dart';
import 'package:photos/ui/create_collection_sheet.dart';
import 'package:photos/utils/delete_file_util.dart';
import 'package:photos/utils/dialog_util.dart';
import 'package:photos/utils/magic_util.dart';
@ -232,36 +231,6 @@ class _OverlayWidgetState extends State<OverlayWidget> {
widget.selectedFiles.clearAll();
}
Future<void> _createCollectionAction(CollectionActionType type) async {
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
widget.selectedFiles,
null,
actionType: type,
),
),
);
}
Future<void> _moveFiles() async {
unawaited(
Navigator.push(
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
widget.selectedFiles,
null,
actionType: CollectionActionType.moveFiles,
),
),
),
);
}
List<Widget> _getActions(BuildContext context) {
final List<Widget> actions = <Widget>[];
if (widget.type == GalleryType.trash) {
@ -291,21 +260,6 @@ class _OverlayWidgetState extends State<OverlayWidget> {
);
}
if (Configuration.instance.hasConfiguredAccount() &&
widget.type == GalleryType.hidden) {
actions.add(
Tooltip(
message: "Unhide",
child: IconButton(
color: Theme.of(context).colorScheme.iconColor,
icon: const Icon(Icons.visibility),
onPressed: () {
_createCollectionAction(CollectionActionType.unHide);
},
),
),
);
}
if (Configuration.instance.hasConfiguredAccount() &&
widget.type == GalleryType.ownedCollection &&
widget.collection!.type != CollectionType.favorites) {
@ -437,12 +391,6 @@ class _OverlayWidgetState extends State<OverlayWidget> {
case 'hide':
await _handleHideRequest(context);
break;
case 'add':
await _createCollectionAction(CollectionActionType.addFiles);
break;
case 'move':
await _moveFiles();
break;
case 'archive':
await _handleVisibilityChangeRequest(context, visibilityArchive);
break;
@ -464,16 +412,11 @@ class _OverlayWidgetState extends State<OverlayWidget> {
Icons.restore,
),
onPressed: () {
Navigator.push(
createCollectionSheet(
widget.selectedFiles,
null,
context,
PageTransition(
type: PageTransitionType.bottomToTop,
child: CreateCollectionPage(
widget.selectedFiles,
null,
actionType: CollectionActionType.restoreFiles,
),
),
actionType: CollectionActionType.restoreFiles,
);
},
),