d5c8d316c5
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>
168 lines
5.1 KiB
Dart
168 lines
5.1 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../../models/admin_user.dart';
|
|
import '../../theme/theme_extension.dart';
|
|
import '../admin_providers.dart';
|
|
import 'typed_confirm_sheet.dart';
|
|
|
|
class AdminUserEditSheet extends ConsumerWidget {
|
|
const AdminUserEditSheet({super.key, required this.user});
|
|
|
|
final AdminUser user;
|
|
|
|
static Future<void> show(BuildContext context, AdminUser user) =>
|
|
showModalBottomSheet<void>(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (_) => AdminUserEditSheet(user: user),
|
|
);
|
|
|
|
@override
|
|
Widget build(BuildContext context, WidgetRef ref) {
|
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
|
final users = ref.read(adminUsersProvider.notifier);
|
|
return Padding(
|
|
padding: EdgeInsets.only(
|
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
|
),
|
|
child: Container(
|
|
color: fs.iron,
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
user.username,
|
|
style: TextStyle(
|
|
color: fs.parchment,
|
|
fontFamily: 'Fraunces',
|
|
fontSize: 20,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
SwitchListTile(
|
|
key: const Key('user_edit_is_admin'),
|
|
title: Text('Admin', style: TextStyle(color: fs.parchment)),
|
|
value: user.isAdmin,
|
|
onChanged: (v) async {
|
|
try {
|
|
await users.setAdmin(user.id, v);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
},
|
|
),
|
|
SwitchListTile(
|
|
key: const Key('user_edit_auto_approve'),
|
|
title: Text('Auto-approve requests',
|
|
style: TextStyle(color: fs.parchment)),
|
|
value: user.autoApproveRequests,
|
|
onChanged: (v) async {
|
|
try {
|
|
await users.setAutoApprove(user.id, v);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
},
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
TextButton(
|
|
key: const Key('user_edit_reset_password'),
|
|
onPressed: () => _resetPassword(context, users),
|
|
child: const Text('Reset password'),
|
|
),
|
|
const Spacer(),
|
|
TextButton(
|
|
key: const Key('user_edit_delete'),
|
|
style: TextButton.styleFrom(foregroundColor: fs.oxblood),
|
|
onPressed: () => _deleteUser(context, users),
|
|
child: const Text('Delete'),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _resetPassword(
|
|
BuildContext context, AdminUsersController users) async {
|
|
final controller = TextEditingController();
|
|
final newPw = await showDialog<String>(
|
|
context: context,
|
|
builder: (ctx) => AlertDialog(
|
|
title: const Text('Reset password'),
|
|
content: TextField(
|
|
controller: controller,
|
|
obscureText: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'New password',
|
|
helperText: 'Minimum 8 characters',
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx),
|
|
child: const Text('Cancel'),
|
|
),
|
|
TextButton(
|
|
onPressed: () {
|
|
if (controller.text.length >= 8) {
|
|
Navigator.pop(ctx, controller.text);
|
|
}
|
|
},
|
|
child: const Text('Reset'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (newPw == null || !context.mounted) return;
|
|
try {
|
|
await users.resetPassword(user.id, newPw);
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Password reset.')),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _deleteUser(
|
|
BuildContext context, AdminUsersController users) async {
|
|
if (!await TypedConfirmSheet.show(
|
|
context,
|
|
title: 'Delete user?',
|
|
message:
|
|
'Permanently delete user "${user.username}" and all their data. '
|
|
'Cannot be undone.',
|
|
)) {
|
|
return;
|
|
}
|
|
try {
|
|
await users.delete(user.id);
|
|
if (context.mounted) Navigator.pop(context);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(content: Text('$e')));
|
|
}
|
|
}
|
|
}
|
|
}
|