Files
minstrel/flutter_client/lib/requests/requests_provider.dart
T
bvandeusen 0db7054518 feat(flutter): user-facing requests view + cancel (#356)
Closes the cleanest remaining #356 gap — Discover submits Lidarr
requests but had no surface for users to view or cancel their own.
Admin had it; user didn't.

- RequestsApi at lib/api/endpoints/requests.dart — listMine() +
  cancel(id). Same /api/requests endpoint the web /requests page
  uses; identical AdminRequest wire shape so the existing model
  is reused (just augmented with matched_*_id fields for the
  "Listen" CTA on completed rows).
- MyRequestsController mirrors the web's auto-poll (#369 piece
  already shipped server-side / web-side): 12s refresh while any
  row is mid-ingest (status='approved'), stops on settle. Riverpod
  Timer in build() that's cancelled in onDispose.
- RequestsScreen with kind/status pills, ingest progress copy,
  Cancel (with confirm dialog) and Listen CTAs per row state.
- Route /requests under the shell. Entry point in settings between
  Profile and Appearance — "My requests".
- Widget tests cover empty / pending / completed / rejected states +
  the Cancel-confirm dialog.

Out of scope this slice: Discover-screen "View your requests" CTA
after submitting a new request. Reasonable follow-up but doesn't
block the management loop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 18:08:23 -04:00

62 lines
2.0 KiB
Dart

import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../api/endpoints/requests.dart';
import '../library/library_providers.dart' show dioProvider;
import '../models/admin_request.dart';
final requestsApiProvider = FutureProvider<RequestsApi>((ref) async {
return RequestsApi(await ref.watch(dioProvider.future));
});
/// Mirrors the web's createMyRequestsQuery auto-poll (#369): refresh
/// every 12s while any row is mid-ingest (status='approved'), stop when
/// all rows settle. Polls regardless of app foreground/background — a
/// future enhancement could pause via WidgetsBindingObserver, but for
/// v1 the small extra refresh is acceptable.
class MyRequestsController extends AsyncNotifier<List<AdminRequest>> {
static const Duration _pollInterval = Duration(seconds: 12);
Timer? _pollTimer;
@override
Future<List<AdminRequest>> build() async {
ref.onDispose(() {
_pollTimer?.cancel();
_pollTimer = null;
});
final api = await ref.watch(requestsApiProvider.future);
final rows = await api.listMine();
_maybeStartPolling(rows);
return rows;
}
void _maybeStartPolling(List<AdminRequest> rows) {
_pollTimer?.cancel();
if (rows.any((r) => r.status == 'approved')) {
_pollTimer = Timer.periodic(_pollInterval, (_) {
ref.invalidateSelf();
});
}
}
/// Optimistic remove + REST cancel. Restores the row on failure so
/// the user can retry — silent failure would lie about success.
Future<void> cancel(String id) async {
final api = await ref.read(requestsApiProvider.future);
final current = state.value ?? const <AdminRequest>[];
state = AsyncData(current.where((r) => r.id != id).toList());
try {
await api.cancel(id);
} catch (e, st) {
state = AsyncData(current);
Error.throwWithStackTrace(e, st);
}
}
}
final myRequestsProvider =
AsyncNotifierProvider<MyRequestsController, List<AdminRequest>>(
MyRequestsController.new);