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,125 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/data/llm_digest_config.dart';
|
||||
import 'package:relationship_saver/features/ai_digest/domain/ai_digest_models.dart';
|
||||
import 'package:workmanager/workmanager.dart';
|
||||
|
||||
const String llmDigestTaskUniqueName = 'relationship_saver_llm_digest_weekly';
|
||||
const String llmDigestTaskName = 'com.relationshipsaver.llm.digest';
|
||||
|
||||
abstract interface class LlmDigestBackgroundScheduler {
|
||||
Future<void> configure(LlmDigestConfigState config);
|
||||
|
||||
Future<void> cancel();
|
||||
}
|
||||
|
||||
class WorkmanagerLlmDigestBackgroundScheduler
|
||||
implements LlmDigestBackgroundScheduler {
|
||||
const WorkmanagerLlmDigestBackgroundScheduler({Workmanager? workmanager})
|
||||
: _workmanager = workmanager;
|
||||
|
||||
final Workmanager? _workmanager;
|
||||
|
||||
Workmanager get _driver => _workmanager ?? Workmanager();
|
||||
|
||||
@override
|
||||
Future<void> configure(LlmDigestConfigState config) async {
|
||||
if (!config.enabled) {
|
||||
await cancel();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await _driver.registerPeriodicTask(
|
||||
llmDigestTaskUniqueName,
|
||||
llmDigestTaskName,
|
||||
frequency: const Duration(days: 7),
|
||||
initialDelay: _initialDelay(config, DateTime.now()),
|
||||
constraints: Constraints(
|
||||
networkType: config.requireWifi
|
||||
? NetworkType.unmetered
|
||||
: NetworkType.connected,
|
||||
requiresCharging: config.requireCharging,
|
||||
),
|
||||
existingWorkPolicy: ExistingPeriodicWorkPolicy.update,
|
||||
);
|
||||
} on UnimplementedError {
|
||||
// Flutter tests and unsupported platforms have no Workmanager backend.
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> cancel() async {
|
||||
try {
|
||||
await _driver.cancelByUniqueName(llmDigestTaskUniqueName);
|
||||
} on UnimplementedError {
|
||||
// Flutter tests and unsupported platforms have no Workmanager backend.
|
||||
}
|
||||
}
|
||||
|
||||
Duration _initialDelay(LlmDigestConfigState config, DateTime now) {
|
||||
DateTime target = DateTime(
|
||||
now.year,
|
||||
now.month,
|
||||
now.day,
|
||||
config.preferredHour,
|
||||
);
|
||||
final int dayDelta = (config.preferredWeekday - now.weekday) % 7;
|
||||
target = target.add(Duration(days: dayDelta));
|
||||
if (!target.isAfter(now)) {
|
||||
target = target.add(const Duration(days: 7));
|
||||
}
|
||||
return target.difference(now);
|
||||
}
|
||||
}
|
||||
|
||||
final Provider<LlmDigestBackgroundScheduler>
|
||||
llmDigestBackgroundSchedulerProvider = Provider<LlmDigestBackgroundScheduler>(
|
||||
(Ref ref) => const WorkmanagerLlmDigestBackgroundScheduler(),
|
||||
);
|
||||
|
||||
class LlmDigestBackgroundRunner extends ConsumerStatefulWidget {
|
||||
const LlmDigestBackgroundRunner({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
ConsumerState<LlmDigestBackgroundRunner> createState() =>
|
||||
_LlmDigestBackgroundRunnerState();
|
||||
}
|
||||
|
||||
class _LlmDigestBackgroundRunnerState
|
||||
extends ConsumerState<LlmDigestBackgroundRunner> {
|
||||
ProviderSubscription<LlmDigestConfigState>? _subscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
unawaited(_initializeAndConfigure());
|
||||
_subscription = ref.listenManual<LlmDigestConfigState>(
|
||||
llmDigestConfigProvider,
|
||||
(LlmDigestConfigState? previous, LlmDigestConfigState next) {
|
||||
unawaited(
|
||||
ref.read(llmDigestBackgroundSchedulerProvider).configure(next),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _initializeAndConfigure() async {
|
||||
await ref.read(llmDigestConfigProvider.notifier).initialize();
|
||||
await ref
|
||||
.read(llmDigestBackgroundSchedulerProvider)
|
||||
.configure(ref.read(llmDigestConfigProvider));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
Reference in New Issue
Block a user