Files
minstrel/flutter_client/lib/admin/widgets/admin_quarantine_row.dart
T
bvandeusen 3e905db906 feat(flutter/admin): quarantine screen + typed-confirm sheet
ExpansionTile per aggregated quarantine row — collapsed shows track,
artist · album, and "{N} reports: {top reason}". Expanded reveals the
nested per-user reports with their reasons + optional notes.

Three-dot menu offers Resolve / Delete file / Delete via Lidarr; both
deletes route through TypedConfirmSheet which requires the user to
type "DELETE" before the action button enables.

Adds AdminQuarantineController with optimistic-removal + rollback,
mirroring AdminRequestsController's pattern.

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

102 lines
3.2 KiB
Dart

import 'package:flutter/material.dart';
import '../../models/admin_quarantine_item.dart';
import '../../theme/theme_extension.dart';
import 'typed_confirm_sheet.dart';
class AdminQuarantineRow extends StatelessWidget {
const AdminQuarantineRow({
super.key,
required this.item,
required this.onResolve,
required this.onDeleteFile,
required this.onDeleteViaLidarr,
});
final AdminQuarantineItem item;
final VoidCallback onResolve;
final VoidCallback onDeleteFile;
final VoidCallback onDeleteViaLidarr;
@override
Widget build(BuildContext context) {
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
return ExpansionTile(
key: Key('admin_quarantine_tile_${item.trackId}'),
title: Text(
item.trackTitle,
style: TextStyle(
color: fs.parchment,
fontFamily: 'Fraunces',
fontSize: 16,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${item.artistName} · ${item.albumTitle}',
style: TextStyle(color: fs.ash)),
Text(
'${item.reportCount} '
'${item.reportCount == 1 ? "report" : "reports"} · ${item.topReasonSummary}',
style: TextStyle(color: fs.error, fontSize: 12),
),
],
),
trailing: PopupMenuButton<String>(
key: Key('admin_quarantine_menu_${item.trackId}'),
icon: Icon(Icons.more_vert, color: fs.parchment),
onSelected: (action) async {
switch (action) {
case 'resolve':
onResolve();
break;
case 'delete_file':
if (await TypedConfirmSheet.show(
context,
title: 'Delete file?',
message:
'Permanently delete the local file for "${item.trackTitle}". '
'Cannot be undone.',
)) {
onDeleteFile();
}
break;
case 'delete_lidarr':
if (await TypedConfirmSheet.show(
context,
title: 'Delete via Lidarr?',
message:
'Ask Lidarr to delete the file for "${item.trackTitle}".',
)) {
onDeleteViaLidarr();
}
break;
}
},
itemBuilder: (_) => const [
PopupMenuItem(value: 'resolve', child: Text('Resolve')),
PopupMenuItem(value: 'delete_file', child: Text('Delete file')),
PopupMenuItem(
value: 'delete_lidarr', child: Text('Delete via Lidarr')),
],
),
childrenPadding: const EdgeInsets.only(left: 16, right: 16, bottom: 8),
children: [
for (final report in item.reports)
ListTile(
dense: true,
title: Text(
'${report.username}${report.reason}',
style: TextStyle(color: fs.parchment, fontSize: 13),
),
subtitle: report.notes != null
? Text(report.notes!,
style: TextStyle(color: fs.ash, fontSize: 12))
: null,
),
],
);
}
}