diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 5894cf681c..4b04bb8d69 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -244,5 +244,7 @@ "permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.", "permission_onboarding_continue_anyway": "Continue anyway", "permission_onboarding_log_out": "Log out", - "login_form_next_button": "Next" + "login_form_next_button": "Next", + "album_thumbnail_shared_by": "Shared by {}", + "album_thumbnail_owned": "Owned" } diff --git a/mobile/lib/modules/album/ui/album_thumbnail_card.dart b/mobile/lib/modules/album/ui/album_thumbnail_card.dart index 6c87519b2c..12e5f27dfe 100644 --- a/mobile/lib/modules/album/ui/album_thumbnail_card.dart +++ b/mobile/lib/modules/album/ui/album_thumbnail_card.dart @@ -1,15 +1,21 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/shared/models/album.dart'; +import 'package:immich_mobile/shared/models/store.dart'; import 'package:immich_mobile/shared/ui/immich_image.dart'; class AlbumThumbnailCard extends StatelessWidget { final Function()? onTap; + /// Whether or not to show the owner of the album (or "Owned") + /// in the subtitle of the album + final bool showOwner; + const AlbumThumbnailCard({ Key? key, required this.album, this.onTap, + this.showOwner = false, }) : super(key: key); final Album album; @@ -43,6 +49,44 @@ class AlbumThumbnailCard extends StatelessWidget { height: cardSize, ); + buildAlbumTextRow() { + // Add the owner name to the subtitle + String? owner; + if (showOwner) { + if (album.ownerId == Store.get(StoreKey.userRemoteId)) { + owner = 'album_thumbnail_owned'.tr(); + } else if (album.ownerName != null) { + owner = 'album_thumbnail_shared_by'.tr(args: [album.ownerName!]); + } + } + + return RichText( + overflow: TextOverflow.fade, + text: TextSpan( + children: [ + TextSpan( + text: album.assetCount == 1 + ? 'album_thumbnail_card_item' + .tr(args: ['${album.assetCount}']) + : 'album_thumbnail_card_items' + .tr(args: ['${album.assetCount}']), + style: TextStyle( + fontFamily: 'WorkSans', + fontSize: 12, + color: isDarkMode ? Colors.white : Colors.black, + ), + ), + if (owner != null) const TextSpan(text: ' ยท '), + if (owner != null) + TextSpan( + text: owner, + style: Theme.of(context).textTheme.labelSmall, + ), + ], + ), + ); + } + return GestureDetector( onTap: onTap, child: Flex( @@ -68,32 +112,16 @@ class AlbumThumbnailCard extends StatelessWidget { width: cardSize, child: Text( album.name, - style: const TextStyle( + style: TextStyle( fontWeight: FontWeight.bold, + color: isDarkMode + ? Theme.of(context).primaryColor + : Colors.black, ), ), ), ), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - album.assetCount == 1 - ? 'album_thumbnail_card_item' - : 'album_thumbnail_card_items', - style: const TextStyle( - fontSize: 12, - ), - ).tr(args: ['${album.assetCount}']), - if (album.shared) - const Text( - 'album_thumbnail_card_shared', - style: TextStyle( - fontSize: 12, - ), - ).tr() - ], - ) + buildAlbumTextRow(), ], ), ), diff --git a/mobile/lib/modules/album/views/sharing_page.dart b/mobile/lib/modules/album/views/sharing_page.dart index 7b1de07f99..32b0a3e145 100644 --- a/mobile/lib/modules/album/views/sharing_page.dart +++ b/mobile/lib/modules/album/views/sharing_page.dart @@ -4,9 +4,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/album/providers/shared_album.provider.dart'; +import 'package:immich_mobile/modules/album/ui/album_thumbnail_card.dart'; import 'package:immich_mobile/modules/album/ui/sharing_sliver_appbar.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/shared/models/album.dart'; +import 'package:immich_mobile/shared/models/store.dart' as store; import 'package:immich_mobile/shared/ui/immich_image.dart'; class SharingPage extends HookConsumerWidget { @@ -15,6 +17,8 @@ class SharingPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final List sharedAlbums = ref.watch(sharedAlbumProvider); + final userId = store.Store.get(store.StoreKey.userRemoteId); + var isDarkMode = Theme.of(context).brightness == Brightness.dark; useEffect( () { @@ -24,11 +28,39 @@ class SharingPage extends HookConsumerWidget { [], ); + buildAlbumGrid() { + return SliverPadding( + padding: const EdgeInsets.all(18.0), + sliver: SliverGrid( + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 250, + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: .7, + ), + delegate: SliverChildBuilderDelegate( + (context, index) { + return AlbumThumbnailCard( + album: sharedAlbums[index], + showOwner: true, + onTap: () { + AutoRouter.of(context) + .push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); + }, + ); + }, + childCount: sharedAlbums.length, + ), + ), + ); + } + buildAlbumList() { return SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { final album = sharedAlbums[index]; + final isOwner = album.ownerId == userId; return ListTile( contentPadding: @@ -42,13 +74,31 @@ class SharingPage extends HookConsumerWidget { ), ), title: Text( - sharedAlbums[index].name, + album.name, maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, + color: isDarkMode + ? Theme.of(context).primaryColor + : Colors.black, ), ), + subtitle: isOwner + ? const Text( + 'Owned', + style: TextStyle( + fontSize: 12.0, + ), + ) + : album.ownerName != null + ? Text( + 'Shared by ${album.ownerName!}', + style: const TextStyle( + fontSize: 12.0, + ), + ) + : null, onTap: () { AutoRouter.of(context) .push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); @@ -124,9 +174,19 @@ class SharingPage extends HookConsumerWidget { ).tr(), ), ), - sharedAlbums.isNotEmpty - ? buildAlbumList() - : buildEmptyListIndication() + SliverLayoutBuilder( + builder: (context, constraints) { + if (sharedAlbums.isEmpty) { + return buildEmptyListIndication(); + } + + if (constraints.crossAxisExtent < 600) { + return buildAlbumList(); + } else { + return buildAlbumGrid(); + } + }, + ), ], ), ); diff --git a/mobile/lib/shared/models/album.dart b/mobile/lib/shared/models/album.dart index 47040b623f..014bf543a6 100644 --- a/mobile/lib/shared/models/album.dart +++ b/mobile/lib/shared/models/album.dart @@ -51,6 +51,24 @@ class Album { @ignore String? get ownerId => owner.value?.id; + @ignore + String? get ownerName { + // Guard null owner + if (owner.value == null) { + return null; + } + + final name = []; + if (owner.value?.firstName != null) { + name.add(owner.value!.firstName); + } + if (owner.value?.lastName != null) { + name.add(owner.value!.lastName); + } + + return name.join(' '); + } + Future loadSortedAssets() async { _sortedAssets = await assets.filter().sortByFileCreatedAt().findAll(); } diff --git a/mobile/test/favorite_provider_test.mocks.dart b/mobile/test/favorite_provider_test.mocks.dart index e70036512d..0447cba0f2 100644 --- a/mobile/test/favorite_provider_test.mocks.dart +++ b/mobile/test/favorite_provider_test.mocks.dart @@ -187,21 +187,34 @@ class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier { returnValueForMissingStub: _i5.Future.value(), ) as _i5.Future); @override - Future onNewAssetUploaded(_i4.Asset? newAsset) => super.noSuchMethod( + _i5.Future clearAllAsset() => (super.noSuchMethod( + Invocation.method( + #clearAllAsset, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + @override + _i5.Future onNewAssetUploaded(_i4.Asset? newAsset) => + (super.noSuchMethod( Invocation.method( #onNewAssetUploaded, [newAsset], ), - returnValueForMissingStub: null, - ); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - Future deleteAssets(Set<_i4.Asset> deleteAssets) => super.noSuchMethod( + _i5.Future deleteAssets(Set<_i4.Asset>? deleteAssets) => + (super.noSuchMethod( Invocation.method( #deleteAssets, [deleteAssets], ), - returnValueForMissingStub: null, - ); + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future toggleFavorite( _i4.Asset? asset,