1
0
mirror of https://github.com/immich-app/immich.git synced 2025-02-15 19:36:04 +02:00

feat(mobile): Share album name and adaptive shared album display (#2017)

* shows the owner name of shared albums

* responsive and better names

* rich text

* localization and overflow

* unused import

* adds on tap

* suppress owner name for regular album view

* aspect ratio

* Add some styling to text

* More styling

* Style album thumbnail name

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
martyfuhry 2023-03-19 15:47:51 -04:00 committed by GitHub
parent b29c43d86a
commit 646b912da8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 153 additions and 32 deletions

View File

@ -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_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_continue_anyway": "Continue anyway",
"permission_onboarding_log_out": "Log out", "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"
} }

View File

@ -1,15 +1,21 @@
import 'package:easy_localization/easy_localization.dart'; import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:immich_mobile/shared/models/album.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'; import 'package:immich_mobile/shared/ui/immich_image.dart';
class AlbumThumbnailCard extends StatelessWidget { class AlbumThumbnailCard extends StatelessWidget {
final Function()? onTap; 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({ const AlbumThumbnailCard({
Key? key, Key? key,
required this.album, required this.album,
this.onTap, this.onTap,
this.showOwner = false,
}) : super(key: key); }) : super(key: key);
final Album album; final Album album;
@ -43,6 +49,44 @@ class AlbumThumbnailCard extends StatelessWidget {
height: cardSize, 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( return GestureDetector(
onTap: onTap, onTap: onTap,
child: Flex( child: Flex(
@ -68,32 +112,16 @@ class AlbumThumbnailCard extends StatelessWidget {
width: cardSize, width: cardSize,
child: Text( child: Text(
album.name, album.name,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
),
),
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( style: TextStyle(
fontSize: 12, fontWeight: FontWeight.bold,
color: isDarkMode
? Theme.of(context).primaryColor
: Colors.black,
), ),
).tr() ),
], ),
) ),
buildAlbumTextRow(),
], ],
), ),
), ),

View File

@ -4,9 +4,11 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.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/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/modules/album/ui/sharing_sliver_appbar.dart';
import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/shared/models/album.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'; import 'package:immich_mobile/shared/ui/immich_image.dart';
class SharingPage extends HookConsumerWidget { class SharingPage extends HookConsumerWidget {
@ -15,6 +17,8 @@ class SharingPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider); final List<Album> sharedAlbums = ref.watch(sharedAlbumProvider);
final userId = store.Store.get(store.StoreKey.userRemoteId);
var isDarkMode = Theme.of(context).brightness == Brightness.dark;
useEffect( 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() { buildAlbumList() {
return SliverList( return SliverList(
delegate: SliverChildBuilderDelegate( delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) { (BuildContext context, int index) {
final album = sharedAlbums[index]; final album = sharedAlbums[index];
final isOwner = album.ownerId == userId;
return ListTile( return ListTile(
contentPadding: contentPadding:
@ -42,13 +74,31 @@ class SharingPage extends HookConsumerWidget {
), ),
), ),
title: Text( title: Text(
sharedAlbums[index].name, album.name,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, 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: () { onTap: () {
AutoRouter.of(context) AutoRouter.of(context)
.push(AlbumViewerRoute(albumId: sharedAlbums[index].id)); .push(AlbumViewerRoute(albumId: sharedAlbums[index].id));
@ -124,9 +174,19 @@ class SharingPage extends HookConsumerWidget {
).tr(), ).tr(),
), ),
), ),
sharedAlbums.isNotEmpty SliverLayoutBuilder(
? buildAlbumList() builder: (context, constraints) {
: buildEmptyListIndication() if (sharedAlbums.isEmpty) {
return buildEmptyListIndication();
}
if (constraints.crossAxisExtent < 600) {
return buildAlbumList();
} else {
return buildAlbumGrid();
}
},
),
], ],
), ),
); );

View File

@ -51,6 +51,24 @@ class Album {
@ignore @ignore
String? get ownerId => owner.value?.id; String? get ownerId => owner.value?.id;
@ignore
String? get ownerName {
// Guard null owner
if (owner.value == null) {
return null;
}
final name = <String>[];
if (owner.value?.firstName != null) {
name.add(owner.value!.firstName);
}
if (owner.value?.lastName != null) {
name.add(owner.value!.lastName);
}
return name.join(' ');
}
Future<void> loadSortedAssets() async { Future<void> loadSortedAssets() async {
_sortedAssets = await assets.filter().sortByFileCreatedAt().findAll(); _sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
} }

View File

@ -187,21 +187,34 @@ class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
returnValueForMissingStub: _i5.Future<void>.value(), returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>); ) as _i5.Future<void>);
@override @override
Future<void> onNewAssetUploaded(_i4.Asset? newAsset) => super.noSuchMethod( _i5.Future<void> clearAllAsset() => (super.noSuchMethod(
Invocation.method(
#clearAllAsset,
[],
),
returnValue: _i5.Future<void>.value(),
returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override
_i5.Future<void> onNewAssetUploaded(_i4.Asset? newAsset) =>
(super.noSuchMethod(
Invocation.method( Invocation.method(
#onNewAssetUploaded, #onNewAssetUploaded,
[newAsset], [newAsset],
), ),
returnValueForMissingStub: null, returnValue: _i5.Future<void>.value(),
); returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override @override
Future<void> deleteAssets(Set<_i4.Asset> deleteAssets) => super.noSuchMethod( _i5.Future<void> deleteAssets(Set<_i4.Asset>? deleteAssets) =>
(super.noSuchMethod(
Invocation.method( Invocation.method(
#deleteAssets, #deleteAssets,
[deleteAssets], [deleteAssets],
), ),
returnValueForMissingStub: null, returnValue: _i5.Future<void>.value(),
); returnValueForMissingStub: _i5.Future<void>.value(),
) as _i5.Future<void>);
@override @override
_i5.Future<bool> toggleFavorite( _i5.Future<bool> toggleFavorite(
_i4.Asset? asset, _i4.Asset? asset,