V1.0.46 oss (#253)
* add a UI trigger for demo annotations * Implement internal link navigation * Simplify bottom padding logic in `EpubReaderScreen` for vertical scroll mode * Add support for Calibre metadata * Implement advanced library management with Room-backed collections, tags, and smart rules. * Display book series information in book details and remove tags from home screen * Implement zoom reset functionality in PDF viewer * Add a "Pages" thumbnail view to the PDF navigation drawer * Implement jump-back navigation in PDF viewer * MainViewModel: disable FolderSyncWorker, add log export, and enhance PDF position logging * Replace Gson with Kotlin Serialization in SmartCollectionEngine * Implement ONNX-based speech bubble detection * Implement speech bubble detection in PDF viewer * Implement "Smart Comic Zoom" feature for PDF manga/comic reading * Implement reader session persistence and restoration in `MainViewModel` * Add tap-to-turn page feature to PDF viewer * Improve book cache management and recovery * Refactor top overlay padding logic in `PdfViewerScreen` * Implement stylus eraser support in PDF reader * Refactor ReaderTextFormatPanel to use ModalBottomSheet and add new formatting controls * Introduce a customizable horizontal margin setting for the EPUB reader * Refine folder sync and metadata handling for better conflict resolution and stability. * Introduce user-adjustable image scaling for the EPUB reader * Use maxWidthPx as default width fallback for blocks * Improve cross-page text selection and header styling in the paginated reader * Refactor speech bubble detection and UI to support segmentation masks and interactive scaling * Improve speech bubble detection masking and rendering quality * Update ONNX speech bubble detector to use `.ort` model and optimize inference * Improve pagination accuracy * Implement hierarchical folder navigation for the library. * Refactor library item layout for improved space efficiency * Add support for browsing and downloading Google Fonts * Persist library landing state and add shelf search functionality * Fix base tts sample. * Move SpeechBubbleDetector to main source set and update ONNX dependency * Implement a "locate" feature and improve synchronization for TTS (Text-to-Speech) playback across EPUB and PDF readers. * Refine TTS synchronization and voice management in the EPUB reader * Adjust OCR checks for OSS flavor and optimize external dictionary intent flags * Add "Locate" button to PDF drawer's pages tab and move the page number to bottom right of thumbnail * fix various crashes * Refactor SpeechBubbleDetector into product flavors * Implement speech bubble detection caching and background prefetching * Implement on-demand download for Bubble Zoom ML model * Add smooth animation for PDF speech bubble expansion * fixes #252 * hide Google Fonts option, in FontsScreen, for offline variant * Bump version to 1.0.46(46)
This commit is contained in:
parent
579b6f25bf
commit
d16bdd70d4
67 changed files with 9724 additions and 1890 deletions
|
|
@ -37,7 +37,10 @@ import kotlinx.coroutines.sync.withLock
|
|||
import kotlinx.coroutines.withContext
|
||||
import androidx.core.content.edit
|
||||
import com.aryan.reader.data.LocalSyncUtils
|
||||
import com.aryan.reader.data.FolderBookMetadata
|
||||
import java.io.File
|
||||
import android.provider.DocumentsContract
|
||||
import java.security.MessageDigest
|
||||
|
||||
class FolderSyncWorker(
|
||||
private val appContext: Context,
|
||||
|
|
@ -138,17 +141,17 @@ class FolderSyncWorker(
|
|||
LocalSyncUtils.migrateLegacySidecarsToSubfolder(appContext, documentTree)
|
||||
|
||||
Timber.tag("FolderSync").d("Phase 1: Importing JSON metadata from folder...")
|
||||
val folderMetadataMap = LocalSyncUtils.getAllFolderMetadata(appContext, folderUri)
|
||||
val folderMetadataMap = LocalSyncUtils.getAllFolderMetadata(appContext, folderUri).toMutableMap()
|
||||
|
||||
Timber.tag("FolderSync").d("Phase 1.5: Preloading annotation sidecars...")
|
||||
val preloadedSidecars = LocalSyncUtils.preloadAnnotationSidecars(appContext, documentTree)
|
||||
val preloadedSidecars = LocalSyncUtils.preloadAnnotationSidecars(appContext, documentTree).toMutableMap()
|
||||
|
||||
folderMetadataMap.forEach { (bookId, remoteMeta) ->
|
||||
val existingItem = recentFilesRepository.getFileByBookId(bookId)
|
||||
|
||||
if (existingItem != null) {
|
||||
if (remoteMeta.lastModifiedTimestamp > existingItem.lastModifiedTimestamp) {
|
||||
Timber.tag("FolderSync").d("Applying remote update for $bookId (Progress: ${remoteMeta.progressPercentage}%)")
|
||||
Timber.tag("PdfPositionDebug").w("FolderSyncWorker applies remote progress for $bookId | Local Page: ${existingItem.lastPage} -> Remote Page: ${remoteMeta.lastPage}")
|
||||
val itemToUpdate = existingItem.copy(
|
||||
lastChapterIndex = remoteMeta.lastChapterIndex,
|
||||
lastPage = remoteMeta.lastPage,
|
||||
|
|
@ -164,6 +167,8 @@ class FolderSyncWorker(
|
|||
timestamp = if (remoteMeta.isRecent) remoteMeta.lastModifiedTimestamp else existingItem.timestamp
|
||||
)
|
||||
recentFilesRepository.addRecentFile(itemToUpdate)
|
||||
} else {
|
||||
Timber.tag("PdfPositionDebug").d("FolderSyncWorker: Local meta is newer/equal for $bookId. Ignoring remote. Local Page: ${existingItem.lastPage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,39 +207,39 @@ class FolderSyncWorker(
|
|||
val contentResolver = appContext.contentResolver
|
||||
val foundBookIds = mutableSetOf<String>()
|
||||
val newOrUpdatedItems = mutableListOf<RecentFileItem>()
|
||||
val existingItemsMap = existingFolderBooks.associateBy { it.bookId }
|
||||
val existingItemsMap = existingFolderBooks.associateBy { it.bookId }.toMutableMap()
|
||||
|
||||
val rootDocId = android.provider.DocumentsContract.getTreeDocumentId(folderUri)
|
||||
val rootDocId = DocumentsContract.getTreeDocumentId(folderUri)
|
||||
val dirQueue = ArrayDeque<String>()
|
||||
dirQueue.add(rootDocId)
|
||||
|
||||
val projection = arrayOf(
|
||||
android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
android.provider.DocumentsContract.Document.COLUMN_SIZE,
|
||||
android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||
DocumentsContract.Document.COLUMN_MIME_TYPE,
|
||||
DocumentsContract.Document.COLUMN_SIZE,
|
||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED
|
||||
)
|
||||
|
||||
while (dirQueue.isNotEmpty()) {
|
||||
if (isStopped) break
|
||||
val currentDocId = dirQueue.removeFirst()
|
||||
val childrenUri = android.provider.DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, currentDocId)
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, currentDocId)
|
||||
|
||||
try {
|
||||
contentResolver.query(childrenUri, projection, null, null, null)?.use { cursor ->
|
||||
val idCol = cursor.getColumnIndexOrThrow(android.provider.DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val nameCol = cursor.getColumnIndexOrThrow(android.provider.DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeCol = cursor.getColumnIndexOrThrow(android.provider.DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeCol = cursor.getColumnIndexOrThrow(android.provider.DocumentsContract.Document.COLUMN_SIZE)
|
||||
val modCol = cursor.getColumnIndexOrThrow(android.provider.DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
val idCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DOCUMENT_ID)
|
||||
val nameCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val mimeCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_MIME_TYPE)
|
||||
val sizeCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_SIZE)
|
||||
val modCol = cursor.getColumnIndexOrThrow(DocumentsContract.Document.COLUMN_LAST_MODIFIED)
|
||||
|
||||
while (cursor.moveToNext() && !isStopped) {
|
||||
val docId = cursor.getString(idCol)
|
||||
val name = cursor.getString(nameCol) ?: ""
|
||||
val mimeType = cursor.getString(mimeCol)
|
||||
|
||||
if (mimeType == android.provider.DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
if (mimeType == DocumentsContract.Document.MIME_TYPE_DIR) {
|
||||
if (!name.startsWith(".") && name != "EpistemeSyncData") {
|
||||
dirQueue.add(docId)
|
||||
}
|
||||
|
|
@ -244,29 +249,49 @@ class FolderSyncWorker(
|
|||
|
||||
val type = getFileType(name, mimeType)
|
||||
if (type != null && type in allowedFileTypes && !name.endsWith(".json") && !name.startsWith(".")) {
|
||||
val stableId = "local_$name"
|
||||
val stableId = buildStableBookId(name, rootDocId, docId)
|
||||
foundBookIds.add(stableId)
|
||||
|
||||
val docUri = android.provider.DocumentsContract.buildDocumentUriUsingTree(folderUri, docId)
|
||||
val docUri = DocumentsContract.buildDocumentUriUsingTree(folderUri, docId)
|
||||
var existingItem = existingItemsMap[stableId]
|
||||
|
||||
if (existingItem != null && existingItem.uriString != docUri.toString()) {
|
||||
val collidedItem = existingItem
|
||||
val collidedStableId = computeStableIdForStoredItem(collidedItem, rootDocId)
|
||||
if (!collidedStableId.isNullOrBlank() && collidedStableId != stableId && collidedStableId != collidedItem.bookId) {
|
||||
Timber.tag("FolderSync").i("Resolving folder ID collision for ${collidedItem.displayName}: ${collidedItem.bookId} -> $collidedStableId")
|
||||
migrateFolderBookId(
|
||||
folderUriString = folderUriString,
|
||||
oldId = collidedItem.bookId,
|
||||
newId = collidedStableId,
|
||||
folderMetadataMap = folderMetadataMap,
|
||||
preloadedSidecars = preloadedSidecars,
|
||||
existingItemsMap = existingItemsMap
|
||||
)
|
||||
existingItem = existingItemsMap[stableId]
|
||||
}
|
||||
}
|
||||
|
||||
if (existingItem == null) {
|
||||
val oldItem = existingItemsMap.values.find { it.bookId.startsWith("local_${name}_") && it.bookId != stableId }
|
||||
val oldItem = existingItemsMap.values.find {
|
||||
it.bookId != stableId && (
|
||||
it.uriString == docUri.toString() ||
|
||||
it.bookId.startsWith("local_${name}_")
|
||||
)
|
||||
}
|
||||
if (oldItem != null) {
|
||||
val oldId = oldItem.bookId
|
||||
Timber.tag("FolderSync").i("Migrating book ID for $name from $oldId to $stableId")
|
||||
|
||||
recentFilesRepository.migrateBookIdLocally(oldId, stableId)
|
||||
existingItem = recentFilesRepository.getFileByBookId(stableId)
|
||||
|
||||
try {
|
||||
val syncDir = documentTree.findFile("EpistemeSyncData")
|
||||
if (syncDir != null) {
|
||||
syncDir.findFile(".$oldId.json")?.delete()
|
||||
syncDir.findFile("$oldId.json")?.delete()
|
||||
syncDir.findFile(".$oldId" + "_annotations.json")?.delete()
|
||||
}
|
||||
} catch (_: Exception) { Timber.tag("FolderSync").e("Failed to clean up orphaned SAF sidecars.") }
|
||||
migrateFolderBookId(
|
||||
folderUriString = folderUriString,
|
||||
oldId = oldId,
|
||||
newId = stableId,
|
||||
folderMetadataMap = folderMetadataMap,
|
||||
preloadedSidecars = preloadedSidecars,
|
||||
existingItemsMap = existingItemsMap
|
||||
)
|
||||
existingItem = existingItemsMap[stableId]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -410,4 +435,78 @@ class FolderSyncWorker(
|
|||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildStableBookId(name: String, rootDocId: String, docId: String): String {
|
||||
val relativePath = buildRelativePath(rootDocId, docId, name)
|
||||
if (relativePath.equals(name, ignoreCase = true)) {
|
||||
return "local_$name"
|
||||
}
|
||||
return "local_${name}_${shortHash(relativePath.lowercase())}"
|
||||
}
|
||||
|
||||
private fun buildRelativePath(rootDocId: String, docId: String, fallbackName: String): String {
|
||||
val rootPath = rootDocId.substringAfter(':', "")
|
||||
val docPath = docId.substringAfter(':', "")
|
||||
if (docPath.isBlank()) return fallbackName
|
||||
val relative = if (rootPath.isNotBlank() && docPath.startsWith(rootPath)) {
|
||||
docPath.removePrefix(rootPath).trimStart('/')
|
||||
} else {
|
||||
docPath.substringAfterLast('/', fallbackName)
|
||||
}
|
||||
return relative.ifBlank { fallbackName }
|
||||
}
|
||||
|
||||
private fun shortHash(value: String): String {
|
||||
val bytes = MessageDigest.getInstance("SHA-256").digest(value.toByteArray())
|
||||
return bytes.joinToString("") { "%02x".format(it) }.take(12)
|
||||
}
|
||||
|
||||
private fun computeStableIdForStoredItem(item: RecentFileItem, rootDocId: String): String? {
|
||||
val uriString = item.uriString ?: return null
|
||||
return try {
|
||||
val docId = DocumentsContract.getDocumentId(uriString.toUri())
|
||||
buildStableBookId(item.displayName, rootDocId, docId)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun migrateFolderBookId(
|
||||
folderUriString: String,
|
||||
oldId: String,
|
||||
newId: String,
|
||||
folderMetadataMap: MutableMap<String, FolderBookMetadata>,
|
||||
preloadedSidecars: MutableMap<String, Pair<Long, String>>,
|
||||
existingItemsMap: MutableMap<String, RecentFileItem>
|
||||
) {
|
||||
if (oldId == newId) return
|
||||
|
||||
recentFilesRepository.migrateBookIdLocally(oldId, newId)
|
||||
|
||||
val oldMetadata = folderMetadataMap.remove(oldId)
|
||||
if (oldMetadata != null && newId !in folderMetadataMap) {
|
||||
val migratedMetadata = oldMetadata.copy(bookId = newId)
|
||||
LocalSyncUtils.saveMetadataToFolder(appContext, folderUriString.toUri(), migratedMetadata)
|
||||
folderMetadataMap[newId] = migratedMetadata
|
||||
}
|
||||
|
||||
val oldSidecar = preloadedSidecars.remove(oldId)
|
||||
if (oldSidecar != null && newId !in preloadedSidecars) {
|
||||
LocalSyncUtils.saveAnnotationSidecar(
|
||||
context = appContext,
|
||||
sourceFolderUri = folderUriString.toUri(),
|
||||
bookId = newId,
|
||||
jsonPayload = oldSidecar.second,
|
||||
timestamp = oldSidecar.first
|
||||
)
|
||||
preloadedSidecars[newId] = oldSidecar
|
||||
}
|
||||
|
||||
LocalSyncUtils.deleteBookSidecars(appContext, folderUriString.toUri(), oldId)
|
||||
|
||||
existingItemsMap.remove(oldId)
|
||||
recentFilesRepository.getFileByBookId(newId)?.let {
|
||||
existingItemsMap[newId] = it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue