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>
78 lines
2.7 KiB
Dart
78 lines
2.7 KiB
Dart
/// Mirrors `requestView` from internal/api/requests.go. Server returns
|
|
/// these as a flat list (no envelope) from GET /api/admin/requests.
|
|
///
|
|
/// Note the requester is exposed only as a UUID (`user_id`) — the
|
|
/// response does not include the username. The Flutter Requests screen
|
|
/// joins client-side against the AdminUser list for display.
|
|
class AdminRequest {
|
|
const AdminRequest({
|
|
required this.id,
|
|
required this.userId,
|
|
required this.status,
|
|
required this.kind,
|
|
required this.artistName,
|
|
this.albumTitle,
|
|
this.trackTitle,
|
|
required this.requestedAt,
|
|
this.decidedAt,
|
|
this.notes,
|
|
required this.importedAlbumCount,
|
|
required this.importedTrackCount,
|
|
this.matchedTrackId,
|
|
this.matchedAlbumId,
|
|
this.matchedArtistId,
|
|
});
|
|
|
|
final String id;
|
|
final String userId;
|
|
|
|
/// One of: pending, approved, rejected, completed, failed.
|
|
final String status;
|
|
|
|
/// One of: artist, album, track.
|
|
final String kind;
|
|
|
|
final String artistName;
|
|
final String? albumTitle;
|
|
final String? trackTitle;
|
|
final String requestedAt;
|
|
final String? decidedAt;
|
|
final String? notes;
|
|
final int importedAlbumCount;
|
|
final int importedTrackCount;
|
|
|
|
/// Set when the ingest matched into the local library. The user-side
|
|
/// "Listen" CTA on a completed request links to whichever id is set
|
|
/// (most-specific first: track → album → artist).
|
|
final String? matchedTrackId;
|
|
final String? matchedAlbumId;
|
|
final String? matchedArtistId;
|
|
|
|
/// Display label depending on the kind of request — what the user
|
|
/// asked for. For an album request it's the album title; for an
|
|
/// artist request it's the artist name; etc.
|
|
String get displayName => switch (kind) {
|
|
'album' => albumTitle ?? artistName,
|
|
'track' => trackTitle ?? artistName,
|
|
_ => artistName,
|
|
};
|
|
|
|
factory AdminRequest.fromJson(Map<String, dynamic> j) => AdminRequest(
|
|
id: j['id'] as String? ?? '',
|
|
userId: j['user_id'] as String? ?? '',
|
|
status: j['status'] as String? ?? 'pending',
|
|
kind: j['kind'] as String? ?? 'artist',
|
|
artistName: j['artist_name'] as String? ?? '',
|
|
albumTitle: j['album_title'] as String?,
|
|
trackTitle: j['track_title'] as String?,
|
|
requestedAt: j['requested_at'] as String? ?? '',
|
|
decidedAt: j['decided_at'] as String?,
|
|
notes: j['notes'] as String?,
|
|
importedAlbumCount: (j['imported_album_count'] as int?) ?? 0,
|
|
importedTrackCount: (j['imported_track_count'] as int?) ?? 0,
|
|
matchedTrackId: j['matched_track_id'] as String?,
|
|
matchedAlbumId: j['matched_album_id'] as String?,
|
|
matchedArtistId: j['matched_artist_id'] as String?,
|
|
);
|
|
}
|