import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../api/endpoints/admin_invites.dart'; import '../api/endpoints/admin_quarantine.dart'; import '../api/endpoints/admin_requests.dart'; import '../api/endpoints/admin_users.dart'; import '../library/library_providers.dart' show dioProvider; import '../models/admin_quarantine_item.dart'; import '../models/admin_request.dart'; import '../models/admin_user.dart'; import '../models/invite.dart'; final adminRequestsApiProvider = FutureProvider((ref) async { return AdminRequestsApi(await ref.watch(dioProvider.future)); }); final adminQuarantineApiProvider = FutureProvider((ref) async { return AdminQuarantineApi(await ref.watch(dioProvider.future)); }); final adminUsersApiProvider = FutureProvider((ref) async { return AdminUsersApi(await ref.watch(dioProvider.future)); }); final adminInvitesApiProvider = FutureProvider((ref) async { return AdminInvitesApi(await ref.watch(dioProvider.future)); }); class AdminCounts { const AdminCounts({ required this.requests, required this.quarantine, required this.users, }); final int requests; final int quarantine; final int users; } /// Fans out to the three list endpoints in parallel and rolls them up /// into `{requests, quarantine, users}` for the landing screen badges. final adminCountsProvider = FutureProvider((ref) async { final requestsApi = await ref.watch(adminRequestsApiProvider.future); final quarantineApi = await ref.watch(adminQuarantineApiProvider.future); final usersApi = await ref.watch(adminUsersApiProvider.future); final results = await Future.wait([ requestsApi.list(), quarantineApi.list(), usersApi.list(), ]); return AdminCounts( requests: (results[0] as List).length, quarantine: (results[1] as List).length, users: (results[2] as List).length, ); }); class AdminRequestsController extends AsyncNotifier> { @override Future> build() async { final api = await ref.watch(adminRequestsApiProvider.future); return api.list(); } Future approve(String id) async { await _decide(id, (api) => api.approve(id)); } Future reject(String id) async { await _decide(id, (api) => api.reject(id)); } Future _decide( String id, Future Function(AdminRequestsApi api) action, ) async { final api = await ref.read(adminRequestsApiProvider.future); final current = state.value ?? const []; state = AsyncData(current.where((r) => r.id != id).toList()); try { await action(api); // Refresh landing badge. ref.invalidate(adminCountsProvider); } catch (e, st) { state = AsyncData(current); Error.throwWithStackTrace(e, st); } } } final adminRequestsProvider = AsyncNotifierProvider>( AdminRequestsController.new); /// Read side originally defined for the Requests screen's /// requester-UUID → username join. Mutation methods (setAdmin, /// setAutoApprove, resetPassword, delete) added alongside the Users /// screen in Slice 3. resetPassword is admin-supplies; the server /// has no auto-generation mode. class AdminUsersController extends AsyncNotifier> { @override Future> build() async { final api = await ref.watch(adminUsersApiProvider.future); return api.list(); } /// Optimistic flip of `is_admin` on the user row; rolls back on /// server error (including the last-admin guard 4xx). Future setAdmin(String id, bool isAdmin) => _patch(id, (u) => _copy(u, isAdmin: isAdmin), (api) => api.setAdmin(id, isAdmin)); Future setAutoApprove(String id, bool autoApprove) => _patch(id, (u) => _copy(u, autoApproveRequests: autoApprove), (api) => api.setAutoApprove(id, autoApprove)); Future delete(String id) async { final api = await ref.read(adminUsersApiProvider.future); final current = state.value ?? const []; state = AsyncData(current.where((u) => u.id != id).toList()); try { await api.delete(id); ref.invalidate(adminCountsProvider); } catch (e, st) { state = AsyncData(current); Error.throwWithStackTrace(e, st); } } Future resetPassword(String id, String newPassword) async { final api = await ref.read(adminUsersApiProvider.future); await api.resetPassword(id, newPassword); } Future _patch( String id, AdminUser Function(AdminUser) mutate, Future Function(AdminUsersApi api) action, ) async { final api = await ref.read(adminUsersApiProvider.future); final current = state.value ?? const []; state = AsyncData([ for (final u in current) u.id == id ? mutate(u) : u, ]); try { await action(api); } catch (e, st) { state = AsyncData(current); Error.throwWithStackTrace(e, st); } } AdminUser _copy(AdminUser u, {bool? isAdmin, bool? autoApproveRequests}) => AdminUser( id: u.id, username: u.username, displayName: u.displayName, isAdmin: isAdmin ?? u.isAdmin, autoApproveRequests: autoApproveRequests ?? u.autoApproveRequests, createdAt: u.createdAt, ); } final adminUsersProvider = AsyncNotifierProvider>( AdminUsersController.new); class AdminQuarantineController extends AsyncNotifier> { @override Future> build() async { final api = await ref.watch(adminQuarantineApiProvider.future); return api.list(); } Future resolve(String trackId) => _act(trackId, (api) => api.resolve(trackId)); Future deleteFile(String trackId) => _act(trackId, (api) => api.deleteFile(trackId)); Future deleteViaLidarr(String trackId) => _act(trackId, (api) => api.deleteViaLidarr(trackId)); Future _act( String trackId, Future Function(AdminQuarantineApi api) action, ) async { final api = await ref.read(adminQuarantineApiProvider.future); final current = state.value ?? const []; state = AsyncData(current.where((q) => q.trackId != trackId).toList()); try { await action(api); ref.invalidate(adminCountsProvider); } catch (e, st) { state = AsyncData(current); Error.throwWithStackTrace(e, st); } } } final adminQuarantineProvider = AsyncNotifierProvider>(AdminQuarantineController.new); class AdminInvitesController extends AsyncNotifier> { @override Future> build() async { final api = await ref.watch(adminInvitesApiProvider.future); return api.list(); } /// Returns the freshly-minted invite so the screen can show the /// token in a copy-once dialog. The new invite is also prepended /// to the local list so it shows up without a refresh. Future create({String? note}) async { final api = await ref.read(adminInvitesApiProvider.future); final invite = await api.create(note: note); final current = state.value ?? const []; state = AsyncData([invite, ...current]); return invite; } Future revoke(String token) async { final api = await ref.read(adminInvitesApiProvider.future); final current = state.value ?? const []; state = AsyncData(current.where((i) => i.token != token).toList()); try { await api.revoke(token); } catch (e, st) { state = AsyncData(current); Error.throwWithStackTrace(e, st); } } } final adminInvitesProvider = AsyncNotifierProvider>( AdminInvitesController.new);