소스 검색

resolved merge conflicts

ashilkn 2 년 전
부모
커밋
7b69dbb61f
35개의 변경된 파일974개의 추가작업 그리고 149개의 파일을 삭제
  1. 1 0
      lib/core/constants.dart
  2. 65 2
      lib/db/files_db.dart
  3. 2 0
      lib/events/files_updated_event.dart
  4. 57 0
      lib/models/api/collection/create_request.dart
  5. 11 0
      lib/models/collection.dart
  6. 8 12
      lib/models/file.dart
  7. 1 0
      lib/models/gallery_type.dart
  8. 21 1
      lib/models/magic_metadata.dart
  9. 39 6
      lib/services/collections_service.dart
  10. 2 4
      lib/services/feature_flag_service.dart
  11. 139 0
      lib/services/hidden_service.dart
  12. 3 3
      lib/services/memories_service.dart
  13. 11 16
      lib/services/search_service.dart
  14. 104 0
      lib/ui/collections/archived_collections_button_widget.dart
  15. 29 43
      lib/ui/collections/hidden_collections_button_widget.dart
  16. 5 2
      lib/ui/collections_gallery_widget.dart
  17. 11 3
      lib/ui/create_collection_page.dart
  18. 5 4
      lib/ui/home/home_gallery_widget.dart
  19. 2 2
      lib/ui/home_widget.dart
  20. 9 2
      lib/ui/tools/editor/image_editor_page.dart
  21. 11 5
      lib/ui/viewer/file/collections_list_of_file_widget.dart
  22. 92 3
      lib/ui/viewer/file/fading_app_bar.dart
  23. 13 6
      lib/ui/viewer/file/fading_bottom_bar.dart
  24. 1 1
      lib/ui/viewer/gallery/archive_page.dart
  25. 7 0
      lib/ui/viewer/gallery/collection_page.dart
  26. 1 0
      lib/ui/viewer/gallery/device_folder_page.dart
  27. 100 0
      lib/ui/viewer/gallery/empty_hidden_widget.dart
  28. 1 0
      lib/ui/viewer/gallery/empty_state.dart
  29. 4 2
      lib/ui/viewer/gallery/gallery.dart
  30. 3 3
      lib/ui/viewer/gallery/gallery_app_bar_widget.dart
  31. 98 22
      lib/ui/viewer/gallery/gallery_overlay_widget.dart
  32. 87 0
      lib/ui/viewer/gallery/hidden_page.dart
  33. 20 0
      lib/utils/date_time_util.dart
  34. 9 6
      lib/utils/magic_util.dart
  35. 2 1
      lib/utils/share_util.dart

+ 1 - 0
lib/core/constants.dart

@@ -11,6 +11,7 @@ const String sentryTunnel = "https://sentry-reporter.ente.io";
 const String roadmapURL = "https://roadmap.ente.io";
 const int microSecondsInDay = 86400000000;
 const int android11SDKINT = 30;
+const int jan011991Time = 31580904000000;
 const int galleryLoadStartTime = -8000000000000000; // Wednesday, March 6, 1748
 const int galleryLoadEndTime = 9223372036854775807; // 2^63 -1
 

+ 65 - 2
lib/db/files_db.dart

@@ -627,6 +627,43 @@ class FilesDB {
     return FileLoadResult(files, files.length == limit);
   }
 
+  Future<FileLoadResult> getFilesInCollections(
+    List<int> collectionIDs,
+    int startTime,
+    int endTime,
+    int userID, {
+    int limit,
+    bool asc,
+  }) async {
+    if (collectionIDs.isEmpty) {
+      return FileLoadResult(<File>[], false);
+    }
+    String inParam = "";
+    for (final id in collectionIDs) {
+      inParam += "'" + id.toString() + "',";
+    }
+    inParam = inParam.substring(0, inParam.length - 1);
+    final db = await instance.database;
+    final order = (asc ?? false ? 'ASC' : 'DESC');
+    String whereClause =
+        '$columnCollectionID  IN ($inParam) AND $columnCreationTime >= ? AND '
+        '$columnCreationTime <= ? AND $columnOwnerID = ?';
+    final List<Object> whereArgs = [startTime, endTime, userID];
+
+    final results = await db.query(
+      filesTable,
+      where: whereClause,
+      whereArgs: whereArgs,
+      orderBy:
+          '$columnCreationTime ' + order + ', $columnModificationTime ' + order,
+      limit: limit,
+    );
+    final files = convertToFiles(results);
+    final dedupeResult = _deduplicatedAndFilterIgnoredFiles(files, {});
+    _logger.info("Fetched " + dedupeResult.length.toString() + " files");
+    return FileLoadResult(files, files.length == limit);
+  }
+
   Future<List<File>> getFilesCreatedWithinDurations(
     List<List<int>> durations,
     Set<int> ignoredCollectionIDs, {
@@ -1223,6 +1260,32 @@ class FilesDB {
     return result;
   }
 
+  Future<Map<int, List<File>>> getAllFilesGroupByCollectionID(
+      List<int> ids) async {
+    final result = <int, List<File>>{};
+    if (ids.isEmpty) {
+      return result;
+    }
+    String inParam = "";
+    for (final id in ids) {
+      inParam += "'" + id.toString() + "',";
+    }
+    inParam = inParam.substring(0, inParam.length - 1);
+    final db = await instance.database;
+    final results = await db.query(
+      filesTable,
+      where: '$columnUploadedFileID IN ($inParam)',
+    );
+    final files = convertToFiles(results);
+    for (File eachFile in files) {
+      if (!result.containsKey(eachFile.collectionID)) {
+        result[eachFile.collectionID] = <File>[];
+      }
+      result[eachFile.collectionID].add(eachFile);
+    }
+    return result;
+  }
+
   Future<Set<int>> getAllCollectionIDsOfFile(
     int uploadedFileID,
   ) async {
@@ -1249,12 +1312,12 @@ class FilesDB {
     return files;
   }
 
-  Future<List<File>> getAllFilesFromDB() async {
+  Future<List<File>> getAllFilesFromDB(Set<int> collectionsToIgnore) async {
     final db = await instance.database;
     final List<Map<String, dynamic>> result = await db.query(filesTable);
     final List<File> files = convertToFiles(result);
     final List<File> deduplicatedFiles =
-        _deduplicatedAndFilterIgnoredFiles(files, null);
+        _deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
     return deduplicatedFiles;
   }
 

+ 2 - 0
lib/events/files_updated_event.dart

@@ -20,4 +20,6 @@ enum EventType {
   deletedFromEverywhere,
   archived,
   unarchived,
+  hide,
+  unhide,
 }

+ 57 - 0
lib/models/api/collection/create_request.dart

@@ -0,0 +1,57 @@
+import 'package:photos/models/collection.dart';
+import 'package:photos/services/file_magic_service.dart';
+
+class CreateRequest {
+  String encryptedKey;
+  String keyDecryptionNonce;
+  String encryptedName;
+  String nameDecryptionNonce;
+  String type;
+  CollectionAttributes? attributes;
+  MetadataRequest? magicMetadata;
+
+  CreateRequest({
+    required this.encryptedKey,
+    required this.keyDecryptionNonce,
+    required this.encryptedName,
+    required this.nameDecryptionNonce,
+    required this.type,
+    this.attributes,
+    this.magicMetadata,
+  });
+
+  CreateRequest copyWith({
+    String? encryptedKey,
+    String? keyDecryptionNonce,
+    String? encryptedName,
+    String? nameDecryptionNonce,
+    String? type,
+    CollectionAttributes? attributes,
+    MetadataRequest? magicMetadata,
+  }) =>
+      CreateRequest(
+        encryptedKey: encryptedKey ?? this.encryptedKey,
+        keyDecryptionNonce: keyDecryptionNonce ?? this.keyDecryptionNonce,
+        encryptedName: encryptedName ?? this.encryptedName,
+        nameDecryptionNonce: nameDecryptionNonce ?? this.nameDecryptionNonce,
+        type: type ?? this.type,
+        attributes: attributes ?? this.attributes,
+        magicMetadata: magicMetadata ?? this.magicMetadata,
+      );
+
+  Map<String, dynamic> toJson() {
+    final map = <String, dynamic>{};
+    map['encryptedKey'] = encryptedKey;
+    map['keyDecryptionNonce'] = keyDecryptionNonce;
+    map['encryptedName'] = encryptedName;
+    map['nameDecryptionNonce'] = nameDecryptionNonce;
+    map['type'] = type;
+    if (attributes != null) {
+      map['attributes'] = attributes!.toMap();
+    }
+    if (magicMetadata != null) {
+      map['magicMetadata'] = magicMetadata!.toJson();
+    }
+    return map;
+  }
+}

+ 11 - 0
lib/models/collection.dart

@@ -46,6 +46,17 @@ class Collection {
     return mMdVersion > 0 && magicMetadata.visibility == visibilityArchive;
   }
 
+  bool isHidden() {
+    if (isDefaultHidden()) {
+      return true;
+    }
+    return mMdVersion > 0 && (magicMetadata.visibility == visibilityHidden);
+  }
+
+  bool isDefaultHidden() {
+    return (magicMetadata.subType ?? 0) == subTypeDefaultHidden;
+  }
+
   static CollectionType typeFromString(String type) {
     switch (type) {
       case "folder":

+ 8 - 12
lib/models/file.dart

@@ -9,6 +9,7 @@ import 'package:photos/models/location.dart';
 import 'package:photos/models/magic_metadata.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/services/feature_flag_service.dart';
+import 'package:photos/utils/date_time_util.dart';
 // ignore: import_of_legacy_library_into_null_safe
 import 'package:photos/utils/exif_util.dart';
 // ignore: import_of_legacy_library_into_null_safe
@@ -74,16 +75,13 @@ class File extends EnteFile {
     file.location = Location(asset.latitude, asset.longitude);
     file.fileType = _fileTypeFromAsset(asset);
     file.creationTime = asset.createDateTime.microsecondsSinceEpoch;
-    if (file.creationTime == 0) {
+    if (file.creationTime == null || (file.creationTime! <= jan011991Time)) {
       try {
-        final parsedDateTime = DateTime.parse(
-          basenameWithoutExtension(file.title!)
-              .replaceAll("IMG_", "")
-              .replaceAll("VID_", "")
-              .replaceAll("DCIM_", "")
-              .replaceAll("_", " "),
-        );
-        file.creationTime = parsedDateTime.microsecondsSinceEpoch;
+        final parsedDateTime =
+            parseDateFromFileName(basenameWithoutExtension(file.title ?? ""));
+
+        file.creationTime = parsedDateTime?.microsecondsSinceEpoch ??
+            asset.modifiedDateTime.microsecondsSinceEpoch;
       } catch (e) {
         file.creationTime = asset.modifiedDateTime.microsecondsSinceEpoch;
       }
@@ -101,9 +99,7 @@ class File extends EnteFile {
         type = FileType.image;
         // PHAssetMediaSubtype.photoLive.rawValue is 8
         // This hack should go away once photos_manager support livePhotos
-        if (asset.subtype != null &&
-            asset.subtype > -1 &&
-            (asset.subtype & 8) != 0) {
+        if (asset.subtype > -1 && (asset.subtype & 8) != 0) {
           type = FileType.livePhoto;
         }
         break;

+ 1 - 0
lib/models/gallery_type.dart

@@ -1,6 +1,7 @@
 enum GalleryType {
   homepage,
   archive,
+  hidden,
   trash,
   localFolder,
   // indicator for gallery view of collections shared with the user

+ 21 - 1
lib/models/magic_metadata.dart

@@ -1,9 +1,16 @@
 import 'dart:convert';
 
+// Visibility Constants
 const visibilityVisible = 0;
 const visibilityArchive = 1;
+const visibilityHidden = 2;
+
+// Collection SubType Constants
+const subTypeDefaultHidden = 1;
 
 const magicKeyVisibility = 'visibility';
+// key for collection subType
+const subTypeKey = 'subType';
 
 const pubMagicKeyEditedTime = 'editedTime';
 const pubMagicKeyEditedName = 'editedName';
@@ -59,7 +66,19 @@ class CollectionMagicMetadata {
   // 2 -> hidden etc?
   int visibility;
 
-  CollectionMagicMetadata({required this.visibility});
+  // null/0 value -> no subType
+  // 1 -> DEFAULT_HIDDEN COLLECTION for files hidden individually
+  int? subType;
+
+  CollectionMagicMetadata({required this.visibility, this.subType});
+
+  Map<String, dynamic> toJson() {
+    final result = {magicKeyVisibility: visibility};
+    if (subType != null) {
+      result[subTypeKey] = subType!;
+    }
+    return result;
+  }
 
   factory CollectionMagicMetadata.fromEncodedJson(String encodedJson) =>
       CollectionMagicMetadata.fromJson(jsonDecode(encodedJson));
@@ -71,6 +90,7 @@ class CollectionMagicMetadata {
     if (map == null) return null;
     return CollectionMagicMetadata(
       visibility: map[magicKeyVisibility] ?? visibilityVisible,
+      subType: map[subTypeKey],
     );
   }
 }

+ 39 - 6
lib/services/collections_service.dart

@@ -21,6 +21,7 @@ import 'package:photos/events/collection_updated_event.dart';
 import 'package:photos/events/files_updated_event.dart';
 import 'package:photos/events/force_reload_home_gallery_event.dart';
 import 'package:photos/events/local_photos_updated_event.dart';
+import 'package:photos/models/api/collection/create_request.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_file_item.dart';
 import 'package:photos/models/file.dart';
@@ -50,6 +51,7 @@ class CollectionsService {
   final _localPathToCollectionID = <String, int>{};
   final _collectionIDToCollections = <int, Collection>{};
   final _cachedKeys = <int, Uint8List>{};
+  Collection cachedDefaultHiddenCollection;
 
   CollectionsService._privateConstructor() {
     _db = CollectionsDB.instance;
@@ -77,6 +79,15 @@ class CollectionsService {
     });
   }
 
+  Configuration get config => _config;
+
+  Map<int, Collection> get collectionIDToCollections =>
+      _collectionIDToCollections;
+
+  FilesDB get filesDB => _filesDB;
+
+  // sync method fetches just sync the collections, not the individual files
+  // within the collection.
   Future<List<Collection>> sync() async {
     _logger.info("Syncing collections");
     final lastCollectionUpdationTime =
@@ -144,6 +155,22 @@ class CollectionsService {
         .toSet();
   }
 
+  Set<int> getHiddenCollections() {
+    return _collectionIDToCollections.values
+        .toList()
+        .where((element) => element.isHidden())
+        .map((e) => e.id)
+        .toSet();
+  }
+
+  Set<int> collectionsHiddenFromTimeline() {
+    return _collectionIDToCollections.values
+        .toList()
+        .where((element) => element.isHidden() || element.isArchived())
+        .map((e) => e.id)
+        .toSet();
+  }
+
   int getCollectionSyncTime(int collectionID) {
     return _prefs
             .getInt(_collectionSyncTimeKeyPrefix + collectionID.toString()) ??
@@ -276,6 +303,7 @@ class CollectionsService {
   }
 
   Uint8List _getDecryptedKey(Collection collection) {
+    debugPrint("Finding collection decryption key for ${collection.id}");
     final encryptedKey = Sodium.base642bin(collection.encryptedKey);
     if (collection.owner.id == _config.getUserID()) {
       if (_config.getKey() == null) {
@@ -798,17 +826,17 @@ class CollectionsService {
     List<File> files,
   ) {
     if (toCollectionID == fromCollectionID) {
-      throw AssertionError("can't move to same album");
+      throw AssertionError("Can't move to same album");
     }
     for (final file in files) {
       if (file.uploadedFileID == null) {
-        throw AssertionError("can only move uploaded memories");
+        throw AssertionError("Can only move uploaded memories");
       }
       if (file.collectionID != fromCollectionID) {
-        throw AssertionError("all memories should belong to the same album");
+        throw AssertionError("All memories should belong to the same album");
       }
       if (file.ownerID != Configuration.instance.getUserID()) {
-        throw AssertionError("can only move memories uploaded by you");
+        throw AssertionError("Can only move memories uploaded by you");
       }
     }
   }
@@ -832,11 +860,16 @@ class CollectionsService {
     RemoteSyncService.instance.sync(silently: true);
   }
 
-  Future<Collection> createAndCacheCollection(Collection collection) async {
+  Future<Collection> createAndCacheCollection(
+    Collection collection, {
+    CreateRequest createRequest,
+  }) async {
+    final dynamic payload =
+        createRequest != null ? createRequest.toJson() : collection.toMap();
     return _enteDio
         .post(
       "/collections",
-      data: collection.toMap(),
+      data: payload,
     )
         .then((response) {
       final collection = Collection.fromMap(response.data["collection"]);

+ 2 - 4
lib/services/feature_flag_service.dart

@@ -73,10 +73,8 @@ class FeatureFlagService {
           .getDio()
           .get("https://static.ente.io/feature_flags.json");
       final flagsResponse = FeatureFlags.fromMap(response.data);
-      if (flagsResponse != null) {
-        _prefs.setString(_featureFlagsKey, flagsResponse.toJson());
-        _featureFlags = flagsResponse;
-      }
+      _prefs.setString(_featureFlagsKey, flagsResponse.toJson());
+      _featureFlags = flagsResponse;
     } catch (e) {
       _logger.severe("Failed to sync feature flags ", e);
     }

+ 139 - 0
lib/services/hidden_service.dart

@@ -0,0 +1,139 @@
+import 'dart:convert';
+import 'dart:typed_data';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_sodium/flutter_sodium.dart';
+import 'package:logging/logging.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/events/force_reload_home_gallery_event.dart';
+import 'package:photos/events/local_photos_updated_event.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/magic_metadata.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/file_magic_service.dart';
+import 'package:photos/utils/crypto_util.dart';
+import 'package:photos/utils/dialog_util.dart';
+
+extension HiddenService on CollectionsService {
+  static final _logger = Logger("HiddenCollectionService");
+
+  // getDefaultHiddenCollection will return null if there's no default
+  // collection
+  Future<Collection> getDefaultHiddenCollection() async {
+    if (cachedDefaultHiddenCollection != null) {
+      return cachedDefaultHiddenCollection;
+    }
+    final int userID = config.getUserID()!;
+    final Collection? defaultHidden =
+        collectionIDToCollections.values.firstWhereOrNull(
+      (element) => element.isDefaultHidden() && element.owner!.id == userID,
+    );
+    if (defaultHidden != null) {
+      cachedDefaultHiddenCollection = defaultHidden;
+      return cachedDefaultHiddenCollection;
+    }
+    final Collection createdHiddenCollection =
+        await _createDefaultHiddenAlbum();
+    cachedDefaultHiddenCollection = createdHiddenCollection;
+    return cachedDefaultHiddenCollection;
+  }
+
+  Future<bool> hideFiles(
+    BuildContext context,
+    List<File> filesToHide, {
+    bool forceHide = false,
+  }) async {
+    final int userID = config.getUserID()!;
+    final List<int> uploadedIDs = <int>[];
+    final dialog = createProgressDialog(
+      context,
+      "Hiding...",
+    );
+    await dialog.show();
+    try {
+      for (File file in filesToHide) {
+        if (file.uploadedFileID == null) {
+          throw AssertionError("Can only hide uploaded files");
+        }
+        if (file.ownerID != userID) {
+          throw AssertionError("Can only hide files owned by user");
+        }
+        uploadedIDs.add(file.uploadedFileID!);
+      }
+
+      final defaultHiddenCollection = await getDefaultHiddenCollection();
+      final Map<int, List<File>> collectionToFilesMap =
+          await filesDB.getAllFilesGroupByCollectionID(uploadedIDs);
+      for (MapEntry<int, List<File>> entry in collectionToFilesMap.entries) {
+        if (entry.key == defaultHiddenCollection.id) {
+          _logger.finest('file already part of hidden collection');
+          continue;
+        }
+        await move(defaultHiddenCollection.id, entry.key, entry.value);
+      }
+      Bus.instance.fire(ForceReloadHomeGalleryEvent());
+      Bus.instance.fire(
+        LocalPhotosUpdatedEvent(filesToHide, type: EventType.unarchived),
+      );
+
+      await dialog.hide();
+    } on AssertionError catch (e) {
+      await dialog.hide();
+      showErrorDialog(context, "Oops", e.message as String);
+    } catch (e, s) {
+      _logger.severe("Could not hide", e, s);
+      await dialog.hide();
+      showGenericErrorDialog(context);
+      return false;
+    } finally {
+      await dialog.hide();
+    }
+    return true;
+  }
+
+  Future<Collection> _createDefaultHiddenAlbum() async {
+    final key = CryptoUtil.generateKey();
+    final encryptedKeyData = CryptoUtil.encryptSync(key, config.getKey()!);
+    final encryptedName = CryptoUtil.encryptSync(
+      utf8.encode(".Hidden") as Uint8List,
+      key,
+    );
+    final jsonToUpdate = CollectionMagicMetadata(
+      visibility: visibilityHidden,
+      subType: subTypeDefaultHidden,
+    ).toJson();
+    assert(jsonToUpdate.length == 2, "metadata should have two keys");
+    final encryptedMMd = await CryptoUtil.encryptChaCha(
+      utf8.encode(jsonEncode(jsonToUpdate)) as Uint8List,
+      key,
+    );
+    final MetadataRequest metadataRequest = MetadataRequest(
+      version: 1,
+      count: jsonToUpdate.length,
+      data: Sodium.bin2base64(encryptedMMd.encryptedData!),
+      header: Sodium.bin2base64(encryptedMMd.header!),
+    );
+    final CreateRequest createRequest = CreateRequest(
+      encryptedKey: Sodium.bin2base64(encryptedKeyData.encryptedData!),
+      keyDecryptionNonce: Sodium.bin2base64(encryptedKeyData.nonce!),
+      encryptedName: Sodium.bin2base64(encryptedName.encryptedData!),
+      nameDecryptionNonce: Sodium.bin2base64(encryptedName.nonce!),
+      type: CollectionType.album.toString(),
+      attributes: CollectionAttributes(),
+      magicMetadata: metadataRequest,
+    );
+
+    _logger.info("Creating Hidden Collection");
+    final collection =
+        await createAndCacheCollection(null, createRequest: createRequest);
+    _logger.info("Creating Hidden Collection Created Successfully");
+    final Collection collectionFromServer =
+        await fetchCollectionByID(collection.id);
+    _logger.info("Fetched Created Hidden Collection Successfully");
+    return collectionFromServer;
+  }
+}

+ 3 - 3
lib/services/memories_service.dart

@@ -74,11 +74,11 @@ class MemoriesService extends ChangeNotifier {
           date.add(const Duration(days: daysAfter)).microsecondsSinceEpoch;
       durations.add([startCreationTime, endCreationTime]);
     }
-    final archivedCollectionIds =
-        CollectionsService.instance.getArchivedCollections();
+    final ignoredCollections =
+        CollectionsService.instance.collectionsHiddenFromTimeline();
     final files = await _filesDB.getFilesCreatedWithinDurations(
       durations,
-      archivedCollectionIds,
+      ignoredCollections,
     );
     final seenTimes = await _memoriesDB.getSeenTimes();
     final List<Memory> memories = [];

+ 11 - 16
lib/services/search_service.dart

@@ -33,28 +33,23 @@ class SearchService {
   static final SearchService instance = SearchService._privateConstructor();
 
   Future<void> init() async {
-    // Intention of delay is to give more CPU cycles to other tasks
-    // 8 is just a magic number
-    Future.delayed(const Duration(seconds: 8), () async {
-      /* In case home screen loads before 8 seconds and user starts search,
-       future will not be null.So here getAllFiles won't run again in that case. */
-      if (_cachedFilesFuture == null) {
-        _getAllFiles();
-      }
-    });
-
     Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
       // only invalidate, let the load happen on demand
       _cachedFilesFuture = null;
     });
   }
 
+  Set<int> ignoreCollections() {
+    return CollectionsService.instance.getHiddenCollections();
+  }
+
   Future<List<File>> _getAllFiles() async {
     if (_cachedFilesFuture != null) {
       return _cachedFilesFuture;
     }
     _logger.fine("Reading all files from db");
-    _cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
+    _cachedFilesFuture =
+        FilesDB.instance.getAllFilesFromDB(ignoreCollections());
     return _cachedFilesFuture;
   }
 
@@ -136,7 +131,7 @@ class SearchService {
       }
       final Collection collection =
           CollectionsService.instance.getCollectionByID(file.collectionID);
-      if (!collection.isArchived() &&
+      if (!collection.isHidden() &&
           collection.name.toLowerCase().contains(query.toLowerCase())) {
         collectionSearchResults
             .add(AlbumSearchResult(CollectionWithThumbnail(collection, file)));
@@ -177,7 +172,7 @@ class SearchService {
         final matchedFiles =
             await FilesDB.instance.getFilesCreatedWithinDurations(
           _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month),
-          null,
+          ignoreCollections(),
           order: 'DESC',
         );
         if (matchedFiles.isNotEmpty) {
@@ -253,7 +248,7 @@ class SearchService {
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsOfMonthInEveryYear(month.monthNumber),
-        null,
+        ignoreCollections(),
         order: 'DESC',
       );
       if (matchedFiles.isNotEmpty) {
@@ -282,7 +277,7 @@ class SearchService {
       final matchedFiles =
           await FilesDB.instance.getFilesCreatedWithinDurations(
         _getDurationsForCalendarDateInEveryYear(day, month, year: year),
-        null,
+        ignoreCollections(),
         order: 'DESC',
       );
       if (matchedFiles.isNotEmpty) {
@@ -310,7 +305,7 @@ class SearchService {
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
     return await FilesDB.instance.getFilesCreatedWithinDurations(
       [durationOfYear],
-      null,
+      ignoreCollections(),
       order: "DESC",
     );
   }

+ 104 - 0
lib/ui/collections/archived_collections_button_widget.dart

@@ -0,0 +1,104 @@
+// @dart=2.9
+
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/models/magic_metadata.dart';
+import 'package:photos/ui/viewer/gallery/archive_page.dart';
+import 'package:photos/utils/navigation_util.dart';
+
+class ArchivedCollectionsButtonWidget extends StatelessWidget {
+  final TextStyle textStyle;
+
+  const ArchivedCollectionsButtonWidget(
+    this.textStyle, {
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    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.archive_outlined,
+                    color: Theme.of(context).iconTheme.color,
+                  ),
+                  const Padding(padding: EdgeInsets.all(6)),
+                  FutureBuilder<int>(
+                    future: FilesDB.instance.fileCountWithVisibility(
+                      visibilityArchive,
+                      Configuration.instance.getUserID(),
+                    ),
+                    builder: (context, snapshot) {
+                      if (snapshot.hasData && snapshot.data > 0) {
+                        return RichText(
+                          text: TextSpan(
+                            style: textStyle,
+                            children: [
+                              TextSpan(
+                                text: "Archive",
+                                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: "Archive",
+                                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 {
+        routeToPage(
+          context,
+          ArchivePage(),
+        );
+      },
+    );
+  }
+}

+ 29 - 43
lib/ui/collections/hidden_collections_button_widget.dart

@@ -1,10 +1,8 @@
 // @dart=2.9
 
 import 'package:flutter/material.dart';
-import 'package:photos/core/configuration.dart';
-import 'package:photos/db/files_db.dart';
-import 'package:photos/models/magic_metadata.dart';
-import 'package:photos/ui/viewer/gallery/archive_page.dart';
+import 'package:photos/services/local_authentication_service.dart';
+import 'package:photos/ui/viewer/gallery/hidden_page.dart';
 import 'package:photos/utils/navigation_util.dart';
 
 class HiddenCollectionsButtonWidget extends StatelessWidget {
@@ -44,44 +42,25 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
                     color: Theme.of(context).iconTheme.color,
                   ),
                   const Padding(padding: EdgeInsets.all(6)),
-                  FutureBuilder<int>(
-                    future: FilesDB.instance.fileCountWithVisibility(
-                      visibilityArchive,
-                      Configuration.instance.getUserID(),
-                    ),
-                    builder: (context, snapshot) {
-                      if (snapshot.hasData && snapshot.data > 0) {
-                        return RichText(
-                          text: TextSpan(
-                            style: textStyle,
-                            children: [
-                              TextSpan(
-                                text: "Hidden",
-                                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: "Hidden",
-                                style: Theme.of(context).textTheme.subtitle1,
-                              ),
-                              //need to query in db and bring this value
-                            ],
+                  RichText(
+                    text: TextSpan(
+                      style: textStyle,
+                      children: [
+                        TextSpan(
+                          text: "Hidden",
+                          style: Theme.of(context).textTheme.subtitle1,
+                        ),
+                        const TextSpan(text: "  \u2022  "),
+                        WidgetSpan(
+                          child: Icon(
+                            Icons.lock_outline,
+                            size: 16,
+                            color: Theme.of(context).iconTheme.color,
                           ),
-                        );
-                      }
-                    },
+                        ),
+                        //need to query in db and bring this value
+                      ],
+                    ),
                   ),
                 ],
               ),
@@ -94,10 +73,17 @@ class HiddenCollectionsButtonWidget extends StatelessWidget {
         ),
       ),
       onPressed: () async {
-        routeToPage(
+        final hasAuthenticated = await LocalAuthenticationService.instance
+            .requestLocalAuthentication(
           context,
-          ArchivePage(),
+          "Please authenticate to view your hidden files",
         );
+        if (hasAuthenticated) {
+          routeToPage(
+            context,
+            HiddenPage(),
+          );
+        }
       },
     );
   }

+ 5 - 2
lib/ui/collections_gallery_widget.dart

@@ -13,6 +13,7 @@ import 'package:photos/events/user_logged_out_event.dart';
 import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/collections/archived_collections_button_widget.dart';
 import 'package:photos/ui/collections/device_folders_grid_view_widget.dart';
 import 'package:photos/ui/collections/hidden_collections_button_widget.dart';
 import 'package:photos/ui/collections/remote_collections_grid_view_widget.dart';
@@ -85,7 +86,7 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
         await collectionsService.getLatestCollectionFiles();
     for (final file in latestCollectionFiles) {
       final c = collectionsService.getCollectionByID(file.collectionID);
-      if (c.owner.id == userID) {
+      if (c.owner.id == userID && !c.isHidden()) {
         collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
       }
     }
@@ -155,9 +156,11 @@ class _CollectionsGalleryWidgetState extends State<CollectionsGalleryWidget>
               padding: const EdgeInsets.symmetric(horizontal: 16),
               child: Column(
                 children: [
-                  TrashButtonWidget(trashAndHiddenTextStyle),
+                  ArchivedCollectionsButtonWidget(trashAndHiddenTextStyle),
                   const SizedBox(height: 12),
                   HiddenCollectionsButtonWidget(trashAndHiddenTextStyle),
+                  const SizedBox(height: 12),
+                  TrashButtonWidget(trashAndHiddenTextStyle),
                 ],
               ),
             ),

+ 11 - 3
lib/ui/create_collection_page.dart

@@ -23,7 +23,7 @@ import 'package:photos/utils/share_util.dart';
 import 'package:photos/utils/toast_util.dart';
 import 'package:receive_sharing_intent/receive_sharing_intent.dart';
 
-enum CollectionActionType { addFiles, moveFiles, restoreFiles }
+enum CollectionActionType { addFiles, moveFiles, restoreFiles, unHide }
 
 String _actionName(CollectionActionType type, bool plural) {
   final titleSuffix = (plural ? "s" : "");
@@ -38,6 +38,9 @@ String _actionName(CollectionActionType type, bool plural) {
     case CollectionActionType.restoreFiles:
       text = "Restore file";
       break;
+    case CollectionActionType.unHide:
+      text = "Unhide file";
+      break;
   }
   return text + titleSuffix;
 }
@@ -193,7 +196,7 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
     for (final file in latestCollectionFiles) {
       final c =
           CollectionsService.instance.getCollectionByID(file.collectionID);
-      if (c.owner.id == Configuration.instance.getUserID()) {
+      if (c.owner.id == Configuration.instance.getUserID() && !c.isHidden()) {
         collectionsWithThumbnail.add(CollectionWithThumbnail(c, file));
       }
     }
@@ -279,6 +282,8 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
         return _addToCollection(collectionID);
       case CollectionActionType.moveFiles:
         return _moveFilesToCollection(collectionID);
+      case CollectionActionType.unHide:
+        return _moveFilesToCollection(collectionID);
       case CollectionActionType.restoreFiles:
         return _restoreFilesToCollection(collectionID);
     }
@@ -286,7 +291,10 @@ class _CreateCollectionPageState extends State<CreateCollectionPage> {
   }
 
   Future<bool> _moveFilesToCollection(int toCollectionID) async {
-    final dialog = createProgressDialog(context, "Moving files to album...");
+    final String message = widget.actionType == CollectionActionType.moveFiles
+        ? "Moving files to album..."
+        : "Unhiding files to album";
+    final dialog = createProgressDialog(context, message);
     await dialog.show();
     try {
       final int fromCollectionID =

+ 5 - 4
lib/ui/home/home_gallery_widget.dart

@@ -33,8 +33,8 @@ class HomeGalleryWidget extends StatelessWidget {
         final ownerID = Configuration.instance.getUserID();
         final hasSelectedAllForBackup =
             Configuration.instance.hasSelectedAllFoldersForBackup();
-        final archivedCollectionIds =
-            CollectionsService.instance.getArchivedCollections();
+        final collectionsToHide =
+            CollectionsService.instance.collectionsHiddenFromTimeline();
         FileLoadResult result;
         if (hasSelectedAllForBackup) {
           result = await FilesDB.instance.getAllLocalAndUploadedFiles(
@@ -43,7 +43,7 @@ class HomeGalleryWidget extends StatelessWidget {
             ownerID,
             limit: limit,
             asc: asc,
-            ignoredCollectionIDs: archivedCollectionIds,
+            ignoredCollectionIDs: collectionsToHide,
           );
         } else {
           result = await FilesDB.instance.getAllPendingOrUploadedFiles(
@@ -52,7 +52,7 @@ class HomeGalleryWidget extends StatelessWidget {
             ownerID,
             limit: limit,
             asc: asc,
-            ignoredCollectionIDs: archivedCollectionIds,
+            ignoredCollectionIDs: collectionsToHide,
           );
         }
 
@@ -70,6 +70,7 @@ class HomeGalleryWidget extends StatelessWidget {
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
         EventType.archived,
+        EventType.hide,
       },
       forceReloadEvents: [
         Bus.instance.on<BackupFoldersUpdatedEvent>(),

+ 2 - 2
lib/ui/home_widget.dart

@@ -249,8 +249,8 @@ class _HomeWidgetState extends State<HomeWidget> {
       child: WillPopScope(
         child: Scaffold(
           drawerScrimColor: getEnteColorScheme(context).strokeFainter,
-          drawerEnableOpenDragGesture:
-              false, //using a hack instead of enabling this as enabling this will create other problems
+          drawerEnableOpenDragGesture: false,
+          //using a hack instead of enabling this as enabling this will create other problems
           drawer: enableDrawer
               ? ConstrainedBox(
                   constraints: const BoxConstraints(maxWidth: 428),

+ 9 - 2
lib/ui/tools/editor/image_editor_page.dart

@@ -1,6 +1,7 @@
 // @dart=2.9
 
 import 'dart:io';
+import 'dart:math';
 import 'dart:typed_data';
 
 import 'package:extended_image/extended_image.dart';
@@ -370,13 +371,19 @@ class _ImageEditorPageState extends State<ImageEditorPage> {
         existingFiles[0].creationTime,
       ))
           .files;
+      // the index could be -1 if the files fetched doesn't contain the newly
+      // edited files
+      final selectionIndex =
+          files.indexWhere((file) => file.generatedID == newFile.generatedID);
+      if (selectionIndex == -1) {
+        files.add(newFile);
+      }
       replacePage(
         context,
         DetailPage(
           widget.detailPageConfig.copyWith(
             files: files,
-            selectedIndex: files
-                .indexWhere((file) => file.generatedID == newFile.generatedID),
+            selectedIndex: min(selectionIndex, files.length - 1),
           ),
         ),
       );

+ 11 - 5
lib/ui/viewer/file/collections_list_of_file_widget.dart

@@ -2,6 +2,7 @@
 
 import 'package:flutter/material.dart';
 import 'package:logging/logging.dart';
+import 'package:photos/models/collection.dart';
 import 'package:photos/models/collection_items.dart';
 import 'package:photos/services/collections_service.dart';
 import 'package:photos/ui/common/loading_widget.dart';
@@ -11,6 +12,7 @@ import 'package:photos/utils/navigation_util.dart';
 
 class CollectionsListOfFileWidget extends StatelessWidget {
   final Future<Set<int>> allCollectionIDsOfFile;
+
   const CollectionsListOfFileWidget(this.allCollectionIDsOfFile, {Key key})
       : super(key: key);
 
@@ -21,19 +23,23 @@ class CollectionsListOfFileWidget extends StatelessWidget {
       builder: (context, snapshot) {
         if (snapshot.hasData) {
           final Set<int> collectionIDs = snapshot.data;
-          final collections = [];
+          final collections = <Collection>[];
           for (var collectionID in collectionIDs) {
-            collections.add(
-              CollectionsService.instance.getCollectionByID(collectionID),
-            );
+            final c =
+                CollectionsService.instance.getCollectionByID(collectionID);
+            collections.add(c);
           }
           return ListView.builder(
             itemCount: collections.length,
             scrollDirection: Axis.horizontal,
             itemBuilder: (context, index) {
+              final bool isHidden = collections[index].isHidden();
               return FileInfoCollectionWidget(
-                name: collections[index].name,
+                name: isHidden ? 'Hidden' : collections[index].name,
                 onTap: () {
+                  if (isHidden) {
+                    return;
+                  }
                   routeToPage(
                     context,
                     CollectionPage(

+ 92 - 3
lib/ui/viewer/file/fading_app_bar.dart

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
 import 'package:like_button/like_button.dart';
 import 'package:logging/logging.dart';
 import 'package:media_extension/media_extension.dart';
+import 'package:page_transition/page_transition.dart';
 import 'package:path/path.dart' as file_path;
 import 'package:photo_manager/photo_manager.dart';
 import 'package:photos/core/event_bus.dart';
@@ -16,11 +17,15 @@ import 'package:photos/events/local_photos_updated_event.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
 import 'package:photos/models/ignored_file.dart';
+import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_file.dart';
+import 'package:photos/services/collections_service.dart';
 import 'package:photos/services/favorites_service.dart';
+import 'package:photos/services/hidden_service.dart';
 import 'package:photos/services/ignored_files_service.dart';
 import 'package:photos/services/local_sync_service.dart';
 import 'package:photos/ui/common/progress_dialog.dart';
+import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/ui/viewer/file/custom_app_bar.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -99,11 +104,21 @@ class FadingAppBarState extends State<FadingAppBar> {
 
   AppBar _buildAppBar() {
     debugPrint("building app bar");
+
     final List<Widget> actions = [];
     final isTrashedFile = widget.file is TrashFile;
     final shouldShowActions = widget.shouldShowActions && !isTrashedFile;
+    final bool isOwnedByUser =
+        widget.file.ownerID == null || widget.file.ownerID == widget.userID;
+    bool isFileHidden = false;
+    if (isOwnedByUser && widget.file.uploadedFileID != null) {
+      isFileHidden = CollectionsService.instance
+              .getCollectionByID(widget.file.collectionID)
+              ?.isHidden() ??
+          false;
+    }
     // only show fav option for files owned by the user
-    if (widget.file.ownerID == null || widget.file.ownerID == widget.userID) {
+    if (isOwnedByUser && !isFileHidden) {
       actions.add(_getFavoriteButton());
     }
     actions.add(
@@ -132,8 +147,7 @@ class FadingAppBarState extends State<FadingAppBar> {
             );
           }
           // options for files owned by the user
-          if (widget.file.ownerID == null ||
-              widget.file.ownerID == widget.userID) {
+          if (isOwnedByUser) {
             items.add(
               PopupMenuItem(
                 value: 2,
@@ -175,6 +189,45 @@ class FadingAppBarState extends State<FadingAppBar> {
               ),
             );
           }
+          if (isOwnedByUser) {
+            if (!isFileHidden) {
+              items.add(
+                PopupMenuItem(
+                  value: 4,
+                  child: Row(
+                    children: [
+                      Icon(
+                        Icons.visibility_off,
+                        color: Theme.of(context).iconTheme.color,
+                      ),
+                      const Padding(
+                        padding: EdgeInsets.all(8),
+                      ),
+                      const Text("Hide"),
+                    ],
+                  ),
+                ),
+              );
+            } else {
+              items.add(
+                PopupMenuItem(
+                  value: 5,
+                  child: Row(
+                    children: [
+                      Icon(
+                        Icons.visibility,
+                        color: Theme.of(context).iconTheme.color,
+                      ),
+                      const Padding(
+                        padding: EdgeInsets.all(8),
+                      ),
+                      const Text("Unhide"),
+                    ],
+                  ),
+                ),
+              );
+            }
+          }
           return items;
         },
         onSelected: (value) {
@@ -184,6 +237,10 @@ class FadingAppBarState extends State<FadingAppBar> {
             _showDeleteSheet(widget.file);
           } else if (value == 3) {
             _setAs(widget.file);
+          } else if (value == 4) {
+            _handleHideRequest(context);
+          } else if (value == 5) {
+            _handleUnHideRequest(context);
           }
         },
       ),
@@ -197,6 +254,38 @@ class FadingAppBarState extends State<FadingAppBar> {
     );
   }
 
+  Future<void> _handleHideRequest(BuildContext context) async {
+    try {
+      final hideResult =
+          await CollectionsService.instance.hideFiles(context, [widget.file]);
+
+      if (hideResult) {
+        // delay to avoid black screen
+        await Future.delayed(const Duration(milliseconds: 300));
+        Navigator.of(context).pop();
+      }
+    } catch (e, s) {
+      _logger.severe("failed to update file visibility", e, s);
+      await showGenericErrorDialog(context);
+    }
+  }
+
+  Future<void> _handleUnHideRequest(BuildContext context) async {
+    final s = SelectedFiles();
+    s.files.add(widget.file);
+    Navigator.push(
+      context,
+      PageTransition(
+        type: PageTransitionType.bottomToTop,
+        child: CreateCollectionPage(
+          s,
+          null,
+          actionType: CollectionActionType.unHide,
+        ),
+      ),
+    );
+  }
+
   Widget _getFavoriteButton() {
     return FutureBuilder(
       future: FavoritesService.instance.isFavorite(widget.file),

+ 13 - 6
lib/ui/viewer/file/fading_bottom_bar.dart

@@ -12,6 +12,7 @@ import 'package:photos/models/file_type.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/models/trash_file.dart';
+import 'package:photos/services/collections_service.dart';
 import 'package:photos/theme/colors.dart';
 import 'package:photos/theme/ente_theme.dart';
 import 'package:photos/ui/create_collection_page.dart';
@@ -90,6 +91,15 @@ class FadingBottomBarState extends State<FadingBottomBar> {
     if (widget.file is TrashFile) {
       _addTrashOptions(children);
     }
+    bool isUploadedByUser = widget.file.uploadedFileID != null &&
+        widget.file.ownerID == Configuration.instance.getUserID();
+    bool isFileHidden = false;
+    if (isUploadedByUser) {
+      isFileHidden = CollectionsService.instance
+              .getCollectionByID(widget.file.collectionID)
+              ?.isHidden() ??
+          false;
+    }
     if (!widget.showOnlyInfoButton && widget.file is! TrashFile) {
       if (widget.file.fileType == FileType.image ||
           widget.file.fileType == FileType.livePhoto) {
@@ -111,20 +121,17 @@ class FadingBottomBarState extends State<FadingBottomBar> {
           ),
         );
       }
-      if (widget.file.uploadedFileID != null &&
-          widget.file.ownerID == Configuration.instance.getUserID()) {
+      if (isUploadedByUser && !isFileHidden) {
         final bool isArchived =
             widget.file.magicMetadata.visibility == visibilityArchive;
         children.add(
           Tooltip(
-            message: isArchived ? "Unhide" : "Hide",
+            message: isArchived ? "Unarchive" : "Archive",
             child: Padding(
               padding: const EdgeInsets.only(top: 12, bottom: 12),
               child: IconButton(
                 icon: Icon(
-                  isArchived
-                      ? Icons.visibility_outlined
-                      : Icons.visibility_off_outlined,
+                  isArchived ? Icons.unarchive : Icons.archive_outlined,
                   color: Colors.white,
                 ),
                 onPressed: () async {

+ 1 - 1
lib/ui/viewer/gallery/archive_page.dart

@@ -66,7 +66,7 @@ class ArchivePage extends StatelessWidget {
         preferredSize: const Size.fromHeight(50.0),
         child: GalleryAppBarWidget(
           appBarType,
-          "Hidden",
+          "Archive",
           _selectedFiles,
         ),
       ),

+ 7 - 0
lib/ui/viewer/gallery/collection_page.dart

@@ -10,6 +10,7 @@ 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/gallery/empty_state.dart';
 import 'package:photos/ui/viewer/gallery/gallery.dart';
 import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
 import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
@@ -19,16 +20,21 @@ class CollectionPage extends StatelessWidget {
   final String tagPrefix;
   final GalleryType appBarType;
   final _selectedFiles = SelectedFiles();
+  bool hasVerifiedLock;
 
   CollectionPage(
     this.c, {
     this.tagPrefix = "collection",
     this.appBarType = GalleryType.ownedCollection,
+    this.hasVerifiedLock = false,
     Key key,
   }) : super(key: key);
 
   @override
   Widget build(Object context) {
+    if (hasVerifiedLock == false && c.collection.isHidden()) {
+      return const EmptyState();
+    }
     final initialFiles = c.thumbnail != null ? [c.thumbnail] : null;
     final gallery = Gallery(
       asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) async {
@@ -55,6 +61,7 @@ class CollectionPage extends StatelessWidget {
       removalEventTypes: const {
         EventType.deletedFromRemote,
         EventType.deletedFromEverywhere,
+        EventType.hide,
       },
       tagPrefix: tagPrefix,
       selectedFiles: _selectedFiles,

+ 1 - 0
lib/ui/viewer/gallery/device_folder_page.dart

@@ -38,6 +38,7 @@ class DeviceFolderPage extends StatelessWidget {
       removalEventTypes: const {
         EventType.deletedFromDevice,
         EventType.deletedFromEverywhere,
+        EventType.hide,
       },
       tagPrefix: "device_folder:" + deviceCollection.name,
       selectedFiles: _selectedFiles,

+ 100 - 0
lib/ui/viewer/gallery/empty_hidden_widget.dart

@@ -0,0 +1,100 @@
+import 'package:flutter/material.dart';
+import 'package:photos/theme/colors.dart';
+import 'package:photos/theme/ente_theme.dart';
+import 'package:photos/theme/text_style.dart';
+
+class EmptyHiddenWidget extends StatelessWidget {
+  const EmptyHiddenWidget({Key? key}) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    final EnteTextTheme enteTextTheme = getEnteTextTheme(context);
+    final EnteColorScheme enteColorScheme = getEnteColorScheme(context);
+    return Padding(
+      padding: const EdgeInsets.all(8.0),
+      child: Column(
+        mainAxisAlignment: MainAxisAlignment.center,
+        children: [
+          Icon(
+            Icons.visibility_off,
+            color: enteColorScheme.strokeMuted,
+            size: 24,
+          ),
+          const SizedBox(height: 10),
+          Text(
+            "No hidden photos or videos",
+            textAlign: TextAlign.center,
+            style: enteTextTheme.body.copyWith(
+              color: enteColorScheme.textMuted,
+            ),
+          ),
+          const SizedBox(height: 36),
+          Column(
+            crossAxisAlignment: CrossAxisAlignment.start,
+            children: [
+              const EmptyHiddenTextWidget("To hide a photo or video"),
+              const SizedBox(height: 4),
+              Padding(
+                padding: const EdgeInsets.only(left: 6),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.start,
+                  children: [
+                    const EmptyHiddenTextWidget("• Open the item"),
+                    const SizedBox(height: 2),
+                    const EmptyHiddenTextWidget(
+                      "• Click on the overflow menu",
+                    ),
+                    const SizedBox(height: 2),
+                    SizedBox(
+                      width: 120,
+                      child: Row(
+                        children: [
+                          const EmptyHiddenTextWidget("• Click "),
+                          const SizedBox(width: 4),
+                          Icon(
+                            Icons.visibility_off,
+                            color: enteColorScheme.strokeBase,
+                            size: 16,
+                          ),
+                          const Padding(
+                            padding: EdgeInsets.all(4),
+                          ),
+                          Text(
+                            "Hide",
+                            style: TextStyle(
+                              color: enteColorScheme.textBase,
+                            ),
+                          ),
+                        ],
+                      ),
+                    ),
+                  ],
+                ),
+              ),
+            ],
+          ),
+        ],
+      ),
+    );
+  }
+}
+
+class EmptyHiddenTextWidget extends StatelessWidget {
+  final String text;
+
+  const EmptyHiddenTextWidget(
+    this.text, {
+    Key? key,
+  }) : super(key: key);
+
+  @override
+  Widget build(BuildContext context) {
+    return Text(
+      text,
+      textAlign: TextAlign.center,
+      style: TextStyle(
+        color: getEnteColorScheme(context).textFaint,
+      ),
+    );
+  }
+}

+ 1 - 0
lib/ui/viewer/gallery/empty_state.dart

@@ -16,6 +16,7 @@ class EmptyState extends StatelessWidget {
         padding: const EdgeInsets.all(8.0),
         child: Text(
           text,
+          textAlign: TextAlign.center,
           style: TextStyle(
             color: Theme.of(context)
                 .colorScheme

+ 4 - 2
lib/ui/viewer/gallery/gallery.dart

@@ -37,6 +37,7 @@ class Gallery extends StatefulWidget {
   final String tagPrefix;
   final Widget header;
   final Widget footer;
+  final Widget emptyState;
   final bool smallerTodayFont;
   final String albumName;
   final double scrollBottomSafeArea;
@@ -51,6 +52,7 @@ class Gallery extends StatefulWidget {
     this.removalEventTypes = const {},
     this.header,
     this.footer = const SizedBox(height: 120),
+    this.emptyState = const EmptyState(),
     this.scrollBottomSafeArea = 120.0,
     this.smallerTodayFont = false,
     this.albumName = '',
@@ -209,8 +211,8 @@ class _GalleryState extends State<Gallery> {
           children.add(widget.header);
         }
         children.add(
-          const Expanded(
-            child: EmptyState(),
+          Expanded(
+            child: widget.emptyState,
           ),
         );
         if (widget.footer != null) {

+ 3 - 3
lib/ui/viewer/gallery/gallery_app_bar_widget.dart

@@ -164,7 +164,7 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
         Tooltip(
           message: "Share",
           child: IconButton(
-            icon: Icon(Icons.adaptive.share),
+            icon: const Icon(Icons.people_outlined),
             onPressed: () async {
               final bool showHiddenWarning =
                   await _shouldShowHiddenFilesWarning(widget.collection);
@@ -212,11 +212,11 @@ class _GalleryAppBarWidgetState extends State<GalleryAppBarWidget> {
           value: 2,
           child: Row(
             children: [
-              Icon(isArchived ? Icons.visibility : Icons.visibility_off),
+              Icon(isArchived ? Icons.unarchive : Icons.archive_outlined),
               const Padding(
                 padding: EdgeInsets.all(8),
               ),
-              Text(isArchived ? "Unhide album" : "Hide album"),
+              Text(isArchived ? "Unarchive album" : "Archive album"),
             ],
           ),
         ),

+ 98 - 22
lib/ui/viewer/gallery/gallery_overlay_widget.dart

@@ -17,6 +17,7 @@ import 'package:photos/models/gallery_type.dart';
 import 'package:photos/models/magic_metadata.dart';
 import 'package:photos/models/selected_files.dart';
 import 'package:photos/services/collections_service.dart';
+import 'package:photos/services/hidden_service.dart';
 import 'package:photos/ui/create_collection_page.dart';
 import 'package:photos/utils/delete_file_util.dart';
 import 'package:photos/utils/dialog_util.dart';
@@ -29,6 +30,7 @@ class GalleryOverlayWidget extends StatefulWidget {
   final SelectedFiles selectedFiles;
   final String path;
   final Collection collection;
+
   const GalleryOverlayWidget(
     this.type,
     this.selectedFiles, {
@@ -118,6 +120,7 @@ class _OverlayWidgetState extends State<OverlayWidget> {
   StreamSubscription _userAuthEventSubscription;
   Function() _selectedFilesListener;
   final GlobalKey shareButtonKey = GlobalKey();
+
   @override
   void initState() {
     _selectedFilesListener = () {
@@ -228,7 +231,7 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     widget.selectedFiles.clearAll();
   }
 
-  Future<void> _createAlbum() async {
+  Future<void> _createCollectionAction(CollectionActionType type) async {
     Navigator.push(
       context,
       PageTransition(
@@ -236,6 +239,7 @@ class _OverlayWidgetState extends State<OverlayWidget> {
         child: CreateCollectionPage(
           widget.selectedFiles,
           null,
+          actionType: type,
         ),
       ),
     );
@@ -263,22 +267,39 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     }
     // skip add button for incoming collection till this feature is implemented
     if (Configuration.instance.hasConfiguredAccount() &&
-        widget.type != GalleryType.sharedCollection) {
-      String msg = "Add";
+        widget.type != GalleryType.sharedCollection &&
+        widget.type != GalleryType.hidden) {
+      String msg = "Add to album";
       IconData iconData = Platform.isAndroid ? Icons.add : CupertinoIcons.add;
       // show upload icon instead of add for files selected in local gallery
       if (widget.type == GalleryType.localFolder) {
-        msg = "Upload";
+        msg = "Upload to album";
         iconData = Icons.cloud_upload_outlined;
       }
       actions.add(
         Tooltip(
-          message: msg,
+          message: "add",
           child: IconButton(
             color: Theme.of(context).colorScheme.iconColor,
             icon: Icon(iconData),
+            onPressed: () async {
+              await onActionSelected("add");
+            },
+          ),
+        ),
+      );
+    }
+
+    if (Configuration.instance.hasConfiguredAccount() &&
+        widget.type == GalleryType.hidden) {
+      actions.add(
+        Tooltip(
+          message: "Unhide",
+          child: IconButton(
+            color: Theme.of(context).colorScheme.iconColor,
+            icon: const Icon(Icons.visibility),
             onPressed: () {
-              _createAlbum();
+              _createCollectionAction(CollectionActionType.unHide);
             },
           ),
         ),
@@ -298,7 +319,7 @@ class _OverlayWidgetState extends State<OverlayWidget> {
                   : CupertinoIcons.arrow_right,
             ),
             onPressed: () {
-              _moveFiles();
+              onActionSelected('move');
             },
           ),
         ),
@@ -319,6 +340,7 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     );
     if (widget.type == GalleryType.homepage ||
         widget.type == GalleryType.archive ||
+        widget.type == GalleryType.hidden ||
         widget.type == GalleryType.localFolder ||
         widget.type == GalleryType.searchResults) {
       actions.add(
@@ -371,27 +393,66 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     if (widget.type == GalleryType.homepage ||
         widget.type == GalleryType.archive) {
       final bool showArchive = widget.type == GalleryType.homepage;
-      actions.add(
-        Tooltip(
-          message: showArchive ? "Hide" : "Unhide",
-          child: IconButton(
-            color: Theme.of(context).colorScheme.iconColor,
-            icon: Icon(
-              showArchive ? Icons.visibility_off : Icons.visibility,
+      if (showArchive) {
+        actions.add(
+          Tooltip(
+            message: 'Archive',
+            child: IconButton(
+              color: Theme.of(context).colorScheme.iconColor,
+              icon: const Icon(
+                Icons.archive_outlined,
+              ),
+              onPressed: () {
+                onActionSelected('archive');
+              },
             ),
-            onPressed: () {
-              _handleVisibilityChangeRequest(
-                context,
-                showArchive ? visibilityArchive : visibilityVisible,
-              );
-            },
           ),
-        ),
-      );
+        );
+      } else {
+        actions.insert(
+          0,
+          Tooltip(
+            message: 'Unarchive',
+            child: IconButton(
+              color: Theme.of(context).colorScheme.iconColor,
+              icon: const Icon(
+                Icons.unarchive,
+              ),
+              onPressed: () {
+                onActionSelected('unarchive');
+              },
+            ),
+          ),
+        );
+      }
     }
+
     return actions;
   }
 
+  Future<void> onActionSelected(String value) async {
+    debugPrint("Action Selected $value");
+    switch (value.toLowerCase()) {
+      case 'hide':
+        await _handleHideRequest(context);
+        break;
+      case 'add':
+        await _createCollectionAction(CollectionActionType.addFiles);
+        break;
+      case 'move':
+        await _moveFiles();
+        break;
+      case 'archive':
+        await _handleVisibilityChangeRequest(context, visibilityArchive);
+        break;
+      case 'unarchive':
+        await _handleVisibilityChangeRequest(context, visibilityVisible);
+        break;
+      default:
+        break;
+    }
+  }
+
   void _addTrashAction(List<Widget> actions) {
     actions.add(
       Tooltip(
@@ -456,6 +517,21 @@ class _OverlayWidgetState extends State<OverlayWidget> {
     }
   }
 
+  // note: Keeping this method here so that it can be used whenever we move to
+  // to bottom UI
+  Future<void> _handleHideRequest(BuildContext context) async {
+    try {
+      final hideResult = await CollectionsService.instance
+          .hideFiles(context, widget.selectedFiles.files.toList());
+      if (hideResult) {
+        _clearSelectedFiles();
+      }
+    } catch (e, s) {
+      _logger.severe("failed to update file visibility", e, s);
+      await showGenericErrorDialog(context);
+    }
+  }
+
   void _shareSelected(BuildContext context) {
     share(
       context,

+ 87 - 0
lib/ui/viewer/gallery/hidden_page.dart

@@ -0,0 +1,87 @@
+// @dart=2.9
+
+import 'package:flutter/material.dart';
+import 'package:photos/core/configuration.dart';
+import 'package:photos/core/event_bus.dart';
+import 'package:photos/db/files_db.dart';
+import 'package:photos/events/files_updated_event.dart';
+import 'package:photos/models/gallery_type.dart';
+import 'package:photos/models/selected_files.dart';
+import 'package:photos/services/collections_service.dart';
+import 'package:photos/ui/viewer/gallery/empty_hidden_widget.dart';
+import 'package:photos/ui/viewer/gallery/gallery.dart';
+import 'package:photos/ui/viewer/gallery/gallery_app_bar_widget.dart';
+import 'package:photos/ui/viewer/gallery/gallery_overlay_widget.dart';
+
+class HiddenPage extends StatelessWidget {
+  final String tagPrefix;
+  final GalleryType appBarType;
+  final GalleryType overlayType;
+  final _selectedFiles = SelectedFiles();
+
+  HiddenPage({
+    this.tagPrefix = "hidden_page",
+    this.appBarType = GalleryType.hidden,
+    this.overlayType = GalleryType.hidden,
+    Key key,
+  }) : super(key: key);
+
+  @override
+  Widget build(Object context) {
+    final gallery = Gallery(
+      asyncLoader: (creationStartTime, creationEndTime, {limit, asc}) {
+        return FilesDB.instance.getFilesInCollections(
+          CollectionsService.instance.getHiddenCollections().toList(),
+          creationStartTime,
+          creationEndTime,
+          Configuration.instance.getUserID(),
+          limit: limit,
+          asc: asc,
+        );
+      },
+      reloadEvent: Bus.instance.on<FilesUpdatedEvent>().where(
+            (event) =>
+                event.updatedFiles.firstWhere(
+                  (element) => element.uploadedFileID != null,
+                  orElse: () => null,
+                ) !=
+                null,
+          ),
+      removalEventTypes: const {EventType.unhide},
+      forceReloadEvents: [
+        Bus.instance.on<FilesUpdatedEvent>().where(
+              (event) =>
+                  event.updatedFiles.firstWhere(
+                    (element) => element.uploadedFileID != null,
+                    orElse: () => null,
+                  ) !=
+                  null,
+            ),
+      ],
+      tagPrefix: tagPrefix,
+      selectedFiles: _selectedFiles,
+      initialFiles: null,
+      emptyState: const EmptyHiddenWidget(),
+    );
+    return Scaffold(
+      appBar: PreferredSize(
+        preferredSize: const Size.fromHeight(50.0),
+        child: GalleryAppBarWidget(
+          appBarType,
+          "Hidden",
+          _selectedFiles,
+        ),
+      ),
+      body: Stack(
+        alignment: Alignment.bottomCenter,
+        children: [
+          gallery,
+          GalleryOverlayWidget(
+            overlayType,
+            _selectedFiles,
+          )
+        ],
+      ),
+    );
+  }
+}

+ 20 - 0
lib/utils/date_time_util.dart

@@ -268,3 +268,23 @@ bool isValidDate({
   }
   return true;
 }
+
+DateTime? parseDateFromFileName(String fileName) {
+  if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
+// Whatsapp media files
+    return DateTime.tryParse(fileName.split('-')[1]);
+  } else if (fileName.startsWith("Screenshot_")) {
+// Screenshots on droid
+    return DateTime.tryParse(
+      (fileName).replaceAll('Screenshot_', '').replaceAll('-', 'T'),
+    );
+  } else {
+    return DateTime.tryParse(
+      (fileName)
+          .replaceAll("IMG_", "")
+          .replaceAll("VID_", "")
+          .replaceAll("DCIM_", "")
+          .replaceAll("_", " "),
+    );
+  }
+}

+ 9 - 6
lib/utils/magic_util.dart

@@ -24,7 +24,10 @@ Future<void> changeVisibility(
 ) async {
   final dialog = createProgressDialog(
     context,
-    newVisibility == visibilityArchive ? "Hiding..." : "Unhiding...",
+    newVisibility == visibilityArchive
+        ? "Archiving..."
+        : "Unarchiving..."
+            "...",
   );
   await dialog.show();
   try {
@@ -32,8 +35,8 @@ Future<void> changeVisibility(
     showShortToast(
       context,
       newVisibility == visibilityArchive
-          ? "Successfully hidden"
-          : "Successfully unhidden",
+          ? "Successfully archived"
+          : "Successfully unarchived",
     );
 
     await dialog.hide();
@@ -51,7 +54,7 @@ Future<void> changeCollectionVisibility(
 ) async {
   final dialog = createProgressDialog(
     context,
-    newVisibility == visibilityArchive ? "Hiding..." : "Unhiding...",
+    newVisibility == visibilityArchive ? "Archiving..." : "Unarchiving...",
   );
   await dialog.show();
   try {
@@ -62,8 +65,8 @@ Future<void> changeCollectionVisibility(
     showShortToast(
       context,
       newVisibility == visibilityArchive
-          ? "Successfully hidden"
-          : "Successfully unhidden",
+          ? "Successfully archived"
+          : "Successfully unarchived",
     );
 
     await dialog.hide();

+ 2 - 1
lib/utils/share_util.dart

@@ -10,6 +10,7 @@ import 'package:photos/core/configuration.dart';
 import 'package:photos/core/constants.dart';
 import 'package:photos/models/file.dart';
 import 'package:photos/models/file_type.dart';
+import 'package:photos/utils/date_time_util.dart';
 import 'package:photos/utils/dialog_util.dart';
 import 'package:photos/utils/exif_util.dart';
 import 'package:photos/utils/file_util.dart';
@@ -111,7 +112,7 @@ Future<List<File>> convertIncomingSharedMediaToFile(
   return localFiles;
 }
 
-DateTime parseDateFromFileName(String fileName) {
+DateTime parseDateFromFileNam1e(String fileName) {
   if (fileName.startsWith('IMG-') || fileName.startsWith('VID-')) {
     // Whatsapp media files
     return DateTime.tryParse(fileName.split('-')[1]);