Merge branch 'main' into map
This commit is contained in:
commit
01f16bddde
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
11
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal 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.
|
|
@ -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
14
hooks/pre-commit-fdroid
Executable 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
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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]!;
|
||||
|
|
30
lib/services/filter/collection_ignore.dart
Normal file
30
lib/services/filter/collection_ignore.dart
Normal 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!);
|
||||
}
|
||||
}
|
58
lib/services/filter/db_filters.dart
Normal file
58
lib/services/filter/db_filters.dart
Normal 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;
|
||||
}
|
20
lib/services/filter/dedupe_by_upload_id.dart
Normal file
20
lib/services/filter/dedupe_by_upload_id.dart
Normal 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;
|
||||
}
|
||||
}
|
5
lib/services/filter/filter.dart
Normal file
5
lib/services/filter/filter.dart
Normal file
|
@ -0,0 +1,5 @@
|
|||
import "package:photos/models/file.dart";
|
||||
|
||||
abstract class Filter {
|
||||
bool filter(File file);
|
||||
}
|
18
lib/services/filter/type_filter.dart
Normal file
18
lib/services/filter/type_filter.dart
Normal 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;
|
||||
}
|
||||
}
|
18
lib/services/filter/upload_ignore.dart
Normal file
18
lib/services/filter/upload_ignore.dart
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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>(),
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
|
|
|
@ -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 (_) {
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
},
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
66
scripts/create_tag.sh
Executable 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
|
Loading…
Reference in a new issue