feat: navigation store feat: implement OPDS entry caching system Add comprehensive caching for OPDS publications with local image storage and Dublin Core Terms metadata extraction. This enables offline viewing, fixes Library screen to show book titles and covers, and improves overall UX. - Add publications table with full metadata (title, authors, series, publisher, ISBN, language) - Implement local image cache service for covers and thumbnails - Extract DCTerms metadata (series, publisher, language, ISBN) from OPDS feeds - Link reading progress to cached publications - Update Library screen to display cached publication data and covers - Cache publications automatically when starting to read - Fix navigation stack issues by using context.push() instead of context.go() - Add explicit back button to EPUB reader with proper PopScope handling - Implement UNIQUE constraint on reading_progress to prevent duplicates - Save reading progress on screen dispose for both EPUB and image readers Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
143 lines
4.3 KiB
Dart
143 lines
4.3 KiB
Dart
import 'package:sqflite/sqflite.dart';
|
|
import 'package:worldhopper/database/database.dart';
|
|
import 'package:worldhopper/database/tables/reading_progress_table.dart';
|
|
import 'package:worldhopper/models/reading_progress.dart';
|
|
|
|
/// Repository for managing reading progress data
|
|
class ReadingProgressRepository {
|
|
final DatabaseHelper _dbHelper;
|
|
|
|
ReadingProgressRepository({DatabaseHelper? dbHelper})
|
|
: _dbHelper = dbHelper ?? DatabaseHelper.instance;
|
|
|
|
/// Get database instance
|
|
Future<Database> get _db async => await _dbHelper.database;
|
|
|
|
/// Get reading progress for a publication
|
|
Future<ReadingProgress?> getProgress(
|
|
String publicationId,
|
|
String serverId,
|
|
) async {
|
|
final db = await _db;
|
|
final List<Map<String, dynamic>> maps = await db.query(
|
|
ReadingProgressTable.tableName,
|
|
where: 'publication_id = ? AND server_id = ?',
|
|
whereArgs: [publicationId, serverId],
|
|
);
|
|
|
|
if (maps.isEmpty) return null;
|
|
return ReadingProgress.fromDatabase(maps.first);
|
|
}
|
|
|
|
/// Save or update reading progress
|
|
Future<void> saveProgress(ReadingProgress progress) async {
|
|
final db = await _db;
|
|
|
|
// First check if progress exists for this publication/server combo
|
|
final existing = await getProgress(progress.publicationId, progress.serverId);
|
|
|
|
if (existing != null) {
|
|
// Update existing entry keeping the same ID
|
|
final updated = progress.copyWith(id: existing.id);
|
|
await db.insert(
|
|
ReadingProgressTable.tableName,
|
|
updated.toDatabase(),
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
} else {
|
|
// Insert new entry
|
|
await db.insert(
|
|
ReadingProgressTable.tableName,
|
|
progress.toDatabase(),
|
|
conflictAlgorithm: ConflictAlgorithm.replace,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Update current page for a publication
|
|
Future<void> updateCurrentPage(
|
|
String publicationId,
|
|
String serverId,
|
|
int currentPage,
|
|
) async {
|
|
final db = await _db;
|
|
await db.update(
|
|
ReadingProgressTable.tableName,
|
|
{
|
|
'current_page': currentPage,
|
|
'last_read_at': DateTime.now().millisecondsSinceEpoch,
|
|
},
|
|
where: 'publication_id = ? AND server_id = ?',
|
|
whereArgs: [publicationId, serverId],
|
|
);
|
|
}
|
|
|
|
/// Get all reading progress for a server
|
|
Future<List<ReadingProgress>> getProgressByServer(String serverId) async {
|
|
final db = await _db;
|
|
final List<Map<String, dynamic>> maps = await db.query(
|
|
ReadingProgressTable.tableName,
|
|
where: 'server_id = ?',
|
|
whereArgs: [serverId],
|
|
orderBy: 'last_read_at DESC',
|
|
);
|
|
|
|
return List.generate(maps.length, (i) {
|
|
return ReadingProgress.fromDatabase(maps[i]);
|
|
});
|
|
}
|
|
|
|
/// Get recently read publications across all servers
|
|
Future<List<ReadingProgress>> getRecentlyRead({int limit = 20}) async {
|
|
final db = await _db;
|
|
final List<Map<String, dynamic>> maps = await db.query(
|
|
ReadingProgressTable.tableName,
|
|
orderBy: 'last_read_at DESC',
|
|
limit: limit,
|
|
);
|
|
|
|
return List.generate(maps.length, (i) {
|
|
return ReadingProgress.fromDatabase(maps[i]);
|
|
});
|
|
}
|
|
|
|
/// Delete progress for a publication
|
|
Future<void> deleteProgress(String publicationId, String serverId) async {
|
|
final db = await _db;
|
|
await db.delete(
|
|
ReadingProgressTable.tableName,
|
|
where: 'publication_id = ? AND server_id = ?',
|
|
whereArgs: [publicationId, serverId],
|
|
);
|
|
}
|
|
|
|
/// Delete all progress for a server
|
|
Future<void> deleteProgressByServer(String serverId) async {
|
|
final db = await _db;
|
|
await db.delete(
|
|
ReadingProgressTable.tableName,
|
|
where: 'server_id = ?',
|
|
whereArgs: [serverId],
|
|
);
|
|
}
|
|
|
|
/// Get total count of publications in progress
|
|
Future<int> getInProgressCount() async {
|
|
final db = await _db;
|
|
final result = await db.rawQuery(
|
|
'SELECT COUNT(*) as count FROM ${ReadingProgressTable.tableName} '
|
|
'WHERE current_page > 0 AND current_page < total_pages - 1',
|
|
);
|
|
return Sqflite.firstIntValue(result) ?? 0;
|
|
}
|
|
|
|
/// Get total count of completed publications
|
|
Future<int> getCompletedCount() async {
|
|
final db = await _db;
|
|
final result = await db.rawQuery(
|
|
'SELECT COUNT(*) as count FROM ${ReadingProgressTable.tableName} '
|
|
'WHERE current_page >= total_pages - 1',
|
|
);
|
|
return Sqflite.firstIntValue(result) ?? 0;
|
|
}
|
|
}
|