feat(flutter/admin): landing screen with section cards + counts
ListView of three AdminSectionCard tiles (Requests / Quarantine / Users) showing live counts from adminCountsProvider. Pull-to-refresh re-fans-out to the three list endpoints. Admin/Quarantine/Users sub-routes will land in subsequent commits. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,66 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../shared/widgets/main_app_bar_actions.dart';
|
||||||
|
import '../theme/theme_extension.dart';
|
||||||
|
import 'admin_providers.dart';
|
||||||
|
import 'widgets/admin_section_card.dart';
|
||||||
|
|
||||||
|
class AdminLandingScreen extends ConsumerWidget {
|
||||||
|
const AdminLandingScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||||
|
final counts = ref.watch(adminCountsProvider);
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: fs.obsidian,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: fs.obsidian,
|
||||||
|
elevation: 0,
|
||||||
|
title: Text('Admin', style: TextStyle(color: fs.parchment)),
|
||||||
|
actions: const [MainAppBarActions(currentRoute: '/admin')],
|
||||||
|
),
|
||||||
|
body: SafeArea(
|
||||||
|
child: counts.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (e, _) => Center(
|
||||||
|
child: Text('$e', style: TextStyle(color: fs.error)),
|
||||||
|
),
|
||||||
|
data: (c) => RefreshIndicator(
|
||||||
|
onRefresh: () async => ref.refresh(adminCountsProvider.future),
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
AdminSectionCard(
|
||||||
|
key: const Key('admin_card_requests'),
|
||||||
|
icon: Icons.inbox,
|
||||||
|
title: 'Requests',
|
||||||
|
subtitle: 'Approve or reject Lidarr requests',
|
||||||
|
count: c.requests,
|
||||||
|
onTap: () => context.push('/admin/requests'),
|
||||||
|
),
|
||||||
|
AdminSectionCard(
|
||||||
|
key: const Key('admin_card_quarantine'),
|
||||||
|
icon: Icons.warning_amber,
|
||||||
|
title: 'Quarantine',
|
||||||
|
subtitle: 'Resolve scan failures',
|
||||||
|
count: c.quarantine,
|
||||||
|
onTap: () => context.push('/admin/quarantine'),
|
||||||
|
),
|
||||||
|
AdminSectionCard(
|
||||||
|
key: const Key('admin_card_users'),
|
||||||
|
icon: Icons.people,
|
||||||
|
title: 'Users',
|
||||||
|
subtitle: 'Manage accounts and invites',
|
||||||
|
count: c.users,
|
||||||
|
onTap: () => context.push('/admin/users'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../theme/theme_extension.dart';
|
||||||
|
|
||||||
|
class AdminSectionCard extends StatelessWidget {
|
||||||
|
const AdminSectionCard({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.count,
|
||||||
|
required this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final int count;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final fs = Theme.of(context).extension<FabledSwordTheme>()!;
|
||||||
|
return Card(
|
||||||
|
color: fs.iron,
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
child: ListTile(
|
||||||
|
leading: Icon(icon, color: fs.parchment),
|
||||||
|
title: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
color: fs.parchment,
|
||||||
|
fontFamily: 'Fraunces',
|
||||||
|
fontSize: 18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
subtitle: Text(subtitle, style: TextStyle(color: fs.ash)),
|
||||||
|
trailing: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: fs.bronze,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'$count',
|
||||||
|
style: TextStyle(color: fs.obsidian, fontWeight: FontWeight.w500),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: onTap,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
import 'package:minstrel/admin/admin_landing_screen.dart';
|
||||||
|
import 'package:minstrel/admin/admin_providers.dart';
|
||||||
|
import 'package:minstrel/auth/auth_provider.dart';
|
||||||
|
import 'package:minstrel/models/user.dart';
|
||||||
|
import 'package:minstrel/theme/theme_data.dart';
|
||||||
|
|
||||||
|
class _StubAuth extends AuthController {
|
||||||
|
@override
|
||||||
|
Future<User?> build() async =>
|
||||||
|
const User(id: 'u1', username: 'admin', isAdmin: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('renders three section cards with counts from provider',
|
||||||
|
(t) async {
|
||||||
|
await t.pumpWidget(
|
||||||
|
ProviderScope(
|
||||||
|
overrides: [
|
||||||
|
authControllerProvider.overrideWith(() => _StubAuth()),
|
||||||
|
adminCountsProvider.overrideWith(
|
||||||
|
(_) async => const AdminCounts(
|
||||||
|
requests: 3,
|
||||||
|
quarantine: 1,
|
||||||
|
users: 5,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: buildThemeData(),
|
||||||
|
home: const AdminLandingScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await t.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('admin_card_requests')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('admin_card_quarantine')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('admin_card_users')), findsOneWidget);
|
||||||
|
expect(find.text('3'), findsOneWidget);
|
||||||
|
expect(find.text('1'), findsOneWidget);
|
||||||
|
expect(find.text('5'), findsOneWidget);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user