Files
sharedinbox/lib/main.dart
T
Thomas SharedInboxandClaude Sonnet 4.6 49fdf83834 feat: pre-fetch email bodies for offline access (#373)
Adds a background body-prefetch mechanism with network-awareness and a
user-configurable cache size limit to keep email bodies available offline
without downloading the entire mailbox.

- Schema v38: adds `prefetch_mode` and `body_cache_limit_mb` columns to
  `user_preferences` (defaults: wifiOnly / 100 MB).
- `PrefetchMode` enum (disabled / wifiOnly / always) in the model.
- `BodyCacheService`: fetches bodies for uncached emails (newest first,
  batch of 20), evicts oldest cached bodies when the size limit is
  exceeded.
- Registers a separate WorkManager periodic task (`si_bg_prefetch`) with
  `NetworkType.unmetered` (Wi-Fi only) or `NetworkType.connected` (any)
  based on the stored preference; cancels the task when disabled.
- On app startup, reads the stored preference and re-registers the task
  with the correct constraint.
- Preferences screen: radio group for prefetch mode + dropdown for
  cache size limit (50 / 100 / 200 / 500 MB).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-04 03:29:03 +02:00

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,
);
}
}