mirror of
				https://github.com/KevinMidboe/immich.git
				synced 2025-10-29 17:40:28 +00: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:
		@@ -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"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
                            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(
 | 
			
		||||
                              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: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<Album> 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();
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      ),
 | 
			
		||||
    );
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = <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 {
 | 
			
		||||
    _sortedAssets = await assets.filter().sortByFileCreatedAt().findAll();
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -187,21 +187,34 @@ class MockAssetNotifier extends _i1.Mock implements _i2.AssetNotifier {
 | 
			
		||||
        returnValueForMissingStub: _i5.Future<void>.value(),
 | 
			
		||||
      ) as _i5.Future<void>);
 | 
			
		||||
  @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(
 | 
			
		||||
          #onNewAssetUploaded,
 | 
			
		||||
          [newAsset],
 | 
			
		||||
        ),
 | 
			
		||||
        returnValueForMissingStub: null,
 | 
			
		||||
      );
 | 
			
		||||
        returnValue: _i5.Future<void>.value(),
 | 
			
		||||
        returnValueForMissingStub: _i5.Future<void>.value(),
 | 
			
		||||
      ) as _i5.Future<void>);
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> deleteAssets(Set<_i4.Asset> deleteAssets) => super.noSuchMethod(
 | 
			
		||||
  _i5.Future<void> deleteAssets(Set<_i4.Asset>? deleteAssets) =>
 | 
			
		||||
      (super.noSuchMethod(
 | 
			
		||||
        Invocation.method(
 | 
			
		||||
          #deleteAssets,
 | 
			
		||||
          [deleteAssets],
 | 
			
		||||
        ),
 | 
			
		||||
        returnValueForMissingStub: null,
 | 
			
		||||
      );
 | 
			
		||||
        returnValue: _i5.Future<void>.value(),
 | 
			
		||||
        returnValueForMissingStub: _i5.Future<void>.value(),
 | 
			
		||||
      ) as _i5.Future<void>);
 | 
			
		||||
  @override
 | 
			
		||||
  _i5.Future<bool> toggleFavorite(
 | 
			
		||||
    _i4.Asset? asset,
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user