Folder import\sync rework (#18)
* feat: rework Folder Sync architecture to separate Managed vs Linked books - Introduced "Managed" (imported to app storage) vs "Linked" (SAF URI) book logic. - Disabled Google Drive/Metadata synchronization for Folder-Linked books to respect privacy and storage. - Updated deletion logic: Folder books are now marked as deleted in the DB to blacklist them from future auto-scans, while Managed books are permanently purged from local and cloud storage. - Enhanced folder sync discoverability by adding a direct "Sync Folder" navigation button in Home screen. - Implemented reactive pager navigation in MainScreen and LibraryScreen via LaunchedEffect to allow programmatic tab switching from the ViewModel. * Implemented a local folder synchronization system to track and update book metadata across devices. Key changes: - Added `FolderBookMetadata` and `LocalSyncUtils` for serializing and managing metadata files in a `.episteme` directory. - Implemented bidirectional sync in `FolderSyncWorker` to reconcile local database state with folder-based metadata using timestamps and Syncthing conflict resolution. - Updated `MainViewModel` to trigger folder metadata sync when closing a book and verify for remote updates upon opening. - Enhanced folder management with manual scan support via `WorkManager` and automatic cleanup of database entries when a folder is disconnected. - Added `RecentFileDao` methods to support bulk deletion and prefix-based file lookups. * Removed API 34 restriction for chapter processing and updated local folder sync directory name * Increased TTS timeouts, added edge-to-edge support in MainScreen, and improved TtsChunk mapping safety * Improved folder synchronization and metadata management. - Added metadata-only sync option and triggered it on app start. - Implemented hidden metadata filenames (prefixed with `.`) to reduce file clutter. - Added automatic creation of `.nomedia` files in the sync directory. - Enhanced sync conflict resolution by picking the latest metadata and cleaning up obsolete or orphaned files. - Added "Sync Metadata" button to the Library screen. - Updated UI labels for local folder sync to clarify Google Drive integration. - Refactored `FolderSyncWorker` to support targeted metadata-only synchronization. * Implement folder sync migration and refactoring - Added a migration dialog to inform users about the folder sync refactor, where books are now read directly from external storage. - Updated `MainViewModel` to handle migration state and trigger a full scan upon completion or dismissal of the dialog. - Modified `FolderSyncWorker` to support legacy book matching during migration, updating file URIs and cleaning up internal app storage for migrated books. - Improved synchronization logic between local and remote metadata, including cleanup of orphaned metadata files. - Refined `RecentFileItem` updates during sync to preserve progress and bookmarks based on last modified timestamps. * Implemented pull-to-refresh for library sync and enhanced folder file deletion. - Added `isRefreshing` state to `MainViewModel` and integrated `PullToRefreshBox` in `HomeScreen`. - Implemented `refreshLibrary` to trigger cloud and folder metadata synchronization. - Updated deletion logic to physically remove files and metadata from synced local folders. - Added a warning to the delete confirmation dialog when removing folder-synced items. - Improved folder sync reliability by performing lazy cleanup of missing files during interaction and sync. - Fixed a bug where sync loading indicators would not retract on failure or cancellation. * Improved bookmark navigation and scroll synchronization in EPUB reader - Refined bookmark navigation logic for vertical scroll mode to handle chunk injection more reliably. - Added `isNavigatingToBookmark` state to show a loading overlay during long jumps. - Implemented `onScrollFinished` callback in `CfiJsBridge` to synchronize UI state with WebView scroll completion. - Updated `ChapterWebView` to use `rememberUpdatedState` for JavaScript bridge callbacks to ensure data consistency. - Improved `getCurrentCfi` and `scrollToCfi` in `epub_reader.js` with better visibility probing and retry logic for detached nodes. * Implemented background metadata extraction for folder sync. Key changes: - Added `MetadataExtractionWorker` to handle heavy metadata and cover extraction for files in the background. - Refactored `FolderSyncWorker` to perform fast file discovery using placeholders, enqueuing the metadata worker upon completion. - Added `getFolderBooksWithoutCovers` query to `RecentFileDao`. - Updated UI components (`EmptyState`, `MainViewModel`) to improve user feedback during sync and provide clear setup actions. - Enhanced `EmptyState` composable to support secondary actions and custom button text. * Updated Library Screen UI
This commit is contained in:
parent
60f6566b31
commit
a81df6921d
21 changed files with 3378 additions and 2478 deletions
115
app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
Normal file
115
app/src/main/java/com/aryan/reader/MetadataExtractionWorker.kt
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// MetadataExtractionWorker.kt
|
||||
package com.aryan.reader
|
||||
|
||||
import android.content.Context
|
||||
import androidx.core.net.toUri
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.WorkerParameters
|
||||
import com.aryan.reader.data.RecentFilesRepository
|
||||
import com.aryan.reader.epub.EpubParser
|
||||
import com.aryan.reader.epub.MobiParser
|
||||
import com.aryan.reader.pdf.PdfCoverGenerator
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import timber.log.Timber
|
||||
|
||||
class MetadataExtractionWorker(
|
||||
private val appContext: Context,
|
||||
workerParams: WorkerParameters
|
||||
) : CoroutineWorker(appContext, workerParams) {
|
||||
|
||||
private val recentFilesRepository = RecentFilesRepository(appContext)
|
||||
private val epubParser = EpubParser(appContext)
|
||||
private val mobiParser = MobiParser(appContext)
|
||||
private val pdfCoverGenerator = PdfCoverGenerator(appContext)
|
||||
|
||||
companion object {
|
||||
const val WORK_NAME = "MetadataExtractionWorker"
|
||||
}
|
||||
|
||||
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// Fetch all books that are from a folder but don't have a cover yet (implies metadata likely missing/basic)
|
||||
val filesToProcess = recentFilesRepository.getFolderBooksWithoutCovers()
|
||||
|
||||
if (filesToProcess.isEmpty()) {
|
||||
return@withContext Result.success()
|
||||
}
|
||||
|
||||
Timber.tag("MetadataWorker").i("Starting background metadata extraction for ${filesToProcess.size} books.")
|
||||
|
||||
filesToProcess.forEach { item ->
|
||||
if (isStopped) return@forEach
|
||||
|
||||
try {
|
||||
val uri = item.uriString?.toUri() ?: return@forEach
|
||||
val type = item.type
|
||||
|
||||
var coverPath: String? = null
|
||||
var title: String? = null
|
||||
var author: String? = null
|
||||
|
||||
// We open the stream briefly to extract metadata
|
||||
appContext.contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
when (type) {
|
||||
FileType.EPUB -> {
|
||||
val book = epubParser.createEpubBook(
|
||||
inputStream = inputStream,
|
||||
originalBookNameHint = item.displayName,
|
||||
parseContent = false
|
||||
)
|
||||
book.let {
|
||||
title = it.title.takeIf { t -> t.isNotBlank() }
|
||||
author = it.author.takeIf { a -> a.isNotBlank() }
|
||||
it.coverImage?.let { img ->
|
||||
coverPath = recentFilesRepository.saveCoverToCache(img, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
FileType.MOBI -> {
|
||||
val book = mobiParser.createMobiBook(
|
||||
inputStream = inputStream,
|
||||
originalBookNameHint = item.displayName
|
||||
)
|
||||
book?.let {
|
||||
title = it.title.takeIf { t -> t.isNotBlank() }
|
||||
author = it.author.takeIf { a -> a.isNotBlank() }
|
||||
it.coverImage?.let { img ->
|
||||
coverPath = recentFilesRepository.saveCoverToCache(img, uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
FileType.PDF -> {
|
||||
// PDF cover generation is heavy, but necessary
|
||||
pdfCoverGenerator.generateCover(uri)?.let {
|
||||
coverPath = recentFilesRepository.saveCoverToCache(it, uri)
|
||||
}
|
||||
title = item.displayName.substringBeforeLast(".") // Clean filename
|
||||
}
|
||||
else -> { /* Text/MD files usually don't have covers */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Only update if we actually found something useful
|
||||
if (coverPath != null || title != null || author != null) {
|
||||
val updatedItem = item.copy(
|
||||
coverImagePath = coverPath ?: item.coverImagePath,
|
||||
title = title ?: item.title ?: item.displayName,
|
||||
author = author ?: item.author
|
||||
)
|
||||
recentFilesRepository.addRecentFile(updatedItem)
|
||||
Timber.tag("MetadataWorker").d("Updated metadata for: ${item.displayName}")
|
||||
}
|
||||
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("MetadataWorker").e(e, "Failed to extract metadata for ${item.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext Result.success()
|
||||
} catch (e: Exception) {
|
||||
Timber.tag("MetadataWorker").e(e, "Metadata extraction failed")
|
||||
return@withContext Result.failure()
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue