Merge pull request #807 from ente-io/uncategorized
This commit is contained in:
commit
98c26bd3ac
26 changed files with 738 additions and 145 deletions
|
@ -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,
|
||||
|
|
|
@ -210,6 +210,11 @@ enum CollectionType {
|
|||
unknown,
|
||||
}
|
||||
|
||||
extension CollectionTypeExtn on CollectionType {
|
||||
bool get canDelete =>
|
||||
this != CollectionType.favorites && this != CollectionType.uncategorized;
|
||||
}
|
||||
|
||||
enum CollectionParticipantRole {
|
||||
unknown,
|
||||
viewer,
|
||||
|
|
36
lib/models/files_split.dart
Normal file
36
lib/models/files_split.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
)) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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"),
|
||||
|
|
112
lib/ui/collections/uncat_collections_button_widget.dart
Normal file
112
lib/ui/collections/uncat_collections_button_widget.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 " +
|
||||
|
|
|
@ -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++;
|
||||
}
|
||||
|
|
|
@ -5,9 +5,10 @@ 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';
|
||||
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';
|
||||
|
@ -47,7 +48,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
|
||||
|
@ -58,7 +59,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();
|
||||
|
@ -74,7 +75,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(() => {});
|
||||
}
|
||||
|
@ -276,7 +277,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,
|
||||
|
|
|
@ -312,7 +312,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;
|
||||
|
@ -324,7 +324,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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
85
lib/ui/viewer/gallery/uncategorized_page.dart
Normal file
85
lib/ui/viewer/gallery/uncategorized_page.dart
Normal 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,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue