import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logging/logging.dart'; import 'package:result_monad/result_monad.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import '../models/auth/profile.dart'; import '../models/exec_error.dart'; import '../models/gallery_data.dart'; import '../models/image_entry.dart'; import '../models/networking/paging_data.dart'; import 'networking/friendica_gallery_client_services.dart'; import 'networking/friendica_image_client_services.dart'; import 'rp_provider_extension.dart'; part 'gallery_services.g.dart'; final _galleryLogger = Logger('GalleriesProvider'); @riverpod class _Galleries extends _$Galleries { @override Future, ExecError>> build( Profile profile, ) async { _galleryLogger.info('Build for $profile'); final result = await updateGalleries(); ref.cacheFor(const Duration(minutes: 30)); return result; } Future, ExecError>> updateGalleries() async { _galleryLogger.info('Updating galleries for $profile'); //may need to force update final result = await ref.read(galleryDataProvider(profile).future); if (result.isFailure) { return result.errorCast(); } final galleriesMap = {for (final g in result.value) g.name: g}; _galleryLogger .info(() => 'New gallery data for $profile : ${galleriesMap.values}'); final rval = Result.ok(galleriesMap).execErrorCast(); state = AsyncData(rval); return rval; } } final _glLogger = Logger('GalleryListProvider'); @riverpod class GalleryList extends _$GalleryList { @override Future, ExecError>> build(Profile profile) async { _glLogger.info('Build for $profile'); final result = await ref .watch(_galleriesProvider(profile).future) .transform((gm) => gm.values.toList()) .execErrorCastAsync(); ref.cacheFor(const Duration(minutes: 30)); return result; } Future, ExecError>> updateGalleries() async { _galleryLogger.info('Updating galleries for $profile'); return await ref .read(_galleriesProvider(profile).notifier) .updateGalleries(); } } @riverpod class Gallery extends _$Gallery { @override Future> build( Profile profile, String galleryName) async { final result = await ref .watch(_galleriesProvider(profile).future) .andThen((gm) => gm.containsKey(galleryName) ? Result.ok(gm[galleryName]!) : buildErrorResult( type: ErrorType.notFound, message: '$galleryName does not exist')) .execErrorCastAsync(); ref.cacheFor(const Duration(minutes: 30)); return result; } Future> rename(String newName) async { if (newName.isEmpty) { return buildErrorResult( type: ErrorType.argumentError, message: 'Gallery name cannot be empty', ); } final result = await ref .read(renameGalleryProvider(profile, galleryName, newName).future) .withResultAsync( (_) async => await ref .read(_galleriesProvider(profile).notifier) .updateGalleries(), ) .withResult((_) => ref.invalidateSelf()); return result.execErrorCast(); } } @riverpod class GalleryImages extends _$GalleryImages { static const _imagesPerPage = 50; final pages = []; @override Future, ExecError>> build( Profile profile, String galleryName) async { final result = await updateGalleryImages( withNextPage: true, ); ref.cacheFor(const Duration(minutes: 30)); return result; } FutureResult, ExecError> updateGalleryImages( {required bool withNextPage, bool nextPageOnly = true}) async { if (pages.isEmpty) { pages.add(PagingData(offset: 0, limit: _imagesPerPage)); } else if (withNextPage) { final offset = pages.last.offset! + _imagesPerPage; pages.add(PagingData(offset: offset, limit: _imagesPerPage)); } final imageSet = switch (state) { AsyncData(:final value) => value.fold( onSuccess: (images) => images.toSet(), onError: (_) => {}), _ => {}, }; final pagesToUse = nextPageOnly ? [pages.last] : pages; for (final page in pagesToUse) { final result = await ref .read(galleryImagesClientProvider(profile, galleryName, page).future); if (result.isFailure) { return result.errorCast(); } else { imageSet.addAll(result.value); } } final result = Result.ok(imageSet.toList()).execErrorCast(); state = AsyncData(result); return result; } FutureResult updateImage(ImageEntry image) async { final List images = switch (state) { AsyncData(:final value) => value.fold( onSuccess: (images) => List.from(images), onError: (_) => []), _ => [], }; final index = images.indexOf(image); if (index < 0) { return buildErrorResult( type: ErrorType.notFound, message: 'Image ${image.id} does not exist for ${image.album}'); } final result = await ref .read(editImageDataProvider(profile, image).future) .withResult((_) { images[index] = image; }); state = AsyncData(result.transform((_) => images).execErrorCast()); return result.execErrorCast(); } FutureResult deleteImage(ImageEntry image) async { final images = switch (state) { AsyncData(:final value) => value.fold( onSuccess: (images) => images.toSet(), onError: (_) => {}), _ => {}, }; if (!images.contains(image)) { return buildErrorResult( type: ErrorType.notFound, message: 'Image ${image.id} does not exist for ${image.album}'); } final result = await ref .read(deleteImageProvider(profile, image).future) .withResult((_) => images.remove(image)); state = AsyncData(result.transform((_) => images.toList()).execErrorCast()); return result.execErrorCast(); } void refresh() { ref.invalidateSelf(); } } @riverpod Future> galleryImage( Ref ref, Profile profile, String galleryName, String id) async { final result = await ref .read(galleryImagesProvider(profile, galleryName).future) .transform((images) => images.where((i) => i.id == id).toList()) .andThen((images) => images.isNotEmpty ? Result.ok(images.first) : buildErrorResult( type: ErrorType.notFound, message: 'Image $id not found in gallery $galleryName', )) .execErrorCastAsync(); return result; }