feat: add local-first private AI digest workflow
Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const String _enabledKey = 'llm_digest_enabled';
|
||||
const String _preferredWeekdayKey = 'llm_digest_preferred_weekday';
|
||||
const String _preferredHourKey = 'llm_digest_preferred_hour';
|
||||
const String _requireWifiKey = 'llm_digest_require_wifi';
|
||||
const String _requireChargingKey = 'llm_digest_require_charging';
|
||||
|
||||
class LlmDigestConfigNotifier extends Notifier<LlmDigestConfigState> {
|
||||
Future<void>? _initializeFuture;
|
||||
|
||||
@override
|
||||
LlmDigestConfigState build() {
|
||||
return const LlmDigestConfigState();
|
||||
}
|
||||
|
||||
Future<void> initialize() {
|
||||
_initializeFuture ??= _initializeFromStorage();
|
||||
return _initializeFuture!;
|
||||
}
|
||||
|
||||
Future<void> _initializeFromStorage() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
state = LlmDigestConfigState(
|
||||
enabled: prefs.getBool(_enabledKey) ?? false,
|
||||
preferredWeekday: prefs.getInt(_preferredWeekdayKey) ?? DateTime.sunday,
|
||||
preferredHour: prefs.getInt(_preferredHourKey) ?? 21,
|
||||
requireWifi: prefs.getBool(_requireWifiKey) ?? true,
|
||||
requireCharging: prefs.getBool(_requireChargingKey) ?? true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setEnabled(bool enabled) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_enabledKey, enabled);
|
||||
state = state.copyWith(enabled: enabled);
|
||||
}
|
||||
|
||||
Future<void> setPreferredRunWindow({
|
||||
required int weekday,
|
||||
required int hour,
|
||||
}) async {
|
||||
final int normalizedWeekday = weekday.clamp(
|
||||
DateTime.monday,
|
||||
DateTime.sunday,
|
||||
);
|
||||
final int normalizedHour = hour.clamp(0, 23);
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_preferredWeekdayKey, normalizedWeekday);
|
||||
await prefs.setInt(_preferredHourKey, normalizedHour);
|
||||
state = state.copyWith(
|
||||
preferredWeekday: normalizedWeekday,
|
||||
preferredHour: normalizedHour,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setNetworkPolicy({
|
||||
required bool requireWifi,
|
||||
required bool requireCharging,
|
||||
}) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_requireWifiKey, requireWifi);
|
||||
await prefs.setBool(_requireChargingKey, requireCharging);
|
||||
state = state.copyWith(
|
||||
requireWifi: requireWifi,
|
||||
requireCharging: requireCharging,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final NotifierProvider<LlmDigestConfigNotifier, LlmDigestConfigState>
|
||||
llmDigestConfigProvider =
|
||||
NotifierProvider<LlmDigestConfigNotifier, LlmDigestConfigState>(
|
||||
LlmDigestConfigNotifier.new,
|
||||
);
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
const String _runStateKey = 'llm_digest_run_state';
|
||||
|
||||
class LlmDigestRunStore {
|
||||
const LlmDigestRunStore();
|
||||
|
||||
Future<LlmDigestRunState> read() async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
final String? raw = prefs.getString(_runStateKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
return const LlmDigestRunState();
|
||||
}
|
||||
try {
|
||||
return LlmDigestRunState.fromJson(
|
||||
jsonDecode(raw) as Map<String, dynamic>,
|
||||
);
|
||||
} on FormatException {
|
||||
return const LlmDigestRunState();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> write(LlmDigestRunState state) async {
|
||||
final SharedPreferences prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_runStateKey, jsonEncode(state.toJson()));
|
||||
}
|
||||
}
|
||||
|
||||
final Provider<LlmDigestRunStore> llmDigestRunStoreProvider =
|
||||
Provider<LlmDigestRunStore>((Ref ref) => const LlmDigestRunStore());
|
||||
Reference in New Issue
Block a user