All checks were successful
ci/woodpecker/pr/pr Pipeline was successful
- 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>
81 lines
2.7 KiB
Dart
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);
|
|
}
|