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) {