import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:logging/logging.dart'; import 'package:provider/provider.dart'; import 'package:result_monad/result_monad.dart'; import '../controls/login_aware_cached_network_image.dart'; import '../controls/standard_appbar.dart'; import '../controls/status_and_refresh_button.dart'; import '../globals.dart'; import '../models/visibility.dart'; import '../serializers/friendica/image_entry_friendica_extensions.dart'; import '../services/gallery_service.dart'; import '../services/network_status_service.dart'; import '../utils/active_profile_selector.dart'; import '../utils/snackbar_builder.dart'; import 'media_viewer_screen.dart'; class GalleryScreen extends StatelessWidget { static final _logger = Logger('$GalleryScreen'); final String galleryName; const GalleryScreen({super.key, required this.galleryName}); @override Widget build(BuildContext context) { _logger.finest('Building $galleryName'); final nss = getIt(); return Scaffold( appBar: StandardAppBar.build(context, galleryName, actions: [ StatusAndRefreshButton( valueListenable: nss.imageGalleryLoadingStatus, refreshFunction: () async => context .read>() .activeEntry .withResultAsync( (gs) async => gs.updateGalleryImageList( galleryName: galleryName, withNextPage: false, nextPageOnly: false, ), ), busyColor: Theme.of(context).appBarTheme.foregroundColor, ), ]), body: _GalleryScreenBody( galleryName: galleryName, ), ); } } class _GalleryScreenBody extends StatelessWidget { static const thumbnailDimension = 350.0; static final _logger = Logger('$_GalleryScreenBody'); final String galleryName; const _GalleryScreenBody({required this.galleryName}); @override Widget build(BuildContext context) { _logger.finest('Building'); final service = context .watch>() .activeEntry .value; return service.getGallery(galleryName).fold( onSuccess: (galleryData) => buildBody(context, service, galleryData.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, GalleryService service, int expectedPhotoCount, ) { final imageResult = service.getGalleryImageList(galleryName); if (imageResult.isFailure) { return buildErrorBody(imageResult.error.message); } final images = imageResult.value; final attachments = images.map((i) => i.toMediaAttachment()).toList(); if (images.isEmpty && service.loaded) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('No images'), ], ), ); } if (images.isEmpty) { return const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ Text('Loading 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) { service.updateGalleryImageList( galleryName: galleryName, 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 .withOpacity(0.7), child: IconButton( onPressed: () => context.push( '/gallery/edit/$galleryName/image/${image.id}', ), icon: const Icon(Icons.edit), ), ), Card( color: Theme.of(context) .scaffoldBackgroundColor .withOpacity(0.7), child: IconButton( onPressed: () async { final confirm = await showYesNoDialog( context, 'Delete image?'); if (confirm != true) { return; } await service .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 showImageCaption( context, image.description, ), style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context) .scaffoldBackgroundColor .withOpacity(0.7)), child: const Text('ALT'), ), ), Positioned( bottom: 5.0, right: 5.0, child: Card( color: Theme.of(context) .scaffoldBackgroundColor .withOpacity(0.7), child: Icon( image.visibility.type == VisibilityType.public ? Icons.public : Icons.lock), ), ), ], ), ), ), ); }); } Future showImageCaption(BuildContext context, String text) async { await showDialog( context: context, barrierDismissible: true, builder: (BuildContext context) { return AlertDialog( content: Text( text, softWrap: true, ), actions: [ ElevatedButton( child: const Text('Dismiss'), onPressed: () { Navigator.pop(context, true); // showDialog() returns true }, ), ], ); }, ); } }