mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	* feat: drift album page * feat: page renderred * feat: asset count * refactor: use statefulwidget * refactor: private widgets * refactor: service layer * refactor: import * feat: get owner name * pr feedback * pr feedback * pr feedback * pr feedback
		
			
				
	
	
		
			768 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			768 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
import 'dart:async';
 | 
						|
import 'dart:math';
 | 
						|
 | 
						|
import 'package:auto_route/auto_route.dart';
 | 
						|
import 'package:easy_localization/easy_localization.dart';
 | 
						|
import 'package:flutter/material.dart';
 | 
						|
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
						|
import 'package:immich_mobile/domain/models/album/album.model.dart';
 | 
						|
import 'package:immich_mobile/extensions/build_context_extensions.dart';
 | 
						|
import 'package:immich_mobile/extensions/theme_extensions.dart';
 | 
						|
import 'package:immich_mobile/extensions/translate_extensions.dart';
 | 
						|
import 'package:immich_mobile/models/albums/album_search.model.dart';
 | 
						|
import 'package:immich_mobile/pages/common/large_leading_tile.dart';
 | 
						|
import 'package:immich_mobile/presentation/widgets/images/thumbnail.widget.dart';
 | 
						|
import 'package:immich_mobile/providers/infrastructure/album.provider.dart';
 | 
						|
import 'package:immich_mobile/providers/user.provider.dart';
 | 
						|
import 'package:immich_mobile/routing/router.dart';
 | 
						|
import 'package:immich_mobile/utils/remote_album.utils.dart';
 | 
						|
import 'package:immich_mobile/widgets/common/immich_sliver_app_bar.dart';
 | 
						|
import 'package:immich_mobile/widgets/common/search_field.dart';
 | 
						|
 | 
						|
@RoutePage()
 | 
						|
class DriftAlbumsPage extends ConsumerStatefulWidget {
 | 
						|
  const DriftAlbumsPage({super.key});
 | 
						|
 | 
						|
  @override
 | 
						|
  ConsumerState<DriftAlbumsPage> createState() => _DriftAlbumsPageState();
 | 
						|
}
 | 
						|
 | 
						|
class _DriftAlbumsPageState extends ConsumerState<DriftAlbumsPage> {
 | 
						|
  bool isGrid = false;
 | 
						|
  final searchController = TextEditingController();
 | 
						|
  QuickFilterMode filterMode = QuickFilterMode.all;
 | 
						|
  final searchFocusNode = FocusNode();
 | 
						|
 | 
						|
  @override
 | 
						|
  void initState() {
 | 
						|
    super.initState();
 | 
						|
 | 
						|
    // Load albums when component mounts
 | 
						|
    WidgetsBinding.instance.addPostFrameCallback((_) {
 | 
						|
      ref.read(remoteAlbumProvider.notifier).getAll();
 | 
						|
    });
 | 
						|
 | 
						|
    searchController.addListener(() {
 | 
						|
      onSearch(searchController.text, filterMode);
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  void onSearch(String searchTerm, QuickFilterMode sortMode) {
 | 
						|
    final userId = ref.watch(currentUserProvider)?.id;
 | 
						|
    ref
 | 
						|
        .read(remoteAlbumProvider.notifier)
 | 
						|
        .searchAlbums(searchTerm, userId, sortMode);
 | 
						|
  }
 | 
						|
 | 
						|
  Future<void> onRefresh() async {
 | 
						|
    await ref.read(remoteAlbumProvider.notifier).refresh();
 | 
						|
  }
 | 
						|
 | 
						|
  void toggleViewMode() {
 | 
						|
    setState(() {
 | 
						|
      isGrid = !isGrid;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  void changeFilter(QuickFilterMode sortMode) {
 | 
						|
    setState(() {
 | 
						|
      filterMode = sortMode;
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  void clearSearch() {
 | 
						|
    setState(() {
 | 
						|
      filterMode = QuickFilterMode.all;
 | 
						|
      searchController.clear();
 | 
						|
      ref.read(remoteAlbumProvider.notifier).clearSearch();
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  void dispose() {
 | 
						|
    searchController.dispose();
 | 
						|
    searchFocusNode.dispose();
 | 
						|
    super.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    final albumState = ref.watch(remoteAlbumProvider);
 | 
						|
    final albums = albumState.filteredAlbums;
 | 
						|
    final isLoading = albumState.isLoading;
 | 
						|
    final error = albumState.error;
 | 
						|
    final userId = ref.watch(currentUserProvider)?.id;
 | 
						|
 | 
						|
    return RefreshIndicator(
 | 
						|
      onRefresh: onRefresh,
 | 
						|
      child: CustomScrollView(
 | 
						|
        slivers: [
 | 
						|
          const ImmichSliverAppBar(),
 | 
						|
          _SearchBar(
 | 
						|
            searchController: searchController,
 | 
						|
            searchFocusNode: searchFocusNode,
 | 
						|
            onSearch: onSearch,
 | 
						|
            filterMode: filterMode,
 | 
						|
            onClearSearch: clearSearch,
 | 
						|
          ),
 | 
						|
          _QuickFilterButtonRow(
 | 
						|
            filterMode: filterMode,
 | 
						|
            onChangeFilter: changeFilter,
 | 
						|
            onSearch: onSearch,
 | 
						|
            searchController: searchController,
 | 
						|
          ),
 | 
						|
          _QuickSortAndViewMode(
 | 
						|
            isGrid: isGrid,
 | 
						|
            onToggleViewMode: toggleViewMode,
 | 
						|
          ),
 | 
						|
          isGrid
 | 
						|
              ? _AlbumGrid(
 | 
						|
                  albums: albums,
 | 
						|
                  userId: userId,
 | 
						|
                  isLoading: isLoading,
 | 
						|
                  error: error,
 | 
						|
                )
 | 
						|
              : _AlbumList(
 | 
						|
                  albums: albums,
 | 
						|
                  userId: userId,
 | 
						|
                  isLoading: isLoading,
 | 
						|
                  error: error,
 | 
						|
                ),
 | 
						|
        ],
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _SortButton extends ConsumerStatefulWidget {
 | 
						|
  const _SortButton();
 | 
						|
 | 
						|
  @override
 | 
						|
  ConsumerState<_SortButton> createState() => _SortButtonState();
 | 
						|
}
 | 
						|
 | 
						|
class _SortButtonState extends ConsumerState<_SortButton> {
 | 
						|
  RemoteAlbumSortMode albumSortOption = RemoteAlbumSortMode.lastModified;
 | 
						|
  bool albumSortIsReverse = true;
 | 
						|
 | 
						|
  void onMenuTapped(RemoteAlbumSortMode sortMode) {
 | 
						|
    final selected = albumSortOption == sortMode;
 | 
						|
    // Switch direction
 | 
						|
    if (selected) {
 | 
						|
      setState(() {
 | 
						|
        albumSortIsReverse = !albumSortIsReverse;
 | 
						|
      });
 | 
						|
      ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
 | 
						|
            sortMode,
 | 
						|
            isReverse: albumSortIsReverse,
 | 
						|
          );
 | 
						|
    } else {
 | 
						|
      setState(() {
 | 
						|
        albumSortOption = sortMode;
 | 
						|
      });
 | 
						|
      ref.read(remoteAlbumProvider.notifier).sortFilteredAlbums(
 | 
						|
            sortMode,
 | 
						|
            isReverse: albumSortIsReverse,
 | 
						|
          );
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return MenuAnchor(
 | 
						|
      style: MenuStyle(
 | 
						|
        elevation: const WidgetStatePropertyAll(1),
 | 
						|
        shape: WidgetStateProperty.all(
 | 
						|
          const RoundedRectangleBorder(
 | 
						|
            borderRadius: BorderRadius.all(
 | 
						|
              Radius.circular(24),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        padding: const WidgetStatePropertyAll(
 | 
						|
          EdgeInsets.all(4),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      consumeOutsideTap: true,
 | 
						|
      menuChildren: RemoteAlbumSortMode.values
 | 
						|
          .map(
 | 
						|
            (sortMode) => MenuItemButton(
 | 
						|
              leadingIcon: albumSortOption == sortMode
 | 
						|
                  ? albumSortIsReverse
 | 
						|
                      ? Icon(
 | 
						|
                          Icons.keyboard_arrow_down,
 | 
						|
                          color: albumSortOption == sortMode
 | 
						|
                              ? context.colorScheme.onPrimary
 | 
						|
                              : context.colorScheme.onSurface,
 | 
						|
                        )
 | 
						|
                      : Icon(
 | 
						|
                          Icons.keyboard_arrow_up_rounded,
 | 
						|
                          color: albumSortOption == sortMode
 | 
						|
                              ? context.colorScheme.onPrimary
 | 
						|
                              : context.colorScheme.onSurface,
 | 
						|
                        )
 | 
						|
                  : const Icon(Icons.abc, color: Colors.transparent),
 | 
						|
              onPressed: () => onMenuTapped(sortMode),
 | 
						|
              style: ButtonStyle(
 | 
						|
                padding: WidgetStateProperty.all(
 | 
						|
                  const EdgeInsets.fromLTRB(16, 16, 32, 16),
 | 
						|
                ),
 | 
						|
                backgroundColor: WidgetStateProperty.all(
 | 
						|
                  albumSortOption == sortMode
 | 
						|
                      ? context.colorScheme.primary
 | 
						|
                      : Colors.transparent,
 | 
						|
                ),
 | 
						|
                shape: WidgetStateProperty.all(
 | 
						|
                  const RoundedRectangleBorder(
 | 
						|
                    borderRadius: BorderRadius.all(
 | 
						|
                      Radius.circular(24),
 | 
						|
                    ),
 | 
						|
                  ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              child: Text(
 | 
						|
                sortMode.key.t(context: context),
 | 
						|
                style: context.textTheme.titleSmall?.copyWith(
 | 
						|
                  fontWeight: FontWeight.w600,
 | 
						|
                  color: albumSortOption == sortMode
 | 
						|
                      ? context.colorScheme.onPrimary
 | 
						|
                      : context.colorScheme.onSurface.withAlpha(185),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          )
 | 
						|
          .toList(),
 | 
						|
      builder: (context, controller, child) {
 | 
						|
        return GestureDetector(
 | 
						|
          onTap: () {
 | 
						|
            if (controller.isOpen) {
 | 
						|
              controller.close();
 | 
						|
            } else {
 | 
						|
              controller.open();
 | 
						|
            }
 | 
						|
          },
 | 
						|
          child: Row(
 | 
						|
            children: [
 | 
						|
              Padding(
 | 
						|
                padding: const EdgeInsets.only(right: 5),
 | 
						|
                child: albumSortIsReverse
 | 
						|
                    ? const Icon(
 | 
						|
                        Icons.keyboard_arrow_down,
 | 
						|
                      )
 | 
						|
                    : const Icon(
 | 
						|
                        Icons.keyboard_arrow_up_rounded,
 | 
						|
                      ),
 | 
						|
              ),
 | 
						|
              Text(
 | 
						|
                albumSortOption.key.t(context: context),
 | 
						|
                style: context.textTheme.bodyLarge?.copyWith(
 | 
						|
                  fontWeight: FontWeight.w500,
 | 
						|
                  color: context.colorScheme.onSurface.withAlpha(225),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ],
 | 
						|
          ),
 | 
						|
        );
 | 
						|
      },
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _SearchBar extends StatelessWidget {
 | 
						|
  const _SearchBar({
 | 
						|
    required this.searchController,
 | 
						|
    required this.searchFocusNode,
 | 
						|
    required this.onSearch,
 | 
						|
    required this.filterMode,
 | 
						|
    required this.onClearSearch,
 | 
						|
  });
 | 
						|
 | 
						|
  final TextEditingController searchController;
 | 
						|
  final FocusNode searchFocusNode;
 | 
						|
  final void Function(String, QuickFilterMode) onSearch;
 | 
						|
  final QuickFilterMode filterMode;
 | 
						|
  final VoidCallback onClearSearch;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return SliverPadding(
 | 
						|
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
 | 
						|
      sliver: SliverToBoxAdapter(
 | 
						|
        child: Container(
 | 
						|
          decoration: BoxDecoration(
 | 
						|
            border: Border.all(
 | 
						|
              color: context.colorScheme.onSurface.withAlpha(0),
 | 
						|
              width: 0,
 | 
						|
            ),
 | 
						|
            borderRadius: const BorderRadius.all(
 | 
						|
              Radius.circular(24),
 | 
						|
            ),
 | 
						|
            gradient: LinearGradient(
 | 
						|
              colors: [
 | 
						|
                context.colorScheme.primary.withValues(alpha: 0.075),
 | 
						|
                context.colorScheme.primary.withValues(alpha: 0.09),
 | 
						|
                context.colorScheme.primary.withValues(alpha: 0.075),
 | 
						|
              ],
 | 
						|
              begin: Alignment.topLeft,
 | 
						|
              end: Alignment.bottomRight,
 | 
						|
              transform: const GradientRotation(0.5 * pi),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
          child: SearchField(
 | 
						|
            autofocus: false,
 | 
						|
            contentPadding: const EdgeInsets.all(16),
 | 
						|
            hintText: 'search_albums'.tr(),
 | 
						|
            prefixIcon: const Icon(Icons.search_rounded),
 | 
						|
            suffixIcon: searchController.text.isNotEmpty
 | 
						|
                ? IconButton(
 | 
						|
                    icon: const Icon(Icons.clear_rounded),
 | 
						|
                    onPressed: onClearSearch,
 | 
						|
                  )
 | 
						|
                : null,
 | 
						|
            controller: searchController,
 | 
						|
            onChanged: (_) => onSearch(searchController.text, filterMode),
 | 
						|
            focusNode: searchFocusNode,
 | 
						|
            onTapOutside: (_) => searchFocusNode.unfocus(),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _QuickFilterButtonRow extends StatelessWidget {
 | 
						|
  const _QuickFilterButtonRow({
 | 
						|
    required this.filterMode,
 | 
						|
    required this.onChangeFilter,
 | 
						|
    required this.onSearch,
 | 
						|
    required this.searchController,
 | 
						|
  });
 | 
						|
 | 
						|
  final QuickFilterMode filterMode;
 | 
						|
  final void Function(QuickFilterMode) onChangeFilter;
 | 
						|
  final void Function(String, QuickFilterMode) onSearch;
 | 
						|
  final TextEditingController searchController;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return SliverPadding(
 | 
						|
      padding: const EdgeInsets.symmetric(horizontal: 16),
 | 
						|
      sliver: SliverToBoxAdapter(
 | 
						|
        child: Wrap(
 | 
						|
          spacing: 4,
 | 
						|
          runSpacing: 4,
 | 
						|
          children: [
 | 
						|
            _QuickFilterButton(
 | 
						|
              label: 'all'.tr(),
 | 
						|
              isSelected: filterMode == QuickFilterMode.all,
 | 
						|
              onTap: () {
 | 
						|
                onChangeFilter(QuickFilterMode.all);
 | 
						|
                onSearch(searchController.text, QuickFilterMode.all);
 | 
						|
              },
 | 
						|
            ),
 | 
						|
            _QuickFilterButton(
 | 
						|
              label: 'shared_with_me'.tr(),
 | 
						|
              isSelected: filterMode == QuickFilterMode.sharedWithMe,
 | 
						|
              onTap: () {
 | 
						|
                onChangeFilter(QuickFilterMode.sharedWithMe);
 | 
						|
                onSearch(
 | 
						|
                  searchController.text,
 | 
						|
                  QuickFilterMode.sharedWithMe,
 | 
						|
                );
 | 
						|
              },
 | 
						|
            ),
 | 
						|
            _QuickFilterButton(
 | 
						|
              label: 'my_albums'.tr(),
 | 
						|
              isSelected: filterMode == QuickFilterMode.myAlbums,
 | 
						|
              onTap: () {
 | 
						|
                onChangeFilter(QuickFilterMode.myAlbums);
 | 
						|
                onSearch(
 | 
						|
                  searchController.text,
 | 
						|
                  QuickFilterMode.myAlbums,
 | 
						|
                );
 | 
						|
              },
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _QuickFilterButton extends StatelessWidget {
 | 
						|
  const _QuickFilterButton({
 | 
						|
    required this.isSelected,
 | 
						|
    required this.onTap,
 | 
						|
    required this.label,
 | 
						|
  });
 | 
						|
 | 
						|
  final bool isSelected;
 | 
						|
  final VoidCallback onTap;
 | 
						|
  final String label;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return TextButton(
 | 
						|
      onPressed: onTap,
 | 
						|
      style: ButtonStyle(
 | 
						|
        backgroundColor: WidgetStateProperty.all(
 | 
						|
          isSelected ? context.colorScheme.primary : Colors.transparent,
 | 
						|
        ),
 | 
						|
        shape: WidgetStateProperty.all(
 | 
						|
          RoundedRectangleBorder(
 | 
						|
            borderRadius: const BorderRadius.all(
 | 
						|
              Radius.circular(20),
 | 
						|
            ),
 | 
						|
            side: BorderSide(
 | 
						|
              color: context.colorScheme.onSurface.withAlpha(25),
 | 
						|
              width: 1,
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
      child: Text(
 | 
						|
        label,
 | 
						|
        style: TextStyle(
 | 
						|
          color: isSelected
 | 
						|
              ? context.colorScheme.onPrimary
 | 
						|
              : context.colorScheme.onSurface,
 | 
						|
          fontSize: 14,
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _QuickSortAndViewMode extends StatelessWidget {
 | 
						|
  const _QuickSortAndViewMode({
 | 
						|
    required this.isGrid,
 | 
						|
    required this.onToggleViewMode,
 | 
						|
  });
 | 
						|
 | 
						|
  final bool isGrid;
 | 
						|
  final VoidCallback onToggleViewMode;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return SliverPadding(
 | 
						|
      padding: const EdgeInsets.symmetric(horizontal: 16),
 | 
						|
      sliver: SliverToBoxAdapter(
 | 
						|
        child: Row(
 | 
						|
          mainAxisAlignment: MainAxisAlignment.spaceBetween,
 | 
						|
          children: [
 | 
						|
            const _SortButton(),
 | 
						|
            IconButton(
 | 
						|
              icon: Icon(
 | 
						|
                isGrid ? Icons.view_list_outlined : Icons.grid_view_outlined,
 | 
						|
                size: 24,
 | 
						|
              ),
 | 
						|
              onPressed: onToggleViewMode,
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _AlbumList extends StatelessWidget {
 | 
						|
  const _AlbumList({
 | 
						|
    required this.isLoading,
 | 
						|
    required this.error,
 | 
						|
    required this.albums,
 | 
						|
    required this.userId,
 | 
						|
  });
 | 
						|
 | 
						|
  final bool isLoading;
 | 
						|
  final String? error;
 | 
						|
  final List<Album> albums;
 | 
						|
  final String? userId;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    if (isLoading) {
 | 
						|
      return const SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: EdgeInsets.all(20.0),
 | 
						|
            child: CircularProgressIndicator(),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (error != null) {
 | 
						|
      return SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: const EdgeInsets.all(20.0),
 | 
						|
            child: Text(
 | 
						|
              'Error loading albums: $error',
 | 
						|
              style: TextStyle(
 | 
						|
                color: context.colorScheme.error,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (albums.isEmpty) {
 | 
						|
      return const SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: EdgeInsets.all(20.0),
 | 
						|
            child: Text('No albums found'),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return SliverPadding(
 | 
						|
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
						|
      sliver: SliverList.builder(
 | 
						|
        itemBuilder: (_, index) {
 | 
						|
          final album = albums[index];
 | 
						|
 | 
						|
          return Padding(
 | 
						|
            padding: const EdgeInsets.only(
 | 
						|
              bottom: 8.0,
 | 
						|
            ),
 | 
						|
            child: LargeLeadingTile(
 | 
						|
              title: Text(
 | 
						|
                album.name,
 | 
						|
                maxLines: 2,
 | 
						|
                overflow: TextOverflow.ellipsis,
 | 
						|
                style: context.textTheme.titleSmall?.copyWith(
 | 
						|
                  fontWeight: FontWeight.w600,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              subtitle: Text(
 | 
						|
                '${'items_count'.t(
 | 
						|
                  context: context,
 | 
						|
                  args: {
 | 
						|
                    'count': album.assetCount,
 | 
						|
                  },
 | 
						|
                )} • ${album.ownerId != userId ? 'shared_by_user'.t(
 | 
						|
                    context: context,
 | 
						|
                    args: {
 | 
						|
                      'user': album.ownerName,
 | 
						|
                    },
 | 
						|
                  ) : 'owned'.t(context: context)}',
 | 
						|
                overflow: TextOverflow.ellipsis,
 | 
						|
                style: context.textTheme.bodyMedium?.copyWith(
 | 
						|
                  color: context.colorScheme.onSurfaceSecondary,
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
              onTap: () => context.router.push(
 | 
						|
                RemoteTimelineRoute(albumId: album.id),
 | 
						|
              ),
 | 
						|
              leadingPadding: const EdgeInsets.only(
 | 
						|
                right: 16,
 | 
						|
              ),
 | 
						|
              leading: album.thumbnailAssetId != null
 | 
						|
                  ? ClipRRect(
 | 
						|
                      borderRadius: const BorderRadius.all(
 | 
						|
                        Radius.circular(15),
 | 
						|
                      ),
 | 
						|
                      child: SizedBox(
 | 
						|
                        width: 80,
 | 
						|
                        height: 80,
 | 
						|
                        child: Thumbnail(
 | 
						|
                          remoteId: album.thumbnailAssetId,
 | 
						|
                        ),
 | 
						|
                      ),
 | 
						|
                    )
 | 
						|
                  : const SizedBox(
 | 
						|
                      width: 80,
 | 
						|
                      height: 80,
 | 
						|
                      child: Icon(
 | 
						|
                        Icons.photo_album_rounded,
 | 
						|
                        size: 40,
 | 
						|
                        color: Colors.grey,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
            ),
 | 
						|
          );
 | 
						|
        },
 | 
						|
        itemCount: albums.length,
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _AlbumGrid extends StatelessWidget {
 | 
						|
  const _AlbumGrid({
 | 
						|
    required this.albums,
 | 
						|
    required this.userId,
 | 
						|
    required this.isLoading,
 | 
						|
    required this.error,
 | 
						|
  });
 | 
						|
 | 
						|
  final List<Album> albums;
 | 
						|
  final String? userId;
 | 
						|
  final bool isLoading;
 | 
						|
  final String? error;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    if (isLoading) {
 | 
						|
      return const SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: EdgeInsets.all(20.0),
 | 
						|
            child: CircularProgressIndicator(),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (error != null) {
 | 
						|
      return SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: const EdgeInsets.all(20.0),
 | 
						|
            child: Text(
 | 
						|
              'Error loading albums: $error',
 | 
						|
              style: TextStyle(
 | 
						|
                color: context.colorScheme.error,
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    if (albums.isEmpty) {
 | 
						|
      return const SliverToBoxAdapter(
 | 
						|
        child: Center(
 | 
						|
          child: Padding(
 | 
						|
            padding: EdgeInsets.all(20.0),
 | 
						|
            child: Text('No albums found'),
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
      );
 | 
						|
    }
 | 
						|
 | 
						|
    return SliverPadding(
 | 
						|
      padding: const EdgeInsets.symmetric(horizontal: 16.0),
 | 
						|
      sliver: SliverGrid(
 | 
						|
        gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
 | 
						|
          maxCrossAxisExtent: 250,
 | 
						|
          mainAxisSpacing: 4,
 | 
						|
          crossAxisSpacing: 4,
 | 
						|
          childAspectRatio: .7,
 | 
						|
        ),
 | 
						|
        delegate: SliverChildBuilderDelegate(
 | 
						|
          (context, index) {
 | 
						|
            final album = albums[index];
 | 
						|
            return _GridAlbumCard(
 | 
						|
              album: album,
 | 
						|
              userId: userId,
 | 
						|
            );
 | 
						|
          },
 | 
						|
          childCount: albums.length,
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 | 
						|
 | 
						|
class _GridAlbumCard extends StatelessWidget {
 | 
						|
  const _GridAlbumCard({
 | 
						|
    required this.album,
 | 
						|
    required this.userId,
 | 
						|
  });
 | 
						|
 | 
						|
  final Album album;
 | 
						|
  final String? userId;
 | 
						|
 | 
						|
  @override
 | 
						|
  Widget build(BuildContext context) {
 | 
						|
    return GestureDetector(
 | 
						|
      onTap: () => context.router.push(
 | 
						|
        RemoteTimelineRoute(albumId: album.id),
 | 
						|
      ),
 | 
						|
      child: Card(
 | 
						|
        elevation: 0,
 | 
						|
        color: context.colorScheme.surfaceBright,
 | 
						|
        shape: RoundedRectangleBorder(
 | 
						|
          borderRadius: const BorderRadius.all(
 | 
						|
            Radius.circular(16),
 | 
						|
          ),
 | 
						|
          side: BorderSide(
 | 
						|
            color: context.colorScheme.onSurface.withAlpha(25),
 | 
						|
            width: 1,
 | 
						|
          ),
 | 
						|
        ),
 | 
						|
        child: Column(
 | 
						|
          crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
          children: [
 | 
						|
            Expanded(
 | 
						|
              flex: 2,
 | 
						|
              child: ClipRRect(
 | 
						|
                borderRadius: const BorderRadius.vertical(
 | 
						|
                  top: Radius.circular(15),
 | 
						|
                ),
 | 
						|
                child: SizedBox(
 | 
						|
                  width: double.infinity,
 | 
						|
                  child: album.thumbnailAssetId != null
 | 
						|
                      ? Thumbnail(
 | 
						|
                          remoteId: album.thumbnailAssetId,
 | 
						|
                        )
 | 
						|
                      : Container(
 | 
						|
                          color: context.colorScheme.surfaceContainerHighest,
 | 
						|
                          child: const Icon(
 | 
						|
                            Icons.photo_album_rounded,
 | 
						|
                            size: 40,
 | 
						|
                            color: Colors.grey,
 | 
						|
                          ),
 | 
						|
                        ),
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
            Expanded(
 | 
						|
              flex: 1,
 | 
						|
              child: Padding(
 | 
						|
                padding: const EdgeInsets.all(12.0),
 | 
						|
                child: Column(
 | 
						|
                  crossAxisAlignment: CrossAxisAlignment.start,
 | 
						|
                  mainAxisAlignment: MainAxisAlignment.spaceEvenly,
 | 
						|
                  children: [
 | 
						|
                    Text(
 | 
						|
                      album.name,
 | 
						|
                      maxLines: 2,
 | 
						|
                      overflow: TextOverflow.ellipsis,
 | 
						|
                      style: context.textTheme.titleSmall?.copyWith(
 | 
						|
                        fontWeight: FontWeight.w600,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                    Text(
 | 
						|
                      '${'items_count'.t(
 | 
						|
                        context: context,
 | 
						|
                        args: {
 | 
						|
                          'count': album.assetCount,
 | 
						|
                        },
 | 
						|
                      )} • ${album.ownerId != userId ? 'shared_by_user'.t(
 | 
						|
                          context: context,
 | 
						|
                          args: {
 | 
						|
                            'user': album.ownerName,
 | 
						|
                          },
 | 
						|
                        ) : 'owned'.t(context: context)}',
 | 
						|
                      maxLines: 1,
 | 
						|
                      overflow: TextOverflow.ellipsis,
 | 
						|
                      style: context.textTheme.labelMedium?.copyWith(
 | 
						|
                        color: context.colorScheme.onSurfaceSecondary,
 | 
						|
                      ),
 | 
						|
                    ),
 | 
						|
                  ],
 | 
						|
                ),
 | 
						|
              ),
 | 
						|
            ),
 | 
						|
          ],
 | 
						|
        ),
 | 
						|
      ),
 | 
						|
    );
 | 
						|
  }
 | 
						|
}
 |