mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-04 03:27:09 -05: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