v1.0.45-oss (#221)

* Implemented ML-based comic panel detection and a panel popup viewer.

* Optimized `ComicPanelDetector` for performance and memory efficiency.

* Optimized comic panel detection performance by introducing a dedicated single-thread dispatcher for ML tasks, replacing mutex-based synchronization.

* Refactored system UI handling and layout padding in `PdfViewerScreen`.

* Refactor `PdfViewerScreen.kt` by extracting components and logic into specialized files.

* replace hardcoded padding with dynamic header height and adjust IME layout logic

* Improve `PdfTextBox` interaction and visual consistency during zoom and pan.

* improve PDF text box dragging and scaling behavior across zoom levels

* optimize color scheme calculation using remember and expand text dimming coverage

* Implement in-app language selection and per-app language preferences.

* Improve PDF lock stability in `PdfVerticalReader`

* Implement "Preserve Image Colors" option for PDF themes

* Optimize TOC locate in ReaderDrawer

* Update library and home screen UI components

* smoother page turn animation in epub pagination mode

* Implement automatic page skipping for TTS when no text is found in PDF viewer

* Refine status bar handling and window insets across main screens

* Optimize ViewModel initialization and integrate AndroidX SplashScreen

* Move ComicPanelDetector to debug source set and introduce IPanelDetector interface

* Optimize TOC scrolling in PdfNavigationDrawerContent

* Bump version to 1.0.45(45)
This commit is contained in:
Aryan 2026-04-22 14:11:52 +05:30 committed by GitHub
parent 71e3614ad6
commit cb7de86224
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 4795 additions and 3447 deletions

View file

@ -28,6 +28,7 @@ import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.database.Cursor
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.provider.DocumentsContract
@ -115,7 +116,9 @@ import java.util.concurrent.CancellationException
import java.util.concurrent.TimeUnit
import androidx.core.graphics.createBitmap
import io.legere.pdfiumandroid.PdfiumCore
import kotlinx.coroutines.asCoroutineDispatcher
import kotlinx.coroutines.flow.distinctUntilChanged
import java.util.concurrent.Executors.newSingleThreadExecutor
private const val KEY_RENDER_MODE = "render_mode"
private const val KEY_FOLDER_SYNC_ENABLED = "folder_sync_enabled"
@ -276,15 +279,21 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private val appContext: Context = application.applicationContext
private val authRepository = AuthRepository(appContext)
private val recentFilesRepository = RecentFilesRepository(appContext)
private val pdfTextRepository = PdfTextRepository(appContext)
private val bookCacheDao = BookCacheDatabase.getDatabase(application).bookCacheDao()
private val epubParser = EpubParser(appContext)
private val mobiParser = MobiParser(appContext)
private val fb2Parser = com.aryan.reader.epub.Fb2Parser(appContext)
private val odtParser = com.aryan.reader.epub.OdtParser(appContext)
private val singleFileImporter = SingleFileImporter(appContext)
private val bookImporter = BookImporter(appContext)
private val pdfTextRepository by lazy { PdfTextRepository(appContext) }
private val bookCacheDao by lazy { BookCacheDatabase.getDatabase(application).bookCacheDao() }
private val epubParser by lazy { EpubParser(appContext) }
private val mobiParser by lazy { MobiParser(appContext) }
private val fb2Parser by lazy { com.aryan.reader.epub.Fb2Parser(appContext) }
private val odtParser by lazy { com.aryan.reader.epub.OdtParser(appContext) }
private val singleFileImporter by lazy { SingleFileImporter(appContext) }
private val bookImporter by lazy { BookImporter(appContext) }
private val pageLayoutRepository by lazy { PageLayoutRepository(appContext) }
private val pdfRichTextRepository by lazy { com.aryan.reader.pdf.PdfRichTextRepository(appContext) }
private val pdfTextBoxRepository by lazy { PdfTextBoxRepository(appContext) }
private val pdfHighlightRepository by lazy { PdfHighlightRepository(appContext) }
private val pdfAnnotationRepository by lazy { PdfAnnotationRepository(appContext) }
private val prefs: SharedPreferences =
application.getSharedPreferences("reader_user_prefs", Context.MODE_PRIVATE)
private val firestoreRepository = FirestoreRepository()
@ -301,16 +310,33 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
private val feedbackRepository = FeedbackRepository(appContext)
private var feedbackListener: Any? = null
private val importMutex = Mutex()
private val pageLayoutRepository = PageLayoutRepository(appContext)
private val pdfRichTextRepository = com.aryan.reader.pdf.PdfRichTextRepository(appContext)
private val pdfTextBoxRepository = PdfTextBoxRepository(appContext)
private val pdfHighlightRepository = PdfHighlightRepository(appContext)
private val _navigationEvent = Channel<NavigationEvent>(Channel.BUFFERED)
@Suppress("unused")
val navigationEvent = _navigationEvent.receiveAsFlow()
private var pendingSwitchDeferred: CompletableDeferred<Boolean>? = null
private var externalOpenedBookId: String? = null
private var panelDetector: com.aryan.reader.ml.IPanelDetector? = null
private val mlDispatcher = newSingleThreadExecutor().asCoroutineDispatcher()
private fun getOrInitDetector(context: Context): com.aryan.reader.ml.IPanelDetector? {
if (panelDetector == null && BuildConfig.DEBUG) {
val modelFile = File(context.getExternalFilesDir(null), "best_float16.tflite")
if (modelFile.exists()) {
try {
val clazz = Class.forName("com.aryan.reader.ml.ComicPanelDetector")
panelDetector = clazz.getConstructor(File::class.java).newInstance(modelFile) as com.aryan.reader.ml.IPanelDetector
} catch (e: Exception) {
Timber.e(e, "Failed to instantiate ComicPanelDetector via reflection")
}
} else {
Timber.e("Model file best_float16.tflite not found in external files dir")
}
}
return panelDetector
}
data class PageModificationResult(
val layout: List<VirtualPage>,
val annotations: Map<Int, List<PdfAnnotation>>,
@ -502,7 +528,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = ReaderScreenState()
initialValue = _internalState.value
)
fun setTabsEnabled(enabled: Boolean) {
@ -796,7 +822,9 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
init {
Timber.d("ViewModel instance created.")
PDFBoxResourceLoader.init(getApplication())
viewModelScope.launch(Dispatchers.IO) {
PDFBoxResourceLoader.init(getApplication())
}
val currentOpenCount = prefs.getInt(KEY_APP_OPEN_COUNT, 0)
prefs.edit { putInt(KEY_APP_OPEN_COUNT, currentOpenCount + 1) }
@ -1181,8 +1209,6 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
}
}
private val pdfAnnotationRepository = PdfAnnotationRepository(appContext)
private fun getFastFileId(context: Context, uri: Uri): String {
var result = uri.toString()
try {
@ -4410,6 +4436,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
super.onCleared()
prefs.unregisterOnSharedPreferenceChangeListener(prefsListener)
firestoreRepository.removeListener(feedbackListener)
panelDetector?.close()
panelDetector = null
Timber.d("ViewModel instance cleared (onCleared).")
}
@ -4627,6 +4655,104 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
return authRepository.getIdToken()
}
fun testPanelDetection(context: Context) {
viewModelScope.launch(mlDispatcher) {
try {
val modelFile = File(context.getExternalFilesDir(null), "best_float16.tflite")
if (!modelFile.exists()) {
withContext(Dispatchers.Main) { showBanner("Model not found", isError = true) }
return@launch
}
val cbzItem = uiState.value.contextualActionItems.firstOrNull { it.type == FileType.CBZ }
?: uiState.value.allRecentFiles.firstOrNull { it.type == FileType.CBZ }
if (cbzItem == null) {
withContext(Dispatchers.Main) { showBanner("No CBZ found in Library.", isError = true) }
return@launch
}
val uri = cbzItem.getUri() ?: return@launch
Timber.d("BATCH TEST START: ${cbzItem.displayName}")
var cacheFile: File? = null
try {
val detector = getOrInitDetector(context) ?: run {
withContext(Dispatchers.Main) { showBanner("Model could not be loaded", isError = true) }
return@launch
}
cacheFile = File(context.cacheDir, "temp_test_batch.cbz")
context.contentResolver.openInputStream(uri)?.use { input ->
cacheFile.outputStream().use { output -> input.copyTo(output) }
}
// 1. Initialize Model Once
val initStartTime = System.currentTimeMillis()
val initDuration = System.currentTimeMillis() - initStartTime
Timber.d(">>> [BATCH] Model Initialization: ${initDuration}ms")
val archiveDoc = com.aryan.reader.pdf.ArchiveDocumentWrapper(cacheFile)
val totalPages = archiveDoc.getPageCount()
val startIndex = 3
val numPagesToTest = 10
val endIndex = minOf(startIndex + numPagesToTest - 1, totalPages - 1)
val resultsLog = StringBuilder()
resultsLog.append("Batch Results:\n")
// 2. Loop through pages
for (i in startIndex..endIndex) {
val page = archiveDoc.openPage(i)
if (page != null) {
val w = page.getPageWidthPoint()
val h = page.getPageHeightPoint()
if (w > 0 && h > 0) {
val bitmap = androidx.core.graphics.createBitmap(w, h)
page.renderPageBitmap(bitmap, 0, 0, w, h, false)
// Measure precise inference time
val pageStartTime = System.currentTimeMillis()
val panels = detector.detectPanels(bitmap)
val pageDuration = System.currentTimeMillis() - pageStartTime
val logLine = "Page $i: ${pageDuration}ms (Found ${panels.size} panels)"
Timber.d(">>>[BATCH] $logLine")
resultsLog.append("$logLine\n")
bitmap.recycle()
delay(20)
}
page.close()
}
}
archiveDoc.close()
withContext(Dispatchers.Main) {
Timber.i(resultsLog.toString())
showBanner("Batch test complete! Check logs for page-by-page timings.")
}
} finally {
cacheFile?.delete()
}
} catch (e: Exception) {
Timber.e(e, "Batch test failed")
}
}
}
suspend fun detectComicPanels(bitmap: Bitmap, context: Context): List<android.graphics.RectF> {
return withContext(mlDispatcher) {
try {
val detector = getOrInitDetector(context)
detector?.detectPanels(bitmap) ?: emptyList()
} catch (e: Exception) {
Timber.e(e, "Error during panel detection")
emptyList()
}
}
}
companion object {
private const val KEY_SORT_ORDER = "sort_order"
internal const val KEY_SHELVES = "shelf_names"