From a1df3878a952cbd54e533b4239ac7fc1962b509c Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 5 Sep 2024 15:29:33 -0500 Subject: [PATCH 01/26] add collection page --- .../pages/collections/collections.page.dart | 118 ++++++++++++++++++ .../lib/pages/common/tab_controller.page.dart | 19 +++ .../providers/app_life_cycle.provider.dart | 2 + mobile/lib/providers/tab.provider.dart | 7 +- mobile/lib/routing/router.dart | 5 + mobile/lib/routing/router.gr.dart | 19 +++ mobile/lib/widgets/common/immich_app_bar.dart | 2 +- 7 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 mobile/lib/pages/collections/collections.page.dart diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart new file mode 100644 index 0000000000000..6863bb9a7de88 --- /dev/null +++ b/mobile/lib/pages/collections/collections.page.dart @@ -0,0 +1,118 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; + +@RoutePage() +class CollectionsPage extends StatelessWidget { + const CollectionsPage({super.key}); + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: const ImmichAppBar( + action: CreateNewButton(), + ), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: ListView( + shrinkWrap: true, + children: [ + Row( + children: [ + ActionButton( + onPressed: () {}, + icon: Icons.favorite_outline_rounded, + label: 'Favorite', + ), + const SizedBox(width: 8), + ActionButton( + onPressed: () {}, + icon: Icons.delete_outline_rounded, + label: 'Trash', + ), + ], + ), + const SizedBox(height: 8), + Row( + children: [ + ActionButton( + onPressed: () {}, + icon: Icons.link_outlined, + label: 'Shared links', + ), + const SizedBox(width: 8), + ActionButton( + onPressed: () {}, + icon: Icons.archive_outlined, + label: 'Archive', + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ), + ); + } +} + +class ActionButton extends StatelessWidget { + final VoidCallback onPressed; + final IconData icon; + final String label; + + const ActionButton({ + super.key, + required this.onPressed, + required this.icon, + required this.label, + }); + + @override + Widget build(BuildContext context) { + return Expanded( + child: FilledButton.icon( + onPressed: onPressed, + label: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + label, + style: TextStyle( + color: context.colorScheme.onSurface, + ), + ), + ), + style: FilledButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + backgroundColor: context.colorScheme.surfaceContainer, + alignment: Alignment.centerLeft, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + icon: Icon( + icon, + color: context.primaryColor, + ), + ), + ); + } +} + +class CreateNewButton extends StatelessWidget { + const CreateNewButton({super.key}); + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () {}, + borderRadius: const BorderRadius.all(Radius.circular(25)), + child: Icon( + Icons.add, + size: 32, + semanticLabel: 'profile_drawer_trash'.tr(), + ), + ); + } +} diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index b619e003d2c3a..f862213040e89 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -94,6 +94,12 @@ class TabControllerPage extends HookConsumerWidget { selectedIcon: const Icon(Icons.photo_album), label: const Text('tab_controller_nav_library').tr(), ), + NavigationRailDestination( + padding: const EdgeInsets.all(4), + icon: const Icon(Icons.photo_album_outlined), + selectedIcon: const Icon(Icons.photo_album), + label: const Text('Collections').tr(), + ), ], ); } @@ -156,6 +162,18 @@ class TabControllerPage extends HookConsumerWidget { ), ), ), + NavigationDestination( + label: 'Collections'.tr(), + icon: const Icon( + Icons.photo_album_outlined, + ), + selectedIcon: buildIcon( + Icon( + Icons.photo_album_rounded, + color: context.primaryColor, + ), + ), + ), ], ); } @@ -167,6 +185,7 @@ class TabControllerPage extends HookConsumerWidget { SearchRoute(), SharingRoute(), LibraryRoute(), + CollectionsRoute(), ], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition( diff --git a/mobile/lib/providers/app_life_cycle.provider.dart b/mobile/lib/providers/app_life_cycle.provider.dart index 938961efb62ac..5dd02d6ad7e55 100644 --- a/mobile/lib/providers/app_life_cycle.provider.dart +++ b/mobile/lib/providers/app_life_cycle.provider.dart @@ -63,6 +63,8 @@ class AppLifeCycleNotifier extends StateNotifier { _ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); case TabEnum.library: _ref.read(albumProvider.notifier).getAllAlbums(); + case TabEnum.collections: + // nothing to do } } diff --git a/mobile/lib/providers/tab.provider.dart b/mobile/lib/providers/tab.provider.dart index 2abed7c395e50..16dfbfd5eaa12 100644 --- a/mobile/lib/providers/tab.provider.dart +++ b/mobile/lib/providers/tab.provider.dart @@ -1,11 +1,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; -enum TabEnum { - home, - search, - sharing, - library, -} +enum TabEnum { home, search, sharing, library, collections } /// Provides the currently active tab final tabProvider = StateProvider( diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 211c847726095..0d9842f2eccf1 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; +import 'package:immich_mobile/pages/collections/collections.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart'; import 'package:immich_mobile/pages/common/album_asset_selection.page.dart'; @@ -114,6 +115,10 @@ class AppRouter extends RootStackRouter { page: LibraryRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: CollectionsRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ], transitionsBuilder: TransitionsBuilders.fadeIn, ), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 90fc4cb0fe96c..1e4dcee1254fd 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -555,6 +555,25 @@ class ChangePasswordRoute extends PageRouteInfo { ); } +/// generated route for +/// [CollectionsPage] +class CollectionsRoute extends PageRouteInfo { + const CollectionsRoute({List? children}) + : super( + CollectionsRoute.name, + initialChildren: children, + ); + + static const String name = 'CollectionsRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const CollectionsPage(); + }, + ); +} + /// generated route for /// [CreateAlbumPage] class CreateAlbumRoute extends PageRouteInfo { diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 8e2465fc9ca3d..4b1640f698396 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -185,7 +185,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { ), actions: [ if (action != null) - Padding(padding: const EdgeInsets.only(right: 20), child: action!), + Padding(padding: const EdgeInsets.only(right: 16), child: action!), Padding( padding: const EdgeInsets.only(right: 20), child: buildBackupIndicator(), From 14a5e982e6d4a197cf6eb257880441af7ff3ff06 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 11:29:29 -0500 Subject: [PATCH 02/26] Added people collections --- .../pages/collections/collections.page.dart | 88 ++++++++++++++++++- 1 file changed, 85 insertions(+), 3 deletions(-) diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 6863bb9a7de88..5254fb546a7e4 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -1,7 +1,12 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; @RoutePage() @@ -49,7 +54,15 @@ class CollectionsPage extends StatelessWidget { ), ], ), - const SizedBox(height: 8), + const SizedBox(height: 16), + const Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + PeopleCollectionCard(), + AlbumsCollectionCard(), + ], + ), ], ), ), @@ -57,6 +70,75 @@ class CollectionsPage extends StatelessWidget { } } +class PeopleCollectionCard extends ConsumerWidget { + const PeopleCollectionCard({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final people = ref.watch(getAllPeopleProvider); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: MediaQuery.of(context).size.width * 0.5, + width: MediaQuery.of(context).size.width * 0.5 - 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.surfaceContainer, + ), + child: people.widgetWhen( + onData: (people) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.only( + top: 18, + left: 12, + right: 12, + bottom: 0, + ), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: people.take(4).map((person) { + return CircleAvatar( + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: ApiService.getRequestHeaders(), + ), + ); + }).toList(), + ); + }, + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text('People', style: context.textTheme.labelLarge), + ), + ], + ); + } +} + +class AlbumsCollectionCard extends StatelessWidget { + const AlbumsCollectionCard({super.key}); + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.of(context).size.width * 0.5, + width: MediaQuery.of(context).size.width * 0.5 - 12, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.surfaceContainer, + ), + child: const Center( + child: Text('Album Collection'), + ), + ); + } +} + class ActionButton extends StatelessWidget { final VoidCallback onPressed; final IconData icon; @@ -85,8 +167,8 @@ class ActionButton extends StatelessWidget { ), style: FilledButton.styleFrom( elevation: 0, - padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), - backgroundColor: context.colorScheme.surfaceContainer, + padding: const EdgeInsets.all(16), + backgroundColor: context.colorScheme.primary.withAlpha(20), alignment: Alignment.centerLeft, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(20)), From c886fcab74e39f4eb46fd34ce4b630b474700e84 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 13:59:32 -0500 Subject: [PATCH 03/26] Added share partner button --- .../pages/collections/collections.page.dart | 151 +++++++++++++----- mobile/lib/pages/library/library.page.dart | 6 +- mobile/lib/pages/sharing/sharing.page.dart | 17 +- .../widgets/album/album_thumbnail_card.dart | 28 ++-- mobile/lib/widgets/common/immich_app_bar.dart | 23 ++- .../widgets/common/share_partner_button.dart | 21 +++ .../widgets/search/search_map_thumbnail.dart | 1 + 7 files changed, 171 insertions(+), 76 deletions(-) create mode 100644 mobile/lib/widgets/common/share_partner_button.dart diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 5254fb546a7e4..b639fa2b5fbea 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -1,13 +1,17 @@ import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; +import 'package:immich_mobile/widgets/common/share_partner_button.dart'; +import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class CollectionsPage extends StatelessWidget { @@ -16,10 +20,11 @@ class CollectionsPage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( appBar: const ImmichAppBar( - action: CreateNewButton(), + showUploadButton: false, + actions: [CreateNewButton(), SharePartnerButton()], ), body: Padding( - padding: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16), child: ListView( shrinkWrap: true, children: [ @@ -33,8 +38,8 @@ class CollectionsPage extends StatelessWidget { const SizedBox(width: 8), ActionButton( onPressed: () {}, - icon: Icons.delete_outline_rounded, - label: 'Trash', + icon: Icons.archive_outlined, + label: 'Archive', ), ], ), @@ -49,18 +54,22 @@ class CollectionsPage extends StatelessWidget { const SizedBox(width: 8), ActionButton( onPressed: () {}, - icon: Icons.archive_outlined, - label: 'Archive', + icon: Icons.delete_outline_rounded, + label: 'Trash', ), ], ), - const SizedBox(height: 16), - const Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + const SizedBox(height: 24), + const Wrap( + spacing: 8, + runSpacing: 16, children: [ PeopleCollectionCard(), AlbumsCollectionCard(), + AlbumsCollectionCard( + isLocal: true, + ), + PlacesCollectionCard(), ], ), ], @@ -81,22 +90,17 @@ class PeopleCollectionCard extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - height: MediaQuery.of(context).size.width * 0.5, - width: MediaQuery.of(context).size.width * 0.5 - 12, + height: MediaQuery.of(context).size.width * 0.5 - 20, + width: MediaQuery.of(context).size.width * 0.5 - 20, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), - color: context.colorScheme.surfaceContainer, + color: context.colorScheme.secondaryContainer.withAlpha(100), ), child: people.widgetWhen( onData: (people) { return GridView.count( crossAxisCount: 2, - padding: const EdgeInsets.only( - top: 18, - left: 12, - right: 12, - bottom: 0, - ), + padding: const EdgeInsets.all(12), crossAxisSpacing: 8, mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), @@ -121,20 +125,85 @@ class PeopleCollectionCard extends ConsumerWidget { } } -class AlbumsCollectionCard extends StatelessWidget { - const AlbumsCollectionCard({super.key}); +class AlbumsCollectionCard extends ConsumerWidget { + final bool isLocal; + + const AlbumsCollectionCard({super.key, this.isLocal = false}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albums = isLocal + ? ref.watch(albumProvider).where((album) => album.isLocal) + : ref.watch(albumProvider).where((album) => album.isRemote); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: MediaQuery.of(context).size.width * 0.5 - 20, + width: MediaQuery.of(context).size.width * 0.5 - 20, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: albums.take(4).map((album) { + return AlbumThumbnailCard( + album: album, + showTitle: false, + ); + }).toList(), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + isLocal ? 'On this device' : 'Albums', + style: context.textTheme.labelLarge, + ), + ), + ], + ); + } +} + +class PlacesCollectionCard extends StatelessWidget { + const PlacesCollectionCard({super.key}); @override Widget build(BuildContext context) { - return Container( - height: MediaQuery.of(context).size.width * 0.5, - width: MediaQuery.of(context).size.width * 0.5 - 12, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.surfaceContainer, - ), - child: const Center( - child: Text('Album Collection'), - ), + final size = MediaQuery.of(context).size.width * 0.5 - 20; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: IgnorePointer( + child: MapThumbnail( + zoom: 5, + centre: const LatLng( + 47, + 5, + ), + showAttribution: false, + themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Places', style: context.textTheme.labelLarge), + ), + ], ); } } @@ -157,21 +226,26 @@ class ActionButton extends StatelessWidget { child: FilledButton.icon( onPressed: onPressed, label: Padding( - padding: const EdgeInsets.only(left: 8.0), + padding: const EdgeInsets.only(left: 4.0), child: Text( label, style: TextStyle( color: context.colorScheme.onSurface, + fontSize: 14, ), ), ), style: FilledButton.styleFrom( elevation: 0, - padding: const EdgeInsets.all(16), - backgroundColor: context.colorScheme.primary.withAlpha(20), + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), + backgroundColor: context.colorScheme.surfaceContainerLow, alignment: Alignment.centerLeft, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(25)), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), ), ), icon: Icon( @@ -190,10 +264,9 @@ class CreateNewButton extends StatelessWidget { return InkWell( onTap: () {}, borderRadius: const BorderRadius.all(Radius.circular(25)), - child: Icon( + child: const Icon( Icons.add, size: 32, - semanticLabel: 'profile_drawer_trash'.tr(), ), ); } diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index 5f03ed68714c8..99d048a8bd19a 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -184,7 +184,7 @@ class LibraryPage extends HookConsumerWidget { final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); final local = albums.where((a) => a.isLocal).toList(); - Widget? shareTrashButton() { + Widget shareTrashButton() { return trashEnabled ? InkWell( onTap: () => context.pushRoute(const TrashRoute()), @@ -195,12 +195,12 @@ class LibraryPage extends HookConsumerWidget { semanticLabel: 'profile_drawer_trash'.tr(), ), ) - : null; + : const SizedBox.shrink(); } return Scaffold( appBar: ImmichAppBar( - action: shareTrashButton(), + actions: [shareTrashButton()], ), body: CustomScrollView( slivers: [ diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart index 98d4cfafe9fe5..903fe6a35c4d9 100644 --- a/mobile/lib/pages/sharing/sharing.page.dart +++ b/mobile/lib/pages/sharing/sharing.page.dart @@ -9,6 +9,7 @@ import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dar import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; import 'package:immich_mobile/providers/partner.provider.dart'; +import 'package:immich_mobile/widgets/common/share_partner_button.dart'; import 'package:immich_mobile/widgets/partner/partner_list.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -215,25 +216,13 @@ class SharingPage extends HookConsumerWidget { ); } - Widget sharePartnerButton() { - return InkWell( - onTap: () => context.pushRoute(const PartnerRoute()), - borderRadius: const BorderRadius.all(Radius.circular(12)), - child: Icon( - Icons.swap_horizontal_circle_rounded, - size: 25, - semanticLabel: 'partner_page_title'.tr(), - ), - ); - } - return RefreshIndicator( onRefresh: () async { ref.read(sharedAlbumProvider.notifier).getAllSharedAlbums(); }, child: Scaffold( - appBar: ImmichAppBar( - action: sharePartnerButton(), + appBar: const ImmichAppBar( + actions: [SharePartnerButton()], ), body: CustomScrollView( slivers: [ diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 42fa55cdd4459..96050c65bd00b 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -12,12 +12,14 @@ class AlbumThumbnailCard extends StatelessWidget { /// Whether or not to show the owner of the album (or "Owned") /// in the subtitle of the album final bool showOwner; + final bool showTitle; const AlbumThumbnailCard({ super.key, required this.album, this.onTap, this.showOwner = false, + this.showTitle = true, }); final Album album; @@ -102,21 +104,23 @@ class AlbumThumbnailCard extends StatelessWidget { : buildAlbumThumbnail(), ), ), - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: SizedBox( - width: cardSize, - child: Text( - album.name, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( - color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, + if (showTitle) ...[ + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: SizedBox( + width: cardSize, + child: Text( + album.name, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), ), ), ), - ), - buildAlbumTextRow(), + buildAlbumTextRow(), + ], ], ), ), diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 4b1640f698396..1831a2d1689ab 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -18,9 +18,10 @@ import 'package:immich_mobile/providers/server_info.provider.dart'; class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { @override Size get preferredSize => const Size.fromHeight(kToolbarHeight); - final Widget? action; + final List? actions; + final bool showUploadButton; - const ImmichAppBar({super.key, this.action}); + const ImmichAppBar({super.key, this.actions, this.showUploadButton = true}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -184,12 +185,18 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { }, ), actions: [ - if (action != null) - Padding(padding: const EdgeInsets.only(right: 16), child: action!), - Padding( - padding: const EdgeInsets.only(right: 20), - child: buildBackupIndicator(), - ), + if (actions != null) + ...actions!.map( + (action) => Padding( + padding: const EdgeInsets.only(right: 16), + child: action, + ), + ), + if (showUploadButton) + Padding( + padding: const EdgeInsets.only(right: 20), + child: buildBackupIndicator(), + ), Padding( padding: const EdgeInsets.only(right: 20), child: buildProfileIndicator(), diff --git a/mobile/lib/widgets/common/share_partner_button.dart b/mobile/lib/widgets/common/share_partner_button.dart new file mode 100644 index 0000000000000..6cce18d23827a --- /dev/null +++ b/mobile/lib/widgets/common/share_partner_button.dart @@ -0,0 +1,21 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:immich_mobile/routing/router.dart'; + +class SharePartnerButton extends StatelessWidget { + const SharePartnerButton({super.key}); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () => context.pushRoute(const PartnerRoute()), + borderRadius: const BorderRadius.all(Radius.circular(12)), + child: Icon( + Icons.swap_horizontal_circle_rounded, + size: 25, + semanticLabel: 'partner_page_title'.tr(), + ), + ); + } +} diff --git a/mobile/lib/widgets/search/search_map_thumbnail.dart b/mobile/lib/widgets/search/search_map_thumbnail.dart index 20747913fb14a..b4a12ab82634b 100644 --- a/mobile/lib/widgets/search/search_map_thumbnail.dart +++ b/mobile/lib/widgets/search/search_map_thumbnail.dart @@ -13,6 +13,7 @@ class SearchMapThumbnail extends StatelessWidget { }); final double size; + final bool showTitle = true; @override Widget build(BuildContext context) { From 9d6a177547c493dd2b30f2bcbcfb3b070e350858 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 14:42:02 -0500 Subject: [PATCH 04/26] Added collection pages --- .../albums/albums_collection.page.dart | 12 +++ .../albums/local_albums_collection.page.dart | 12 +++ .../albums/people_collection.page.dart | 12 +++ .../albums/places_collection.part.dart | 12 +++ .../pages/collections/collections.page.dart | 27 +++---- mobile/lib/routing/router.dart | 20 +++++ mobile/lib/routing/router.gr.dart | 76 +++++++++++++++++++ 7 files changed, 158 insertions(+), 13 deletions(-) create mode 100644 mobile/lib/pages/collections/albums/albums_collection.page.dart create mode 100644 mobile/lib/pages/collections/albums/local_albums_collection.page.dart create mode 100644 mobile/lib/pages/collections/albums/people_collection.page.dart create mode 100644 mobile/lib/pages/collections/albums/places_collection.part.dart diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart new file mode 100644 index 0000000000000..6e90ce862045b --- /dev/null +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -0,0 +1,12 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +@RoutePage() +class AlbumsCollectionPage extends HookConsumerWidget { + const AlbumsCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container(); + } +} diff --git a/mobile/lib/pages/collections/albums/local_albums_collection.page.dart b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart new file mode 100644 index 0000000000000..2352ea3a568b9 --- /dev/null +++ b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart @@ -0,0 +1,12 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +@RoutePage() +class LocalAlbumsCollectionPage extends HookConsumerWidget { + const LocalAlbumsCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container(); + } +} diff --git a/mobile/lib/pages/collections/albums/people_collection.page.dart b/mobile/lib/pages/collections/albums/people_collection.page.dart new file mode 100644 index 0000000000000..d5c58fb922dec --- /dev/null +++ b/mobile/lib/pages/collections/albums/people_collection.page.dart @@ -0,0 +1,12 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +@RoutePage() +class PeopleCollectionPage extends HookConsumerWidget { + const PeopleCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container(); + } +} diff --git a/mobile/lib/pages/collections/albums/places_collection.part.dart b/mobile/lib/pages/collections/albums/places_collection.part.dart new file mode 100644 index 0000000000000..9fce76c597c88 --- /dev/null +++ b/mobile/lib/pages/collections/albums/places_collection.part.dart @@ -0,0 +1,12 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +@RoutePage() +class PlacesCollectionPage extends HookConsumerWidget { + const PlacesCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + return Container(); + } +} diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index b639fa2b5fbea..042287705f615 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; @@ -31,13 +32,13 @@ class CollectionsPage extends StatelessWidget { Row( children: [ ActionButton( - onPressed: () {}, + onPressed: () => context.pushRoute(const FavoritesRoute()), icon: Icons.favorite_outline_rounded, label: 'Favorite', ), const SizedBox(width: 8), ActionButton( - onPressed: () {}, + onPressed: () => context.pushRoute(const ArchiveRoute()), icon: Icons.archive_outlined, label: 'Archive', ), @@ -47,13 +48,13 @@ class CollectionsPage extends StatelessWidget { Row( children: [ ActionButton( - onPressed: () {}, + onPressed: () => context.pushRoute(const SharedLinkRoute()), icon: Icons.link_outlined, label: 'Shared links', ), const SizedBox(width: 8), ActionButton( - onPressed: () {}, + onPressed: () => context.pushRoute(const TrashRoute()), icon: Icons.delete_outline_rounded, label: 'Trash', ), @@ -85,13 +86,13 @@ class PeopleCollectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final people = ref.watch(getAllPeopleProvider); - + final size = MediaQuery.of(context).size.width * 0.5 - 20; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - height: MediaQuery.of(context).size.width * 0.5 - 20, - width: MediaQuery.of(context).size.width * 0.5 - 20, + height: size, + width: size, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: context.colorScheme.secondaryContainer.withAlpha(100), @@ -135,13 +136,13 @@ class AlbumsCollectionCard extends ConsumerWidget { final albums = isLocal ? ref.watch(albumProvider).where((album) => album.isLocal) : ref.watch(albumProvider).where((album) => album.isRemote); - + final size = MediaQuery.of(context).size.width * 0.5 - 20; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - height: MediaQuery.of(context).size.width * 0.5 - 20, - width: MediaQuery.of(context).size.width * 0.5 - 20, + height: size, + width: size, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20), color: context.colorScheme.secondaryContainer.withAlpha(100), @@ -189,10 +190,10 @@ class PlacesCollectionCard extends StatelessWidget { ), child: IgnorePointer( child: MapThumbnail( - zoom: 5, + zoom: 8, centre: const LatLng( - 47, - 5, + 21.44950, + -157.91959, ), showAttribution: false, themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 0d9842f2eccf1..1c93bd8d9234f 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -13,6 +13,10 @@ import 'package:immich_mobile/pages/backup/backup_album_selection.page.dart'; import 'package:immich_mobile/pages/backup/backup_controller.page.dart'; import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; +import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart'; +import 'package:immich_mobile/pages/collections/albums/local_albums_collection.page.dart'; +import 'package:immich_mobile/pages/collections/albums/people_collection.page.dart'; +import 'package:immich_mobile/pages/collections/albums/places_collection.part.dart'; import 'package:immich_mobile/pages/collections/collections.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart'; @@ -236,6 +240,22 @@ class AppRouter extends RootStackRouter { page: HeaderSettingsRoute.page, guards: [_duplicateGuard], ), + AutoRoute( + page: PeopleCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: AlbumsCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: LocalAlbumsCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), + AutoRoute( + page: PlacesCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ]; } diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 1e4dcee1254fd..16f65f948ba8f 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -319,6 +319,25 @@ class AlbumViewerRouteArgs { } } +/// generated route for +/// [AlbumsCollectionPage] +class AlbumsCollectionRoute extends PageRouteInfo { + const AlbumsCollectionRoute({List? children}) + : super( + AlbumsCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'AlbumsCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AlbumsCollectionPage(); + }, + ); +} + /// generated route for /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { @@ -876,6 +895,25 @@ class LibraryRoute extends PageRouteInfo { ); } +/// generated route for +/// [LocalAlbumsCollectionPage] +class LocalAlbumsCollectionRoute extends PageRouteInfo { + const LocalAlbumsCollectionRoute({List? children}) + : super( + LocalAlbumsCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'LocalAlbumsCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LocalAlbumsCollectionPage(); + }, + ); +} + /// generated route for /// [LoginPage] class LoginRoute extends PageRouteInfo { @@ -1078,6 +1116,25 @@ class PartnerRoute extends PageRouteInfo { ); } +/// generated route for +/// [PeopleCollectionPage] +class PeopleCollectionRoute extends PageRouteInfo { + const PeopleCollectionRoute({List? children}) + : super( + PeopleCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'PeopleCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PeopleCollectionPage(); + }, + ); +} + /// generated route for /// [PermissionOnboardingPage] class PermissionOnboardingRoute extends PageRouteInfo { @@ -1168,6 +1225,25 @@ class PhotosRoute extends PageRouteInfo { ); } +/// generated route for +/// [PlacesCollectionPage] +class PlacesCollectionRoute extends PageRouteInfo { + const PlacesCollectionRoute({List? children}) + : super( + PlacesCollectionRoute.name, + initialChildren: children, + ); + + static const String name = 'PlacesCollectionRoute'; + + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PlacesCollectionPage(); + }, + ); +} + /// generated route for /// [RecentlyAddedPage] class RecentlyAddedRoute extends PageRouteInfo { From 746354b77911122cf600e6228199c74a2528fc04 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 14:47:09 -0500 Subject: [PATCH 05/26] Added rounting mechanism --- .../albums/albums_collection.page.dart | 9 +- .../albums/local_albums_collection.page.dart | 9 +- .../albums/people_collection.page.dart | 9 +- .../albums/places_collection.part.dart | 9 +- .../pages/collections/collections.page.dart | 186 ++++++++++-------- mobile/lib/routing/router.dart | 12 +- 6 files changed, 139 insertions(+), 95 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 6e90ce862045b..a55ccf5fb8d59 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -7,6 +7,13 @@ class AlbumsCollectionPage extends HookConsumerWidget { const AlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return Container(); + return Scaffold( + appBar: AppBar( + title: const Text('albums_collection_page_title'), + ), + body: const Center( + child: Text('albums_collection_page_content'), + ), + ); } } diff --git a/mobile/lib/pages/collections/albums/local_albums_collection.page.dart b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart index 2352ea3a568b9..4f31cc9e01f46 100644 --- a/mobile/lib/pages/collections/albums/local_albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart @@ -7,6 +7,13 @@ class LocalAlbumsCollectionPage extends HookConsumerWidget { const LocalAlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return Container(); + return Scaffold( + appBar: AppBar( + title: const Text('on_this_device'), + ), + body: const Center( + child: Text('on_this_device_content'), + ), + ); } } diff --git a/mobile/lib/pages/collections/albums/people_collection.page.dart b/mobile/lib/pages/collections/albums/people_collection.page.dart index d5c58fb922dec..a37bb256a4c05 100644 --- a/mobile/lib/pages/collections/albums/people_collection.page.dart +++ b/mobile/lib/pages/collections/albums/people_collection.page.dart @@ -7,6 +7,13 @@ class PeopleCollectionPage extends HookConsumerWidget { const PeopleCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return Container(); + return Scaffold( + appBar: AppBar( + title: const Text('people'), + ), + body: const Center( + child: Text('people'), + ), + ); } } diff --git a/mobile/lib/pages/collections/albums/places_collection.part.dart b/mobile/lib/pages/collections/albums/places_collection.part.dart index 9fce76c597c88..665e915fe3adc 100644 --- a/mobile/lib/pages/collections/albums/places_collection.part.dart +++ b/mobile/lib/pages/collections/albums/places_collection.part.dart @@ -7,6 +7,13 @@ class PlacesCollectionPage extends HookConsumerWidget { const PlacesCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - return Container(); + return Scaffold( + appBar: AppBar( + title: const Text('places'), + ), + body: const Center( + child: Text('places'), + ), + ); } } diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 042287705f615..91492386ccbd4 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -87,41 +87,44 @@ class PeopleCollectionCard extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final people = ref.watch(getAllPeopleProvider); final size = MediaQuery.of(context).size.width * 0.5 - 20; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.secondaryContainer.withAlpha(100), + return GestureDetector( + onTap: () => context.pushRoute(const PeopleCollectionRoute()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: people.widgetWhen( + onData: (people) { + return GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: people.take(4).map((person) { + return CircleAvatar( + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: ApiService.getRequestHeaders(), + ), + ); + }).toList(), + ); + }, + ), ), - child: people.widgetWhen( - onData: (people) { - return GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: people.take(4).map((person) { - return CircleAvatar( - backgroundImage: NetworkImage( - getFaceThumbnailUrl(person.id), - headers: ApiService.getRequestHeaders(), - ), - ); - }).toList(), - ); - }, + Padding( + padding: const EdgeInsets.all(8.0), + child: Text('People', style: context.textTheme.labelLarge), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text('People', style: context.textTheme.labelLarge), - ), - ], + ], + ), ); } } @@ -137,38 +140,43 @@ class AlbumsCollectionCard extends ConsumerWidget { ? ref.watch(albumProvider).where((album) => album.isLocal) : ref.watch(albumProvider).where((album) => album.isRemote); final size = MediaQuery.of(context).size.width * 0.5 - 20; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.secondaryContainer.withAlpha(100), + return GestureDetector( + onTap: () => context.pushRoute(isLocal + ? const LocalAlbumsCollectionRoute() + : const AlbumsCollectionRoute()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: GridView.count( + crossAxisCount: 2, + padding: const EdgeInsets.all(12), + crossAxisSpacing: 8, + mainAxisSpacing: 8, + physics: const NeverScrollableScrollPhysics(), + children: albums.take(4).map((album) { + return AlbumThumbnailCard( + album: album, + showTitle: false, + ); + }).toList(), + ), ), - child: GridView.count( - crossAxisCount: 2, - padding: const EdgeInsets.all(12), - crossAxisSpacing: 8, - mainAxisSpacing: 8, - physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { - return AlbumThumbnailCard( - album: album, - showTitle: false, - ); - }).toList(), + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + isLocal ? 'On this device' : 'Albums', + style: context.textTheme.labelLarge, + ), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - isLocal ? 'On this device' : 'Albums', - style: context.textTheme.labelLarge, - ), - ), - ], + ], + ), ); } } @@ -178,33 +186,37 @@ class PlacesCollectionCard extends StatelessWidget { @override Widget build(BuildContext context) { final size = MediaQuery.of(context).size.width * 0.5 - 20; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - height: size, - width: size, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: context.colorScheme.secondaryContainer.withAlpha(100), - ), - child: IgnorePointer( - child: MapThumbnail( - zoom: 8, - centre: const LatLng( - 21.44950, - -157.91959, + return GestureDetector( + onTap: () => context.pushRoute(const PlacesCollectionRoute()), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + height: size, + width: size, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: context.colorScheme.secondaryContainer.withAlpha(100), + ), + child: IgnorePointer( + child: MapThumbnail( + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, + ), + showAttribution: false, + themeMode: + context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), - showAttribution: false, - themeMode: context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, ), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Text('Places', style: context.textTheme.labelLarge), - ), - ], + Padding( + padding: const EdgeInsets.all(8.0), + child: Text('Places', style: context.textTheme.labelLarge), + ), + ], + ), ); } } diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 1c93bd8d9234f..81ada3b106d9f 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -240,21 +240,25 @@ class AppRouter extends RootStackRouter { page: HeaderSettingsRoute.page, guards: [_duplicateGuard], ), - AutoRoute( + CustomRoute( page: PeopleCollectionRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), - AutoRoute( + CustomRoute( page: AlbumsCollectionRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), - AutoRoute( + CustomRoute( page: LocalAlbumsCollectionRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), - AutoRoute( + CustomRoute( page: PlacesCollectionRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), ]; } From 27d5b134ac83d9347de5524c53fcc169c3267a78 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 16:04:22 -0500 Subject: [PATCH 06/26] Update on rounting --- .../albums/albums_collection.page.dart | 186 +++++++++++++++++- .../pages/collections/collections.page.dart | 28 ++- mobile/lib/routing/router.dart | 27 ++- 3 files changed, 223 insertions(+), 18 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index a55ccf5fb8d59..e05c68f7ec77d 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -1,18 +1,198 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; 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/server_info.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; +import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; @RoutePage() class AlbumsCollectionPage extends HookConsumerWidget { const AlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(albumProvider); + 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 local = albums.where((a) => a.isLocal).toList(); + + useEffect( + () { + ref.read(albumProvider.notifier).getAllAlbums(); + return null; + }, + [], + ); + return Scaffold( appBar: AppBar( - title: const Text('albums_collection_page_title'), + title: const Text("Albums"), ), - body: const Center( - child: Text('albums_collection_page_content'), + body: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + bottom: 20.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SortButton(), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + childCount: sorted.length, + (context, index) { + return AlbumThumbnailCard( + album: sorted[index], + onTap: () => context.pushRoute( + AlbumViewerRoute( + albumId: sorted[index].id, + ), + ), + ); + }, + ), + ), + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + bottom: 20.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'library_page_device_albums', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + childCount: local.length, + (context, index) => AlbumThumbnailCard( + album: local[index], + onTap: () => context.pushRoute( + AlbumViewerRoute( + albumId: local[index].id, + ), + ), + ), + ), + ), + ), + ], + ), + ); + } +} + +class SortButton extends ConsumerWidget { + const SortButton({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final albumSortOption = ref.watch(albumSortByOptionsProvider); + final albumSortIsReverse = ref.watch(albumSortOrderProvider); + + return PopupMenuButton( + position: PopupMenuPosition.over, + itemBuilder: (BuildContext context) { + return AlbumSortMode.values + .map>((option) { + final selected = albumSortOption == option; + return PopupMenuItem( + value: option, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 12.0), + child: Icon( + Icons.check, + color: selected ? context.primaryColor : Colors.transparent, + ), + ), + Text( + option.label.tr(), + style: TextStyle( + color: selected ? context.primaryColor : null, + fontSize: 14.0, + ), + ), + ], + ), + ); + }).toList(); + }, + onSelected: (AlbumSortMode value) { + final selected = albumSortOption == value; + // Switch direction + if (selected) { + ref + .read(albumSortOrderProvider.notifier) + .changeSortDirection(!albumSortIsReverse); + } else { + ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value); + } + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 5), + child: Icon( + albumSortIsReverse + ? Icons.arrow_downward_rounded + : Icons.arrow_upward_rounded, + size: 14, + color: context.primaryColor, + ), + ), + Text( + albumSortOption.label.tr(), + style: context.textTheme.labelLarge?.copyWith( + color: context.primaryColor, + ), + ), + ], ), ); } diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 91492386ccbd4..1210b5bb2706f 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -5,6 +5,7 @@ import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; @@ -15,10 +16,13 @@ import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() -class CollectionsPage extends StatelessWidget { +class CollectionsPage extends ConsumerWidget { const CollectionsPage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final trashEnabled = + ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); + return Scaffold( appBar: const ImmichAppBar( showUploadButton: false, @@ -53,11 +57,13 @@ class CollectionsPage extends StatelessWidget { label: 'Shared links', ), const SizedBox(width: 8), - ActionButton( - onPressed: () => context.pushRoute(const TrashRoute()), - icon: Icons.delete_outline_rounded, - label: 'Trash', - ), + trashEnabled + ? ActionButton( + onPressed: () => context.pushRoute(const TrashRoute()), + icon: Icons.delete_outline_rounded, + label: 'Trash', + ) + : const SizedBox.shrink(), ], ), const SizedBox(height: 24), @@ -141,9 +147,11 @@ class AlbumsCollectionCard extends ConsumerWidget { : ref.watch(albumProvider).where((album) => album.isRemote); final size = MediaQuery.of(context).size.width * 0.5 - 20; return GestureDetector( - onTap: () => context.pushRoute(isLocal - ? const LocalAlbumsCollectionRoute() - : const AlbumsCollectionRoute()), + onTap: () => context.pushRoute( + isLocal + ? const LocalAlbumsCollectionRoute() + : const AlbumsCollectionRoute(), + ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 81ada3b106d9f..c840c0c0e2175 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -145,7 +145,11 @@ class AppRouter extends RootStackRouter { ), AutoRoute(page: EditImageRoute.page), AutoRoute(page: CropImageRoute.page), - AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), + CustomRoute( + page: FavoritesRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute( page: AllMotionPhotosRoute.page, @@ -191,8 +195,16 @@ class AppRouter extends RootStackRouter { AutoRoute(page: SettingsSubRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogRoute.page, guards: [_duplicateGuard]), AutoRoute(page: AppLogDetailRoute.page, guards: [_duplicateGuard]), - AutoRoute(page: ArchiveRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute(page: PartnerRoute.page, guards: [_authGuard, _duplicateGuard]), + CustomRoute( + page: ArchiveRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( + page: PartnerRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), AutoRoute( page: PartnerDetailRoute.page, guards: [_authGuard, _duplicateGuard], @@ -208,10 +220,15 @@ class AppRouter extends RootStackRouter { page: AlbumOptionsRoute.page, guards: [_authGuard, _duplicateGuard], ), - AutoRoute(page: TrashRoute.page, guards: [_authGuard, _duplicateGuard]), - AutoRoute( + CustomRoute( + page: TrashRoute.page, + guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, + ), + CustomRoute( page: SharedLinkRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideLeft, ), AutoRoute( page: SharedLinkEditRoute.page, From 34ea42c00524a50d044db31ee0311413798a1717 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 16:16:06 -0500 Subject: [PATCH 07/26] wip: album collesction page --- .../albums/albums_collection.page.dart | 161 +++++++++--------- 1 file changed, 79 insertions(+), 82 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index e05c68f7ec77d..676c447a30b9c 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -6,10 +6,8 @@ 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/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; -import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; @RoutePage() class AlbumsCollectionPage extends HookConsumerWidget { @@ -35,92 +33,94 @@ class AlbumsCollectionPage extends HookConsumerWidget { appBar: AppBar( title: const Text("Albums"), ), - body: CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.only( - top: 12.0, - left: 12.0, - right: 12.0, - bottom: 20.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SortButton(), - ], + body: SafeArea( + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + bottom: 20.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SortButton(), + ], + ), ), ), - ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, + SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + childCount: sorted.length, + (context, index) { + return AlbumThumbnailCard( + album: sorted[index], + onTap: () => context.pushRoute( + AlbumViewerRoute( + albumId: sorted[index].id, + ), + ), + ); + }, + ), ), - delegate: SliverChildBuilderDelegate( - childCount: sorted.length, - (context, index) { - return AlbumThumbnailCard( - album: sorted[index], + ), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.only( + top: 12.0, + left: 12.0, + right: 12.0, + bottom: 20.0, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'library_page_device_albums', + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ).tr(), + ], + ), + ), + ), + SliverPadding( + padding: const EdgeInsets.all(12.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + childCount: local.length, + (context, index) => AlbumThumbnailCard( + album: local[index], onTap: () => context.pushRoute( AlbumViewerRoute( - albumId: sorted[index].id, + albumId: local[index].id, ), ), - ); - }, - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 12.0, - left: 12.0, - right: 12.0, - bottom: 20.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'library_page_device_albums', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - childCount: local.length, - (context, index) => AlbumThumbnailCard( - album: local[index], - onTap: () => context.pushRoute( - AlbumViewerRoute( - albumId: local[index].id, - ), ), ), ), ), - ), - ], + ], + ), ), ); } @@ -182,15 +182,12 @@ class SortButton extends ConsumerWidget { albumSortIsReverse ? Icons.arrow_downward_rounded : Icons.arrow_upward_rounded, - size: 14, - color: context.primaryColor, + size: 18, ), ), Text( albumSortOption.label.tr(), - style: context.textTheme.labelLarge?.copyWith( - color: context.primaryColor, - ), + style: context.textTheme.labelLarge?.copyWith(), ), ], ), From 30a3f827a24de8030208c3aa8cb1806c963de177 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 17:17:06 -0500 Subject: [PATCH 08/26] wip: album collections page --- .../albums/albums_collection.page.dart | 119 +++++------------- 1 file changed, 32 insertions(+), 87 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 676c447a30b9c..d5fae3d1742df 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -20,6 +20,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { final remote = albums.where((a) => a.isRemote).toList(); final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); final local = albums.where((a) => a.isLocal).toList(); + final isGrid = useState(false); useEffect( () { @@ -29,98 +30,42 @@ class AlbumsCollectionPage extends HookConsumerWidget { [], ); + toggleViewMode() { + isGrid.value = !isGrid.value; + } + return Scaffold( appBar: AppBar( title: const Text("Albums"), ), - body: SafeArea( - child: CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.only( - top: 12.0, - left: 12.0, - right: 12.0, - bottom: 20.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SortButton(), - ], - ), + body: ListView( + shrinkWrap: true, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SortButton(), + IconButton( + onPressed: toggleViewMode, + icon: Icon(isGrid.value ? Icons.list : Icons.grid_view), ), - ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - childCount: sorted.length, - (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute( - AlbumViewerRoute( - albumId: sorted[index].id, - ), - ), - ); - }, - ), - ), - ), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.only( - top: 12.0, - left: 12.0, - right: 12.0, - bottom: 20.0, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - 'library_page_device_albums', - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - ), - ).tr(), - ], - ), - ), - ), - SliverPadding( - padding: const EdgeInsets.all(12.0), - sliver: SliverGrid( - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - delegate: SliverChildBuilderDelegate( - childCount: local.length, - (context, index) => AlbumThumbnailCard( - album: local[index], - onTap: () => context.pushRoute( - AlbumViewerRoute( - albumId: local[index].id, - ), - ), - ), - ), - ), - ), - ], - ), + ], + ), + GridView.count( + shrinkWrap: true, + physics: const ScrollPhysics(), + crossAxisCount: 2, + mainAxisSpacing: 32, + crossAxisSpacing: 32, + children: sorted.map((album) { + return AlbumThumbnailCard( + album: album, + onTap: () => + context.pushRoute(AlbumViewerRoute(albumId: album.id)), + ); + }).toList(), + ), + ], ), ); } From 424de032042f297e4ac62b248bea508e6afe5697 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 17:42:01 -0500 Subject: [PATCH 09/26] wip: album collections page --- .../albums/albums_collection.page.dart | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index d5fae3d1742df..80906f3e491a6 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -39,32 +39,20 @@ class AlbumsCollectionPage extends HookConsumerWidget { title: const Text("Albums"), ), body: ListView( - shrinkWrap: true, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SortButton(), + const SizedBox(width: 10), IconButton( + icon: Icon( + isGrid.value ? Icons.list_rounded : Icons.grid_view_outlined, + ), onPressed: toggleViewMode, - icon: Icon(isGrid.value ? Icons.list : Icons.grid_view), ), ], ), - GridView.count( - shrinkWrap: true, - physics: const ScrollPhysics(), - crossAxisCount: 2, - mainAxisSpacing: 32, - crossAxisSpacing: 32, - children: sorted.map((album) { - return AlbumThumbnailCard( - album: album, - onTap: () => - context.pushRoute(AlbumViewerRoute(albumId: album.id)), - ); - }).toList(), - ), ], ), ); From 31f2b94396fd483c5ee13f4675cf9682e1991374 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 22:05:52 -0500 Subject: [PATCH 10/26] better anchor menu --- .../albums/albums_collection.page.dart | 169 ++++++++++++------ .../pages/collections/collections.page.dart | 4 +- mobile/lib/routing/router.dart | 3 +- 3 files changed, 120 insertions(+), 56 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 80906f3e491a6..63a474d80bed7 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; @@ -39,6 +41,8 @@ class AlbumsCollectionPage extends HookConsumerWidget { title: const Text("Albums"), ), body: ListView( + shrinkWrap: true, + padding: const EdgeInsets.all(18.0), children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -53,6 +57,40 @@ class AlbumsCollectionPage extends HookConsumerWidget { ), ], ), + if (isGrid.value) + GridView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + itemBuilder: (context, index) { + return AlbumThumbnailCard( + album: sorted[index], + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + ); + }, + itemCount: sorted.length, + ) + else + ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: sorted.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(sorted[index].name), + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + ); + }, + ), ], ), ); @@ -67,63 +105,90 @@ class SortButton extends ConsumerWidget { final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); - return PopupMenuButton( - position: PopupMenuPosition.over, - itemBuilder: (BuildContext context) { - return AlbumSortMode.values - .map>((option) { - final selected = albumSortOption == option; - return PopupMenuItem( - value: option, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 12.0), + return MenuAnchor( + menuChildren: AlbumSortMode.values + .map( + (mode) => MenuItemButton( + leadingIcon: albumSortOption == mode + ? albumSortIsReverse + ? Icon( + Icons.keyboard_arrow_down, + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : Icon( + Icons.keyboard_arrow_up_rounded, + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ) + : const Icon(Icons.abc, color: Colors.transparent), + onPressed: () { + final selected = albumSortOption == mode; + // Switch direction + if (selected) { + ref + .read(albumSortOrderProvider.notifier) + .changeSortDirection(!albumSortIsReverse); + } else { + ref + .read(albumSortByOptionsProvider.notifier) + .changeSortMode(mode); + } + }, + style: ButtonStyle( + padding: WidgetStateProperty.all(const EdgeInsets.all(8)), + backgroundColor: WidgetStateProperty.all( + albumSortOption == mode + ? context.colorScheme.primary + : Colors.transparent, + ), + ), + child: Text( + mode.label.tr(), + style: context.textTheme.bodyMedium?.copyWith( + color: albumSortOption == mode + ? context.colorScheme.onPrimary + : context.colorScheme.onSurface, + ), + ), + ), + ) + .toList(), + builder: (context, controller, child) { + return GestureDetector( + onTap: () { + if (controller.isOpen) { + controller.close(); + } else { + controller.open(); + } + }, + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(right: 5), + child: Transform.rotate( + angle: 90 * pi / 180, child: Icon( - Icons.check, - color: selected ? context.primaryColor : Colors.transparent, + Icons.compare_arrows_rounded, + size: 18, + color: context.colorScheme.onSurface.withAlpha(200), ), ), - Text( - option.label.tr(), - style: TextStyle( - color: selected ? context.primaryColor : null, - fontSize: 14.0, - ), + ), + Text( + albumSortOption.label.tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.onSurface.withAlpha(200), ), - ], - ), - ); - }).toList(); - }, - onSelected: (AlbumSortMode value) { - final selected = albumSortOption == value; - // Switch direction - if (selected) { - ref - .read(albumSortOrderProvider.notifier) - .changeSortDirection(!albumSortIsReverse); - } else { - ref.read(albumSortByOptionsProvider.notifier).changeSortMode(value); - } - }, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(right: 5), - child: Icon( - albumSortIsReverse - ? Icons.arrow_downward_rounded - : Icons.arrow_upward_rounded, - size: 18, - ), + ), + ], ), - Text( - albumSortOption.label.tr(), - style: context.textTheme.labelLarge?.copyWith(), - ), - ], - ), + ); + }, ); } } diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 1210b5bb2706f..c44646a62ceac 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -73,9 +73,7 @@ class CollectionsPage extends ConsumerWidget { children: [ PeopleCollectionCard(), AlbumsCollectionCard(), - AlbumsCollectionCard( - isLocal: true, - ), + AlbumsCollectionCard(isLocal: true), PlacesCollectionCard(), ], ), diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index c840c0c0e2175..e02064d95165c 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -169,9 +169,10 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], transitionsBuilder: TransitionsBuilders.slideBottom, ), - AutoRoute( + CustomRoute( page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard], + transitionsBuilder: TransitionsBuilders.slideBottom, ), CustomRoute( page: AlbumAdditionalSharedUserSelectionRoute.page, From 77bfa5d44527823256ef894c97f2fc21c0ba0a2b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 22:46:38 -0500 Subject: [PATCH 11/26] search album --- .../albums/albums_collection.page.dart | 140 +++++++++++++----- .../lib/providers/album/album.provider.dart | 13 +- 2 files changed, 116 insertions(+), 37 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 63a474d80bed7..611bed2c2ee74 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:math'; import 'package:auto_route/auto_route.dart'; @@ -21,8 +22,9 @@ class AlbumsCollectionPage extends HookConsumerWidget { final albumSortIsReverse = ref.watch(albumSortOrderProvider); final remote = albums.where((a) => a.isRemote).toList(); final sorted = albumSortOption.sortFn(remote, albumSortIsReverse); - final local = albums.where((a) => a.isLocal).toList(); - final isGrid = useState(false); + final isGrid = useState(true); + final searchController = useTextEditingController(); + final debounceTimer = useRef(null); useEffect( () { @@ -36,6 +38,28 @@ class AlbumsCollectionPage extends HookConsumerWidget { isGrid.value = !isGrid.value; } + onSearch(String value) { + debounceTimer.value?.cancel(); + debounceTimer.value = Timer(const Duration(milliseconds: 300), () { + ref.read(albumProvider.notifier).searchAlbums(value); + }); + } + + useEffect( + () { + searchController.addListener(() { + onSearch(searchController.text); + }); + return () { + searchController.removeListener(() { + onSearch(searchController.text); + }); + debounceTimer.value?.cancel(); + }; + }, + [], + ); + return Scaffold( appBar: AppBar( title: const Text("Albums"), @@ -44,6 +68,30 @@ class AlbumsCollectionPage extends HookConsumerWidget { shrinkWrap: true, padding: const EdgeInsets.all(18.0), children: [ + SearchBar( + backgroundColor: WidgetStatePropertyAll( + context.colorScheme.surfaceContainer, + ), + autoFocus: false, + hintText: "Search albums", + onChanged: onSearch, + elevation: const WidgetStatePropertyAll(0.25), + controller: searchController, + leading: const Icon(Icons.search_rounded), + padding: WidgetStateProperty.all( + const EdgeInsets.symmetric(horizontal: 16), + ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: context.colorScheme.onSurface.withAlpha(10), + width: 1, + ), + ), + ), + ), + const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -51,46 +99,53 @@ class AlbumsCollectionPage extends HookConsumerWidget { const SizedBox(width: 10), IconButton( icon: Icon( - isGrid.value ? Icons.list_rounded : Icons.grid_view_outlined, + isGrid.value + ? Icons.view_list_rounded + : Icons.grid_view_outlined, + size: 24, ), onPressed: toggleViewMode, ), ], ), - if (isGrid.value) - GridView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( - maxCrossAxisExtent: 250, - mainAxisSpacing: 12, - crossAxisSpacing: 12, - childAspectRatio: .7, - ), - itemBuilder: (context, index) { - return AlbumThumbnailCard( - album: sorted[index], - onTap: () => context.pushRoute( - AlbumViewerRoute(albumId: sorted[index].id), + const SizedBox(height: 16), + AnimatedSwitcher( + duration: const Duration(milliseconds: 500), + child: isGrid.value + ? GridView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + gridDelegate: + const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + itemBuilder: (context, index) { + return AlbumThumbnailCard( + album: sorted[index], + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + ); + }, + itemCount: sorted.length, + ) + : ListView.builder( + shrinkWrap: true, + physics: const ClampingScrollPhysics(), + itemCount: sorted.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(sorted[index].name), + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + ); + }, ), - ); - }, - itemCount: sorted.length, - ) - else - ListView.builder( - shrinkWrap: true, - physics: const ClampingScrollPhysics(), - itemCount: sorted.length, - itemBuilder: (context, index) { - return ListTile( - title: Text(sorted[index].name), - onTap: () => context.pushRoute( - AlbumViewerRoute(albumId: sorted[index].id), - ), - ); - }, - ), + ), ], ), ); @@ -106,6 +161,14 @@ class SortButton extends ConsumerWidget { final albumSortIsReverse = ref.watch(albumSortOrderProvider); return MenuAnchor( + style: MenuStyle( + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + ), + ), + ), + consumeOutsideTap: true, menuChildren: AlbumSortMode.values .map( (mode) => MenuItemButton( @@ -144,6 +207,11 @@ class SortButton extends ConsumerWidget { ? context.colorScheme.primary : Colors.transparent, ), + shape: WidgetStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), ), child: Text( mode.label.tr(), diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index ed9dc07f5e5c0..136c9286fe774 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/utils/renderlist_generator.dart'; import 'package:isar/isar.dart'; class AlbumNotifier extends StateNotifier> { - AlbumNotifier(this._albumService, Isar db) : super([]) { + AlbumNotifier(this._albumService, this.db) : super([]) { final query = db.albums .filter() .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)); @@ -25,6 +25,7 @@ class AlbumNotifier extends StateNotifier> { } final AlbumService _albumService; + final Isar db; late final StreamSubscription> _streamSub; Future getAllAlbums() => Future.wait([ @@ -64,6 +65,16 @@ class AlbumNotifier extends StateNotifier> { _streamSub.cancel(); super.dispose(); } + + void searchAlbums(String value) async { + final query = db.albums + .filter() + .owner((q) => q.isarIdEqualTo(Store.get(StoreKey.currentUser).isarId)) + .nameContains(value, caseSensitive: false); + + final albums = await query.findAll(); + state = albums; + } } final albumProvider = From 2320e7a0d75c66e3d221c2600fd388b8c7b5dd1e Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 6 Sep 2024 22:53:04 -0500 Subject: [PATCH 12/26] smooth transition --- .../lib/pages/collections/albums/albums_collection.page.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 611bed2c2ee74..48e8b349365ed 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -28,7 +28,9 @@ class AlbumsCollectionPage extends HookConsumerWidget { useEffect( () { - ref.read(albumProvider.notifier).getAllAlbums(); + Future.delayed(const Duration(seconds: 1), () { + ref.read(albumProvider.notifier).getAllAlbums(); + }); return null; }, [], From 2dc73c2987d3b955dd0a3d20f16fc9a9688523c2 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Sep 2024 14:48:32 -0500 Subject: [PATCH 13/26] 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), + ); +}); From 3417330bdffe5b83fd662e9b0eebe191b6aae9a3 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Sep 2024 15:48:22 -0500 Subject: [PATCH 14/26] better album tile --- .../albums/albums_collection.page.dart | 68 ++++++++++++++++--- .../widgets/album/album_thumbnail_card.dart | 6 +- 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 3a150c00fd15e..19c02952d1404 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -7,10 +7,13 @@ 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/extensions/theme_extensions.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/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; +import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; enum QuickFilterMode { all, @@ -27,10 +30,11 @@ class AlbumsCollectionPage extends HookConsumerWidget { final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); - final isGrid = useState(true); + final isGrid = useState(false); final searchController = useTextEditingController(); final debounceTimer = useRef(null); final filterMode = useState(QuickFilterMode.all); + final userId = ref.watch(currentUserProvider)?.id; toggleViewMode() { isGrid.value = !isGrid.value; @@ -46,7 +50,6 @@ class AlbumsCollectionPage extends HookConsumerWidget { changeFilter(QuickFilterMode mode) { filterMode.value = mode; - searchController.clear(); ref.read(albumProviderV2.notifier).filterAlbums(mode); } @@ -97,7 +100,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { ), ), ), - const SizedBox(height: 8), + const SizedBox(height: 16), Wrap( spacing: 4, runSpacing: 4, @@ -135,7 +138,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { ), ], ), - const SizedBox(height: 16), + const SizedBox(height: 5), AnimatedSwitcher( duration: const Duration(milliseconds: 500), child: isGrid.value @@ -155,6 +158,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { onTap: () => context.pushRoute( AlbumViewerRoute(albumId: sorted[index].id), ), + showOwner: true, ); }, itemCount: sorted.length, @@ -164,10 +168,54 @@ class AlbumsCollectionPage extends HookConsumerWidget { physics: const ClampingScrollPhysics(), itemCount: sorted.length, itemBuilder: (context, index) { - return ListTile( - title: Text(sorted[index].name), - onTap: () => context.pushRoute( - AlbumViewerRoute(albumId: sorted[index].id), + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: ListTile( + title: Text( + sorted[index].name, + style: context.textTheme.titleSmall?.copyWith( + // fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + subtitle: sorted[index].ownerId == userId + ? Text( + '${sorted[index].assetCount} items', + style: context.textTheme.bodyMedium?.copyWith( + color: + context.colorScheme.onSurfaceSecondary, + ), + ) + : sorted[index].ownerName != null + ? Text( + '${sorted[index].assetCount} items • ${'album_thumbnail_shared_by'.tr( + args: [ + sorted[index].ownerName!, + ], + )}', + style: context.textTheme.bodyMedium + ?.copyWith( + color: context + .colorScheme.onSurfaceSecondary, + ), + ) + : null, + onTap: () => context.pushRoute( + AlbumViewerRoute(albumId: sorted[index].id), + ), + contentPadding: const EdgeInsets.all(0), + dense: false, + visualDensity: VisualDensity.comfortable, + leading: ClipRRect( + borderRadius: + const BorderRadius.all(Radius.circular(15)), + child: ImmichThumbnail( + asset: sorted[index].thumbnail.value, + width: 60, + height: 90, + ), + ), + minVerticalPadding: 1, ), ); }, @@ -319,7 +367,7 @@ class SortButton extends ConsumerWidget { child: Icon( Icons.compare_arrows_rounded, size: 18, - color: context.colorScheme.onSurface.withAlpha(200), + color: context.colorScheme.onSurface.withAlpha(225), ), ), ), @@ -327,7 +375,7 @@ class SortButton extends ConsumerWidget { albumSortOption.label.tr(), style: context.textTheme.bodyLarge?.copyWith( fontWeight: FontWeight.w500, - color: context.colorScheme.onSurface.withAlpha(200), + color: context.colorScheme.onSurface.withAlpha(225), ), ), ], diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 96050c65bd00b..6b039bbdf4623 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -78,7 +78,7 @@ class AlbumThumbnailCard extends StatelessWidget { : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), ), - if (owner != null) const TextSpan(text: ' · '), + if (owner != null) const TextSpan(text: ' • '), if (owner != null) TextSpan(text: owner), ], ), @@ -112,9 +112,9 @@ class AlbumThumbnailCard extends StatelessWidget { child: Text( album.name, overflow: TextOverflow.ellipsis, - style: context.textTheme.bodyMedium?.copyWith( + style: context.textTheme.labelLarge?.copyWith( color: context.colorScheme.onSurface, - fontWeight: FontWeight.w500, + fontSize: 16, ), ), ), From 9266197cd216d2f87547d3a4547f87e0b3384962 Mon Sep 17 00:00:00 2001 From: Alex Date: Sat, 7 Sep 2024 16:07:31 -0500 Subject: [PATCH 15/26] wip --- .../lib/pages/collections/albums/albums_collection.page.dart | 3 ++- mobile/lib/pages/collections/collections.page.dart | 5 +++-- mobile/lib/providers/album/albumv2.provider.dart | 2 +- mobile/lib/widgets/album/album_thumbnail_card.dart | 4 ++-- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index 19c02952d1404..d11760d9e820a 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -26,7 +26,8 @@ class AlbumsCollectionPage extends HookConsumerWidget { const AlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final albums = ref.watch(albumProviderV2); + final albums = + ref.watch(albumProviderV2).where((album) => album.isRemote).toList(); final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); final sorted = albumSortOption.sortFn(albums, albumSortIsReverse); diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index c44646a62ceac..028bc94c02291 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -4,6 +4,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.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/albumv2.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -141,8 +142,8 @@ class AlbumsCollectionCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final albums = isLocal - ? ref.watch(albumProvider).where((album) => album.isLocal) - : ref.watch(albumProvider).where((album) => album.isRemote); + ? ref.watch(albumProviderV2).where((album) => album.isLocal) + : ref.watch(albumProviderV2).where((album) => album.isRemote); final size = MediaQuery.of(context).size.width * 0.5 - 20; return GestureDetector( onTap: () => context.pushRoute( diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart index 7d42ac98e4a3e..4e4c7fa0a9f14 100644 --- a/mobile/lib/providers/album/albumv2.provider.dart +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -12,7 +12,7 @@ import 'package:isar/isar.dart'; class AlbumNotifierV2 extends StateNotifier> { AlbumNotifierV2(this._albumService, this.db) : super([]) { - final query = db.albums.filter().remoteIdIsNotNull(); + final query = db.albums.where(); query.findAll().then((value) { if (mounted) { diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 6b039bbdf4623..b728f2b5415fe 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -112,9 +112,9 @@ class AlbumThumbnailCard extends StatelessWidget { child: Text( album.name, overflow: TextOverflow.ellipsis, - style: context.textTheme.labelLarge?.copyWith( + style: context.textTheme.titleSmall?.copyWith( color: context.colorScheme.onSurface, - fontSize: 16, + fontWeight: FontWeight.w500, ), ), ), From 27d390a7561baf67eaa98ea02748ed86435a57cc Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sat, 7 Sep 2024 21:51:46 -0500 Subject: [PATCH 16/26] wip --- .../pages/collections/collections.page.dart | 24 ++++++++++++++----- .../lib/providers/album/albumv2.provider.dart | 10 +++++++- .../lib/routing/tab_navigation_observer.dart | 5 ++++ 3 files changed, 32 insertions(+), 7 deletions(-) diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 028bc94c02291..20d9f8fa47d1b 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -1,9 +1,9 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.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/albumv2.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -134,17 +134,29 @@ class PeopleCollectionCard extends ConsumerWidget { } } -class AlbumsCollectionCard extends ConsumerWidget { +class AlbumsCollectionCard extends HookConsumerWidget { final bool isLocal; const AlbumsCollectionCard({super.key, this.isLocal = false}); @override Widget build(BuildContext context, WidgetRef ref) { - final albums = isLocal - ? ref.watch(albumProviderV2).where((album) => album.isLocal) - : ref.watch(albumProviderV2).where((album) => album.isRemote); + final albums = useState([]); + final size = MediaQuery.of(context).size.width * 0.5 - 20; + + useEffect( + () { + Future.microtask(() async { + albums.value = isLocal + ? await ref.read(albumProviderV2.notifier).getLocalAlbums() + : await ref.read(albumProviderV2.notifier).getRemoteAlbums(); + }); + return null; + }, + [], + ); + return GestureDetector( onTap: () => context.pushRoute( isLocal @@ -167,7 +179,7 @@ class AlbumsCollectionCard extends ConsumerWidget { crossAxisSpacing: 8, mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), - children: albums.take(4).map((album) { + children: albums.value.take(4).map((album) { return AlbumThumbnailCard( album: album, showTitle: false, diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart index 4e4c7fa0a9f14..85caeb4e0e6a7 100644 --- a/mobile/lib/providers/album/albumv2.provider.dart +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -26,7 +26,7 @@ class AlbumNotifierV2 extends StateNotifier> { final Isar db; late final StreamSubscription> _streamSub; - Future getAllAlbums() { + Future refreshAlbums() { return Future.wait([ _albumService.refreshDeviceAlbums(), _albumService.refreshRemoteAlbums(isShared: true), @@ -103,6 +103,14 @@ class AlbumNotifierV2 extends StateNotifier> { } } + Future> getRemoteAlbums() { + return db.albums.filter().remoteIdIsNotNull().findAll(); + } + + Future> getLocalAlbums() { + return db.albums.filter().not().remoteIdIsNotNull().findAll(); + } + @override void dispose() { _streamSub.cancel(); diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart index e16fecb32392a..bbb870b8b0e5a 100644 --- a/mobile/lib/routing/tab_navigation_observer.dart +++ b/mobile/lib/routing/tab_navigation_observer.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/album/albumv2.provider.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -50,6 +51,10 @@ class TabNavigationObserver extends AutoRouterObserver { ref.read(albumProvider.notifier).getAllAlbums(); } + if (route.name == 'CollectionsRoute') { + ref.read(albumProviderV2.notifier).refreshAlbums(); + } + if (route.name == 'HomeRoute') { ref.invalidate(memoryFutureProvider); Future(() => ref.read(assetProvider.notifier).getAllAsset()); From c26baea5303abbd8c24570326b1f97a6e489dbdc Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Sun, 8 Sep 2024 00:23:20 -0500 Subject: [PATCH 17/26] album on collections page does not change --- .../pages/collections/collections.page.dart | 19 +---- .../lib/pages/common/tab_controller.page.dart | 72 +++++++++--------- .../lib/providers/album/albumv2.provider.dart | 73 +++++++++++++++---- 3 files changed, 100 insertions(+), 64 deletions(-) diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 20d9f8fa47d1b..317a93e5b3bce 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -1,6 +1,5 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; @@ -141,22 +140,12 @@ class AlbumsCollectionCard extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final albums = useState([]); + final albums = isLocal + ? ref.watch(localAlbumsProvider) + : ref.watch(remoteAlbumsProvider); final size = MediaQuery.of(context).size.width * 0.5 - 20; - useEffect( - () { - Future.microtask(() async { - albums.value = isLocal - ? await ref.read(albumProviderV2.notifier).getLocalAlbums() - : await ref.read(albumProviderV2.notifier).getRemoteAlbums(); - }); - return null; - }, - [], - ); - return GestureDetector( onTap: () => context.pushRoute( isLocal @@ -179,7 +168,7 @@ class AlbumsCollectionCard extends HookConsumerWidget { crossAxisSpacing: 8, mainAxisSpacing: 8, physics: const NeverScrollableScrollPhysics(), - children: albums.value.take(4).map((album) { + children: albums.take(4).map((album) { return AlbumThumbnailCard( album: album, showTitle: false, diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index f862213040e89..ca3274bd6bcda 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -82,18 +82,18 @@ class TabControllerPage extends HookConsumerWidget { selectedIcon: const Icon(Icons.search), label: const Text('tab_controller_nav_search').tr(), ), - NavigationRailDestination( - padding: const EdgeInsets.all(4), - icon: const Icon(Icons.share_rounded), - selectedIcon: const Icon(Icons.share), - label: const Text('tab_controller_nav_sharing').tr(), - ), - NavigationRailDestination( - padding: const EdgeInsets.all(4), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: const Icon(Icons.photo_album), - label: const Text('tab_controller_nav_library').tr(), - ), + // NavigationRailDestination( + // padding: const EdgeInsets.all(4), + // icon: const Icon(Icons.share_rounded), + // selectedIcon: const Icon(Icons.share), + // label: const Text('tab_controller_nav_sharing').tr(), + // ), + // NavigationRailDestination( + // padding: const EdgeInsets.all(4), + // icon: const Icon(Icons.photo_album_outlined), + // selectedIcon: const Icon(Icons.photo_album), + // label: const Text('tab_controller_nav_library').tr(), + // ), NavigationRailDestination( padding: const EdgeInsets.all(4), icon: const Icon(Icons.photo_album_outlined), @@ -140,28 +140,28 @@ class TabControllerPage extends HookConsumerWidget { color: context.primaryColor, ), ), - NavigationDestination( - label: 'tab_controller_nav_sharing'.tr(), - icon: const Icon( - Icons.group_outlined, - ), - selectedIcon: Icon( - Icons.group, - color: context.primaryColor, - ), - ), - NavigationDestination( - label: 'tab_controller_nav_library'.tr(), - icon: const Icon( - Icons.photo_album_outlined, - ), - selectedIcon: buildIcon( - Icon( - Icons.photo_album_rounded, - color: context.primaryColor, - ), - ), - ), + // NavigationDestination( + // label: 'tab_controller_nav_sharing'.tr(), + // icon: const Icon( + // Icons.group_outlined, + // ), + // selectedIcon: Icon( + // Icons.group, + // color: context.primaryColor, + // ), + // ), + // NavigationDestination( + // label: 'tab_controller_nav_library'.tr(), + // icon: const Icon( + // Icons.photo_album_outlined, + // ), + // selectedIcon: buildIcon( + // Icon( + // Icons.photo_album_rounded, + // color: context.primaryColor, + // ), + // ), + // ), NavigationDestination( label: 'Collections'.tr(), icon: const Icon( @@ -183,8 +183,8 @@ class TabControllerPage extends HookConsumerWidget { routes: const [ PhotosRoute(), SearchRoute(), - SharingRoute(), - LibraryRoute(), + // SharingRoute(), + // LibraryRoute(), CollectionsRoute(), ], duration: const Duration(milliseconds: 600), diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart index 85caeb4e0e6a7..3683a7d24f10b 100644 --- a/mobile/lib/providers/album/albumv2.provider.dart +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -26,11 +26,10 @@ class AlbumNotifierV2 extends StateNotifier> { final Isar db; late final StreamSubscription> _streamSub; - Future refreshAlbums() { - return Future.wait([ - _albumService.refreshDeviceAlbums(), - _albumService.refreshRemoteAlbums(isShared: true), - ]); + Future refreshAlbums() async { + await _albumService.refreshDeviceAlbums(); + await _albumService.refreshRemoteAlbums(isShared: false); + await _albumService.refreshRemoteAlbums(isShared: true); } Future getDeviceAlbums() { @@ -103,14 +102,6 @@ class AlbumNotifierV2 extends StateNotifier> { } } - Future> getRemoteAlbums() { - return db.albums.filter().remoteIdIsNotNull().findAll(); - } - - Future> getLocalAlbums() { - return db.albums.filter().not().remoteIdIsNotNull().findAll(); - } - @override void dispose() { _streamSub.cancel(); @@ -125,3 +116,59 @@ final albumProviderV2 = ref.watch(dbProvider), ); }); + +class RemoteAlbumsNotifier extends StateNotifier> { + RemoteAlbumsNotifier(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 Isar db; + late final StreamSubscription> _streamSub; + + @override + void dispose() { + _streamSub.cancel(); + super.dispose(); + } +} + +class LocalAlbumsNotifier extends StateNotifier> { + LocalAlbumsNotifier(this.db) : super([]) { + final query = db.albums.filter().not().remoteIdIsNotNull(); + + query.findAll().then((value) { + if (mounted) { + state = value; + } + }); + + _streamSub = query.watch().listen((data) => state = data); + } + + final Isar db; + late final StreamSubscription> _streamSub; + + @override + void dispose() { + _streamSub.cancel(); + super.dispose(); + } +} + +final localAlbumsProvider = + StateNotifierProvider.autoDispose>((ref) { + return LocalAlbumsNotifier(ref.watch(dbProvider)); +}); + +final remoteAlbumsProvider = + StateNotifierProvider.autoDispose>((ref) { + return RemoteAlbumsNotifier(ref.watch(dbProvider)); +}); From bb50ccb1ca448912296167e97b8ccf0a2cdda10d Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 8 Sep 2024 14:41:00 -0500 Subject: [PATCH 18/26] local albums --- mobile/assets/i18n/en-US.json | 12 +++++- .../albums/albums_collection.page.dart | 6 +-- .../albums/local_albums_collection.page.dart | 42 +++++++++++++++++-- .../pages/collections/collections.page.dart | 32 ++++++++++---- .../lib/pages/common/tab_controller.page.dart | 4 +- 5 files changed, 78 insertions(+), 18 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 324c9069fdf46..8941bb3f53a26 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,4 +1,14 @@ { + "collections": "Collections", + "on_this_device": "On this device", + "places": "Places", + "albums": "Albums", + "people": "People", + "shared_links": "Shared links", + "trash": "Trash", + "archived": "Archived", + "favorites": "Favorites", + "search_albums": "Search albums", "action_common_back": "Back", "action_common_cancel": "Cancel", "action_common_clear": "Clear", @@ -589,4 +599,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index d11760d9e820a..f51b6dc01ab05 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -83,7 +83,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { context.colorScheme.surfaceContainer, ), autoFocus: false, - hintText: "Search albums", + hintText: "search_albums".tr(), onChanged: onSearch, elevation: const WidgetStatePropertyAll(0.25), controller: searchController, @@ -96,7 +96,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { borderRadius: BorderRadius.circular(20), side: BorderSide( color: context.colorScheme.onSurface.withAlpha(10), - width: 1, + width: 0.5, ), ), ), @@ -127,7 +127,6 @@ class AlbumsCollectionPage extends HookConsumerWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SortButton(), - const SizedBox(width: 10), IconButton( icon: Icon( isGrid.value @@ -175,7 +174,6 @@ class AlbumsCollectionPage extends HookConsumerWidget { title: Text( sorted[index].name, style: context.textTheme.titleSmall?.copyWith( - // fontSize: 16, fontWeight: FontWeight.w600, ), ), diff --git a/mobile/lib/pages/collections/albums/local_albums_collection.page.dart b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart index 4f31cc9e01f46..1be821d3c92da 100644 --- a/mobile/lib/pages/collections/albums/local_albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/local_albums_collection.page.dart @@ -1,18 +1,54 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/albumv2.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @RoutePage() class LocalAlbumsCollectionPage extends HookConsumerWidget { const LocalAlbumsCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final albums = ref.watch(localAlbumsProvider); + return Scaffold( appBar: AppBar( - title: const Text('on_this_device'), + title: Text('on_this_device'.tr()), ), - body: const Center( - child: Text('on_this_device_content'), + body: ListView.builder( + padding: const EdgeInsets.all(18.0), + itemCount: albums.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: ListTile( + contentPadding: const EdgeInsets.all(0), + dense: false, + visualDensity: VisualDensity.comfortable, + leading: ClipRRect( + borderRadius: const BorderRadius.all(Radius.circular(15)), + child: ImmichThumbnail( + asset: albums[index].thumbnail.value, + width: 60, + height: 90, + ), + ), + minVerticalPadding: 1, + title: Text( + albums[index].name, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + subtitle: Text('${albums[index].assetCount} items'), + onTap: () => context + .pushRoute(AlbumViewerRoute(albumId: albums[index].id)), + ), + ); + }, ), ); } diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 317a93e5b3bce..8cf13f8354b4d 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; @@ -38,13 +39,13 @@ class CollectionsPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const FavoritesRoute()), icon: Icons.favorite_outline_rounded, - label: 'Favorite', + label: 'favorites'.tr(), ), const SizedBox(width: 8), ActionButton( onPressed: () => context.pushRoute(const ArchiveRoute()), icon: Icons.archive_outlined, - label: 'Archive', + label: 'archived'.tr(), ), ], ), @@ -54,14 +55,14 @@ class CollectionsPage extends ConsumerWidget { ActionButton( onPressed: () => context.pushRoute(const SharedLinkRoute()), icon: Icons.link_outlined, - label: 'Shared links', + label: 'shared_links'.tr(), ), const SizedBox(width: 8), trashEnabled ? ActionButton( onPressed: () => context.pushRoute(const TrashRoute()), icon: Icons.delete_outline_rounded, - label: 'Trash', + label: 'trash'.tr(), ) : const SizedBox.shrink(), ], @@ -125,7 +126,13 @@ class PeopleCollectionCard extends ConsumerWidget { ), Padding( padding: const EdgeInsets.all(8.0), - child: Text('People', style: context.textTheme.labelLarge), + child: Text( + 'people'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), ), ], ), @@ -179,8 +186,11 @@ class AlbumsCollectionCard extends HookConsumerWidget { Padding( padding: const EdgeInsets.all(8.0), child: Text( - isLocal ? 'On this device' : 'Albums', - style: context.textTheme.labelLarge, + isLocal ? 'on_this_device'.tr() : 'albums'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), ), ), ], @@ -221,7 +231,13 @@ class PlacesCollectionCard extends StatelessWidget { ), Padding( padding: const EdgeInsets.all(8.0), - child: Text('Places', style: context.textTheme.labelLarge), + child: Text( + 'places'.tr(), + style: context.textTheme.titleSmall?.copyWith( + color: context.colorScheme.onSurface, + fontWeight: FontWeight.w500, + ), + ), ), ], ), diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index ca3274bd6bcda..234ec1153e9e8 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -98,7 +98,7 @@ class TabControllerPage extends HookConsumerWidget { padding: const EdgeInsets.all(4), icon: const Icon(Icons.photo_album_outlined), selectedIcon: const Icon(Icons.photo_album), - label: const Text('Collections').tr(), + label: const Text('collections').tr(), ), ], ); @@ -163,7 +163,7 @@ class TabControllerPage extends HookConsumerWidget { // ), // ), NavigationDestination( - label: 'Collections'.tr(), + label: 'collections'.tr(), icon: const Icon( Icons.photo_album_outlined, ), From 0a9d8ac3805d5152056c78714b20bac479866317 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 9 Sep 2024 14:58:15 -0500 Subject: [PATCH 19/26] swipe to go back from album view --- mobile/lib/routing/router.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index e02064d95165c..c840c0c0e2175 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -169,10 +169,9 @@ class AppRouter extends RootStackRouter { guards: [_authGuard, _duplicateGuard], transitionsBuilder: TransitionsBuilders.slideBottom, ), - CustomRoute( + AutoRoute( page: AlbumViewerRoute.page, guards: [_authGuard, _duplicateGuard], - transitionsBuilder: TransitionsBuilders.slideBottom, ), CustomRoute( page: AlbumAdditionalSharedUserSelectionRoute.page, From 8475cfb7a5db0b908c8456639b5968b7b2d0c39f Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 10 Sep 2024 10:50:49 -0500 Subject: [PATCH 20/26] people collection page --- mobile/assets/i18n/en-US.json | 1 + .../albums/people_collection.page.dart | 19 ---- .../people/people_collection.page.dart | 104 ++++++++++++++++++ .../places_collection.part.dart | 0 .../lib/pages/search/person_result.page.dart | 9 +- mobile/lib/routing/router.dart | 4 +- 6 files changed, 113 insertions(+), 24 deletions(-) delete mode 100644 mobile/lib/pages/collections/albums/people_collection.page.dart create mode 100644 mobile/lib/pages/collections/people/people_collection.page.dart rename mobile/lib/pages/collections/{albums => places}/places_collection.part.dart (100%) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 8941bb3f53a26..c28698e6836aa 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -1,6 +1,7 @@ { "collections": "Collections", "on_this_device": "On this device", + "add_a_name": "Add a name", "places": "Places", "albums": "Albums", "people": "People", diff --git a/mobile/lib/pages/collections/albums/people_collection.page.dart b/mobile/lib/pages/collections/albums/people_collection.page.dart deleted file mode 100644 index a37bb256a4c05..0000000000000 --- a/mobile/lib/pages/collections/albums/people_collection.page.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; - -@RoutePage() -class PeopleCollectionPage extends HookConsumerWidget { - const PeopleCollectionPage({super.key}); - @override - Widget build(BuildContext context, WidgetRef ref) { - return Scaffold( - appBar: AppBar( - title: const Text('people'), - ), - body: const Center( - child: Text('people'), - ), - ); - } -} diff --git a/mobile/lib/pages/collections/people/people_collection.page.dart b/mobile/lib/pages/collections/people/people_collection.page.dart new file mode 100644 index 0000000000000..b3f688280810c --- /dev/null +++ b/mobile/lib/pages/collections/people/people_collection.page.dart @@ -0,0 +1,104 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/search/people.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/utils/image_url_builder.dart'; +import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; + +@RoutePage() +class PeopleCollectionPage extends HookConsumerWidget { + const PeopleCollectionPage({super.key}); + @override + Widget build(BuildContext context, WidgetRef ref) { + final people = ref.watch(getAllPeopleProvider); + final headers = ApiService.getRequestHeaders(); + + showNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ); + } + + return Scaffold( + appBar: AppBar( + title: Text('people'.tr()), + ), + body: people.when( + data: (people) { + return GridView.builder( + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 0.85, + ), + padding: const EdgeInsets.symmetric(vertical: 32), + itemCount: people.length, + itemBuilder: (context, index) { + final person = people[index]; + + return Column( + children: [ + GestureDetector( + onTap: () { + context.pushRoute( + PersonResultRoute( + personId: person.id, + personName: person.name, + ), + ); + }, + child: Material( + shape: const CircleBorder(side: BorderSide.none), + elevation: 3, + child: CircleAvatar( + maxRadius: 96 / 2, + backgroundImage: NetworkImage( + getFaceThumbnailUrl(person.id), + headers: headers, + ), + ), + ), + ), + const SizedBox(height: 12), + GestureDetector( + onTap: () => showNameEditModel(person.id, person.name), + child: person.name.isEmpty + ? Text( + 'add_a_name'.tr(), + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + color: context.colorScheme.primary, + ), + ) + : Padding( + padding: + const EdgeInsets.symmetric(horizontal: 16.0), + child: Text( + person.name, + overflow: TextOverflow.ellipsis, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ], + ); + }, + ); + }, + error: (error, stack) => const Text("error"), + loading: () => const CircularProgressIndicator(), + ), + ); + } +} diff --git a/mobile/lib/pages/collections/albums/places_collection.part.dart b/mobile/lib/pages/collections/places/places_collection.part.dart similarity index 100% rename from mobile/lib/pages/collections/albums/places_collection.part.dart rename to mobile/lib/pages/collections/places/places_collection.part.dart diff --git a/mobile/lib/pages/search/person_result.page.dart b/mobile/lib/pages/search/person_result.page.dart index 55824b8db91f6..8627c65bcccef 100644 --- a/mobile/lib/pages/search/person_result.page.dart +++ b/mobile/lib/pages/search/person_result.page.dart @@ -92,6 +92,7 @@ class PersonResultPage extends HookConsumerWidget { Text( name.value, style: context.textTheme.titleLarge, + overflow: TextOverflow.ellipsis, ), ], ), @@ -125,9 +126,11 @@ class PersonResultPage extends HookConsumerWidget { headers: ApiService.getRequestHeaders(), ), ), - Padding( - padding: const EdgeInsets.only(left: 16.0), - child: buildTitleBlock(), + Expanded( + child: Padding( + padding: const EdgeInsets.only(left: 16.0, right: 16.0), + child: buildTitleBlock(), + ), ), ], ), diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index c840c0c0e2175..4521442fb5940 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -15,8 +15,8 @@ import 'package:immich_mobile/pages/backup/backup_options.page.dart'; import 'package:immich_mobile/pages/backup/failed_backup_status.page.dart'; import 'package:immich_mobile/pages/collections/albums/albums_collection.page.dart'; import 'package:immich_mobile/pages/collections/albums/local_albums_collection.page.dart'; -import 'package:immich_mobile/pages/collections/albums/people_collection.page.dart'; -import 'package:immich_mobile/pages/collections/albums/places_collection.part.dart'; +import 'package:immich_mobile/pages/collections/people/people_collection.page.dart'; +import 'package:immich_mobile/pages/collections/places/places_collection.part.dart'; import 'package:immich_mobile/pages/collections/collections.page.dart'; import 'package:immich_mobile/pages/common/activities.page.dart'; import 'package:immich_mobile/pages/common/album_additional_shared_user_selection.page.dart'; From 701b2be633ef20bec227740cb0b262ed49e1ec48 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 10 Sep 2024 15:12:39 -0500 Subject: [PATCH 21/26] place collections --- .../places/places_collection.part.dart | 120 +++++++++++++++++- 1 file changed, 117 insertions(+), 3 deletions(-) diff --git a/mobile/lib/pages/collections/places/places_collection.part.dart b/mobile/lib/pages/collections/places/places_collection.part.dart index 665e915fe3adc..97c6dffddba0c 100644 --- a/mobile/lib/pages/collections/places/places_collection.part.dart +++ b/mobile/lib/pages/collections/places/places_collection.part.dart @@ -1,18 +1,132 @@ import 'package:auto_route/auto_route.dart'; +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/models/search/search_curated_content.model.dart'; +import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/widgets/map/map_thumbnail.dart'; +import 'package:maplibre_gl/maplibre_gl.dart'; @RoutePage() class PlacesCollectionPage extends HookConsumerWidget { const PlacesCollectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { + final places = ref.watch(getAllPlacesProvider); + return Scaffold( appBar: AppBar( - title: const Text('places'), + title: Text('places'.tr()), ), - body: const Center( - child: Text('places'), + body: ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: SizedBox( + height: 200, + width: context.width, + child: MapThumbnail( + onTap: (_, __) => context.pushRoute(const MapRoute()), + zoom: 8, + centre: const LatLng( + 21.44950, + -157.91959, + ), + showAttribution: false, + themeMode: + context.isDarkTheme ? ThemeMode.dark : ThemeMode.light, + ), + ), + ), + places.when( + data: (places) { + return ListView.builder( + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: places.length, + itemBuilder: (context, index) { + final place = places[index]; + + return PlaceTile(id: place.id, name: place.label); + }, + ); + }, + error: (error, stask) => const Text('Error getting places'), + loading: () => const CircularProgressIndicator(), + ), + ], + ), + ); + } +} + +class PlaceTile extends StatelessWidget { + const PlaceTile({super.key, required this.id, required this.name}); + + final String id; + final String name; + + @override + Widget build(BuildContext context) { + final thumbnailUrl = + '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail'; + + void navigateToPlace() { + context.pushRoute( + SearchInputRoute( + prefilter: SearchFilter( + people: {}, + location: SearchLocationFilter( + city: name, + ), + camera: SearchCameraFilter(), + date: SearchDateFilter(), + display: SearchDisplayFilters( + isNotInAlbum: false, + isArchive: false, + isFavorite: false, + ), + mediaType: AssetType.other, + ), + ), + ); + } + + return InkWell( + onTap: () => navigateToPlace(), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16.0), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + width: 80, + height: 80, + fit: BoxFit.cover, + imageUrl: thumbnailUrl, + httpHeaders: ApiService.getRequestHeaders(), + errorWidget: (context, url, error) => + const Icon(Icons.image_not_supported_outlined), + ), + ), + ), + Text( + name, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + ], ), ); } From 4d45e36ea04eb9b1e1e10d6213be7e1e3efda8ef Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 08:46:59 -0500 Subject: [PATCH 22/26] wip --- .../lib/providers/album/albumv2.provider.dart | 7 +++--- mobile/lib/services/album.service.dart | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart index 3683a7d24f10b..9d6cc63075dfb 100644 --- a/mobile/lib/providers/album/albumv2.provider.dart +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -27,9 +27,10 @@ class AlbumNotifierV2 extends StateNotifier> { late final StreamSubscription> _streamSub; Future refreshAlbums() async { - await _albumService.refreshDeviceAlbums(); - await _albumService.refreshRemoteAlbums(isShared: false); - await _albumService.refreshRemoteAlbums(isShared: true); + Future.wait([ + _albumService.refreshDeviceAlbums(), + _albumService.refreshAllRemoteAlbums(), + ]); } Future getDeviceAlbums() { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index ef56f9bf6c12a..d6127cd1a6046 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -173,6 +173,30 @@ class AlbumService { return changes; } + /// V2 + Future refreshAllRemoteAlbums() async { + final Stopwatch sw = Stopwatch()..start(); + try { + final [sharedAlbums, ownedAlbums] = await Future.wait([ + _apiService.albumsApi.getAllAlbums(shared: true), + _apiService.albumsApi.getAllAlbums(shared: false), + ]); + + final List allAlbums = [ + ...sharedAlbums ?? [], + ...ownedAlbums ?? [], + ]; + + print("All albums: ${allAlbums.length}"); + + debugPrint("refreshAllAlbums took ${sw.elapsedMilliseconds}ms"); + return true; + } catch (e) { + debugPrint("Error refreshing all albums: $e"); + return false; + } + } + Future createAlbum( String albumName, Iterable assets, [ From bee7cd9683819c579856cef380f90f2d8077e4b5 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 11:22:51 -0500 Subject: [PATCH 23/26] add albums to dedicated item menu --- .../lib/pages/common/tab_controller.page.dart | 21 ++++++++++++++++++- mobile/lib/routing/router.dart | 4 ++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index 234ec1153e9e8..33faf101b6e15 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -98,6 +98,12 @@ class TabControllerPage extends HookConsumerWidget { padding: const EdgeInsets.all(4), icon: const Icon(Icons.photo_album_outlined), selectedIcon: const Icon(Icons.photo_album), + label: const Text('albums').tr(), + ), + NavigationRailDestination( + padding: const EdgeInsets.all(4), + icon: const Icon(Icons.space_dashboard_outlined), + selectedIcon: const Icon(Icons.space_dashboard_rounded), label: const Text('collections').tr(), ), ], @@ -163,7 +169,7 @@ class TabControllerPage extends HookConsumerWidget { // ), // ), NavigationDestination( - label: 'collections'.tr(), + label: 'albums'.tr(), icon: const Icon( Icons.photo_album_outlined, ), @@ -174,6 +180,18 @@ class TabControllerPage extends HookConsumerWidget { ), ), ), + NavigationDestination( + label: 'collections'.tr(), + icon: const Icon( + Icons.space_dashboard_outlined, + ), + selectedIcon: buildIcon( + Icon( + Icons.space_dashboard_rounded, + color: context.primaryColor, + ), + ), + ), ], ); } @@ -185,6 +203,7 @@ class TabControllerPage extends HookConsumerWidget { SearchRoute(), // SharingRoute(), // LibraryRoute(), + AlbumsCollectionRoute(), CollectionsRoute(), ], duration: const Duration(milliseconds: 600), diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 4521442fb5940..7705de009aa65 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -123,6 +123,10 @@ class AppRouter extends RootStackRouter { page: CollectionsRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute( + page: AlbumsCollectionRoute.page, + guards: [_authGuard, _duplicateGuard], + ), ], transitionsBuilder: TransitionsBuilders.fadeIn, ), From e69bcf3f705a01e1058cff76ac0bbf7dd044436d Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 13:56:31 -0500 Subject: [PATCH 24/26] add albums to dedicated item menu --- .../albums/albums_collection.page.dart | 34 ++++++-- .../pages/collections/collections.page.dart | 10 +-- .../places/places_collection.part.dart | 46 +++++------ .../lib/pages/common/large_leading_tile.dart | 47 +++++++++++ .../lib/pages/common/tab_controller.page.dart | 82 +++++++++---------- mobile/lib/routing/router.gr.dart | 36 +++++++- 6 files changed, 169 insertions(+), 86 deletions(-) create mode 100644 mobile/lib/pages/common/large_leading_tile.dart diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index f51b6dc01ab05..f71bfbafc0a52 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -8,11 +8,14 @@ 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/extensions/theme_extensions.dart'; +import 'package:immich_mobile/main.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.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/providers/user.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; +import 'package:immich_mobile/widgets/common/immich_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; enum QuickFilterMode { @@ -23,7 +26,10 @@ enum QuickFilterMode { @RoutePage() class AlbumsCollectionPage extends HookConsumerWidget { - const AlbumsCollectionPage({super.key}); + const AlbumsCollectionPage({super.key, this.showImmichAppbar = false}); + + final bool showImmichAppbar; + @override Widget build(BuildContext context, WidgetRef ref) { final albums = @@ -72,7 +78,13 @@ class AlbumsCollectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: Text("Albums ${albums.length}"), + title: showImmichAppbar ? null : Text('albums'.tr()), + bottom: showImmichAppbar + ? const PreferredSize( + preferredSize: Size.fromHeight(0), + child: ImmichAppBar(), + ) + : null, ), body: ListView( shrinkWrap: true, @@ -170,9 +182,11 @@ class AlbumsCollectionPage extends HookConsumerWidget { itemBuilder: (context, index) { return Padding( padding: const EdgeInsets.only(bottom: 8.0), - child: ListTile( + child: LargeLeadingTile( title: Text( sorted[index].name, + maxLines: 2, + overflow: TextOverflow.ellipsis, style: context.textTheme.titleSmall?.copyWith( fontWeight: FontWeight.w600, ), @@ -180,6 +194,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { subtitle: sorted[index].ownerId == userId ? Text( '${sorted[index].assetCount} items', + overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( color: context.colorScheme.onSurfaceSecondary, @@ -192,6 +207,7 @@ class AlbumsCollectionPage extends HookConsumerWidget { sorted[index].ownerName!, ], )}', + overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium ?.copyWith( color: context @@ -202,19 +218,19 @@ class AlbumsCollectionPage extends HookConsumerWidget { onTap: () => context.pushRoute( AlbumViewerRoute(albumId: sorted[index].id), ), - contentPadding: const EdgeInsets.all(0), - dense: false, - visualDensity: VisualDensity.comfortable, + leadingPadding: const EdgeInsets.only( + right: 16, + ), leading: ClipRRect( borderRadius: const BorderRadius.all(Radius.circular(15)), child: ImmichThumbnail( asset: sorted[index].thumbnail.value, - width: 60, - height: 90, + width: 80, + height: 80, ), ), - minVerticalPadding: 1, + // minVerticalPadding: 1, ), ); }, diff --git a/mobile/lib/pages/collections/collections.page.dart b/mobile/lib/pages/collections/collections.page.dart index 8cf13f8354b4d..c37103788161f 100644 --- a/mobile/lib/pages/collections/collections.page.dart +++ b/mobile/lib/pages/collections/collections.page.dart @@ -154,11 +154,11 @@ class AlbumsCollectionCard extends HookConsumerWidget { final size = MediaQuery.of(context).size.width * 0.5 - 20; return GestureDetector( - onTap: () => context.pushRoute( - isLocal - ? const LocalAlbumsCollectionRoute() - : const AlbumsCollectionRoute(), - ), + onTap: () => isLocal + ? context.pushRoute( + const LocalAlbumsCollectionRoute(), + ) + : context.pushRoute(AlbumsCollectionRoute()), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ diff --git a/mobile/lib/pages/collections/places/places_collection.part.dart b/mobile/lib/pages/collections/places/places_collection.part.dart index 97c6dffddba0c..639c5f0c6c356 100644 --- a/mobile/lib/pages/collections/places/places_collection.part.dart +++ b/mobile/lib/pages/collections/places/places_collection.part.dart @@ -6,8 +6,8 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; +import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/search/search_page_state.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/services/api.service.dart'; @@ -100,33 +100,25 @@ class PlaceTile extends StatelessWidget { ); } - return InkWell( + return LargeLeadingTile( onTap: () => navigateToPlace(), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16.0), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: CachedNetworkImage( - width: 80, - height: 80, - fit: BoxFit.cover, - imageUrl: thumbnailUrl, - httpHeaders: ApiService.getRequestHeaders(), - errorWidget: (context, url, error) => - const Icon(Icons.image_not_supported_outlined), - ), - ), - ), - Text( - name, - style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w500, - ), - ), - ], + title: Text( + name, + style: context.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + leading: ClipRRect( + borderRadius: BorderRadius.circular(20), + child: CachedNetworkImage( + width: 80, + height: 80, + fit: BoxFit.cover, + imageUrl: thumbnailUrl, + httpHeaders: ApiService.getRequestHeaders(), + errorWidget: (context, url, error) => + const Icon(Icons.image_not_supported_outlined), + ), ), ); } diff --git a/mobile/lib/pages/common/large_leading_tile.dart b/mobile/lib/pages/common/large_leading_tile.dart new file mode 100644 index 0000000000000..7f1eb44dc6c76 --- /dev/null +++ b/mobile/lib/pages/common/large_leading_tile.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +class LargeLeadingTile extends StatelessWidget { + const LargeLeadingTile({ + super.key, + required this.leading, + required this.onTap, + required this.title, + this.subtitle, + this.leadingPadding = const EdgeInsets.symmetric( + vertical: 8, + horizontal: 16.0, + ), + }); + + final Widget leading; + final VoidCallback onTap; + final Widget title; + final Widget? subtitle; + final EdgeInsetsGeometry leadingPadding; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Padding( + padding: leadingPadding, + child: leading, + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: MediaQuery.of(context).size.width * 0.6, + child: title, + ), + subtitle ?? const SizedBox.shrink(), + ], + ), + ], + ), + ); + } +} diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index 33faf101b6e15..8866f169c732f 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -76,6 +76,18 @@ class TabControllerPage extends HookConsumerWidget { selectedIcon: const Icon(Icons.photo_library), label: const Text('tab_controller_nav_photos').tr(), ), + NavigationRailDestination( + padding: const EdgeInsets.all(4), + icon: const Icon(Icons.photo_album_outlined), + selectedIcon: const Icon(Icons.photo_album), + label: const Text('albums').tr(), + ), + NavigationRailDestination( + padding: const EdgeInsets.all(4), + icon: const Icon(Icons.space_dashboard_outlined), + selectedIcon: const Icon(Icons.space_dashboard_rounded), + label: const Text('collections').tr(), + ), NavigationRailDestination( padding: const EdgeInsets.all(4), icon: const Icon(Icons.search_rounded), @@ -94,18 +106,6 @@ class TabControllerPage extends HookConsumerWidget { // selectedIcon: const Icon(Icons.photo_album), // label: const Text('tab_controller_nav_library').tr(), // ), - NavigationRailDestination( - padding: const EdgeInsets.all(4), - icon: const Icon(Icons.photo_album_outlined), - selectedIcon: const Icon(Icons.photo_album), - label: const Text('albums').tr(), - ), - NavigationRailDestination( - padding: const EdgeInsets.all(4), - icon: const Icon(Icons.space_dashboard_outlined), - selectedIcon: const Icon(Icons.space_dashboard_rounded), - label: const Text('collections').tr(), - ), ], ); } @@ -136,6 +136,30 @@ class TabControllerPage extends HookConsumerWidget { ), ), ), + NavigationDestination( + label: 'albums'.tr(), + icon: const Icon( + Icons.photo_album_outlined, + ), + selectedIcon: buildIcon( + Icon( + Icons.photo_album_rounded, + color: context.primaryColor, + ), + ), + ), + NavigationDestination( + label: 'collections'.tr(), + icon: const Icon( + Icons.space_dashboard_outlined, + ), + selectedIcon: buildIcon( + Icon( + Icons.space_dashboard_rounded, + color: context.primaryColor, + ), + ), + ), NavigationDestination( label: 'tab_controller_nav_search'.tr(), icon: const Icon( @@ -168,43 +192,19 @@ class TabControllerPage extends HookConsumerWidget { // ), // ), // ), - NavigationDestination( - label: 'albums'.tr(), - icon: const Icon( - Icons.photo_album_outlined, - ), - selectedIcon: buildIcon( - Icon( - Icons.photo_album_rounded, - color: context.primaryColor, - ), - ), - ), - NavigationDestination( - label: 'collections'.tr(), - icon: const Icon( - Icons.space_dashboard_outlined, - ), - selectedIcon: buildIcon( - Icon( - Icons.space_dashboard_rounded, - color: context.primaryColor, - ), - ), - ), ], ); } final multiselectEnabled = ref.watch(multiselectProvider); return AutoTabsRouter( - routes: const [ - PhotosRoute(), - SearchRoute(), + routes: [ + const PhotosRoute(), + AlbumsCollectionRoute(showImmichAppbar: true), + const CollectionsRoute(), // SharingRoute(), // LibraryRoute(), - AlbumsCollectionRoute(), - CollectionsRoute(), + const SearchRoute(), ], duration: const Duration(milliseconds: 600), transitionBuilder: (context, child, animation) => FadeTransition( diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 16f65f948ba8f..cbd461d8845f5 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -321,10 +321,17 @@ class AlbumViewerRouteArgs { /// generated route for /// [AlbumsCollectionPage] -class AlbumsCollectionRoute extends PageRouteInfo { - const AlbumsCollectionRoute({List? children}) - : super( +class AlbumsCollectionRoute extends PageRouteInfo { + AlbumsCollectionRoute({ + Key? key, + bool showImmichAppbar = false, + List? children, + }) : super( AlbumsCollectionRoute.name, + args: AlbumsCollectionRouteArgs( + key: key, + showImmichAppbar: showImmichAppbar, + ), initialChildren: children, ); @@ -333,11 +340,32 @@ class AlbumsCollectionRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - return const AlbumsCollectionPage(); + final args = data.argsAs( + orElse: () => const AlbumsCollectionRouteArgs()); + return AlbumsCollectionPage( + key: args.key, + showImmichAppbar: args.showImmichAppbar, + ); }, ); } +class AlbumsCollectionRouteArgs { + const AlbumsCollectionRouteArgs({ + this.key, + this.showImmichAppbar = false, + }); + + final Key? key; + + final bool showImmichAppbar; + + @override + String toString() { + return 'AlbumsCollectionRouteArgs{key: $key, showImmichAppbar: $showImmichAppbar}'; + } +} + /// generated route for /// [AllMotionPhotosPage] class AllMotionPhotosRoute extends PageRouteInfo { From 4c9a7a7ed5c9a14a4ce56aefbc7152630d46e8f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Sep 2024 14:26:36 -0500 Subject: [PATCH 25/26] wip --- .../albums/albums_collection.page.dart | 7 +++- .../lib/providers/album/albumv2.provider.dart | 11 ++++-- mobile/lib/services/album.service.dart | 37 ++++++++++++++----- 3 files changed, 40 insertions(+), 15 deletions(-) diff --git a/mobile/lib/pages/collections/albums/albums_collection.page.dart b/mobile/lib/pages/collections/albums/albums_collection.page.dart index f71bfbafc0a52..07912b681def1 100644 --- a/mobile/lib/pages/collections/albums/albums_collection.page.dart +++ b/mobile/lib/pages/collections/albums/albums_collection.page.dart @@ -8,7 +8,6 @@ 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/extensions/theme_extensions.dart'; -import 'package:immich_mobile/main.dart'; import 'package:immich_mobile/pages/common/large_leading_tile.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/albumv2.provider.dart'; @@ -78,7 +77,11 @@ class AlbumsCollectionPage extends HookConsumerWidget { return Scaffold( appBar: AppBar( - title: showImmichAppbar ? null : Text('albums'.tr()), + title: showImmichAppbar + ? null + : Text( + "${'albums'.tr()} ${albums.length}", + ), bottom: showImmichAppbar ? const PreferredSize( preferredSize: Size.fromHeight(0), diff --git a/mobile/lib/providers/album/albumv2.provider.dart b/mobile/lib/providers/album/albumv2.provider.dart index 9d6cc63075dfb..23d6329c36ab1 100644 --- a/mobile/lib/providers/album/albumv2.provider.dart +++ b/mobile/lib/providers/album/albumv2.provider.dart @@ -27,10 +27,13 @@ class AlbumNotifierV2 extends StateNotifier> { late final StreamSubscription> _streamSub; Future refreshAlbums() async { - Future.wait([ - _albumService.refreshDeviceAlbums(), - _albumService.refreshAllRemoteAlbums(), - ]); + // Future.wait([ + // _albumService.refreshDeviceAlbums(), + // _albumService.refreshAllRemoteAlbums(), + // ]); + await _albumService.refreshDeviceAlbums(); + await _albumService.refreshRemoteAlbums(isShared: false); + await _albumService.refreshRemoteAlbums(isShared: true); } Future getDeviceAlbums() { diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index d6127cd1a6046..315e6e16b7f53 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -175,26 +175,45 @@ class AlbumService { /// V2 Future refreshAllRemoteAlbums() async { + if (!_remoteCompleter.isCompleted) { + // guard against concurrent calls + return _remoteCompleter.future; + } + _remoteCompleter = Completer(); final Stopwatch sw = Stopwatch()..start(); + bool changes = false; try { - final [sharedAlbums, ownedAlbums] = await Future.wait([ + final albumList = await Future.wait([ _apiService.albumsApi.getAllAlbums(shared: true), _apiService.albumsApi.getAllAlbums(shared: false), ]); - final List allAlbums = [ - ...sharedAlbums ?? [], - ...ownedAlbums ?? [], - ]; + // for (int i = 0; i < albumList.length; i++) { + // final albums = albumList[i]; + // final isShared = i == 1; + // if (albums != null) { + // final hasChange = await _syncService.syncRemoteAlbumsToDb( + // albums, + // isShared: isShared, + // loadDetails: (dto) async => dto.assetCount == dto.assets.length + // ? dto + // : (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto, + // ); - print("All albums: ${allAlbums.length}"); - - debugPrint("refreshAllAlbums took ${sw.elapsedMilliseconds}ms"); - return true; + // if (hasChange) { + // changes = true; + // } + // } + // } } catch (e) { debugPrint("Error refreshing all albums: $e"); return false; + } finally { + _remoteCompleter.complete(changes); } + + debugPrint("refreshAllRemoteAlbums took ${sw.elapsedMilliseconds}ms"); + return changes; } Future createAlbum( From 39017922a9555b938e6d6d12396ea17e7b20908f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 30 Sep 2024 11:34:55 +0700 Subject: [PATCH 26/26] fixed build --- mobile/lib/services/album.service.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index a3320e2e65201..d01a009c30e92 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -186,8 +186,8 @@ class AlbumService { bool changes = false; try { final albumList = await Future.wait([ - _apiService.albumsApi.getAllAlbums(shared: true), - _apiService.albumsApi.getAllAlbums(shared: false), + // _apiService.albumsApi.getAllAlbums(shared: true), + // _apiService.albumsApi.getAllAlbums(shared: false), ]); // for (int i = 0; i < albumList.length; i++) {