diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..fb07c2230 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,11 @@ +--- +name: Feature request +about: Suggest a feature or improvement +title: '' +labels: feature +assignees: '' + +--- + +**Describe the feature** +A clear description of what the feature is. diff --git a/hooks/pre-commit b/hooks/pre-commit index 242916516..83b93f9e1 100755 --- a/hooks/pre-commit +++ b/hooks/pre-commit @@ -1,37 +1,9 @@ #!/bin/sh # This git hook fails if a user is trying to add a new file which is # not null safe. +exec ./hooks/pre-commit-fdroid -# Check the contents of each file that is being added(A) or modified(N) or -# copied (C) -for file in `git diff --name-only --diff-filter=ACM --cached`; do - # Ignore the hooks from any pre-commit check - if echo "$file" | grep -q 'hooks/'; then - continue - fi - # Get the contents of the newly added lines in the file - newContent=`git diff --cached --unified=0 $file | grep '^+'` - oldContent=`git diff --cached --unified=0 $file | grep '^-'` - initialContent=`head -5 $file` - # Check if user has added "// @dart=2.9" in the file - if echo "$newContent" | grep -q '// @dart=2.9'; then - echo "😑 File $file looks like a newly created file but it's not null-safe" - exit 1 - elif echo "$oldContent" | grep -q '// @dart=2.9'; then - echo "πŸ’šπŸ’š Thank you for making $file null-safe" - continue - elif echo "$initialContent" | grep -q '// @dart=2.9'; then - echo "πŸ”₯πŸ”₯πŸ”₯πŸ”₯ Please make $file null-safe" - continue - else - continue - fi -done - -nullUnsafeFiles=$(grep '// @dart=2.9' -r lib/ | wc -l) -# The xargs at the end is to trim whitepsaces https://stackoverflow.com/a/12973694/546896 -echo "πŸ₯ΊπŸ₯Ί $nullUnsafeFiles files are still waiting for their nullSafety migrator" | xargs # If the script gets to this point, all files passed the check exit 0 diff --git a/hooks/pre-commit-fdroid b/hooks/pre-commit-fdroid new file mode 100755 index 000000000..9b2514adc --- /dev/null +++ b/hooks/pre-commit-fdroid @@ -0,0 +1,14 @@ +#!/bin/sh +# Get the current branch +current_branch=$(git rev-parse --abbrev-ref HEAD) + +if [ "$current_branch" = "f-droid" ]; then + # Verify that the pubspec.yaml doesn't contain certain dependencies + WORDS=("in_app_purchase" "firebase") + for word in ${WORDS[@]}; do + if grep -q $word pubspec.yaml; then + echo "The pubspec.yaml file dependency on '$word', which is not allowed on the f-droid branch." + exit 1 + fi + done +fi diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 8c2cfdb43..b917c7564 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -1,4 +1,3 @@ -import 'dart:developer' as dev; import 'dart:io' as io; import 'package:flutter/foundation.dart'; @@ -12,6 +11,7 @@ import 'package:photos/models/file_load_result.dart'; import 'package:photos/models/file_type.dart'; import 'package:photos/models/location/location.dart'; import "package:photos/models/metadata/common_keys.dart"; +import "package:photos/services/filter/db_filters.dart"; import 'package:photos/utils/file_uploader_util.dart'; import 'package:sqflite/sqflite.dart'; import 'package:sqflite_migration/sqflite_migration.dart'; @@ -391,7 +391,7 @@ class FilesDB { ConflictAlgorithm conflictAlgorithm = ConflictAlgorithm.replace, }) async { final startTime = DateTime.now(); - final db = await instance.database; + final db = await database; var batch = db.batch(); int batchCounter = 0; for (File file in files) { @@ -508,10 +508,10 @@ class FilesDB { int? limit, bool? asc, int visibility = visibleVisibility, - Set? ignoredCollectionIDs, + DBFilterOptions? filterOptions, bool applyOwnerCheck = false, }) async { - final stopWatch = Stopwatch()..start(); + final stopWatch = EnteWatch('getAllPendingOrUploadedFiles')..start(); late String whereQuery; late List? whereArgs; if (applyOwnerCheck) { @@ -537,14 +537,13 @@ class FilesDB { '$columnCreationTime ' + order + ', $columnModificationTime ' + order, limit: limit, ); + stopWatch.log('queryDone'); final files = convertToFiles(results); - final List deduplicatedFiles = - _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); - dev.log( - "getAllPendingOrUploadedFiles time taken: ${stopWatch.elapsedMilliseconds} ms", - ); + stopWatch.log('convertDone'); + final filteredFiles = await applyDBFilters(files, filterOptions); + stopWatch.log('filteringDone'); stopWatch.stop(); - return FileLoadResult(deduplicatedFiles, files.length == limit); + return FileLoadResult(filteredFiles, files.length == limit); } Future getAllLocalAndUploadedFiles( @@ -553,7 +552,7 @@ class FilesDB { int ownerID, { int? limit, bool? asc, - Set? ignoredCollectionIDs, + required DBFilterOptions filterOptions, }) async { final db = await instance.database; final order = (asc ?? false ? 'ASC' : 'DESC'); @@ -568,9 +567,8 @@ class FilesDB { limit: limit, ); final files = convertToFiles(results); - final List deduplicatedFiles = - _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); - return FileLoadResult(deduplicatedFiles, files.length == limit); + final List filteredFiles = await applyDBFilters(files, filterOptions); + return FileLoadResult(filteredFiles, files.length == limit); } List deduplicateByLocalID(List files) { @@ -590,43 +588,6 @@ class FilesDB { return deduplicatedFiles; } - List _deduplicatedAndFilterIgnoredFiles( - List files, - Set? ignoredCollectionIDs, - ) { - final Set uploadedFileIDs = {}; - // ignoredFileUploadIDs is to keep a track of files which are part of - // archived collection - final Set ignoredFileUploadIDs = {}; - final List deduplicatedFiles = []; - for (final file in files) { - final id = file.uploadedFileID; - final bool isFileUploaded = id != null && id != -1; - final bool isCollectionIgnored = ignoredCollectionIDs != null && - ignoredCollectionIDs.contains(file.collectionID); - if (isCollectionIgnored || ignoredFileUploadIDs.contains(id)) { - if (isFileUploaded) { - ignoredFileUploadIDs.add(id); - // remove the file from the list of deduplicated files - if (uploadedFileIDs.contains(id)) { - deduplicatedFiles - .removeWhere((element) => element.uploadedFileID == id); - uploadedFileIDs.remove(id); - } - } - continue; - } - if (isFileUploaded && uploadedFileIDs.contains(id)) { - continue; - } - if (isFileUploaded) { - uploadedFileIDs.add(id); - } - deduplicatedFiles.add(file); - } - return deduplicatedFiles; - } - Future getFilesInCollection( int collectionID, int startTime, @@ -698,7 +659,8 @@ class FilesDB { limit: limit, ); final files = convertToFiles(results); - final dedupeResult = _deduplicatedAndFilterIgnoredFiles(files, {}); + final dedupeResult = + await applyDBFilters(files, DBFilterOptions.dedupeOption); _logger.info("Fetched " + dedupeResult.length.toString() + " files"); return FileLoadResult(files, files.length == limit); } @@ -730,7 +692,10 @@ class FilesDB { orderBy: '$columnCreationTime ' + order, ); final files = convertToFiles(results); - return _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); + return applyDBFilters( + files, + DBFilterOptions(ignoredCollectionIDs: ignoredCollectionIDs), + ); } // Files which user added to a collection manually but they are not @@ -1254,6 +1219,50 @@ class FilesDB { return result; } + // getCollectionLatestFileTime returns map of collectionID to the max + // creationTime of the files in the collection. + Future> getCollectionIDToMaxCreationTime() async { + final db = await instance.database; + final rows = await db.rawQuery( + ''' + SELECT $columnCollectionID, MAX($columnCreationTime) AS max_creation_time + FROM $filesTable + WHERE + ($columnCollectionID IS NOT NULL AND $columnCollectionID IS NOT -1 + AND $columnUploadedFileID IS NOT NULL AND $columnUploadedFileID IS + NOT -1) + GROUP BY $columnCollectionID; + ''', + ); + final result = {}; + for (final row in rows) { + result[row[columnCollectionID] as int] = row['max_creation_time'] as int; + } + return result; + } + + // getCollectionFileFirstOrLast returns the first or last uploaded file in + // the collection based on the given collectionID and the order. + Future getCollectionFileFirstOrLast( + int collectionID, + bool sortAsc, + ) async { + final db = await instance.database; + final order = sortAsc ? 'ASC' : 'DESC'; + final rows = await db.query( + filesTable, + where: '$columnCollectionID = ? AND $columnUploadedFileID IS NOT NULL', + whereArgs: [collectionID], + orderBy: + '$columnCreationTime ' + order + ', $columnModificationTime ' + order, + limit: 1, + ); + if (rows.isEmpty) { + return null; + } + return convertToFiles(rows).first; + } + Future markForReUploadIfLocationMissing(List localIDs) async { if (localIDs.isEmpty) { return; @@ -1425,22 +1434,6 @@ class FilesDB { return result; } - // For given list of localIDs and ownerID, get a list of uploaded files - // owned by given user - Future> getFilesForLocalIDs( - List localIDs, - int ownerID, - ) async { - final db = await instance.database; - final rows = await db.query( - filesTable, - where: - '$columnLocalID IN (${localIDs.map((e) => "'$e'").join(',')}) AND $columnOwnerID = ?', - whereArgs: [ownerID], - ); - return _deduplicatedAndFilterIgnoredFiles(convertToFiles(rows), {}); - } - // For a given userID, return unique uploadedFileId for the given userID Future> getUploadIDsWithMissingSize(int userId) async { final db = await instance.database; @@ -1484,8 +1477,10 @@ class FilesDB { final List> result = await db.query(filesTable, orderBy: '$columnCreationTime DESC'); final List files = convertToFiles(result); - final List deduplicatedFiles = - _deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore); + final List deduplicatedFiles = await applyDBFilters( + files, + DBFilterOptions(ignoredCollectionIDs: collectionsToIgnore), + ); return deduplicatedFiles; } @@ -1509,7 +1504,7 @@ class FilesDB { int endTime, { int? limit, bool? asc, - Set? ignoredCollectionIDs, + required DBFilterOptions? filterOptions, }) async { final db = await instance.database; final order = (asc ?? false ? 'ASC' : 'DESC'); @@ -1525,9 +1520,8 @@ class FilesDB { limit: limit, ); final files = convertToFiles(results); - final List deduplicatedFiles = - _deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs); - return FileLoadResult(deduplicatedFiles, files.length == limit); + final List filteredFiles = await applyDBFilters(files, filterOptions); + return FileLoadResult(filteredFiles, files.length == limit); } Map _getRowForFile(File file) { diff --git a/lib/extensions/stop_watch.dart b/lib/extensions/stop_watch.dart index 8b394504e..a381fcbc1 100644 --- a/lib/extensions/stop_watch.dart +++ b/lib/extensions/stop_watch.dart @@ -2,15 +2,25 @@ import 'package:flutter/foundation.dart'; class EnteWatch extends Stopwatch { final String context; + int previousElapsed = 0; EnteWatch(this.context) : super(); void log(String msg) { - debugPrint("[$context]: $msg took ${elapsed.inMilliseconds} ms"); + if (kDebugMode) { + debugPrint("[$context]: $msg took ${Duration( + microseconds: elapsedMicroseconds - previousElapsed, + ).inMilliseconds} ms total: " + "${elapsed.inMilliseconds} ms"); + } + previousElapsed = elapsedMicroseconds; } void logAndReset(String msg) { - debugPrint("[$context]: $msg took ${elapsed.inMilliseconds} ms"); + if (kDebugMode) { + debugPrint("[$context]: $msg took ${elapsed.inMilliseconds} ms"); + } reset(); + previousElapsed = 0; } } diff --git a/lib/models/collection.dart b/lib/models/collection.dart index 6fae45d25..bd35f0701 100644 --- a/lib/models/collection.dart +++ b/lib/models/collection.dart @@ -130,6 +130,25 @@ class Collection { return (owner?.id ?? 0) == userID; } + CollectionParticipantRole getRole(int userID) { + if (isOwner(userID)) { + return CollectionParticipantRole.owner; + } + if (sharees == null) { + return CollectionParticipantRole.unknown; + } + for (final User? u in sharees!) { + if (u != null && u.id == userID) { + if (u.isViewer) { + return CollectionParticipantRole.viewer; + } else if (u.isCollaborator) { + return CollectionParticipantRole.collaborator; + } + } + } + return CollectionParticipantRole.unknown; + } + // canLinkToDevicePath returns true if the collection can be linked to local // device album based on path. The path is nothing but the name of the device // album. diff --git a/lib/services/collections_service.dart b/lib/services/collections_service.dart index 48c445a60..2a8350f41 100644 --- a/lib/services/collections_service.dart +++ b/lib/services/collections_service.dart @@ -218,6 +218,36 @@ class CollectionsService { return _cachedLatestFiles!; } + final Map _coverCache = {}; + + Future getCover(Collection c) async { + final int localSyncTime = getCollectionSyncTime(c.id); + final String coverKey = '${c.id}_${localSyncTime}_${c.updationTime}'; + if (_coverCache.containsKey(coverKey)) { + return Future.value(_coverCache[coverKey]!); + } + if (kDebugMode) { + debugPrint("getCover for collection ${c.id} ${c.displayName}"); + } + final coverID = c.pubMagicMetadata.coverID; + if (coverID != null) { + final File? cover = await filesDB.getUploadedFile(coverID, c.id); + if (cover != null) { + _coverCache[coverKey] = cover; + return Future.value(cover); + } + } + final coverFile = await filesDB.getCollectionFileFirstOrLast( + c.id, + c.pubMagicMetadata.asc ?? false, + ); + if (coverFile != null) { + _coverCache[coverKey] = coverFile; + return Future.value(coverFile); + } + return null; + } + Future setCollectionSyncTime(int collectionID, int? time) async { final key = _collectionSyncTimeKeyPrefix + collectionID.toString(); if (time == null) { @@ -234,6 +264,33 @@ class CollectionsService { .toList(); } + // returns collections after removing deleted,uncategorized, and hidden + // collections + List getCollectionsForUI({ + bool includedShared = false, + bool includeCollab = false, + }) { + final Set allowedRoles = { + CollectionParticipantRole.owner, + }; + if (includedShared) { + allowedRoles.add(CollectionParticipantRole.viewer); + } + if (includedShared || includeCollab) { + allowedRoles.add(CollectionParticipantRole.collaborator); + } + final int userID = _config.getUserID()!; + return _collectionIDToCollections.values + .where( + (c) => + !c.isDeleted || + c.type != CollectionType.uncategorized || + !c.isHidden() || + allowedRoles.contains(c.getRole(userID)), + ) + .toList(); + } + User getFileOwner(int userID, int? collectionID) { if (_cachedUserIdToUser.containsKey(userID)) { return _cachedUserIdToUser[userID]!; diff --git a/lib/services/filter/collection_ignore.dart b/lib/services/filter/collection_ignore.dart new file mode 100644 index 000000000..e21fc8a23 --- /dev/null +++ b/lib/services/filter/collection_ignore.dart @@ -0,0 +1,30 @@ +import "package:photos/models/file.dart"; +import "package:photos/services/filter/filter.dart"; + +class CollectionsIgnoreFilter extends Filter { + final Set collectionIDs; + + Set? _ignoredUploadIDs; + + CollectionsIgnoreFilter(this.collectionIDs, List files) : super() { + init(files); + } + + void init(List files) { + _ignoredUploadIDs = {}; + if (collectionIDs.isEmpty) return; + for (var file in files) { + if (file.collectionID != null && + file.isUploaded && + collectionIDs.contains(file.collectionID!)) { + _ignoredUploadIDs!.add(file.uploadedFileID!); + } + } + } + + @override + bool filter(File file) { + return file.isUploaded && + !_ignoredUploadIDs!.contains(file.uploadedFileID!); + } +} diff --git a/lib/services/filter/db_filters.dart b/lib/services/filter/db_filters.dart new file mode 100644 index 000000000..604313b6f --- /dev/null +++ b/lib/services/filter/db_filters.dart @@ -0,0 +1,58 @@ +import "package:photos/models/file.dart"; +import "package:photos/services/filter/collection_ignore.dart"; +import "package:photos/services/filter/dedupe_by_upload_id.dart"; +import "package:photos/services/filter/filter.dart"; +import "package:photos/services/filter/upload_ignore.dart"; +import "package:photos/services/ignored_files_service.dart"; + +class DBFilterOptions { + // typically used for filtering out all files which are present in hidden + // (searchable files result) or archived collections or both (ex: home + // timeline) + Set? ignoredCollectionIDs; + bool dedupeUploadID; + bool hideIgnoredForUpload; + + DBFilterOptions({ + this.ignoredCollectionIDs, + this.hideIgnoredForUpload = false, + this.dedupeUploadID = true, + }); + + static DBFilterOptions dedupeOption = DBFilterOptions( + dedupeUploadID: true, + ); +} + +Future> applyDBFilters( + List files, + DBFilterOptions? options, +) async { + if (options == null) { + return files; + } + final List filters = []; + if (options.hideIgnoredForUpload) { + final Set ignoredIDs = + await IgnoredFilesService.instance.ignoredIDs; + if (ignoredIDs.isNotEmpty) { + filters.add(UploadIgnoreFilter(ignoredIDs)); + } + } + if (options.dedupeUploadID) { + filters.add(DedupeUploadIDFilter()); + } + if (options.ignoredCollectionIDs != null && + options.ignoredCollectionIDs!.isNotEmpty) { + final collectionIgnoreFilter = + CollectionsIgnoreFilter(options.ignoredCollectionIDs!, files); + filters.add(collectionIgnoreFilter); + } + final List filterFiles = []; + for (final file in files) { + if (filters.every((f) => f.filter(file))) { + filterFiles.add(file); + } + } + return filterFiles; +} diff --git a/lib/services/filter/dedupe_by_upload_id.dart b/lib/services/filter/dedupe_by_upload_id.dart new file mode 100644 index 000000000..7e9282a8a --- /dev/null +++ b/lib/services/filter/dedupe_by_upload_id.dart @@ -0,0 +1,20 @@ +import "package:photos/models/file.dart"; +import "package:photos/services/filter/filter.dart"; + +// DedupeUploadIDFilter will filter out files where were previously filtered +// during the same filtering session +class DedupeUploadIDFilter extends Filter { + final Set trackedUploadIDs = {}; + + @override + bool filter(File file) { + if (!file.isUploaded) { + return true; + } + if (trackedUploadIDs.contains(file.uploadedFileID!)) { + return false; + } + trackedUploadIDs.add(file.uploadedFileID!); + return true; + } +} diff --git a/lib/services/filter/filter.dart b/lib/services/filter/filter.dart new file mode 100644 index 000000000..dd39fbb8c --- /dev/null +++ b/lib/services/filter/filter.dart @@ -0,0 +1,5 @@ +import "package:photos/models/file.dart"; + +abstract class Filter { + bool filter(File file); +} diff --git a/lib/services/filter/type_filter.dart b/lib/services/filter/type_filter.dart new file mode 100644 index 000000000..e2c30444b --- /dev/null +++ b/lib/services/filter/type_filter.dart @@ -0,0 +1,18 @@ +import "package:photos/models/file.dart"; +import "package:photos/models/file_type.dart"; +import "package:photos/services/filter/filter.dart"; + +class TypeFilter extends Filter { + final FileType type; + final bool reverse; + + TypeFilter( + this.type, { + this.reverse = false, + }); + + @override + bool filter(File file) { + return reverse ? file.fileType != type : file.fileType == type; + } +} diff --git a/lib/services/filter/upload_ignore.dart b/lib/services/filter/upload_ignore.dart new file mode 100644 index 000000000..4836f5f3e --- /dev/null +++ b/lib/services/filter/upload_ignore.dart @@ -0,0 +1,18 @@ +import "package:photos/models/file.dart"; +import "package:photos/services/filter/filter.dart"; +import "package:photos/services/ignored_files_service.dart"; + +// UploadIgnoreFilter hides the unuploaded files that are ignored from for +// upload +class UploadIgnoreFilter extends Filter { + Set ignoredIDs; + + UploadIgnoreFilter(this.ignoredIDs) : super(); + + @override + bool filter(File file) { + // Already uploaded files pass the filter + if (file.isUploaded) return true; + return !IgnoredFilesService.instance.shouldSkipUpload(ignoredIDs, file); + } +} diff --git a/lib/ui/home/home_gallery_widget.dart b/lib/ui/home/home_gallery_widget.dart index 9943dfcf1..81195ca48 100644 --- a/lib/ui/home/home_gallery_widget.dart +++ b/lib/ui/home/home_gallery_widget.dart @@ -10,7 +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/collections_service.dart'; -import 'package:photos/services/ignored_files_service.dart'; +import "package:photos/services/filter/db_filters.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import 'package:photos/ui/viewer/gallery/gallery.dart'; @@ -37,6 +37,11 @@ class HomeGalleryWidget extends StatelessWidget { final collectionsToHide = CollectionsService.instance.archivedOrHiddenCollections(); FileLoadResult result; + final DBFilterOptions filterOptions = DBFilterOptions( + hideIgnoredForUpload: true, + dedupeUploadID: true, + ignoredCollectionIDs: collectionsToHide, + ); if (hasSelectedAllForBackup) { result = await FilesDB.instance.getAllLocalAndUploadedFiles( creationStartTime, @@ -44,7 +49,7 @@ class HomeGalleryWidget extends StatelessWidget { ownerID!, limit: limit, asc: asc, - ignoredCollectionIDs: collectionsToHide, + filterOptions: filterOptions, ); } else { result = await FilesDB.instance.getAllPendingOrUploadedFiles( @@ -53,17 +58,10 @@ class HomeGalleryWidget extends StatelessWidget { ownerID!, limit: limit, asc: asc, - ignoredCollectionIDs: collectionsToHide, + filterOptions: filterOptions, ); } - // 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(), diff --git a/lib/ui/tabs/shared/incoming_album_item.dart b/lib/ui/tabs/shared/incoming_album_item.dart index 68c7ed6d4..036f3816a 100644 --- a/lib/ui/tabs/shared/incoming_album_item.dart +++ b/lib/ui/tabs/shared/incoming_album_item.dart @@ -3,7 +3,9 @@ import "dart:math"; import "package:flutter/material.dart"; import "package:photos/db/files_db.dart"; import "package:photos/models/collection_items.dart"; +import "package:photos/models/file.dart"; import "package:photos/models/gallery_type.dart"; +import "package:photos/services/collections_service.dart"; import "package:photos/ui/sharing/user_avator_widget.dart"; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; @@ -12,6 +14,7 @@ import "package:photos/utils/navigation_util.dart"; class IncomingAlbumItem extends StatelessWidget { final CollectionWithThumbnail c; + static const String heroTagPrefix = "shared_collection"; const IncomingAlbumItem( this.c, { @@ -20,7 +23,6 @@ class IncomingAlbumItem extends StatelessWidget { @override Widget build(BuildContext context) { - final heroTag = "shared_collection" + (c.thumbnail?.tag ?? ''); const double horizontalPaddingOfGridRow = 16; const double crossAxisSpacingOfGrid = 9; final TextStyle albumTitleTextStyle = @@ -42,18 +44,26 @@ class IncomingAlbumItem extends StatelessWidget { width: sideOfThumbnail, child: Stack( children: [ - c.thumbnail != null - ? Hero( + FutureBuilder( + future: CollectionsService.instance.getCover(c.collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + final heroTag = heroTagPrefix + snapshot.data!.tag; + return Hero( tag: heroTag, child: ThumbnailWidget( - c.thumbnail, + snapshot.data!, key: Key(heroTag), shouldShowArchiveStatus: c.collection.hasShareeArchived(), shouldShowSyncStatus: false, ), - ) - : const NoThumbnailWidget(), + ); + } else { + return const NoThumbnailWidget(); + } + }, + ), Align( alignment: Alignment.bottomRight, child: Padding( @@ -109,7 +119,7 @@ class IncomingAlbumItem extends StatelessWidget { CollectionPage( c, appBarType: GalleryType.sharedCollection, - tagPrefix: "shared_collection", + tagPrefix: heroTagPrefix, ), ); }, diff --git a/lib/ui/tabs/shared/outgoing_album_item.dart b/lib/ui/tabs/shared/outgoing_album_item.dart index 80b4c2f15..931f037e9 100644 --- a/lib/ui/tabs/shared/outgoing_album_item.dart +++ b/lib/ui/tabs/shared/outgoing_album_item.dart @@ -1,7 +1,9 @@ import "package:flutter/material.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/collection_items.dart"; +import "package:photos/models/file.dart"; import "package:photos/models/gallery_type.dart"; +import "package:photos/services/collections_service.dart"; import 'package:photos/theme/colors.dart'; import "package:photos/ui/viewer/file/no_thumbnail_widget.dart"; import "package:photos/ui/viewer/file/thumbnail_widget.dart"; @@ -10,6 +12,7 @@ import "package:photos/utils/navigation_util.dart"; class OutgoingAlbumItem extends StatelessWidget { final CollectionWithThumbnail c; + static const heroTagPrefix = "outgoing_collection"; const OutgoingAlbumItem({super.key, required this.c}); @@ -40,7 +43,7 @@ class OutgoingAlbumItem extends StatelessWidget { } } } - final String heroTag = "outgoing_collection" + (c.thumbnail?.tag ?? ''); + return GestureDetector( behavior: HitTestBehavior.opaque, child: Container( @@ -52,15 +55,23 @@ class OutgoingAlbumItem extends StatelessWidget { child: SizedBox( height: 60, width: 60, - child: c.thumbnail != null - ? Hero( + child: FutureBuilder( + future: CollectionsService.instance.getCover(c.collection), + builder: (context, snapshot) { + if (snapshot.hasData) { + final String heroTag = heroTagPrefix + snapshot.data!.tag; + return Hero( tag: heroTag, child: ThumbnailWidget( - c.thumbnail, + snapshot.data!, key: ValueKey(heroTag), ), - ) - : const NoThumbnailWidget(), + ); + } else { + return const NoThumbnailWidget(); + } + }, + ), ), ), const Padding(padding: EdgeInsets.all(8)), @@ -111,7 +122,7 @@ class OutgoingAlbumItem extends StatelessWidget { final page = CollectionPage( c, appBarType: GalleryType.ownedCollection, - tagPrefix: "outgoing_collection", + tagPrefix: heroTagPrefix, ); routeToPage(context, page); }, diff --git a/lib/ui/viewer/actions/delete_empty_albums.dart b/lib/ui/viewer/actions/delete_empty_albums.dart index f979ea345..ab90e8c34 100644 --- a/lib/ui/viewer/actions/delete_empty_albums.dart +++ b/lib/ui/viewer/actions/delete_empty_albums.dart @@ -1,5 +1,6 @@ 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/generated/l10n.dart"; import 'package:photos/models/collection.dart'; @@ -88,12 +89,13 @@ class _DeleteEmptyAlbumsState extends State { } Future _deleteEmptyAlbums() async { - final collections = - await CollectionsService.instance.getCollectionsWithThumbnails(); + final collections = CollectionsService.instance.getCollectionsForUI(); + final idToFileTimeStamp = + await FilesDB.instance.getCollectionIDToMaxCreationTime(); + // remove collections which are not empty or can't be deleted collections.removeWhere( - (element) => - element.thumbnail != null || !element.collection.type.canDelete, + (c) => !c.type.canDelete || idToFileTimeStamp.containsKey(c.id), ); int failedCount = 0; for (int i = 0; i < collections.length; i++) { @@ -105,7 +107,7 @@ class _DeleteEmptyAlbumsState extends State { S.of(context).deleteProgress(currentlyDeleting, collections.length); try { await CollectionsService.instance.trashEmptyCollection( - collections[i].collection, + collections[i], isBulkDelete: true, ); } catch (_) { diff --git a/lib/ui/viewer/gallery/archive_page.dart b/lib/ui/viewer/gallery/archive_page.dart index 5977771d7..f43b52f25 100644 --- a/lib/ui/viewer/gallery/archive_page.dart +++ b/lib/ui/viewer/gallery/archive_page.dart @@ -9,6 +9,7 @@ import 'package:photos/models/gallery_type.dart'; import "package:photos/models/metadata/common_keys.dart"; import 'package:photos/models/selected_files.dart'; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/filter/db_filters.dart"; import "package:photos/ui/collections/album/horizontal_list.dart"; import 'package:photos/ui/viewer/actions/file_selection_overlay_bar.dart'; import "package:photos/ui/viewer/gallery/empty_state.dart"; @@ -41,7 +42,11 @@ class ArchivePage extends StatelessWidget { visibility: archiveVisibility, limit: limit, asc: asc, - ignoredCollectionIDs: hiddenCollectionIDs, + filterOptions: DBFilterOptions( + hideIgnoredForUpload: true, + dedupeUploadID: true, + ignoredCollectionIDs: hiddenCollectionIDs, + ), applyOwnerCheck: true, ); }, diff --git a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart index fd5556ab2..549563a98 100644 --- a/lib/ui/viewer/gallery/gallery_app_bar_widget.dart +++ b/lib/ui/viewer/gallery/gallery_app_bar_widget.dart @@ -1,7 +1,6 @@ import 'dart:async'; import 'dart:io'; -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; @@ -247,6 +246,9 @@ class _GalleryAppBarWidgetState extends State { List _getDefaultActions(BuildContext context) { final List actions = []; + if (widget.selectedFiles.files.isNotEmpty) { + return actions; + } if (Configuration.instance.hasConfiguredAccount() && widget.selectedFiles.files.isEmpty && (widget.type == GalleryType.ownedCollection || @@ -502,14 +504,9 @@ class _GalleryAppBarWidgetState extends State { } Future _trashCollection() async { - final collectionWithThumbnail = - await CollectionsService.instance.getCollectionsWithThumbnails(); - final bool isEmptyCollection = collectionWithThumbnail - .firstWhereOrNull( - (element) => element.collection.id == widget.collection!.id, - ) - ?.thumbnail == - null; + final int count = + await FilesDB.instance.collectionFileCount(widget.collection!.id); + final bool isEmptyCollection = count == 0; if (isEmptyCollection) { final dialog = createProgressDialog( context, diff --git a/lib/ui/viewer/location/dynamic_location_gallery_widget.dart b/lib/ui/viewer/location/dynamic_location_gallery_widget.dart index 9690db0c4..5ad0c8744 100644 --- a/lib/ui/viewer/location/dynamic_location_gallery_widget.dart +++ b/lib/ui/viewer/location/dynamic_location_gallery_widget.dart @@ -7,7 +7,7 @@ import "package:photos/db/files_db.dart"; import "package:photos/models/file.dart"; import "package:photos/models/file_load_result.dart"; import "package:photos/services/collections_service.dart"; -import "package:photos/services/files_service.dart"; +import "package:photos/services/filter/db_filters.dart"; import "package:photos/services/location_service.dart"; import 'package:photos/states/location_state.dart'; import "package:photos/ui/viewer/gallery/gallery.dart"; @@ -32,7 +32,6 @@ class DynamicLocationGalleryWidget extends StatefulWidget { class _DynamicLocationGalleryWidgetState extends State { late final Future fileLoadResult; - late Future removeIgnoredFiles; double heightOfGallery = 0; @override @@ -45,10 +44,12 @@ class _DynamicLocationGalleryWidgetState galleryLoadEndTime, limit: null, asc: false, - ignoredCollectionIDs: collectionsToHide, + filterOptions: DBFilterOptions( + ignoredCollectionIDs: collectionsToHide, + hideIgnoredForUpload: true, + ), ); - removeIgnoredFiles = - FilesService.instance.removeIgnoredFiles(fileLoadResult); + super.initState(); } @@ -58,8 +59,6 @@ class _DynamicLocationGalleryWidgetState final selectedRadius = InheritedLocationTagData.of(context).selectedRadius; Future filterFiles() async { final FileLoadResult result = await fileLoadResult; - //wait for ignored files to be removed after init - await removeIgnoredFiles; final stopWatch = Stopwatch()..start(); final copyOfFiles = List.from(result.files); copyOfFiles.removeWhere((f) { diff --git a/lib/ui/viewer/location/location_screen.dart b/lib/ui/viewer/location/location_screen.dart index 6cec76cd6..387debe95 100644 --- a/lib/ui/viewer/location/location_screen.dart +++ b/lib/ui/viewer/location/location_screen.dart @@ -13,7 +13,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/collections_service.dart"; -import "package:photos/services/files_service.dart"; +import "package:photos/services/filter/db_filters.dart"; import "package:photos/services/location_service.dart"; import "package:photos/states/location_screen_state.dart"; import "package:photos/theme/colors.dart"; @@ -134,7 +134,7 @@ class LocationGalleryWidget extends StatefulWidget { class _LocationGalleryWidgetState extends State { late final Future fileLoadResult; - late Future removeIgnoredFiles; + late Widget galleryHeaderWidget; final _selectedFiles = SelectedFiles(); @override @@ -147,10 +147,11 @@ class _LocationGalleryWidgetState extends State { galleryLoadEndTime, limit: null, asc: false, - ignoredCollectionIDs: collectionsToHide, + filterOptions: DBFilterOptions( + ignoredCollectionIDs: collectionsToHide, + hideIgnoredForUpload: true, + ), ); - removeIgnoredFiles = - FilesService.instance.removeIgnoredFiles(fileLoadResult); galleryHeaderWidget = const GalleryHeaderWidget(); super.initState(); } @@ -172,7 +173,6 @@ class _LocationGalleryWidgetState extends State { Future filterFiles() async { final FileLoadResult result = await fileLoadResult; //wait for ignored files to be removed after init - await removeIgnoredFiles; final stopWatch = Stopwatch()..start(); final copyOfFiles = List.from(result.files); copyOfFiles.removeWhere((f) { diff --git a/lib/ui/viewer/location/pick_center_point_widget.dart b/lib/ui/viewer/location/pick_center_point_widget.dart index e501cae13..a8e73eeaa 100644 --- a/lib/ui/viewer/location/pick_center_point_widget.dart +++ b/lib/ui/viewer/location/pick_center_point_widget.dart @@ -13,7 +13,7 @@ import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location_tag/location_tag.dart"; import "package:photos/models/selected_files.dart"; import "package:photos/services/collections_service.dart"; -import "package:photos/services/ignored_files_service.dart"; +import "package:photos/services/filter/db_filters.dart"; import "package:photos/theme/colors.dart"; import "package:photos/theme/ente_theme.dart"; import "package:photos/ui/components/bottom_of_title_bar_widget.dart"; @@ -101,17 +101,10 @@ class PickCenterPointWidget extends StatelessWidget { galleryLoadEndTime, limit: null, asc: false, - ignoredCollectionIDs: collectionsToHide, - ); - - // hide ignored files from UI - final ignoredIDs = - await IgnoredFilesService.instance.ignoredIDs; - result.files.removeWhere( - (f) => - f.uploadedFileID == null && - IgnoredFilesService.instance - .shouldSkipUpload(ignoredIDs, f), + filterOptions: DBFilterOptions( + ignoredCollectionIDs: collectionsToHide, + hideIgnoredForUpload: true, + ), ); return result; }, diff --git a/scripts/create_tag.sh b/scripts/create_tag.sh new file mode 100755 index 000000000..0190afa88 --- /dev/null +++ b/scripts/create_tag.sh @@ -0,0 +1,66 @@ +#!/bin/sh + +#!/bin/bash + +# Function to display usage +usage() { + echo "Usage: $0 tag" + exit 1 +} + +# Ensure a tag was provided +[[ $# -eq 0 ]] && usage + +# Exit immediately if a command exits with a non-zero status +set -e + +# Go to the project root directory +cd "$(dirname "$0")/.." + +# Get the tag from the command line argument +TAG=$1 + +# Get the current branch +BRANCH=$(git rev-parse --abbrev-ref HEAD) + +# Get the version from the pubspec.yaml file and cut everything after the + +VERSION=$(grep "^version:" pubspec.yaml | awk '{ print $2 }' | cut -d '+' -f 1) + + +# Check the current branch and set the tag prefix +if [[ $BRANCH == "independent" ]]; then + PREFIX="v" +elif [[ $BRANCH == "f-droid" ]]; then + PREFIX="fdroid-" + # Additional checks for f-droid branch + # Verify that the pubspec.yaml doesn't contain certain words + WORDS=("in_app_purchase" "firebase") + for word in ${WORDS[@]}; do + if grep -q $word pubspec.yaml; then + echo "The pubspec.yaml file dependency on '$word', which is not allowed on the f-droid branch." + exit 1 + fi + done +else + echo "Tags can only be created on the independent or f-droid branches." + exit 1 +fi + +# Ensure the tag has the correct prefix +if [[ $TAG != $PREFIX* ]]; then + echo "Invalid tag. On the $BRANCH branch, tags must start with '$PREFIX'." + exit 1 +fi + +# Ensure the tag version is in the pubspec.yaml file +if [[ $TAG != *$VERSION ]]; then + echo "Invalid tag." + echo "The version $VERSION in pubspec doesn't match the version in tag $TAG." + exit 1 +fi + +## If all checks pass, create the tag +git tag $TAG +echo "Tag $TAG created." + +exit 0