148 lines
4.8 KiB
Dart
148 lines
4.8 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.
|
|
///
|
|
/// 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,
|
|
);
|
|
});
|