Files
HouseHoldKeaper/lib/features/rooms/presentation/rooms_screen.dart
Jean-Luc Makiola 76cd98300d fix: use ValueKey<String> for reorderable grid and add status bar padding
flutter_reorderable_grid_view requires String keys, not int. Also
adds safe area top padding so the room grid doesn't overlap the
system status bar.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 22:59:24 +01:00

191 lines
5.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_reorderable_grid_view/widgets/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:household_keeper/features/rooms/data/rooms_dao.dart';
import 'package:household_keeper/features/rooms/presentation/room_card.dart';
import 'package:household_keeper/features/rooms/presentation/room_providers.dart';
import 'package:household_keeper/l10n/app_localizations.dart';
/// Rooms screen showing a 2-column reorderable card grid of rooms.
///
/// Displays an empty state when no rooms exist, with a button to create one.
/// FAB always visible for quick room creation.
class RoomsScreen extends ConsumerWidget {
const RoomsScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final asyncRooms = ref.watch(roomWithStatsListProvider);
return Scaffold(
body: asyncRooms.when(
data: (rooms) {
if (rooms.isEmpty) {
return _EmptyState(l10n: l10n);
}
return _RoomGrid(rooms: rooms);
},
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('Fehler: $error'),
const SizedBox(height: 16),
FilledButton.tonal(
onPressed: () => ref.invalidate(roomWithStatsListProvider),
child: const Text('Erneut versuchen'),
),
],
),
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => context.go('/rooms/new'),
child: const Icon(Icons.add),
),
);
}
}
/// Empty state shown when no rooms have been created yet.
class _EmptyState extends StatelessWidget {
const _EmptyState({required this.l10n});
final AppLocalizations l10n;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.door_front_door_rounded,
size: 80,
color: theme.colorScheme.onSurface.withValues(alpha: 0.4),
),
const SizedBox(height: 24),
Text(
l10n.roomsEmptyTitle,
style: theme.textTheme.headlineSmall,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
l10n.roomsEmptyMessage,
style: theme.textTheme.bodyLarge?.copyWith(
color: theme.colorScheme.onSurface.withValues(alpha: 0.6),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
FilledButton.tonal(
onPressed: () => context.go('/rooms/new'),
child: Text(l10n.roomsEmptyAction),
),
],
),
),
);
}
}
/// 2-column reorderable grid of room cards.
class _RoomGrid extends ConsumerStatefulWidget {
const _RoomGrid({required this.rooms});
final List<RoomWithStats> rooms;
@override
ConsumerState<_RoomGrid> createState() => _RoomGridState();
}
class _RoomGridState extends ConsumerState<_RoomGrid> {
final _scrollController = ScrollController();
final _gridViewKey = GlobalKey();
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final children = widget.rooms.map((rws) {
return RoomCard(
key: ValueKey('room-${rws.room.id}'),
roomWithStats: rws,
onEdit: () => context.go('/rooms/${rws.room.id}/edit'),
onDelete: () => _showDeleteConfirmation(context, rws, l10n),
);
}).toList();
return ReorderableBuilder<Widget>(
scrollController: _scrollController,
onReorder: (ReorderedListFunction<Widget> reorderFunc) {
final reordered = reorderFunc(children);
final newOrder = reordered.map((w) {
final keyStr = (w.key! as ValueKey<String>).value;
return int.parse(keyStr.replaceFirst('room-', ''));
}).toList();
ref.read(roomActionsProvider.notifier).reorderRooms(newOrder);
},
builder: (reorderableChildren) {
final topPadding = MediaQuery.of(context).padding.top;
return GridView.count(
key: _gridViewKey,
controller: _scrollController,
crossAxisCount: 2,
padding: EdgeInsets.fromLTRB(12, 12 + topPadding, 12, 12),
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 1.0,
children: reorderableChildren,
);
},
children: children,
);
}
void _showDeleteConfirmation(
BuildContext context,
RoomWithStats rws,
AppLocalizations l10n,
) {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: Text(l10n.roomDeleteConfirmTitle),
content: Text(l10n.roomDeleteConfirmMessage),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(l10n.cancel),
),
FilledButton(
onPressed: () {
Navigator.pop(ctx);
ref.read(roomActionsProvider.notifier).deleteRoom(rws.room.id);
},
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
foregroundColor: Theme.of(context).colorScheme.onError,
),
child: Text(l10n.roomDeleteConfirmAction),
),
],
),
);
}
}