bible-tracker/lib/screens/home_screen.dart

244 lines
7.9 KiB
Dart
Raw Normal View History

2026-04-20 19:13:58 +00:00
import 'package:collection/collection.dart';
2026-04-20 18:39:49 +00:00
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
2026-04-20 19:13:58 +00:00
import '../data/bible.dart';
2026-04-20 18:39:49 +00:00
import '../models/tracker.dart';
2026-04-20 19:13:58 +00:00
import '../services/last_location.dart';
2026-04-20 18:39:49 +00:00
import '../services/tracker_service.dart';
import '../widgets/theme.dart';
import '../widgets/tracker_tile.dart';
2026-04-20 19:13:58 +00:00
import 'book_screen.dart';
2026-04-20 18:39:49 +00:00
import 'create_tracker_screen.dart';
import 'tracker_screen.dart';
2026-04-20 19:13:58 +00:00
class HomeScreen extends ConsumerStatefulWidget {
2026-04-20 18:39:49 +00:00
const HomeScreen({super.key});
@override
2026-04-20 19:13:58 +00:00
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends ConsumerState<HomeScreen> {
bool _restoreAttempted = false;
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) => _maybeRestore());
}
void _maybeRestore() {
if (_restoreAttempted || !mounted) return;
_restoreAttempted = true;
final LastLocation? pending = ref.read(pendingRestoreProvider);
if (pending == null) return;
final List<Tracker> trackers = ref.read(trackerServiceProvider);
final Tracker? tracker = trackers.firstWhereOrNull(
(Tracker t) => t.id == pending.trackerId,
);
if (tracker == null) return;
final NavigatorState navigator = Navigator.of(context);
navigator.push<void>(
MaterialPageRoute<void>(
settings: RouteSettings(name: '/tracker/${tracker.id}'),
builder: (_) => TrackerScreen(trackerId: tracker.id),
),
);
final int? bookIndex = pending.bookIndex;
if (bookIndex != null && bookIndex >= 0 && bookIndex < Bible.books.length) {
navigator.push<void>(
MaterialPageRoute<void>(
settings: RouteSettings(
name: '/tracker/${tracker.id}/book/$bookIndex',
),
builder: (_) =>
BookScreen(trackerId: tracker.id, bookIndex: bookIndex),
),
);
}
}
@override
Widget build(BuildContext context) {
final List<Tracker> trackers = ref.watch(trackerServiceProvider);
2026-04-20 18:39:49 +00:00
return Scaffold(
appBar: AppBar(title: const Text('My Bible Tracker')),
2026-04-20 19:13:58 +00:00
body: trackers.isEmpty
? const _EmptyState()
: ListView.separated(
padding: const EdgeInsets.all(AppTokens.spaceL),
itemCount: trackers.length,
separatorBuilder: (_, _) =>
const SizedBox(height: AppTokens.spaceM),
itemBuilder: (BuildContext context, int index) {
final Tracker tracker = trackers[index];
return TrackerTile(
tracker: tracker,
onTap: () => Navigator.of(context).push<void>(
MaterialPageRoute<void>(
settings: RouteSettings(name: '/tracker/${tracker.id}'),
builder: (_) => TrackerScreen(trackerId: tracker.id),
),
2026-04-20 18:39:49 +00:00
),
2026-04-20 19:13:58 +00:00
onLongPress: () => _showTrackerMenu(context, ref, tracker),
);
},
),
2026-04-20 18:39:49 +00:00
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Navigator.of(context).push<void>(
MaterialPageRoute<void>(builder: (_) => const CreateTrackerScreen()),
),
icon: const Icon(Icons.add),
label: const Text('New Tracker'),
),
);
}
Future<void> _showTrackerMenu(
BuildContext context,
WidgetRef ref,
Tracker tracker,
) async {
final _TrackerMenuAction? action =
await showModalBottomSheet<_TrackerMenuAction>(
context: context,
showDragHandle: true,
builder: (BuildContext context) {
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
leading: const Icon(Icons.edit_outlined),
title: const Text('Rename / recolor'),
onTap: () =>
Navigator.of(context).pop(_TrackerMenuAction.edit),
),
ListTile(
leading: const Icon(Icons.refresh),
title: const Text('Reset progress'),
onTap: () =>
Navigator.of(context).pop(_TrackerMenuAction.reset),
),
ListTile(
leading: const Icon(Icons.delete_outline),
iconColor: Theme.of(context).colorScheme.error,
textColor: Theme.of(context).colorScheme.error,
title: const Text('Delete'),
onTap: () =>
Navigator.of(context).pop(_TrackerMenuAction.delete),
),
const SizedBox(height: AppTokens.spaceS),
],
),
);
},
);
if (action == null || !context.mounted) return;
switch (action) {
case _TrackerMenuAction.edit:
await Navigator.of(context).push<void>(
MaterialPageRoute<void>(
builder: (_) => CreateTrackerScreen(existing: tracker),
),
);
case _TrackerMenuAction.reset:
await _confirmAndRun(
context,
title: 'Reset progress?',
message:
'All checked chapters for "${tracker.name}" will be cleared.',
destructive: true,
run: () =>
ref.read(trackerServiceProvider.notifier).reset(tracker.id),
);
case _TrackerMenuAction.delete:
await _confirmAndRun(
context,
title: 'Delete tracker?',
message: '"${tracker.name}" will be removed permanently.',
destructive: true,
run: () =>
ref.read(trackerServiceProvider.notifier).delete(tracker.id),
);
}
}
Future<void> _confirmAndRun(
BuildContext context, {
required String title,
required String message,
required bool destructive,
required Future<void> Function() run,
}) async {
final bool? confirmed = await showDialog<bool>(
context: context,
builder: (BuildContext context) => AlertDialog(
title: Text(title),
content: Text(message),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(context).pop(false),
child: const Text('Cancel'),
),
FilledButton(
style: destructive
? FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.error,
)
: null,
onPressed: () => Navigator.of(context).pop(true),
child: const Text('Confirm'),
),
],
),
);
if (confirmed ?? false) {
await run();
}
}
}
enum _TrackerMenuAction { edit, reset, delete }
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(AppTokens.spaceXXL),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(
Icons.menu_book_outlined,
size: 72,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(height: AppTokens.spaceL),
Text(
'Start your first tracker',
style: Theme.of(context).textTheme.titleLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: AppTokens.spaceS),
Text(
'Create a tracker to mark the chapters you have read. '
'Read at your own pace — there are no deadlines.',
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
),
);
}
}