From 2dc73c2987d3b955dd0a3d20f16fc9a9688523c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Sep 2024 14:48:32 -0500 Subject: [PATCH] quick filter for album --- .../albums/albums_collection.page.dart | 109 +++++++++++++--- .../lib/providers/album/albumv2.provider.dart | 119 ++++++++++++++++++ 2 files changed, 211 insertions(+), 17 deletions(-) create mode 100644 mobile/lib/providers/album/albumv2.provider.dart diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 48e8b349365ed..3a150c00fd15e 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -7,34 +7,30 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; +import 'package:immich_mobile/providers/album/albumv2.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; +enum QuickFilterMode { + all, + sharedWithMe, + myAlbums, +} + @RoutePage() class AlbumsCollectionPage extends HookConsumerWidget { const AlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProvider); + final albums = ref.watch(albumProviderV2); final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); - final remote = albums.where((a) => a.isRemote).toList(); - final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); + final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); final isGrid = useState(true); final searchController = useTextEditingController(); final debounceTimer = useRef(null); - - useEffect( - () { - Future.delayed(const Duration(seconds: 1), () { - ref.read(albumProvider.notifier).getAllAlbums(); - }); - return null; - }, - [], - ); + final filterMode = useState(QuickFilterMode.all); toggleViewMode() { isGrid.value = !isGrid.value; @@ -43,15 +39,23 @@ class AlbumsCollectionPage extends HookConsumerWidget { onSearch(String value) { debounceTimer.value?.cancel(); debounceTimer.value = Timer(const Duration(milliseconds: 300), () { - ref.read(albumProvider.notifier).searchAlbums(value); + filterMode.value = QuickFilterMode.all; + ref.read(albumProviderV2.notifier).searchAlbums(value); }); } + changeFilter(QuickFilterMode mode) { + filterMode.value = mode; + searchController.clear(); + ref.read(albumProviderV2.notifier).filterAlbums(mode); + } + useEffect( () { searchController.addListener(() { onSearch(searchController.text); }); + return () { searchController.removeListener(() { onSearch(searchController.text); @@ -64,7 +68,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: const Text("Albums"), + title: Text("Albums ${albums.length}"), ), body: ListView( shrinkWrap: true, @@ -93,7 +97,28 @@ class AlbumsCollectionPage extends HookConsumerWidget { ), ), ), - const SizedBox(height: 16), + const SizedBox(height: 8), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + QuickFilterButton( + label: 'All', + isSelected: filterMode.value == QuickFilterMode.all, + onTap: () => changeFilter(QuickFilterMode.all), + ), + QuickFilterButton( + label: 'Shared with me', + isSelected: filterMode.value == QuickFilterMode.sharedWithMe, + onTap: () => changeFilter(QuickFilterMode.sharedWithMe), + ), + QuickFilterButton( + label: 'My albums', + isSelected: filterMode.value == QuickFilterMode.myAlbums, + onTap: () => changeFilter(QuickFilterMode.myAlbums), + ), + ], + ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -154,6 +179,56 @@ class AlbumsCollectionPage extends HookConsumerWidget { } } +class QuickFilterButton extends StatelessWidget { + const QuickFilterButton({ + super.key, + required this.isSelected, + required this.onTap, + required this.label, + }); + + final bool isSelected; + final VoidCallback onTap; + final String label; + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: onTap, + icon: isSelected + ? Icon( + Icons.check_rounded, + color: context.colorScheme.onPrimary, + size: 18, + ) + : const SizedBox.shrink(), + style: ButtonStyle( + backgroundColor: WidgetStateProperty.all( + isSelected ? context.colorScheme.primary : Colors.transparent, + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(25), + width: 1, + ), + ), + ), + ), + label: Text( + label, + style: TextStyle( + color: isSelected + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + fontSize: 14, + ), + ), + ); + } +} + class SortButton extends ConsumerWidget { const SortButton({super.key}); diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart new file mode 100644 index 0000000000000..7d42ac98e4a3e --- /dev/null +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -0,0 +1,119 @@ +import 'dart:async'; + +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/album.entity.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:isar/isar.dart'; + +class AlbumNotifierV2 extends StateNotifier> { + AlbumNotifierV2(this._albumService, this.db) : super([]) { + final query = db.albums.filter().remoteIdIsNotNull(); + + query.findAll().then((value) { + if (mounted) { + state = value; + } + }); + _streamSub = query.watch().listen((data) => state = data); + } + + final AlbumService _albumService; + final Isar db; + late final StreamSubscription> _streamSub; + + Future getAllAlbums() { + return Future.wait([ + _albumService.refreshDeviceAlbums(), + _albumService.refreshRemoteAlbums(isShared: true), + ]); + } + + Future getDeviceAlbums() { + return _albumService.refreshDeviceAlbums(); + } + + Future deleteAlbum(Album album) { + return _albumService.deleteAlbum(album); + } + + Future createAlbum( + String albumTitle, + Set assets, + ) { + return _albumService.createAlbum(albumTitle, assets, []); + } + + Future getAlbumByName(String albumName, {bool remoteOnly = false}) { + return _albumService.getAlbumByName(albumName, remoteOnly); + } + + /// Create an album on the server with the same name as the selected album for backup + /// First this will check if the album already exists on the server with name + /// If it does not exist, it will create the album on the server + Future createSyncAlbum( + String albumName, + ) async { + final album = await getAlbumByName(albumName, remoteOnly: true); + if (album != null) { + return; + } + + await createAlbum(albumName, {}); + } + + void searchAlbums(String value) async { + final query = db.albums + .filter() + .remoteIdIsNotNull() + .nameContains(value, caseSensitive: false); + + final albums = await query.findAll(); + state = albums; + } + + void filterAlbums(QuickFilterMode mode) async { + switch (mode) { + case QuickFilterMode.all: + state = await db.albums.filter().remoteIdIsNotNull().findAll(); + return; + case QuickFilterMode.sharedWithMe: + state = await db.albums + .filter() + .remoteIdIsNotNull() + .owner( + (q) => + q.not().isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ) + .findAll(); + return; + case QuickFilterMode.myAlbums: + state = await db.albums + .filter() + .remoteIdIsNotNull() + .owner( + (q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId), + ) + .findAll(); + return; + } + } + + @override + void dispose() { + _streamSub.cancel(); + super.dispose(); + } +} + +final albumProviderV2 = + StateNotifierProvider.autoDispose>((ref) { + return AlbumNotifierV2( + ref.watch(albumServiceProvider), + ref.watch(dbProvider), + ); +});