Closes #373 ## Summary - **Schema v38**: two new columns on `user_preferences` — `prefetch_mode` (default `wifiOnly`) and `body_cache_limit_mb` (default 100 MB). - **`BodyCacheService`**: queries for emails that have no cached body, fetches them newest-first in batches of 20, and evicts the oldest cached bodies when the configured size limit is exceeded. - **Separate WorkManager task** (`si_bg_prefetch`): runs hourly with `NetworkType.unmetered` (Wi-Fi) or `NetworkType.connected` (any) depending on the user's choice. The task is cancelled when prefetch is disabled. - **App startup**: reads the stored preference from the DB and re-registers the WorkManager task with the correct constraint. - **Preferences screen**: radio group for prefetch mode (Wi-Fi only / Any network / Disabled) and a dropdown for cache size limit (50 / 100 / 200 / 500 MB). ## What is NOT downloaded Binary attachments are never fetched — `getEmailBody()` stores only `textBody` and `htmlBody`. The cache size limit + per-run batch cap (20 emails) keep storage bounded even on large mailboxes. ## Test plan - [x] `task analyze` — no issues - [x] `task test` — all 492 tests pass (incl. updated migration_test.dart for v38) Co-authored-by: Thomas SharedInbox <sharedinbox@thomas-guettler.de> Reviewed-on: https://codeberg.org/guettli/sharedinbox/pulls/400
106 lines
3.2 KiB
Dart
106 lines
3.2 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:flutter_riverpod/misc.dart' show Override;
|
|
|
|
import 'package:sharedinbox/core/models/user_preferences.dart';
|
|
import 'package:sharedinbox/core/services/notification_service.dart';
|
|
import 'package:sharedinbox/core/sync/background_sync.dart';
|
|
import 'package:sharedinbox/data/db/database.dart';
|
|
import 'package:sharedinbox/di.dart';
|
|
import 'package:sharedinbox/ui/router.dart';
|
|
import 'package:sharedinbox/ui/screens/crash_screen.dart';
|
|
|
|
void main({List<Override> overrides = const []}) async {
|
|
unawaited(
|
|
runZonedGuarded(
|
|
() async {
|
|
WidgetsFlutterBinding.ensureInitialized();
|
|
|
|
// Catch errors during build (e.g. layout exceptions) and show CrashScreen.
|
|
ErrorWidget.builder = (details) => CrashScreen(
|
|
exception: details.exception,
|
|
stackTrace: details.stack,
|
|
);
|
|
|
|
// Catch framework-level errors (e.g. from gestures, timers).
|
|
FlutterError.onError = (details) {
|
|
FlutterError.presentError(details);
|
|
runApp(
|
|
CrashScreen(
|
|
exception: details.exception,
|
|
stackTrace: details.stack,
|
|
),
|
|
);
|
|
};
|
|
|
|
await initDatabasePath();
|
|
if (Platform.isAndroid) {
|
|
await initNotifications();
|
|
await registerBackgroundSync();
|
|
await _registerPrefetchTaskFromStoredPrefs();
|
|
}
|
|
runApp(
|
|
ProviderScope(overrides: overrides, child: const SharedInboxApp()),
|
|
);
|
|
},
|
|
(error, stack) {
|
|
// Catch unhandled async errors.
|
|
runApp(CrashScreen(exception: error, stackTrace: stack));
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
/// Reads the stored prefetch preference and registers the WorkManager task
|
|
/// with the correct network constraint for it. Opens and immediately closes
|
|
/// a temporary DB connection; safe because initDatabasePath() has already run.
|
|
Future<void> _registerPrefetchTaskFromStoredPrefs() async {
|
|
final db = AppDatabase();
|
|
try {
|
|
final row = await db.select(db.userPreferences).getSingleOrNull();
|
|
final mode = PrefetchMode.fromString(row?.prefetchMode);
|
|
await registerBodyPrefetchTask(mode);
|
|
} finally {
|
|
await db.close();
|
|
}
|
|
}
|
|
|
|
class SharedInboxApp extends ConsumerStatefulWidget {
|
|
const SharedInboxApp({super.key});
|
|
|
|
@override
|
|
ConsumerState<SharedInboxApp> createState() => _SharedInboxAppState();
|
|
}
|
|
|
|
class _SharedInboxAppState extends ConsumerState<SharedInboxApp> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
// Start background IMAP sync once — runs for the lifetime of the app.
|
|
ref.read(syncManagerProvider).start();
|
|
ref.read(reliabilityRunnerProvider).start();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp.router(
|
|
title: 'sharedinbox.de',
|
|
theme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
|
|
useMaterial3: true,
|
|
),
|
|
darkTheme: ThemeData(
|
|
colorScheme: ColorScheme.fromSeed(
|
|
seedColor: Colors.indigo,
|
|
brightness: Brightness.dark,
|
|
),
|
|
useMaterial3: true,
|
|
),
|
|
routerConfig: router,
|
|
);
|
|
}
|
|
}
|