Files
minstrel/flutter_client/lib/models/admin_quarantine_item.dart
bvandeusen d5c8d316c5 fix(flutter): clear analyze --fatal-infos errors from admin parity slice
CI caught six issues against the new admin slice:

- AsyncValue<T> in this Riverpod 3.3.1 codebase exposes `.value`
  (returns T?), not `.valueOrNull`. Switched the three admin readers
  to match the established convention (player_bar, queue_screen,
  now_playing_screen all use `.value`).
- home_screen.dart still has ctx.push calls in _albumsRow / _artistsRow
  (carousel tap handlers) — restored the go_router import that
  Task 2 over-eagerly removed.
- admin_user_edit_sheet.dart had a single-line `if (!await ...) return;`
  that violated curly_braces_in_flow_control_structures. Wrapped in
  braces.
- admin_quarantine_item.dart doc comment had `<top>` placeholders that
  the analyzer flagged as unintended HTML. Rephrased without angle
  brackets.

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

93 lines
3.0 KiB
Dart

/// One user's report under an aggregated quarantine row.
class AdminQuarantineReport {
const AdminQuarantineReport({
required this.userId,
required this.username,
required this.reason,
this.notes,
required this.createdAt,
});
final String userId;
final String username;
final String reason;
final String? notes;
final String createdAt;
factory AdminQuarantineReport.fromJson(Map<String, dynamic> j) =>
AdminQuarantineReport(
userId: j['user_id'] as String? ?? '',
username: j['username'] as String? ?? '',
reason: j['reason'] as String? ?? '',
notes: j['notes'] as String?,
createdAt: j['created_at'] as String? ?? '',
);
}
/// Mirrors `adminQueueRowView` from internal/api/admin_quarantine.go.
/// One row aggregates all reports against a single track, with
/// per-reason counts and the underlying per-user reports nested.
class AdminQuarantineItem {
const AdminQuarantineItem({
required this.trackId,
required this.trackTitle,
required this.artistName,
required this.albumTitle,
required this.albumId,
this.lidarrAlbumMbid,
required this.reportCount,
required this.latestAt,
required this.reasonCounts,
required this.reports,
});
final String trackId;
final String trackTitle;
final String artistName;
final String albumTitle;
final String albumId;
final String? lidarrAlbumMbid;
final int reportCount;
final String latestAt;
/// Map of reason → number of reports citing that reason.
final Map<String, int> reasonCounts;
final List<AdminQuarantineReport> reports;
/// One-line summary of the most-cited reason. Returns just the top
/// reason when only one reason exists, or `top (+N more)` when there
/// are additional distinct reasons.
String get topReasonSummary {
if (reasonCounts.isEmpty) return '';
final entries = reasonCounts.entries.toList()
..sort((a, b) => b.value.compareTo(a.value));
final top = entries.first.key;
return entries.length > 1
? '$top (+${entries.length - 1} more)'
: top;
}
factory AdminQuarantineItem.fromJson(Map<String, dynamic> j) {
final reasonCountsRaw =
(j['reason_counts'] as Map?)?.cast<String, dynamic>() ?? const {};
final reportsRaw = (j['reports'] as List?) ?? const [];
return AdminQuarantineItem(
trackId: j['track_id'] as String? ?? '',
trackTitle: j['track_title'] as String? ?? '',
artistName: j['artist_name'] as String? ?? '',
albumTitle: j['album_title'] as String? ?? '',
albumId: j['album_id'] as String? ?? '',
lidarrAlbumMbid: j['lidarr_album_mbid'] as String?,
reportCount: (j['report_count'] as int?) ?? 0,
latestAt: j['latest_at'] as String? ?? '',
reasonCounts:
reasonCountsRaw.map((k, v) => MapEntry(k, (v as int?) ?? 0)),
reports: reportsRaw
.map((e) =>
AdminQuarantineReport.fromJson((e as Map).cast<String, dynamic>()))
.toList(growable: false),
);
}
}