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. /// /// Kept as a plain service (no repository) because storage access is /// simple read/write-all and there are no complex queries or caches. class TrackerService extends StateNotifier>> { TrackerService(this._storage, {Uuid? uuid}) : _uuid = uuid ?? const Uuid(), super(const AsyncValue>.loading()) { unawaitedLoad(); } final TrackerStorage _storage; final Uuid _uuid; @visibleForTesting Future unawaitedLoad() => _load(); Future _load() async { try { final List trackers = await _storage.loadAll(); state = AsyncValue>.data(trackers); } catch (error, stack) { state = AsyncValue>.error(error, stack); } } Future _persist(List trackers) async { state = AsyncValue>.data(trackers); await _storage.saveAll(trackers); } Future create({required String name, required Color color}) async { final Tracker tracker = Tracker( id: _uuid.v4(), name: name, // ignore: deprecated_member_use colorValue: color.value, startDate: DateTime.now(), ); final List next = [ ...state.value ?? const [], tracker, ]; await _persist(next); return tracker; } Future updateMeta(String id, {String? name, Color? color}) async { final List current = state.value ?? const []; final List next = current .map( (Tracker tracker) => tracker.id == id ? tracker.copyWith( name: name ?? tracker.name, // ignore: deprecated_member_use colorValue: color?.value ?? tracker.colorValue, ) : tracker, ) .toList(growable: false); await _persist(next); } Future delete(String id) async { final List current = state.value ?? const []; final List next = current .where((Tracker tracker) => tracker.id != id) .toList(growable: false); await _persist(next); } Future toggleChapter(String id, int bookIndex, int chapter) async { final String key = Bible.chapterKey(bookIndex, chapter); final List current = state.value ?? const []; final List next = current .map((Tracker tracker) { if (tracker.id != id) return tracker; final Set updated = {...tracker.readChapters}; if (!updated.remove(key)) { updated.add(key); } return tracker.copyWith(readChapters: updated); }) .toList(growable: false); await _persist(next); } Future setBookRead( String id, int bookIndex, { required bool read, }) async { final Book book = Bible.books[bookIndex]; final List current = state.value ?? const []; final List next = current .map((Tracker tracker) { if (tracker.id != id) return tracker; final Set updated = {...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 reset(String id) async { final List current = state.value ?? const []; final List next = current .map( (Tracker tracker) => tracker.id == id ? tracker.copyWith(readChapters: const {}) : tracker, ) .toList(growable: false); await _persist(next); } } final StateNotifierProvider>> trackerServiceProvider = StateNotifierProvider>>( (Ref ref) => TrackerService(ref.read(trackerStorageProvider)), ); /// Convenience selector for a single tracker by id. final ProviderFamily trackerByIdProvider = Provider.family((Ref ref, String id) { final AsyncValue> state = ref.watch(trackerServiceProvider); return state.value?.cast().firstWhere( (Tracker? t) => t?.id == id, orElse: () => null, ); });