bible-tracker/lib/services/tracker_service.dart
2026-04-20 15:13:58 -04:00

123 lines
3.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:uuid/uuid.dart';
import '../data/bible.dart';
import '../models/tracker.dart';
import 'tracker_storage.dart';
/// Manages the list of [Tracker]s and exposes them as state to the UI.
///
/// Initial state is loaded synchronously from [TrackerStorage] so the first
/// frame already has the full list. Mutations persist asynchronously.
class TrackerService extends StateNotifier<List<Tracker>> {
TrackerService(this._storage, {Uuid? uuid})
: _uuid = uuid ?? const Uuid(),
super(_storage.loadAll());
final TrackerStorage _storage;
final Uuid _uuid;
Future<void> _persist(List<Tracker> trackers) async {
state = trackers;
await _storage.saveAll(trackers);
}
Future<Tracker> create({required String name, required Color color}) async {
final Tracker tracker = Tracker(
id: _uuid.v4(),
name: name,
colorValue: color.toARGB32(),
startDate: DateTime.now(),
);
final List<Tracker> next = <Tracker>[...state, tracker];
await _persist(next);
return tracker;
}
Future<void> updateMeta(String id, {String? name, Color? color}) async {
final List<Tracker> next = state
.map(
(Tracker tracker) => tracker.id == id
? tracker.copyWith(
name: name ?? tracker.name,
colorValue: color?.toARGB32() ?? tracker.colorValue,
)
: tracker,
)
.toList(growable: false);
await _persist(next);
}
Future<void> delete(String id) async {
final List<Tracker> next = state
.where((Tracker tracker) => tracker.id != id)
.toList(growable: false);
await _persist(next);
}
Future<void> toggleChapter(String id, int bookIndex, int chapter) async {
final String key = Bible.chapterKey(bookIndex, chapter);
final List<Tracker> next = state
.map((Tracker tracker) {
if (tracker.id != id) return tracker;
final Set<String> updated = <String>{...tracker.readChapters};
if (!updated.remove(key)) {
updated.add(key);
}
return tracker.copyWith(readChapters: updated);
})
.toList(growable: false);
await _persist(next);
}
Future<void> setBookRead(
String id,
int bookIndex, {
required bool read,
}) async {
final Book book = Bible.books[bookIndex];
final List<Tracker> next = state
.map((Tracker tracker) {
if (tracker.id != id) return tracker;
final Set<String> updated = <String>{...tracker.readChapters};
for (int chapter = 1; chapter <= book.chapterCount; chapter++) {
final String key = Bible.chapterKey(bookIndex, chapter);
if (read) {
updated.add(key);
} else {
updated.remove(key);
}
}
return tracker.copyWith(readChapters: updated);
})
.toList(growable: false);
await _persist(next);
}
Future<void> reset(String id) async {
final List<Tracker> next = state
.map(
(Tracker tracker) => tracker.id == id
? tracker.copyWith(readChapters: const <String>{})
: tracker,
)
.toList(growable: false);
await _persist(next);
}
}
final StateNotifierProvider<TrackerService, List<Tracker>>
trackerServiceProvider = StateNotifierProvider<TrackerService, List<Tracker>>(
(Ref ref) => TrackerService(ref.read(trackerStorageProvider)),
);
/// Convenience selector for a single tracker by id.
final ProviderFamily<Tracker?, String> trackerByIdProvider =
Provider.family<Tracker?, String>((Ref ref, String id) {
final List<Tracker> trackers = ref.watch(trackerServiceProvider);
for (final Tracker tracker in trackers) {
if (tracker.id == id) return tracker;
}
return null;
});