f655adfbea
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.
126 lines
3.7 KiB
Dart
126 lines
3.7 KiB
Dart
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;
|
|
}
|