bible-tracker/lib/services/tracker_service.dart

149 lines
4.8 KiB
Dart
Raw Normal View History

2026-04-20 18:39:49 +00:00
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<AsyncValue<List<Tracker>>> {
TrackerService(this._storage, {Uuid? uuid})
: _uuid = uuid ?? const Uuid(),
super(const AsyncValue<List<Tracker>>.loading()) {
unawaitedLoad();
}
final TrackerStorage _storage;
final Uuid _uuid;
@visibleForTesting
Future<void> unawaitedLoad() => _load();
Future<void> _load() async {
try {
final List<Tracker> trackers = await _storage.loadAll();
state = AsyncValue<List<Tracker>>.data(trackers);
} catch (error, stack) {
state = AsyncValue<List<Tracker>>.error(error, stack);
}
}
Future<void> _persist(List<Tracker> trackers) async {
state = AsyncValue<List<Tracker>>.data(trackers);
await _storage.saveAll(trackers);
}
Future<Tracker> 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<Tracker> next = <Tracker>[
...state.value ?? const <Tracker>[],
tracker,
];
await _persist(next);
return tracker;
}
Future<void> updateMeta(String id, {String? name, Color? color}) async {
final List<Tracker> current = state.value ?? const <Tracker>[];
final List<Tracker> 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<void> delete(String id) async {
final List<Tracker> current = state.value ?? const <Tracker>[];
final List<Tracker> next = current
.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> current = state.value ?? const <Tracker>[];
final List<Tracker> next = current
.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> current = state.value ?? const <Tracker>[];
final List<Tracker> next = current
.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> current = state.value ?? const <Tracker>[];
final List<Tracker> next = current
.map(
(Tracker tracker) => tracker.id == id
? tracker.copyWith(readChapters: const <String>{})
: tracker,
)
.toList(growable: false);
await _persist(next);
}
}
final StateNotifierProvider<TrackerService, AsyncValue<List<Tracker>>>
trackerServiceProvider =
StateNotifierProvider<TrackerService, AsyncValue<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 AsyncValue<List<Tracker>> state = ref.watch(trackerServiceProvider);
return state.value?.cast<Tracker?>().firstWhere(
(Tracker? t) => t?.id == id,
orElse: () => null,
);
});