Merge branch 'main' into map

This commit is contained in:
Neeraj Gupta 2023-06-20 20:23:13 +05:30
commit 01f16bddde
23 changed files with 477 additions and 170 deletions

View file

@ -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.

View file

@ -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

14
hooks/pre-commit-fdroid Executable file
View file

@ -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

View file

@ -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<int>? ignoredCollectionIDs,
DBFilterOptions? filterOptions,
bool applyOwnerCheck = false,
}) async {
final stopWatch = Stopwatch()..start();
final stopWatch = EnteWatch('getAllPendingOrUploadedFiles')..start();
late String whereQuery;
late List<Object?>? whereArgs;
if (applyOwnerCheck) {
@ -537,14 +537,13 @@ class FilesDB {
'$columnCreationTime ' + order + ', $columnModificationTime ' + order,
limit: limit,
);
stopWatch.log('queryDone');
final files = convertToFiles(results);
final List<File> 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<FileLoadResult> getAllLocalAndUploadedFiles(
@ -553,7 +552,7 @@ class FilesDB {
int ownerID, {
int? limit,
bool? asc,
Set<int>? 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<File> deduplicatedFiles =
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
return FileLoadResult(deduplicatedFiles, files.length == limit);
final List<File> filteredFiles = await applyDBFilters(files, filterOptions);
return FileLoadResult(filteredFiles, files.length == limit);
}
List<File> deduplicateByLocalID(List<File> files) {
@ -590,43 +588,6 @@ class FilesDB {
return deduplicatedFiles;
}
List<File> _deduplicatedAndFilterIgnoredFiles(
List<File> files,
Set<int>? ignoredCollectionIDs,
) {
final Set<int> uploadedFileIDs = <int>{};
// ignoredFileUploadIDs is to keep a track of files which are part of
// archived collection
final Set<int> ignoredFileUploadIDs = <int>{};
final List<File> 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<FileLoadResult> 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<Map<int, int>> 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 = <int, int>{};
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<File?> 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<void> markForReUploadIfLocationMissing(List<String> 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<List<File>> getFilesForLocalIDs(
List<String> 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<List<int>> getUploadIDsWithMissingSize(int userId) async {
final db = await instance.database;
@ -1484,8 +1477,10 @@ class FilesDB {
final List<Map<String, dynamic>> result =
await db.query(filesTable, orderBy: '$columnCreationTime DESC');
final List<File> files = convertToFiles(result);
final List<File> deduplicatedFiles =
_deduplicatedAndFilterIgnoredFiles(files, collectionsToIgnore);
final List<File> deduplicatedFiles = await applyDBFilters(
files,
DBFilterOptions(ignoredCollectionIDs: collectionsToIgnore),
);
return deduplicatedFiles;
}
@ -1509,7 +1504,7 @@ class FilesDB {
int endTime, {
int? limit,
bool? asc,
Set<int>? 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<File> deduplicatedFiles =
_deduplicatedAndFilterIgnoredFiles(files, ignoredCollectionIDs);
return FileLoadResult(deduplicatedFiles, files.length == limit);
final List<File> filteredFiles = await applyDBFilters(files, filterOptions);
return FileLoadResult(filteredFiles, files.length == limit);
}
Map<String, dynamic> _getRowForFile(File file) {

View file

@ -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;
}
}

View file

@ -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.

View file

@ -218,6 +218,36 @@ class CollectionsService {
return _cachedLatestFiles!;
}
final Map<String, File> _coverCache = <String, File>{};
Future<File?> 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<bool> 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<Collection> getCollectionsForUI({
bool includedShared = false,
bool includeCollab = false,
}) {
final Set<CollectionParticipantRole> 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]!;

View file

@ -0,0 +1,30 @@
import "package:photos/models/file.dart";
import "package:photos/services/filter/filter.dart";
class CollectionsIgnoreFilter extends Filter {
final Set<int> collectionIDs;
Set<int>? _ignoredUploadIDs;
CollectionsIgnoreFilter(this.collectionIDs, List<File> files) : super() {
init(files);
}
void init(List<File> 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!);
}
}

View file

@ -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<int>? ignoredCollectionIDs;
bool dedupeUploadID;
bool hideIgnoredForUpload;
DBFilterOptions({
this.ignoredCollectionIDs,
this.hideIgnoredForUpload = false,
this.dedupeUploadID = true,
});
static DBFilterOptions dedupeOption = DBFilterOptions(
dedupeUploadID: true,
);
}
Future<List<File>> applyDBFilters(
List<File> files,
DBFilterOptions? options,
) async {
if (options == null) {
return files;
}
final List<Filter> filters = [];
if (options.hideIgnoredForUpload) {
final Set<String> 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<File> filterFiles = [];
for (final file in files) {
if (filters.every((f) => f.filter(file))) {
filterFiles.add(file);
}
}
return filterFiles;
}

View file

@ -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<int> trackedUploadIDs = {};
@override
bool filter(File file) {
if (!file.isUploaded) {
return true;
}
if (trackedUploadIDs.contains(file.uploadedFileID!)) {
return false;
}
trackedUploadIDs.add(file.uploadedFileID!);
return true;
}
}

View file

@ -0,0 +1,5 @@
import "package:photos/models/file.dart";
abstract class Filter {
bool filter(File file);
}

View file

@ -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;
}
}

View file

@ -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<String> 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);
}
}

View file

@ -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<LocalPhotosUpdatedEvent>(),

View file

@ -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<File?>(
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,
),
);
},

View file

@ -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<File?>(
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);
},

View file

@ -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<DeleteEmptyAlbums> {
}
Future<void> _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<DeleteEmptyAlbums> {
S.of(context).deleteProgress(currentlyDeleting, collections.length);
try {
await CollectionsService.instance.trashEmptyCollection(
collections[i].collection,
collections[i],
isBulkDelete: true,
);
} catch (_) {

View file

@ -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,
);
},

View file

@ -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<GalleryAppBarWidget> {
List<Widget> _getDefaultActions(BuildContext context) {
final List<Widget> actions = <Widget>[];
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<GalleryAppBarWidget> {
}
Future<void> _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,

View file

@ -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<DynamicLocationGalleryWidget> {
late final Future<FileLoadResult> fileLoadResult;
late Future<void> 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<FileLoadResult> 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<File>.from(result.files);
copyOfFiles.removeWhere((f) {

View file

@ -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<LocationGalleryWidget> {
late final Future<FileLoadResult> fileLoadResult;
late Future<void> removeIgnoredFiles;
late Widget galleryHeaderWidget;
final _selectedFiles = SelectedFiles();
@override
@ -147,10 +147,11 @@ class _LocationGalleryWidgetState extends State<LocationGalleryWidget> {
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<LocationGalleryWidget> {
Future<FileLoadResult> 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<File>.from(result.files);
copyOfFiles.removeWhere((f) {

View file

@ -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;
},

66
scripts/create_tag.sh Executable file
View file

@ -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