import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import '../controls/async_value_widget.dart'; import '../controls/login_aware_cached_network_image.dart'; import '../controls/padding.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; import '../models/auth/profile.dart'; import '../models/visibility.dart'; import '../riverpod_controllers/account_services.dart'; import '../riverpod_controllers/gallery_services.dart'; import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../utils/snackbar_builder.dart'; import 'media_viewer_screen.dart'; class GalleryScreen extends ConsumerWidget { static final _logger = Logger('$GalleryScreen'); final String galleryName; const GalleryScreen({super.key, required this.galleryName}); @override Widget build(BuildContext context, WidgetRef ref) { _logger.finer('Building $galleryName'); final profile = ref.watch(activeProfileProvider); final loading = switch (ref.watch(galleryProvider(profile, galleryName))) { AsyncData() => false, _ => true, }; return Scaffold( appBar: StandardAppBar.build(context, galleryName, actions: [ StatusAndRefreshButton( executing: loading, refreshFunction: () async => ref .read(galleryImagesProvider(profile, galleryName).notifier) .updateGalleryImages( withNextPage: false, nextPageOnly: false, ), busyColor: Theme.of(context).appBarTheme.foregroundColor, ), ]), body: _GalleryScreenBody( profile: profile, galleryName: galleryName, ), ); } } class _GalleryScreenBody extends ConsumerWidget { static const thumbnailDimension = 350.0; static final _logger = Logger('$_GalleryScreenBody'); final Profile profile; final String galleryName; const _GalleryScreenBody({required this.profile, required this.galleryName}); @override Widget build(BuildContext context, WidgetRef ref) { _logger.finer('Building'); return AsyncValueWidget(ref.watch(galleryProvider(profile, galleryName)), valueBuilder: (context, ref, galleriesResult) { return galleriesResult.fold( onSuccess: (gallery) => buildBody(context, ref, gallery.count), onError: (error) => buildErrorBody(error.message), ); }); } Widget buildErrorBody(String error) { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Error getting images for gallery: $error'), ], ), ); } Widget buildBody( BuildContext context, WidgetRef ref, int expectedPhotoCount, ) { return AsyncValueWidget( ref.watch(galleryImagesProvider(profile, galleryName)), loadingBuilder: (_, __) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Loading images'), VerticalPadding(), CircularProgressIndicator(), ], ), ), valueBuilder: (context, ref, galleryImagesResult) { return galleryImagesResult.fold( onSuccess: (images) { final attachments = images.map((i) => i.toMediaAttachment()).toList(); if (images.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('No images'), ], ), ); } return GridView.builder( itemCount: images.length, padding: const EdgeInsets.all(5.0), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: thumbnailDimension), itemBuilder: (context, index) { final image = images[index]; if (images.length < expectedPhotoCount && index == images.length - 1) { ref .read(galleryImagesProvider(profile, galleryName) .notifier) .updateGalleryImages( withNextPage: true, nextPageOnly: true, ); } return Padding( padding: const EdgeInsets.all(2.0), child: GestureDetector( onTap: () { Navigator.push(context, MaterialPageRoute(builder: (context) { return MediaViewerScreen( attachments: attachments, initialIndex: index, ); })); }, child: Card( child: Stack( children: [ LoginAwareCachedNetworkImage( width: thumbnailDimension, height: thumbnailDimension, imageUrl: image.thumbnailUrl, ), Positioned( top: 5.0, right: 5.0, child: Row( children: [ Card( color: Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.7), child: IconButton( onPressed: () => context.push( '/gallery/edit/$galleryName/image/${image.id}', ), icon: const Icon(Icons.edit), ), ), Card( color: Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.7), child: IconButton( onPressed: () async { final confirm = await showYesNoDialog( context, 'Delete image?'); if (confirm != true) { return; } await ref .read(galleryImagesProvider( profile, galleryName) .notifier) .deleteImage(image) .withError((error) { if (context.mounted) { buildSnackbar(context, 'Error deleting image: $error'); } }); }, icon: const Icon(Icons.delete), ), ), ], ), ), if (image.description.isNotEmpty) Positioned( bottom: 5.0, left: 5.0, child: ElevatedButton( onPressed: () async => await showInfoDialog( context, image.description, ), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.7)), child: const Text('ALT'), ), ), Positioned( bottom: 5.0, right: 5.0, child: Card( color: Theme.of(context) .scaffoldBackgroundColor .withValues(alpha: 0.7), child: Icon(image.visibility.type == VisibilityType.public ? Icons.public : Icons.lock), ), ), ], ), ), ), ); }); }, onError: (error) => buildErrorBody(error.message), ); }); } }