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:
parent
b29c43d86a
commit
646b912da8
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
Loading…
x
Reference in New Issue
Block a user