worldhopper/lib/providers/koreader_sync_provider.dart
Felipe M. bb579719f9
All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
fix: prevent double-save on exit, surface KOReader sync auth failures
- Add guard flag to prevent both onPopInvokedWithResult and dispose()
  from saving progress (double DB writes + double sync pushes)
- Capture ref-dependent values synchronously in _saveProgress before
  any await to prevent accessing disposed ref after super.dispose()
- Guard ref.invalidate() calls with mounted check for dispose safety
- Add KoreaderSyncAuthException so 401 errors in getProgress and
  updateProgress propagate instead of being silently swallowed
- Show a warning snackbar (once per session) when auth fails so the
  user knows sync credentials need updating

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 00:12:06 +01:00

81 lines
2.7 KiB
Dart

import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worldhopper/models/koreader_progress.dart';
import 'package:worldhopper/models/opds_server.dart';
import 'package:worldhopper/providers/server_provider.dart';
import 'package:worldhopper/services/koreader_sync_service.dart';
/// Provider for creating a KoreaderSyncService for a given server.
/// Only returns a service if the server has KOReader sync enabled.
final koreaderSyncServiceProvider =
Provider.family<KoreaderSyncService?, OPDSServer>((ref, server) {
if (!server.koreaderSyncEnabled ||
server.koreaderSyncUrl == null ||
server.koreaderSyncUrl!.isEmpty) {
return null;
}
return KoreaderSyncService(server);
});
/// Provider to get a KoreaderSyncService by server ID.
/// Returns null if server not found or KOReader sync is not enabled.
final koreaderSyncServiceByIdProvider =
FutureProvider.family<KoreaderSyncService?, String>((ref, serverId) async {
final server = await ref.watch(serverProvider(serverId).future);
if (server == null) return null;
return ref.watch(koreaderSyncServiceProvider(server));
});
/// Push reading progress to KOReader sync server.
/// Returns true if sync was successful, false otherwise.
/// Fails silently (logs error) to not interrupt the reading experience.
Future<bool> pushKoreaderProgress({
required KoreaderSyncService syncService,
required String documentHash,
required double percentage,
required String progress,
}) async {
try {
return await syncService.updateProgress(
documentHash: documentHash,
percentage: percentage,
progress: progress,
);
} on KoreaderSyncAuthException {
rethrow;
} catch (e) {
debugPrint('Failed to push KOReader progress: $e');
return false;
}
}
/// Pull reading progress from KOReader sync server.
/// Returns the remote progress if available, null otherwise.
Future<KoreaderProgress?> pullKoreaderProgress({
required KoreaderSyncService syncService,
required String documentHash,
}) async {
try {
return await syncService.getProgress(documentHash);
} on KoreaderSyncAuthException {
rethrow;
} catch (e) {
debugPrint('Failed to pull KOReader progress: $e');
return null;
}
}
/// Compute the document hash for a given entry.
/// If the EPUB file is available, uses the file's partial MD5 hash (compatible with KOReader).
/// Falls back to hashing the OPDS entry ID.
Future<String> computeDocumentHash({
required String entryId,
File? epubFile,
}) async {
if (epubFile != null && await epubFile.exists()) {
return await KoreaderSyncService.computeFileHash(epubFile);
}
return KoreaderSyncService.computeStringHash(entryId);
}