Merge branch 'main' into redesign-add-to-album

This commit is contained in:
ashilkn 2023-01-25 10:40:52 +05:30
commit 2bcfc37de3
30 changed files with 804 additions and 148 deletions

View file

@ -30,6 +30,7 @@ import 'package:photos/services/memories_service.dart';
import 'package:photos/services/search_service.dart';
import 'package:photos/services/sync_service.dart';
import 'package:photos/utils/crypto_util.dart';
import 'package:photos/utils/file_uploader.dart';
import 'package:photos/utils/validator_util.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:uuid/uuid.dart';
@ -162,6 +163,7 @@ class Configuration {
await UploadLocksDB.instance.clearTable();
await IgnoredFilesService.instance.reset();
await TrashDB.instance.clearTable();
FileUploader.instance.clearCachedUploadURLs();
if (!autoLogout) {
CollectionsService.instance.clearCache();
FavoritesService.instance.clearCache();

View file

@ -610,6 +610,19 @@ class FilesDB {
return FileLoadResult(files, files.length == limit);
}
Future<List<File>> getAllFilesCollection(int collectionID) async {
final db = await instance.database;
const String whereClause = '$columnCollectionID = ?';
final List<Object> whereArgs = [collectionID];
final results = await db.query(
filesTable,
where: whereClause,
whereArgs: whereArgs,
);
final files = convertToFiles(results);
return files;
}
Future<FileLoadResult> getFilesInCollections(
List<int> collectionIDs,
int startTime,

View file

@ -212,6 +212,11 @@ enum CollectionType {
unknown,
}
extension CollectionTypeExtn on CollectionType {
bool get canDelete =>
this != CollectionType.favorites && this != CollectionType.uncategorized;
}
enum CollectionParticipantRole {
unknown,
viewer,

View file

@ -0,0 +1,36 @@
import 'package:photos/models/file.dart';
class FilesSplit {
final List<File> pendingUploads;
final List<File> ownedByCurrentUser;
final List<File> ownedByOtherUsers;
FilesSplit({
required this.pendingUploads,
required this.ownedByCurrentUser,
required this.ownedByOtherUsers,
});
int get totalFileOwnedCount =>
pendingUploads.length + ownedByCurrentUser.length;
static FilesSplit split(Iterable<File> files, int currentUserID) {
final List<File> ownedByCurrentUser = [],
ownedByOtherUsers = [],
pendingUploads = [];
for (var f in files) {
if (f.ownerID == null || f.uploadedFileID == null) {
pendingUploads.add(f);
} else if (f.ownerID == currentUserID) {
ownedByCurrentUser.add(f);
} else {
ownedByOtherUsers.add(f);
}
}
return FilesSplit(
pendingUploads: pendingUploads,
ownedByCurrentUser: ownedByCurrentUser,
ownedByOtherUsers: ownedByOtherUsers,
);
}
}

View file

@ -1,6 +1,7 @@
enum GalleryType {
homepage,
archive,
uncategorized,
hidden,
favorite,
trash,
@ -23,6 +24,7 @@ extension GalleyTypeExtension on GalleryType {
return true;
case GalleryType.hidden:
case GalleryType.uncategorized:
case GalleryType.trash:
case GalleryType.sharedCollection:
return false;
@ -32,6 +34,7 @@ extension GalleyTypeExtension on GalleryType {
bool showMoveToAlbum() {
switch (this) {
case GalleryType.ownedCollection:
case GalleryType.uncategorized:
return true;
case GalleryType.hidden:
@ -55,6 +58,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.homepage:
case GalleryType.favorite:
case GalleryType.localFolder:
case GalleryType.uncategorized:
return true;
case GalleryType.trash:
case GalleryType.archive:
@ -70,6 +74,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.searchResults:
case GalleryType.homepage:
case GalleryType.favorite:
case GalleryType.uncategorized:
case GalleryType.archive:
case GalleryType.hidden:
case GalleryType.localFolder:
@ -87,6 +92,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.homepage:
case GalleryType.favorite:
case GalleryType.archive:
case GalleryType.uncategorized:
return true;
case GalleryType.hidden:
case GalleryType.localFolder:
@ -102,6 +108,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.sharedCollection:
return true;
case GalleryType.hidden:
case GalleryType.uncategorized:
case GalleryType.favorite:
case GalleryType.searchResults:
case GalleryType.homepage:
@ -116,6 +123,7 @@ extension GalleyTypeExtension on GalleryType {
switch (this) {
case GalleryType.ownedCollection:
case GalleryType.homepage:
case GalleryType.uncategorized:
return true;
case GalleryType.hidden:
@ -139,6 +147,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.homepage:
case GalleryType.searchResults:
case GalleryType.archive:
case GalleryType.uncategorized:
return true;
case GalleryType.hidden:
@ -159,6 +168,7 @@ extension GalleyTypeExtension on GalleryType {
case GalleryType.ownedCollection:
case GalleryType.homepage:
case GalleryType.searchResults:
case GalleryType.uncategorized:
return true;
case GalleryType.hidden:

View file

@ -7,18 +7,8 @@ class KeyAttributes {
final String publicKey;
final String encryptedSecretKey;
final String secretKeyDecryptionNonce;
// Note: For users who signed in before we started storing memLimit and
// optsLimit, these fields will be null. To update these values, they need to
// either log in again or client needs to fetch these values from server.
// (internal monologue: Hopefully, the mem/ops limit used to generate the
// key is same as it's stored on the server)
// https://github.com/ente-io/photos-app/commit/8cb7f885b343f2c796e4cc9ce1f7d70c9a13a003#diff-02f19d9ee0a60ee9674372d2c780da5d5284128dc9ea65dec6cdcddfc559ebb3
final int? memLimit;
final int? opsLimit;
// The recovery key attributes can be null for old users who haven't generated
// their recovery keys yet.
// https://github.com/ente-io/photos-app/commit/d7acc95855c62ecdf2a29c4102e648105e17bd8c#diff-02f19d9ee0a60ee9674372d2c780da5d5284128dc9ea65dec6cdcddfc559ebb3
final String? masterKeyEncryptedWithRecoveryKey;
final String? masterKeyDecryptionNonce;
final String? recoveryKeyEncryptedWithMasterKey;

View file

@ -1,16 +0,0 @@
import 'package:photos/models/file.dart';
class SelectedFileSplit {
final List<File> pendingUploads;
final List<File> ownedByCurrentUser;
final List<File> ownedByOtherUsers;
SelectedFileSplit({
required this.pendingUploads,
required this.ownedByCurrentUser,
required this.ownedByOtherUsers,
});
int get totalFileOwnedCount =>
pendingUploads.length + ownedByCurrentUser.length;
}

View file

@ -3,7 +3,6 @@ import 'package:flutter/foundation.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/clear_selections_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/selected_file_breakup.dart';
class SelectedFiles extends ChangeNotifier {
final files = <File>{};
@ -66,26 +65,6 @@ class SelectedFiles extends ChangeNotifier {
return false;
}
SelectedFileSplit split(int currentUseID) {
final List<File> ownedByCurrentUser = [],
ownedByOtherUsers = [],
pendingUploads = [];
for (var f in files) {
if (f.ownerID == null || f.uploadedFileID == null) {
pendingUploads.add(f);
} else if (f.ownerID == currentUseID) {
ownedByCurrentUser.add(f);
} else {
ownedByOtherUsers.add(f);
}
}
return SelectedFileSplit(
pendingUploads: pendingUploads,
ownedByCurrentUser: ownedByCurrentUser,
ownedByOtherUsers: ownedByOtherUsers,
);
}
void clearAll() {
Bus.instance.fire(ClearSelectionsEvent());
lastSelections.addAll(files);

View file

@ -57,6 +57,7 @@ class CollectionsService {
final _cachedUserIdToUser = <int, User>{};
Collection? cachedDefaultHiddenCollection;
Future<List<File>>? _cachedLatestFiles;
Collection? cachedUncategorizedCollection;
CollectionsService._privateConstructor() {
_db = CollectionsDB.instance;
@ -360,33 +361,13 @@ class CollectionsService {
}
}
Future<void> trashCollection(
Future<void> trashNonEmptyCollection(
Collection collection,
bool isEmptyCollection,
) async {
try {
// Turn off automatic back-up for the on device folder only when the
// collection is non-empty. This is to handle the case when the existing
// files in the on-device folders where automatically uploaded in some
// other collection or from different device
if (!isEmptyCollection) {
final deviceCollections = await _filesDB.getDeviceCollections();
final Map<String, bool> deivcePathIDsToUnsync = Map.fromEntries(
deviceCollections
.where((e) => e.shouldBackup && e.collectionID == collection.id)
.map((e) => MapEntry(e.id, false)),
);
if (deivcePathIDsToUnsync.isNotEmpty) {
_logger.info(
'turning off backup status for folders $deivcePathIDsToUnsync',
);
await RemoteSyncService.instance
.updateDeviceFolderSyncStatus(deivcePathIDsToUnsync);
}
}
await _turnOffDeviceFolderSync(collection);
await _enteDio.delete(
"/collections/v2/${collection.id}",
"/collections/v3/${collection.id}?keepFiles=False&collectionID=${collection.id}",
);
await _handleCollectionDeletion(collection);
} catch (e) {
@ -395,8 +376,33 @@ class CollectionsService {
}
}
Future<void> trashEmptyCollection(Collection collection) async {
Future<void> _turnOffDeviceFolderSync(Collection collection) async {
final deviceCollections = await _filesDB.getDeviceCollections();
final Map<String, bool> deivcePathIDsToUnsync = Map.fromEntries(
deviceCollections
.where((e) => e.shouldBackup && e.collectionID == collection.id)
.map((e) => MapEntry(e.id, false)),
);
if (deivcePathIDsToUnsync.isNotEmpty) {
_logger.info(
'turning off backup status for folders $deivcePathIDsToUnsync',
);
await RemoteSyncService.instance
.updateDeviceFolderSyncStatus(deivcePathIDsToUnsync);
}
}
Future<void> trashEmptyCollection(
Collection collection, {
// during bulk deletion, this event is not fired to avoid quick refresh
// of the collection gallery
bool isBulkDelete = false,
}) async {
try {
if (!isBulkDelete) {
await _turnOffDeviceFolderSync(collection);
}
// While trashing empty albums, we must pass keepFiles flag as True.
// The server will verify that the collection is actually empty before
// deleting the files. If keepFiles is set as False and the collection
@ -404,9 +410,13 @@ class CollectionsService {
await _enteDio.delete(
"/collections/v3/${collection.id}?keepFiles=True&collectionID=${collection.id}",
);
final deletedCollection = collection.copyWith(isDeleted: true);
_collectionIDToCollections[collection.id] = deletedCollection;
unawaited(_db.insert([deletedCollection]));
if (isBulkDelete) {
final deletedCollection = collection.copyWith(isDeleted: true);
_collectionIDToCollections[collection.id] = deletedCollection;
unawaited(_db.insert([deletedCollection]));
} else {
await _handleCollectionDeletion(collection);
}
} on DioError catch (e) {
if (e.response != null) {
debugPrint("Error " + e.response!.toString());
@ -421,6 +431,7 @@ class CollectionsService {
Future<void> _handleCollectionDeletion(Collection collection) async {
await _filesDB.deleteCollection(collection.id);
final deletedCollection = collection.copyWith(isDeleted: true);
unawaited(_db.insert([deletedCollection]));
_collectionIDToCollections[collection.id] = deletedCollection;
Bus.instance.fire(
CollectionUpdatedEvent(
@ -431,8 +442,7 @@ class CollectionsService {
),
);
sync().ignore();
unawaited(_db.insert([deletedCollection]));
unawaited(LocalSyncService.instance.syncAll());
LocalSyncService.instance.syncAll().ignore();
}
Uint8List getCollectionKey(int collectionID) {

View file

@ -14,11 +14,14 @@ import 'package:photos/models/collection.dart';
import 'package:photos/models/file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/remote_sync_service.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import 'package:photos/utils/crypto_util.dart';
class FavoritesService {
late Configuration _config;
late CollectionsService _collectionsService;
late CollectionActions _collectionActions;
late FilesDB _filesDB;
int? _cachedFavoritesCollectionID;
final Set<int> _cachedFavUploadedIDs = {};
@ -26,9 +29,11 @@ class FavoritesService {
late StreamSubscription<CollectionUpdatedEvent>
_collectionUpdatesSubscription;
FavoritesService._privateConstructor() {
FavoritesService._privateConstructor() {}
Future<void> initFav() async {
_config = Configuration.instance;
_collectionsService = CollectionsService.instance;
_collectionActions = CollectionActions(_collectionsService);
_filesDB = FilesDB.instance;
_collectionUpdatesSubscription =
Bus.instance.on<CollectionUpdatedEvent>().listen((event) {
@ -46,8 +51,6 @@ class FavoritesService {
}
}
});
}
Future<void> initFav() async {
await _warmUpCache();
}
@ -120,7 +123,7 @@ class FavoritesService {
}
}
Future<void> addToFavorites(File file) async {
Future<void> addToFavorites(BuildContext context, File file) async {
final collectionID = await _getOrCreateFavoriteCollectionID();
final List<File> files = [file];
if (file.uploadedFileID == null) {
@ -134,7 +137,8 @@ class FavoritesService {
RemoteSyncService.instance.sync(silently: true).ignore();
}
Future<void> updateFavorites(List<File> files, bool favFlag) async {
Future<void> updateFavorites(
BuildContext context, List<File> files, bool favFlag) async {
final int currentUserID = Configuration.instance.getUserID()!;
if (files.any((f) => f.uploadedFileID == null)) {
throw AssertionError("Can only favorite uploaded items");
@ -146,18 +150,27 @@ class FavoritesService {
if (favFlag) {
await _collectionsService.addToCollection(collectionID, files);
} else {
await _collectionsService.removeFromCollection(collectionID, files);
final Collection? favCollection = await _getFavoritesCollection();
await _collectionActions.moveFilesFromCurrentCollection(
context,
favCollection!,
files,
);
}
_updateFavoriteFilesCache(files, favFlag: favFlag);
}
Future<void> removeFromFavorites(File file) async {
final collectionID = await _getOrCreateFavoriteCollectionID();
Future<void> removeFromFavorites(BuildContext context, File file) async {
final fileID = file.uploadedFileID;
if (fileID == null) {
// Do nothing, ignore
} else {
await _collectionsService.removeFromCollection(collectionID, [file]);
final Collection? favCollection = await _getFavoritesCollection();
await _collectionActions.moveFilesFromCurrentCollection(
context,
favCollection!,
[file],
);
}
_updateFavoriteFilesCache([file], favFlag: false);
}

View file

@ -41,6 +41,26 @@ extension HiddenService on CollectionsService {
return cachedDefaultHiddenCollection!;
}
// getUncategorizedCollection will return the uncategorized collection
// for the given user
Future<Collection> getUncategorizedCollection() async {
if (cachedUncategorizedCollection != null) {
return cachedUncategorizedCollection!;
}
final int userID = config.getUserID()!;
final Collection? matchedCollection =
collectionIDToCollections.values.firstWhereOrNull(
(element) =>
element.type == CollectionType.uncategorized &&
element.owner!.id == userID,
);
if (matchedCollection != null) {
cachedUncategorizedCollection = matchedCollection;
return cachedUncategorizedCollection!;
}
return _createUncategorizedCollection();
}
Future<bool> hideFiles(
BuildContext context,
List<File> filesToHide, {
@ -113,6 +133,25 @@ extension HiddenService on CollectionsService {
return collectionFromServer;
}
Future<Collection> _createUncategorizedCollection() async {
final key = CryptoUtil.generateKey();
final encKey = CryptoUtil.encryptSync(key, config.getKey()!);
final encName =
CryptoUtil.encryptSync(utf8.encode("Uncategorized") as Uint8List, key);
final collection = await createAndCacheCollection(
CreateRequest(
encryptedKey: Sodium.bin2base64(encKey.encryptedData!),
keyDecryptionNonce: Sodium.bin2base64(encKey.nonce!),
encryptedName: Sodium.bin2base64(encName.encryptedData!),
nameDecryptionNonce: Sodium.bin2base64(encName.nonce!),
type: CollectionType.uncategorized,
attributes: CollectionAttributes(),
),
);
cachedUncategorizedCollection = collection;
return cachedUncategorizedCollection!;
}
Future<CreateRequest> buildCollectionCreateRequest(
String name, {
required int visibility,

View file

@ -6,6 +6,7 @@ import 'package:photos/data/months.dart';
import 'package:photos/data/years.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_type.dart';
@ -114,7 +115,8 @@ class SearchService {
break;
}
if (!c.collection.isHidden() &&
if (!c.collection.isHidden() && c.collection.type != CollectionType
.uncategorized &&
c.collection.name!.toLowerCase().contains(
query.toLowerCase(),
)) {

View file

@ -7,10 +7,61 @@ import 'package:photos/services/favorites_service.dart';
import 'package:photos/theme/ente_theme.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
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/utils/dialog_util.dart';
import 'package:photos/utils/toast_util.dart';
extension CollectionFileActions on CollectionActions {
Future<void> showRemoveFromCollectionSheetV2(
BuildContext bContext,
Collection collection,
SelectedFiles selectedFiles,
) async {
final actionResult = await showActionSheet(
context: bContext,
buttons: [
ButtonWidget(
labelText: "Yes, remove",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
shouldStickToDarkTheme: true,
isInAlert: true,
onTap: () async {
try {
await moveFilesFromCurrentCollection(
bContext,
collection,
selectedFiles.files,
);
} catch (e) {
logger.severe("Failed to move files", e);
rethrow;
}
},
),
const ButtonWidget(
labelText: "Cancel",
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
title: "Remove from album?",
body: "Selected items will be removed from this album. Items which are "
"only in this album will be moved to Uncategorized.",
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult != null && actionResult == ButtonAction.error) {
showGenericErrorDialog(context: bContext);
} else {
selectedFiles.clearAll();
}
}
Future<void> showRemoveFromCollectionSheet(
BuildContext context,
Collection collection,
@ -120,7 +171,8 @@ extension CollectionFileActions on CollectionActions {
await dialog.show();
try {
await FavoritesService.instance.updateFavorites(files, markAsFavorite);
await FavoritesService.instance
.updateFavorites(context, files, markAsFavorite);
return true;
} catch (e, s) {
logger.severe(e, s);

View file

@ -1,15 +1,19 @@
import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/ente_theme_data.dart';
import 'package:photos/models/api/collection/create_request.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/files_split.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/services/user_service.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/payment/subscription.dart';
import 'package:photos/utils/date_time_util.dart';
import 'package:photos/utils/dialog_util.dart';
@ -30,15 +34,38 @@ class CollectionActions {
) async {
// confirm if user wants to disable the url
if (!enable) {
final choice = await showNewChoiceDialog(
context,
title: "Remove public link",
final ButtonAction? result = await showActionSheet(
context: context,
buttons: [
ButtonWidget(
buttonType: ButtonType.critical,
isInAlert: true,
shouldStickToDarkTheme: true,
buttonAction: ButtonAction.first,
shouldSurfaceExecutionStates: true,
labelText: "Yes, remove",
onTap: () async {
await CollectionsService.instance.disableShareUrl(collection);
},
),
const ButtonWidget(
buttonType: ButtonType.secondary,
buttonAction: ButtonAction.second,
isInAlert: true,
shouldStickToDarkTheme: true,
labelText: "Cancel",
)
],
title: "Remove public link?",
body:
'This will remove the public link for accessing "${collection.name}"',
firstButtonLabel: "Yes, remove",
'This will remove the public link for accessing "${collection.name}".',
);
if (choice != ButtonAction.first) {
return false;
if (result != null) {
if (result == ButtonAction.error) {
showGenericErrorDialog(context: context);
}
// return
return result == ButtonAction.first;
}
}
final dialog = createProgressDialog(
@ -239,6 +266,218 @@ class CollectionActions {
}
}
Future<bool> deleteCollectionSheet(
BuildContext bContext,
Collection collection,
) async {
final currentUserID = Configuration.instance.getUserID()!;
if (collection.owner!.id != currentUserID) {
throw AssertionError("Can not delete album owned by others");
}
final actionResult = await showActionSheet(
context: bContext,
buttons: [
ButtonWidget(
labelText: "Keep Photos",
buttonType: ButtonType.neutral,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.first,
shouldStickToDarkTheme: true,
isInAlert: true,
onTap: () async {
try {
final List<File> files =
await FilesDB.instance.getAllFilesCollection(collection.id);
await moveFilesFromCurrentCollection(bContext, collection, files);
// collection should be empty on server now
await collectionsService.trashEmptyCollection(collection);
} catch (e) {
logger.severe("Failed to keep photos and delete collection", e);
rethrow;
}
},
),
ButtonWidget(
labelText: "Delete photos",
buttonType: ButtonType.critical,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.second,
shouldStickToDarkTheme: true,
isInAlert: true,
onTap: () async {
try {
await collectionsService.trashNonEmptyCollection(collection);
} catch (e) {
logger.severe("Failed to delete collection", e);
rethrow;
}
},
),
const ButtonWidget(
labelText: "Cancel",
buttonType: ButtonType.secondary,
buttonSize: ButtonSize.large,
buttonAction: ButtonAction.third,
shouldStickToDarkTheme: true,
isInAlert: true,
),
],
title: "Delete album?",
body: "This album will be deleted. Do you also want to delete the "
"photos (and videos) that are present in this album?",
bodyHighlight: "They will be deleted from all albums.",
actionSheetType: ActionSheetType.defaultActionSheet,
);
if (actionResult != null && actionResult == ButtonAction.error) {
showGenericErrorDialog(context: bContext);
return false;
}
if (actionResult != null &&
(actionResult == ButtonAction.first ||
actionResult == ButtonAction.second)) {
return true;
}
return false;
}
/*
_moveFilesFromCurrentCollection removes the file from the current
collection. Based on the file and collection ownership, files will be
either moved to different collection (Case A). or will just get removed
from current collection (Case B).
-------------------------------
Case A: Files and collection belong to the same user. Such files
will be moved to a collection which belongs to the user and removed from
the current collection as part of move operation.
Note: Even files are present in the
destination collection, we need to make move API call on the server
so that the files are removed from current collection and are actually
moved to a collection owned by the user.
-------------------------------
Case B: Owner of files and collections are different. In such cases,
we will just remove (not move) the files from the given collection.
*/
Future<void> moveFilesFromCurrentCollection(
BuildContext context,
Collection collection,
Iterable<File> files,
) async {
final int currentUserID = Configuration.instance.getUserID()!;
final isCollectionOwner = collection.owner!.id == currentUserID;
if (!isCollectionOwner) {
// Todo: Support for removing own files from a collection owner by
// someone else will be added along with collaboration changes
showShortToast(context, "Only collection owner can remove");
return;
}
final FilesSplit split = FilesSplit.split(
files,
Configuration.instance.getUserID()!,
);
if (split.ownedByOtherUsers.isNotEmpty) {
// Todo: Support for removing own files from a collection owner by
// someone else will be added along with collaboration changes
showShortToast(context, "Can only remove files owned by you");
return;
}
// pendingAssignMap keeps a track of files which are yet to be assigned to
// to destination collection.
final Map<int, File> pendingAssignMap = {};
// destCollectionToFilesMap contains the destination collection and
// files entry which needs to be moved in destination.
// After the end of mapping logic, the number of files entries in
// pendingAssignMap should be equal to files in destCollectionToFilesMap
final Map<int, List<File>> destCollectionToFilesMap = {};
final List<int> uploadedIDs = [];
for (File f in split.ownedByCurrentUser) {
if (f.uploadedFileID != null) {
pendingAssignMap[f.uploadedFileID!] = f;
uploadedIDs.add(f.uploadedFileID!);
}
}
final Map<int, List<File>> collectionToFilesMap =
await FilesDB.instance.getAllFilesGroupByCollectionID(uploadedIDs);
// Find and map the files from current collection to to entries in other
// collections. This mapping is done to avoid moving all the files to
// uncategorized during remove from album.
for (MapEntry<int, List<File>> entry in collectionToFilesMap.entries) {
if (!_isAutoMoveCandidate(collection.id, entry.key, currentUserID)) {
continue;
}
final targetCollection = collectionsService.getCollectionByID(entry.key)!;
// for each file which already exist in the destination collection
// add entries in the moveDestCollectionToFiles map
for (File file in entry.value) {
// Check if the uploaded file is still waiting to be mapped
if (pendingAssignMap.containsKey(file.uploadedFileID)) {
if (!destCollectionToFilesMap.containsKey(targetCollection.id)) {
destCollectionToFilesMap[targetCollection.id] = <File>[];
}
destCollectionToFilesMap[targetCollection.id]!
.add(pendingAssignMap[file.uploadedFileID!]!);
pendingAssignMap.remove(file.uploadedFileID);
}
}
}
// Move the remaining files to uncategorized collection
if (pendingAssignMap.isNotEmpty) {
final Collection uncategorizedCollection =
await collectionsService.getUncategorizedCollection();
final int toCollectionID = uncategorizedCollection.id;
for (MapEntry<int, File> entry in pendingAssignMap.entries) {
final file = entry.value;
if (pendingAssignMap.containsKey(file.uploadedFileID)) {
if (!destCollectionToFilesMap.containsKey(toCollectionID)) {
destCollectionToFilesMap[toCollectionID] = <File>[];
}
destCollectionToFilesMap[toCollectionID]!
.add(pendingAssignMap[file.uploadedFileID!]!);
}
}
}
// Verify that all files are mapped.
int mappedFilesCount = 0;
destCollectionToFilesMap.forEach((key, value) {
mappedFilesCount += value.length;
});
if (mappedFilesCount != uploadedIDs.length) {
throw AssertionError(
"Failed to map all files toMap: ${uploadedIDs.length} and mapped "
"$mappedFilesCount",
);
}
for (MapEntry<int, List<File>> entry in destCollectionToFilesMap.entries) {
await collectionsService.move(entry.key, collection.id, entry.value);
}
}
// This method returns true if the given destination collection is a good
// target to moving files during file remove or delete collection but keey
// photos action. Uncategorized or favorite type of collections are not
// good auto-move candidates. Uncategorized will be fall back for all files
// which could not be mapped to a potential target collection
bool _isAutoMoveCandidate(int fromCollectionID, toCollectionID, int userID) {
if (fromCollectionID == toCollectionID) {
return false;
}
final Collection? targetCollection =
collectionsService.getCollectionByID(toCollectionID);
// ignore non-cached collections, uncategorized and favorite
// collections and collections ignored by others
if (targetCollection == null ||
(CollectionType.uncategorized == targetCollection.type ||
targetCollection.type == CollectionType.favorites) ||
targetCollection.owner!.id != userID) {
return false;
}
return true;
}
void _showUnSupportedAlert(BuildContext context) {
final AlertDialog alert = AlertDialog(
title: const Text("Sorry"),

View file

@ -0,0 +1,112 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
import 'package:photos/ui/viewer/gallery/uncategorized_page.dart';
import 'package:photos/utils/navigation_util.dart';
class UnCatCollectionsButtonWidget extends StatelessWidget {
final TextStyle textStyle;
const UnCatCollectionsButtonWidget(
this.textStyle, {
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final Collection? collection = CollectionsService.instance
.getActiveCollections()
.firstWhereOrNull((e) => e.type == CollectionType.uncategorized);
if (collection == null) {
// create uncategorized collection if it's not already created
CollectionsService.instance.getUncategorizedCollection().ignore();
}
return OutlinedButton(
style: OutlinedButton.styleFrom(
backgroundColor: Theme.of(context).backgroundColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(0),
side: BorderSide(
width: 0.5,
color: Theme.of(context).iconTheme.color!.withOpacity(0.24),
),
),
child: SizedBox(
height: 48,
width: double.infinity,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.category_outlined,
color: Theme.of(context).iconTheme.color,
),
const Padding(padding: EdgeInsets.all(6)),
FutureBuilder<int>(
future: collection == null
? Future.value(0)
: FilesDB.instance.collectionFileCount(collection.id),
builder: (context, snapshot) {
if (snapshot.hasData && snapshot.data! > 0) {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Uncategorized",
style: Theme.of(context).textTheme.subtitle1,
),
const TextSpan(text: " \u2022 "),
TextSpan(
text: snapshot.data.toString(),
),
//need to query in db and bring this value
],
),
);
} else {
return RichText(
text: TextSpan(
style: textStyle,
children: [
TextSpan(
text: "Uncategorized",
style: Theme.of(context).textTheme.subtitle1,
),
//need to query in db and bring this value
],
),
);
}
},
),
],
),
Icon(
Icons.chevron_right,
color: Theme.of(context).iconTheme.color,
),
],
),
),
),
onPressed: () async {
if (collection != null) {
routeToPage(
context,
UnCategorizedPage(collection),
);
}
},
);
}
}

View file

@ -19,6 +19,7 @@ import 'package:photos/ui/collections/hidden_collections_button_widget.dart';
import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
import 'package:photos/ui/collections/section_title.dart';
import 'package:photos/ui/collections/trash_button_widget.dart';
import 'package:photos/ui/collections/uncat_collections_button_widget.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/ui/viewer/actions/delete_empty_albums.dart';
import 'package:photos/ui/viewer/gallery/empty_state.dart';
@ -83,10 +84,15 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
Future<List<CollectionWithThumbnail>> _getCollections() async {
final List<CollectionWithThumbnail> collectionsWithThumbnail =
await CollectionsService.instance.getCollectionsWithThumbnails();
// Remove uncategorized collection
collectionsWithThumbnail
.removeWhere((t) => t.collection.type == CollectionType.uncategorized);
final ListMatch<CollectionWithThumbnail> favMathResult =
collectionsWithThumbnail.splitMatch(
(element) => element.collection.type == CollectionType.favorites,
);
// Hide fav collection if it's empty and not shared
favMathResult.matched.removeWhere(
(element) =>
@ -168,6 +174,8 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column(
children: [
UnCatCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
const SizedBox(height: 12),
HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),

View file

@ -18,7 +18,7 @@ enum ActionSheetType {
Future<ButtonAction?> showActionSheet({
required BuildContext context,
required List<ButtonWidget> buttons,
required ActionSheetType actionSheetType,
ActionSheetType actionSheetType = ActionSheetType.defaultActionSheet,
bool enableDrag = true,
bool isDismissible = true,
bool isCheckIconGreen = false,

View file

@ -438,11 +438,9 @@ class _ButtonChildWidgetState extends State<ButtonChildWidget> {
executionState = ExecutionState.successful;
Future.delayed(
Duration(
seconds: widget.isInAlert
? 1
: widget.shouldSurfaceExecutionStates
? 2
: 0,
seconds: widget.shouldSurfaceExecutionStates
? (widget.isInAlert ? 1 : 2)
: 0,
), () {
widget.isInAlert
? Navigator.of(context, rootNavigator: true)

View file

@ -193,6 +193,7 @@ class BackupSectionWidgetState extends State<BackupSectionWidget> {
final String countText = result.count.toString() +
" duplicate file" +
(result.count == 1 ? "" : "s");
final DialogWidget dialog = choiceDialog(
title: "✨ Success",
body: "You have cleaned up " +

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/file.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/ui/components/action_sheet_widget.dart';
@ -86,7 +87,11 @@ class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
Future<void> _deleteEmptyAlbums() async {
final collections =
await CollectionsService.instance.getCollectionsWithThumbnails();
collections.removeWhere((element) => element.thumbnail != null);
// remove collections which are not empty or can't be deleted
collections.removeWhere(
(element) =>
element.thumbnail != null || !element.collection.type.canDelete,
);
int failedCount = 0;
for (int i = 0; i < collections.length; i++) {
if (mounted && !_isCancelled) {
@ -94,8 +99,9 @@ class _DeleteEmptyAlbumsState extends State<DeleteEmptyAlbums> {
"Deleting ${(i + 1).toString().padLeft(collections.length.toString().length, '0')} / "
"${collections.length}";
try {
await CollectionsService.instance
.trashEmptyCollection(collections[i].collection);
await CollectionsService.instance.trashEmptyCollection(
collections[i].collection,
isBulkDelete: true);
} catch (_) {
failedCount++;
}

View file

@ -4,9 +4,10 @@ import 'package:flutter/services.dart';
import 'package:photos/core/configuration.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/device_collection.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/files_split.dart';
import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/magic_metadata.dart';
import 'package:photos/models/selected_file_breakup.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/collections_service.dart';
import 'package:photos/services/hidden_service.dart';
@ -46,7 +47,7 @@ class FileSelectionActionWidget extends StatefulWidget {
class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
late int currentUserID;
late SelectedFileSplit split;
late FilesSplit split;
late CollectionActions collectionActions;
// _cachedCollectionForSharedLink is primarly used to avoid creating duplicate
@ -57,7 +58,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
@override
void initState() {
currentUserID = Configuration.instance.getUserID()!;
split = widget.selectedFiles.split(currentUserID);
split = FilesSplit.split(<File>[], currentUserID);
widget.selectedFiles.addListener(_selectFileChangeListener);
collectionActions = CollectionActions(CollectionsService.instance);
super.initState();
@ -73,7 +74,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
if (_cachedCollectionForSharedLink != null) {
_cachedCollectionForSharedLink = null;
}
split = widget.selectedFiles.split(currentUserID);
split = FilesSplit.split(widget.selectedFiles.files, currentUserID);
if (mounted) {
setState(() => {});
}
@ -284,7 +285,7 @@ class _FileSelectionActionWidgetState extends State<FileSelectionActionWidget> {
widget.selectedFiles
.unSelectAll(split.ownedByOtherUsers.toSet(), skipNotify: true);
}
await collectionActions.showRemoveFromCollectionSheet(
await collectionActions.showRemoveFromCollectionSheetV2(
context,
widget.collection!,
widget.selectedFiles,

View file

@ -307,7 +307,7 @@ class FadingAppBarState extends State<FadingAppBar> {
await dialog.show();
}
try {
await FavoritesService.instance.addToFavorites(file);
await FavoritesService.instance.addToFavorites(context, file);
} catch (e, s) {
_logger.severe(e, s);
hasError = true;
@ -319,7 +319,7 @@ class FadingAppBarState extends State<FadingAppBar> {
}
} else {
try {
await FavoritesService.instance.removeFromFavorites(file);
await FavoritesService.instance.removeFromFavorites(context, file);
} catch (e, s) {
_logger.severe(e, s);
hasError = true;

View file

@ -241,7 +241,10 @@ class _ThumbnailWidgetState extends State<ThumbnailWidget> {
_cacheAndRender(imageProvider);
}
ThumbnailInMemoryLruCache.put(
widget.file!, thumbData, thumbnailSmallSize);
widget.file!,
thumbData,
thumbnailSmallSize,
);
}).catchError((e) {
_logger.warning("Could not load image: ", e);
_errorLoadingLocalThumbnail = true;

View file

@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
@ -13,6 +14,7 @@ import 'package:photos/events/local_photos_updated_event.dart';
import 'package:photos/models/file.dart';
import 'package:photos/ui/common/loading_widget.dart';
import 'package:photos/utils/file_util.dart';
import 'package:photos/utils/image_util.dart';
import 'package:photos/utils/thumbnail_util.dart';
class ZoomableImage extends StatefulWidget {
@ -35,7 +37,7 @@ class ZoomableImage extends StatefulWidget {
class _ZoomableImageState extends State<ZoomableImage>
with SingleTickerProviderStateMixin {
final Logger _logger = Logger("ZoomableImage");
late Logger _logger;
late File _photo;
ImageProvider? _imageProvider;
bool _loadedSmallThumbnail = false;
@ -45,10 +47,13 @@ class _ZoomableImageState extends State<ZoomableImage>
bool _loadedFinalImage = false;
ValueChanged<PhotoViewScaleState>? _scaleStateChangedCallback;
bool _isZooming = false;
PhotoViewController _photoViewController = PhotoViewController();
int? _thumbnailWidth;
@override
void initState() {
_photo = widget.photo;
_logger = Logger("ZoomableImage_" + _photo.displayName);
debugPrint('initState for ${_photo.toString()}');
_scaleStateChangedCallback = (value) {
if (widget.shouldDisableScroll != null) {
@ -61,6 +66,12 @@ class _ZoomableImageState extends State<ZoomableImage>
super.initState();
}
@override
void dispose() {
_photoViewController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_photo.isRemoteFile) {
@ -75,6 +86,7 @@ class _ZoomableImageState extends State<ZoomableImage>
axis: Axis.vertical,
child: PhotoView(
imageProvider: _imageProvider,
controller: _photoViewController,
scaleStateChangedCallback: _scaleStateChangedCallback,
minScale: PhotoViewComputedScale.contained,
gaplessPlayback: true,
@ -106,6 +118,7 @@ class _ZoomableImageState extends State<ZoomableImage>
if (cachedThumbnail != null) {
_imageProvider = Image.memory(cachedThumbnail).image;
_loadedSmallThumbnail = true;
_captureThumbnailDimensions(_imageProvider!);
} else {
getThumbnailFromServer(_photo).then((file) {
final imageProvider = Image.memory(file).image;
@ -115,6 +128,7 @@ class _ZoomableImageState extends State<ZoomableImage>
setState(() {
_imageProvider = imageProvider;
_loadedSmallThumbnail = true;
_captureThumbnailDimensions(_imageProvider!);
});
}
}).catchError((e) {
@ -125,7 +139,8 @@ class _ZoomableImageState extends State<ZoomableImage>
});
}
}
if (!_loadedFinalImage) {
if (!_loadedFinalImage && !_loadingFinalImage) {
_loadingFinalImage = true;
getFileFromServer(_photo).then((file) {
_onFinalImageLoaded(
Image.file(
@ -209,16 +224,42 @@ class _ZoomableImageState extends State<ZoomableImage>
void _onFinalImageLoaded(ImageProvider imageProvider) {
if (mounted) {
precacheImage(imageProvider, context).then((value) {
precacheImage(imageProvider, context).then((value) async {
if (mounted) {
await _updatePhotoViewController(imageProvider);
setState(() {
_imageProvider = imageProvider;
_loadedFinalImage = true;
_logger.info("Final image loaded");
});
}
});
}
}
Future<void> _captureThumbnailDimensions(ImageProvider imageProvider) async {
final imageInfo = await getImageInfo(imageProvider);
_thumbnailWidth = imageInfo.image.width;
}
Future<void> _updatePhotoViewController(ImageProvider imageProvider) async {
if (_thumbnailWidth == null || _photoViewController.scale == null) {
return;
}
final imageInfo = await getImageInfo(imageProvider);
final scale = _photoViewController.scale! /
(imageInfo.image.width / _thumbnailWidth!);
final currentPosition = _photoViewController.value.position;
final positionScaleFactor = 1 / scale;
final newPosition = currentPosition.scale(
positionScaleFactor,
positionScaleFactor,
);
_photoViewController = PhotoViewController(
initialPosition: newPosition,
initialScale: scale,
);
}
bool _isGIF() => _photo.displayName.toLowerCase().endsWith(".gif");
}

View file

@ -3,6 +3,7 @@ import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/collection_items.dart';
import 'package:photos/models/file.dart';
import 'package:photos/models/file_load_result.dart';
@ -55,6 +56,8 @@ class _CollectionPageState extends State<CollectionPage> {
if (widget.hasVerifiedLock == false && widget.c.collection.isHidden()) {
return const EmptyState();
}
final appBarTypeValue = widget.c.collection.type == CollectionType
.uncategorized ? GalleryType.uncategorized : widget.appBarType;
final List<File>? initialFiles =
widget.c.thumbnail != null ? [widget.c.thumbnail!] : null;
final gallery = Gallery(
@ -93,7 +96,7 @@ class _CollectionPageState extends State<CollectionPage> {
appBar: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget(
widget.appBarType,
appBarTypeValue,
widget.c.collection.name,
_selectedFiles,
collection: widget.c.collection,
@ -104,7 +107,7 @@ class _CollectionPageState extends State<CollectionPage> {
children: [
gallery,
FileSelectionOverlayBar(
widget.appBarType,
appBarTypeValue,
_selectedFiles,
collection: widget.c.collection,
)

View file

@ -258,11 +258,16 @@ class _GalleryState extends State<Gallery> {
return gallery;
},
labelTextBuilder: (int index) {
return getMonthAndYear(
DateTime.fromMicrosecondsSinceEpoch(
_collatedFiles[index][0].creationTime!,
),
);
try {
return getMonthAndYear(
DateTime.fromMicrosecondsSinceEpoch(
_collatedFiles[index][0].creationTime!,
),
);
} catch (e) {
_logger.severe("label text builder failed", e);
return "";
}
},
thumbBackgroundColor:
Theme.of(context).colorScheme.galleryThumbBackgroundColor,

View file

@ -16,7 +16,7 @@ import 'package:photos/models/selected_files.dart';
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/common/dialogs.dart';
import 'package:photos/ui/actions/collection/collection_sharing_actions.dart';
import 'package:photos/ui/common/rename_dialog.dart';
import 'package:photos/ui/components/button_widget.dart';
import 'package:photos/ui/components/dialog_widget.dart';
@ -55,6 +55,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
late StreamSubscription _userAuthEventSubscription;
late Function() _selectedFilesListener;
String? _appBarTitle;
late CollectionActions collectionActions;
final GlobalKey shareButtonKey = GlobalKey();
@override
@ -62,6 +63,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
_selectedFilesListener = () {
setState(() {});
};
collectionActions = CollectionActions(CollectionsService.instance);
widget.selectedFiles.addListener(_selectedFilesListener);
_userAuthEventSubscription =
Bus.instance.on<SubscriptionPurchasedEvent>().listen((event) {
@ -369,35 +371,32 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
)
?.thumbnail ==
null;
if (!isEmptyCollection) {
final result = await showChoiceDialog(
if (isEmptyCollection) {
final dialog = createProgressDialog(
context,
"Delete album?",
"Files that are unique to this album "
"will be moved to trash, and this album will be deleted.",
firstAction: "Cancel",
secondAction: "Delete album",
secondActionColor: Colors.red,
"Please wait, deleting album",
);
if (result != DialogUserChoice.secondChoice) {
return;
await dialog.show();
try {
await CollectionsService.instance
.trashEmptyCollection(widget.collection!);
await dialog.hide();
Navigator.of(context).pop();
} catch (e, s) {
_logger.severe("failed to trash collection", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
}
} else {
final bool result = await collectionActions.deleteCollectionSheet(
context,
widget.collection!,
);
if (result == true) {
Navigator.of(context).pop();
} else {
debugPrint("No pop");
}
}
final dialog = createProgressDialog(
context,
"Please wait, deleting album",
);
await dialog.show();
try {
await CollectionsService.instance
.trashCollection(widget.collection!, isEmptyCollection);
showShortToast(context, "Successfully deleted album");
await dialog.hide();
Navigator.of(context).pop();
} catch (e, s) {
_logger.severe("failed to trash collection", e, s);
await dialog.hide();
showGenericErrorDialog(context: context);
}
}

View file

@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:photos/core/event_bus.dart';
import 'package:photos/db/files_db.dart';
import 'package:photos/events/collection_updated_event.dart';
import 'package:photos/events/files_updated_event.dart';
import 'package:photos/models/collection.dart';
import 'package:photos/models/file_load_result.dart';
import 'package:photos/models/gallery_type.dart';
import 'package:photos/models/selected_files.dart';
import 'package:photos/services/ignored_files_service.dart';
import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart';
import 'package:photos/ui/viewer/gallery/gallery.dart';
import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
class UnCategorizedPage extends StatelessWidget {
final String tagPrefix;
final Collection collection;
final GalleryType appBarType;
final GalleryType overlayType;
final _selectedFiles = SelectedFiles();
UnCategorizedPage(
this.collection, {
this.tagPrefix = "Uncategorized_page",
this.appBarType = GalleryType.uncategorized,
this.overlayType = GalleryType.uncategorized,
Key? key,
}) : super(key: key);
@override
Widget build(Object context) {
final gallery = Gallery(
asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
final FileLoadResult result =
await FilesDB.instance.getFilesInCollection(
collection.id,
creationStartTime,
creationEndTime,
limit: limit,
asc: asc,
);
// hide ignored files from home page UI
final ignoredIDs = await IgnoredFilesService.instance.ignoredIDs;
result.files.removeWhere(
(f) =>
f.uploadedFileID == null &&
IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, f),
);
return result;
},
reloadEvent: Bus.instance
.on<CollectionUpdatedEvent>()
.where((event) => event.collectionID == collection.id),
removalEventTypes: const {
EventType.deletedFromRemote,
EventType.deletedFromEverywhere,
EventType.hide,
},
tagPrefix: tagPrefix,
selectedFiles: _selectedFiles,
initialFiles: null,
albumName: "Uncategorized",
);
return Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(50.0),
child: GalleryAppBarWidget(
appBarType,
"Uncategorized",
_selectedFiles,
),
),
body: Stack(
alignment: Alignment.bottomCenter,
children: [
gallery,
FileSelectionOverlayBar(
overlayType,
_selectedFiles,
),
],
),
);
}
}

View file

@ -176,6 +176,10 @@ class FileUploader {
_totalCountInUploadSession = 0;
}
void clearCachedUploadURLs() {
_uploadURLs.clear();
}
void removeFromQueueWhere(final bool Function(File) fn, final Error reason) {
final List<String> uploadsToBeRemoved = [];
_queue.entries

16
lib/utils/image_util.dart Normal file
View file

@ -0,0 +1,16 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
Future<ImageInfo> getImageInfo(ImageProvider imageProvider) {
final completer = Completer<ImageInfo>();
final imageStream = imageProvider.resolve(const ImageConfiguration());
final listener = ImageStreamListener(
((imageInfo, _) {
completer.complete(imageInfo);
}),
);
imageStream.addListener(listener);
completer.future.whenComplete(() => imageStream.removeListener(listener));
return completer.future;
}