diff --git a/lib/model/entry.dart b/lib/model/entry.dart index 6177764b8..5da5eb27a 100644 --- a/lib/model/entry.dart +++ b/lib/model/entry.dart @@ -160,6 +160,7 @@ class AvesEntry { String? get path => _path; + // directory path, without the trailing separator String? get directory { _directory ??= path != null ? pContext.dirname(path!) : null; return _directory; @@ -170,6 +171,7 @@ class AvesEntry { return _filename; } + // file extension, including the `.` String? get extension { _extension ??= path != null ? pContext.extension(path!) : null; return _extension; diff --git a/lib/model/filters/path.dart b/lib/model/filters/path.dart index 8ae97aeab..406b7de89 100644 --- a/lib/model/filters/path.dart +++ b/lib/model/filters/path.dart @@ -1,14 +1,19 @@ import 'package:aves/model/filters/filters.dart'; +import 'package:aves/services/common/services.dart'; class PathFilter extends CollectionFilter { static const type = 'path'; + // including trailing separator final String path; + // without trailing separator + final String _rootAlbum; + @override List get props => [path]; - const PathFilter(this.path); + PathFilter(this.path) : _rootAlbum = path.substring(0, path.length - 1); PathFilter.fromMap(Map json) : this( @@ -22,7 +27,12 @@ class PathFilter extends CollectionFilter { }; @override - EntryFilter get test => (entry) => entry.directory?.startsWith(path) ?? false; + EntryFilter get test => (entry) { + final dir = entry.directory; + if (dir == null) return false; + // avoid string building in most cases + return dir.startsWith(_rootAlbum) && '$dir${pContext.separator}'.startsWith(path); + }; @override String get universalLabel => path; diff --git a/test/model/filters_test.dart b/test/model/filters_test.dart index f8a8ed2ad..b111cc957 100644 --- a/test/model/filters_test.dart +++ b/test/model/filters_test.dart @@ -3,12 +3,27 @@ import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; +import 'package:aves/model/filters/path.dart'; import 'package:aves/model/filters/query.dart'; import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/filters/type.dart'; +import 'package:aves/services/common/services.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import '../fake/media_store_service.dart'; +import '../fake/storage_service.dart'; + void main() { + setUp(() async { + // specify Posix style path context for consistent behaviour when running tests on Windows + getIt.registerLazySingleton(() => p.Context(style: p.Style.posix)); + }); + + tearDown(() async { + await getIt.reset(); + }); + test('Filter serialization', () { CollectionFilter? jsonRoundTrip(filter) => CollectionFilter.fromJson(filter.toJson()); @@ -21,16 +36,34 @@ void main() { final location = LocationFilter(LocationLevel.country, 'France${LocationFilter.locationSeparator}FR'); expect(location, jsonRoundTrip(location)); - final type = TypeFilter.sphericalVideo; - expect(type, jsonRoundTrip(type)); - final mime = MimeFilter.video; expect(mime, jsonRoundTrip(mime)); + final path = PathFilter('/some/path/'); + expect(path, jsonRoundTrip(path)); + final query = QueryFilter('some query'); expect(query, jsonRoundTrip(query)); final tag = TagFilter('some tag'); expect(tag, jsonRoundTrip(tag)); + + final type = TypeFilter.sphericalVideo; + expect(type, jsonRoundTrip(type)); + }); + + test('Path filter', () { + const rootAlbum = '${FakeStorageService.primaryPath}Pictures/test'; + const subAlbum = '${FakeStorageService.primaryPath}Pictures/test/sub'; + const siblingAlbum = '${FakeStorageService.primaryPath}Pictures/test sibling'; + + final rootImage = FakeMediaStoreService.newImage(rootAlbum, 'image1'); + final subImage = FakeMediaStoreService.newImage(subAlbum, 'image1'); + final siblingImage = FakeMediaStoreService.newImage(siblingAlbum, 'image1'); + + final path = PathFilter('$rootAlbum/'); + expect(path.test(rootImage), true); + expect(path.test(subImage), true); + expect(path.test(siblingImage), false); }); }