0db7054518
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>
62 lines
2.0 KiB
Dart
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);
|