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,
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|