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>
83 lines
2.6 KiB
Dart
83 lines
2.6 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:sharedinbox/core/repositories/account_repository.dart';
|
|
import 'package:sharedinbox/data/db/database.dart';
|
|
import 'package:sharedinbox/data/repositories/email_repository_impl.dart';
|
|
|
|
/// Prefetches email bodies in the background and enforces a local cache size
|
|
/// limit by evicting the oldest cached bodies when the limit is exceeded.
|
|
class BodyCacheService {
|
|
BodyCacheService(this._db, this._accountRepo);
|
|
|
|
final AppDatabase _db;
|
|
final AccountRepository _accountRepo;
|
|
|
|
static const _batchSize = 20;
|
|
|
|
Future<void> run() async {
|
|
final prefs = await (_db.select(
|
|
_db.userPreferences,
|
|
)).getSingleOrNull();
|
|
final limitMb = prefs?.bodyCacheLimitMb ?? 100;
|
|
final limitBytes = limitMb * 1024 * 1024;
|
|
|
|
await _evictIfNeeded(limitBytes);
|
|
|
|
final candidates = await _fetchCandidates();
|
|
if (candidates.isEmpty) return;
|
|
|
|
final emailRepo = EmailRepositoryImpl(_db, _accountRepo);
|
|
|
|
for (final emailId in candidates) {
|
|
final currentSize = await _getCacheSizeBytes();
|
|
if (currentSize >= limitBytes) break;
|
|
try {
|
|
await emailRepo.getEmailBody(emailId);
|
|
} catch (_) {
|
|
// Skip emails that fail to fetch.
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _evictIfNeeded(int limitBytes) async {
|
|
final currentSize = await _getCacheSizeBytes();
|
|
if (currentSize <= limitBytes) return;
|
|
|
|
final bodies = await (_db.select(_db.emailBodies)
|
|
..where((t) => t.cachedAt.isNotNull())
|
|
..orderBy([(t) => OrderingTerm.asc(t.cachedAt)]))
|
|
.get();
|
|
|
|
var remaining = currentSize;
|
|
for (final body in bodies) {
|
|
if (remaining <= limitBytes) break;
|
|
final bodySize =
|
|
(body.textBody?.length ?? 0) + (body.htmlBody?.length ?? 0);
|
|
await (_db.delete(_db.emailBodies)
|
|
..where((t) => t.emailId.equals(body.emailId)))
|
|
.go();
|
|
remaining -= bodySize;
|
|
}
|
|
}
|
|
|
|
Future<int> _getCacheSizeBytes() async {
|
|
final result = await _db
|
|
.customSelect(
|
|
"SELECT COALESCE(SUM(LENGTH(COALESCE(text_body, '')) + LENGTH(COALESCE(html_body, ''))), 0) AS total FROM email_bodies",
|
|
)
|
|
.getSingle();
|
|
return result.read<int>('total');
|
|
}
|
|
|
|
Future<List<String>> _fetchCandidates() async {
|
|
final rows = await _db.customSelect(
|
|
'SELECT e.id FROM emails e '
|
|
'LEFT JOIN email_bodies eb ON eb.email_id = e.id '
|
|
'WHERE eb.email_id IS NULL '
|
|
'ORDER BY e.received_at DESC '
|
|
'LIMIT ?',
|
|
variables: [Variable.withInt(_batchSize)],
|
|
).get();
|
|
return rows.map((r) => r.read<String>('id')).toList();
|
|
}
|
|
}
|