General improvments (#114)

* Add "Keep Screen On" feature to PDF and Epub readers.

* Add `rawLibraryFiles` to `MainViewModel` and improve file path resolution in `SharedComposables`.

Specifically:
- Updated `MainViewModel` state and UI logic to track and provide unfiltered library files.
- Enhanced file path formatting in `SharedComposables` to better handle Document, Tree, and primary storage URIs.
- Added scrollable support for long text values in `DetailItem` composable.
- Passed `rawLibraryFiles` to `FolderSyncScreen` to ensure sync logic uses the base file list.

* Update info bar background and text colors in EpubReaderScreen to match the selected theme.

* Prevent accidental UI bar toggles immediately after scrolling in EPUB vertical mode.

* feat: implement EPUB highlights sync and legacy migration

- Implement Cloud and Local Folder sync for EPUB highlights.
- Add highlight JSON serialization/deserialization helpers in EpubReaderAnnotations.
- Implement automatic migration of highlights from SharedPreferences to the Database on book open.
- Add cleanup logic to remove legacy SharedPreferences data after successful DB migration.
- Update RecentFilesRepository to trigger local folder metadata sync when highlights are modified.
- Update FolderSyncWorker to pull highlights and custom names from linked folder metadata.

* Update intent filters
This commit is contained in:
Aryan 2026-03-26 13:22:05 +05:30 committed by GitHub
parent 60dacf9c12
commit 4c5cd20b21
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 404 additions and 84 deletions

View file

@ -54,7 +54,7 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<!-- PDF Filter --> <!-- PDF -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -64,7 +64,7 @@
<data android:mimeType="application/pdf" /> <data android:mimeType="application/pdf" />
</intent-filter> </intent-filter>
<!-- EPUB Filter --> <!-- EPUB -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -74,7 +74,7 @@
<data android:mimeType="application/epub+zip" /> <data android:mimeType="application/epub+zip" />
</intent-filter> </intent-filter>
<!-- MOBI / Kindle Filter --> <!-- MOBI/AZW3 known MIME types -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -84,13 +84,9 @@
<data android:mimeType="application/x-mobipocket-ebook" /> <data android:mimeType="application/x-mobipocket-ebook" />
<data android:mimeType="application/vnd.amazon.mobi8-ebook" /> <data android:mimeType="application/vnd.amazon.mobi8-ebook" />
<data android:mimeType="application/vnd.amazon.ebook" /> <data android:mimeType="application/vnd.amazon.ebook" />
<data android:mimeType="application/octet-stream" />
<data android:host="*" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\.azw3" />
</intent-filter> </intent-filter>
<!-- FB2 Filter 1: Catch by specific and common MIME types --> <!-- FB2 known MIME types -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -99,14 +95,10 @@
<data android:scheme="file" /> <data android:scheme="file" />
<data android:mimeType="application/x-fictionbook+xml" /> <data android:mimeType="application/x-fictionbook+xml" />
<data android:mimeType="application/x-fictionbook" /> <data android:mimeType="application/x-fictionbook" />
<data android:mimeType="application/fb2+xml" />
<data android:mimeType="application/x-fb2" /> <data android:mimeType="application/x-fb2" />
<data android:mimeType="application/fb2" />
<data android:mimeType="application/x-zip-compressed-fb2" />
<data android:mimeType="application/zip" />
</intent-filter> </intent-filter>
<!-- FB2 Filter 2: Catch by Extension when MIME is unknown (octet-stream) --> <!-- FALLBACK: catches azw3/fb2/mobi when MIME type is unknown -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -114,14 +106,28 @@
<data android:scheme="content" /> <data android:scheme="content" />
<data android:scheme="file" /> <data android:scheme="file" />
<data android:mimeType="application/octet-stream" /> <data android:mimeType="application/octet-stream" />
<data android:mimeType="text/xml" />
<data android:mimeType="text/plain" />
<data android:host="*" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\.fb2\\.zip" />
</intent-filter> </intent-filter>
<!-- Markdown and Plain Text Filter --> <!-- pathPattern fallback for file:// URIs -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.azw3" />
<data android:pathPattern=".*\\..*\\.azw3" />
<data android:pathPattern=".*\\..*\\..*\\.azw3" />
<data android:pathPattern=".*\\.fb2" />
<data android:pathPattern=".*\\..*\\.fb2" />
<data android:pathPattern=".*\\..*\\..*\\.fb2" />
<data android:pathPattern=".*\\.mobi" />
<data android:pathPattern=".*\\..*\\.mobi" />
<data android:pathPattern=".*\\..*\\..*\\.mobi" />
</intent-filter>
<!-- Markdown/Text -->
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
@ -131,7 +137,72 @@
<data android:mimeType="text/plain" /> <data android:mimeType="text/plain" />
<data android:mimeType="text/markdown" /> <data android:mimeType="text/markdown" />
<data android:mimeType="text/x-markdown" /> <data android:mimeType="text/x-markdown" />
<data android:mimeType="text/*" /> </intent-filter>
<!-- CBZ known MIME types -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="application/x-cbz" />
<data android:mimeType="application/vnd.comicbook+zip" />
</intent-filter>
<!-- CBR known MIME types -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="application/x-cbr" />
<data android:mimeType="application/vnd.comicbook-rar" />
</intent-filter>
<!-- CB7 known MIME types -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="content" />
<data android:scheme="file" />
<data android:mimeType="application/x-cb7" />
</intent-filter>
<!-- pathPattern fallback for file:// URIs (comic books) -->
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.cbz" />
<data android:pathPattern=".*\\..*\\.cbz" />
<data android:pathPattern=".*\\..*\\..*\\.cbz" />
<data android:pathPattern=".*\\.cbr" />
<data android:pathPattern=".*\\..*\\.cbr" />
<data android:pathPattern=".*\\..*\\..*\\.cbr" />
<data android:pathPattern=".*\\.cb7" />
<data android:pathPattern=".*\\..*\\.cb7" />
<data android:pathPattern=".*\\..*\\..*\\.cb7" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="file" />
<data android:host="*" />
<data android:mimeType="*/*" />
<data android:pathPattern=".*\\.md" />
<data android:pathPattern=".*\\..*\\.md" />
<data android:pathPattern=".*\\..*\\..*\\.md" />
<data android:pathPattern=".*\\.markdown" />
<data android:pathPattern=".*\\..*\\.markdown" />
<data android:pathPattern=".*\\..*\\..*\\.markdown" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>

View file

@ -152,6 +152,8 @@ class FolderSyncWorker(
lastPositionCfi = remoteMeta.lastPositionCfi, lastPositionCfi = remoteMeta.lastPositionCfi,
progressPercentage = remoteMeta.progressPercentage, progressPercentage = remoteMeta.progressPercentage,
bookmarksJson = remoteMeta.bookmarksJson, bookmarksJson = remoteMeta.bookmarksJson,
highlightsJson = remoteMeta.highlightsJson,
customName = remoteMeta.customName,
locatorBlockIndex = remoteMeta.locatorBlockIndex, locatorBlockIndex = remoteMeta.locatorBlockIndex,
locatorCharOffset = remoteMeta.locatorCharOffset, locatorCharOffset = remoteMeta.locatorCharOffset,
lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp, lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp,
@ -247,6 +249,8 @@ class FolderSyncWorker(
lastPositionCfi = remoteMeta?.lastPositionCfi, lastPositionCfi = remoteMeta?.lastPositionCfi,
progressPercentage = remoteMeta?.progressPercentage, progressPercentage = remoteMeta?.progressPercentage,
bookmarksJson = remoteMeta?.bookmarksJson, bookmarksJson = remoteMeta?.bookmarksJson,
highlightsJson = remoteMeta?.highlightsJson,
customName = remoteMeta?.customName,
locatorBlockIndex = remoteMeta?.locatorBlockIndex, locatorBlockIndex = remoteMeta?.locatorBlockIndex,
locatorCharOffset = remoteMeta?.locatorCharOffset locatorCharOffset = remoteMeta?.locatorCharOffset
) )

View file

@ -134,6 +134,7 @@ fun LibraryScreen(
val isShelfContextualModeActive = selectedShelves.isNotEmpty() val isShelfContextualModeActive = selectedShelves.isNotEmpty()
val sortOrder = uiState.sortOrder val sortOrder = uiState.sortOrder
val shelves = uiState.shelves val shelves = uiState.shelves
val rawLibraryFiles = uiState.rawLibraryFiles
val pagerState = rememberPagerState( val pagerState = rememberPagerState(
initialPage = uiState.libraryScreenStartPage, initialPage = uiState.libraryScreenStartPage,
pageCount = { 3 } pageCount = { 3 }
@ -234,6 +235,7 @@ fun LibraryScreen(
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
LibraryScreenContent( LibraryScreenContent(
recentFiles = uiState.allRecentFiles, recentFiles = uiState.allRecentFiles,
rawLibraryFiles = rawLibraryFiles,
shelves = shelves, shelves = shelves,
selectedItems = selectedItems, selectedItems = selectedItems,
selectedShelves = selectedShelves, selectedShelves = selectedShelves,
@ -460,6 +462,7 @@ fun ShelfScreen(
@Composable @Composable
fun LibraryScreenContent( fun LibraryScreenContent(
recentFiles: List<RecentFileItem>, recentFiles: List<RecentFileItem>,
rawLibraryFiles: List<RecentFileItem>,
shelves: List<Shelf>, shelves: List<Shelf>,
selectedItems: Set<RecentFileItem>, selectedItems: Set<RecentFileItem>,
selectedShelves: Set<String>, selectedShelves: Set<String>,
@ -759,7 +762,7 @@ fun LibraryScreenContent(
2 -> { 2 -> {
FolderSyncScreen( FolderSyncScreen(
syncedFolders = syncedFolders, syncedFolders = syncedFolders,
allRecentFiles = recentFiles, allRecentFiles = rawLibraryFiles,
onAddFolderClick = onAddFolderClick, onAddFolderClick = onAddFolderClick,
onRemoveFolderClick = onRemoveFolderClick, onRemoveFolderClick = onRemoveFolderClick,
onEditFolderFiltersClick = onEditFolderFiltersClick, onEditFolderFiltersClick = onEditFolderFiltersClick,

View file

@ -192,6 +192,7 @@ data class ReaderScreenState(
val initialLocator: Locator? = null, val initialLocator: Locator? = null,
val initialCfi: String? = null, val initialCfi: String? = null,
val initialBookmarksJson: String? = null, val initialBookmarksJson: String? = null,
val initialHighlightsJson: String? = null,
val initialPageInBook: Int? = null, val initialPageInBook: Int? = null,
val shelves: List<Shelf> = emptyList(), val shelves: List<Shelf> = emptyList(),
val viewingShelfName: String? = null, val viewingShelfName: String? = null,
@ -225,6 +226,7 @@ data class ReaderScreenState(
val reflowProgress: Float? = null, val reflowProgress: Float? = null,
val recentFiles: List<RecentFileItem> = emptyList(), val recentFiles: List<RecentFileItem> = emptyList(),
val allRecentFiles: List<RecentFileItem> = emptyList(), val allRecentFiles: List<RecentFileItem> = emptyList(),
val rawLibraryFiles: List<RecentFileItem> = emptyList(),
val pinnedHomeBookIds: Set<String> = emptySet(), val pinnedHomeBookIds: Set<String> = emptySet(),
val pinnedLibraryBookIds: Set<String> = emptySet(), val pinnedLibraryBookIds: Set<String> = emptySet(),
val libraryFilters: LibraryFilters = LibraryFilters(), val libraryFilters: LibraryFilters = LibraryFilters(),
@ -421,6 +423,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
internalState.copy( internalState.copy(
recentFiles = visibleRecentFiles, recentFiles = visibleRecentFiles,
allRecentFiles = sortedLibraryFiles, allRecentFiles = sortedLibraryFiles,
rawLibraryFiles = baseVisibleFiles,
contextualActionItems = validContextualItems, contextualActionItems = validContextualItems,
shelves = allShelves, shelves = allShelves,
booksAvailableForAdding = booksAvailableForAdding booksAvailableForAdding = booksAvailableForAdding
@ -2565,6 +2568,19 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
} }
} }
fun saveHighlights(bookId: String, highlightsJson: String) {
viewModelScope.launch {
val currentBookUri = _internalState.value.selectedPdfUri ?: _internalState.value.selectedEpubUri
if (currentBookUri != null) {
recentFilesRepository.getFileByUri(currentBookUri.toString())?.let { item ->
recentFilesRepository.updateHighlights(item.bookId, highlightsJson)
}
} else if (bookId.isNotBlank()) {
recentFilesRepository.updateHighlights(bookId, highlightsJson)
}
}
}
val reflowWorkInfo: Flow<WorkInfo?> = val reflowWorkInfo: Flow<WorkInfo?> =
WorkManager.getInstance(appContext).getWorkInfosByTagFlow(ReflowWorker.WORK_NAME) WorkManager.getInstance(appContext).getWorkInfosByTagFlow(ReflowWorker.WORK_NAME)
.map { list -> .map { list ->
@ -2646,6 +2662,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
), ),
initialCfi = null, initialCfi = null,
initialBookmarksJson = item.bookmarksJson, initialBookmarksJson = item.bookmarksJson,
initialHighlightsJson = item.highlightsJson,
isLoading = false isLoading = false
) )
} }
@ -2847,7 +2864,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
selectedEpubUri = uri, selectedEpubUri = uri,
initialLocator = locator, initialLocator = locator,
initialCfi = recentItem?.lastPositionCfi, initialCfi = recentItem?.lastPositionCfi,
initialBookmarksJson = recentItem?.bookmarksJson initialBookmarksJson = recentItem?.bookmarksJson,
initialHighlightsJson = recentItem?.highlightsJson,
) )
} }

View file

@ -48,6 +48,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
@ -333,24 +334,36 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(item.timestamp)) SimpleDateFormat("MMM dd, yyyy HH:mm", Locale.getDefault()).format(Date(item.timestamp))
} }
val pathText = remember(item.sourceFolderUri, item.displayName) { val context = LocalContext.current
if (item.sourceFolderUri != null) { val pathText = remember(item.sourceFolderUri, item.uriString, item.displayName, context) {
if (item.sourceFolderUri != null && item.uriString != null) {
try { try {
val uri = item.sourceFolderUri.toUri() val uri = item.uriString.toUri()
val docId = android.provider.DocumentsContract.getTreeDocumentId(uri) val docId = if (android.provider.DocumentsContract.isDocumentUri(context, uri)) {
android.provider.DocumentsContract.getDocumentId(uri)
} else if (android.provider.DocumentsContract.isTreeUri(uri)) {
android.provider.DocumentsContract.getTreeDocumentId(uri)
} else {
Uri.decode(uri.toString())
}
val split = docId.split(":") val split = docId.split(":")
val storageName = if (split[0].equals("primary", ignoreCase = true)) "Internal storage" else split[0] val storageName = if (split[0].equals("primary", ignoreCase = true)) "Internal storage" else split[0]
val relativePath = if (split.size > 1) { var relativePath = if (split.size > 1) {
Uri.decode(split[1]).removeSuffix("/") Uri.decode(split[1]).removeSuffix("/")
} else "" } else ""
if (!relativePath.endsWith(item.displayName)) {
relativePath = if (relativePath.isEmpty()) item.displayName else "$relativePath/${item.displayName}"
}
val leadingSlash = if (relativePath.isNotEmpty() && !relativePath.startsWith("/")) "/" else "" val leadingSlash = if (relativePath.isNotEmpty() && !relativePath.startsWith("/")) "/" else ""
"/$storageName$leadingSlash$relativePath/${item.displayName}" "/$storageName$leadingSlash$relativePath"
} catch (_: Exception) { } catch (_: Exception) {
val decoded = Uri.decode(item.sourceFolderUri) val decoded = Uri.decode(item.uriString)
if (decoded.contains("primary:")) { if (decoded.contains("primary:")) {
"/Internal storage/${decoded.substringAfter("primary:").removeSuffix("/")}/${item.displayName}" "/Internal storage/${decoded.substringAfter("primary:").substringBeforeLast("/")}/${item.displayName}"
} else { } else {
item.displayName item.displayName
} }
@ -435,6 +448,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
label = "Location", label = "Location",
value = pathText, value = pathText,
maxLines = 4, maxLines = 4,
isScrollable = true,
onCopy = { onCopy = {
clipboardManager.setText(AnnotatedString(pathText)) clipboardManager.setText(AnnotatedString(pathText))
} }
@ -471,6 +485,7 @@ private fun InfoRowDetailed(
label: String, label: String,
value: String, value: String,
maxLines: Int = 1, maxLines: Int = 1,
isScrollable: Boolean = false, // ADD THIS
onCopy: (() -> Unit)? = null onCopy: (() -> Unit)? = null
) { ) {
Row( Row(
@ -486,15 +501,23 @@ private fun InfoRowDetailed(
.width(85.dp) .width(85.dp)
.padding(top = 2.dp) .padding(top = 2.dp)
) )
val scrollModifier = if (isScrollable) {
Modifier
.heightIn(max = 66.dp)
.verticalScroll(androidx.compose.foundation.rememberScrollState())
} else Modifier
Text( Text(
text = value, text = value,
style = MaterialTheme.typography.bodyMedium, style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface, color = MaterialTheme.colorScheme.onSurface,
maxLines = maxLines, maxLines = if (isScrollable) Int.MAX_VALUE else maxLines,
overflow = TextOverflow.Ellipsis, overflow = if (isScrollable) TextOverflow.Clip else TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.weight(1f) .weight(1f)
.padding(top = 2.dp) .padding(top = 2.dp)
.then(scrollModifier)
) )
if (onCopy != null) { if (onCopy != null) {
IconButton( IconButton(

View file

@ -27,7 +27,7 @@ import androidx.room.TypeConverters
import androidx.room.migration.Migration import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
@Database(entities =[RecentFileEntity::class, CustomFontEntity::class], version = 14, exportSchema = false) @Database(entities =[RecentFileEntity::class, CustomFontEntity::class], version = 15, exportSchema = false)
@TypeConverters(FileTypeConverter::class) @TypeConverters(FileTypeConverter::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
abstract fun recentFileDao(): RecentFileDao abstract fun recentFileDao(): RecentFileDao
@ -179,6 +179,12 @@ abstract class AppDatabase : RoomDatabase() {
} }
} }
val MIGRATION_14_15 = object : Migration(14, 15) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("ALTER TABLE recent_files ADD COLUMN highlights TEXT DEFAULT NULL")
}
}
fun getDatabase(context: Context): AppDatabase { fun getDatabase(context: Context): AppDatabase {
return INSTANCE ?: synchronized(this) { return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder( val instance = Room.databaseBuilder(
@ -190,7 +196,7 @@ abstract class AppDatabase : RoomDatabase() {
MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5, MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5,
MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9, MIGRATION_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9,
MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12, MIGRATION_9_10, MIGRATION_10_11, MIGRATION_11_12,
MIGRATION_12_13, MIGRATION_13_14 MIGRATION_12_13, MIGRATION_13_14, MIGRATION_14_15
) )
.fallbackToDestructiveMigration(false) .fallbackToDestructiveMigration(false)
.build() .build()

View file

@ -19,7 +19,8 @@ data class FolderBookMetadata(
val bookmarksJson: String?, val bookmarksJson: String?,
val locatorBlockIndex: Int?, val locatorBlockIndex: Int?,
val locatorCharOffset: Int?, val locatorCharOffset: Int?,
val customName: String? val customName: String?,
val highlightsJson: String?
) { ) {
fun toJsonString(): String { fun toJsonString(): String {
val json = JSONObject() val json = JSONObject()
@ -38,6 +39,7 @@ data class FolderBookMetadata(
json.put("locatorBlockIndex", locatorBlockIndex ?: -1) json.put("locatorBlockIndex", locatorBlockIndex ?: -1)
json.put("locatorCharOffset", locatorCharOffset ?: -1) json.put("locatorCharOffset", locatorCharOffset ?: -1)
json.put("customName", customName) json.put("customName", customName)
json.put("highlightsJson", highlightsJson)
return json.toString() return json.toString()
} }
@ -69,7 +71,8 @@ data class FolderBookMetadata(
bookmarksJson = json.optStringNull("bookmarksJson"), bookmarksJson = json.optStringNull("bookmarksJson"),
locatorBlockIndex = json.optIntNull("locatorBlockIndex"), locatorBlockIndex = json.optIntNull("locatorBlockIndex"),
locatorCharOffset = json.optIntNull("locatorCharOffset"), locatorCharOffset = json.optIntNull("locatorCharOffset"),
customName = json.optStringNull("customName") customName = json.optStringNull("customName"),
highlightsJson = json.optStringNull("highlightsJson")
) )
} }
} }
@ -97,6 +100,7 @@ fun FolderBookMetadata.toRecentFileItem(uriString: String?, coverPath: String?,
isDeleted = false, isDeleted = false,
bookmarksJson = this.bookmarksJson, bookmarksJson = this.bookmarksJson,
sourceFolderUri = sourceFolderUri, sourceFolderUri = sourceFolderUri,
customName = this.customName customName = this.customName,
highlightsJson = this.highlightsJson
) )
} }

View file

@ -93,4 +93,7 @@ interface RecentFileDao {
@Query("UPDATE recent_files SET sourceFolderUri = NULL WHERE sourceFolderUri IS NOT NULL") @Query("UPDATE recent_files SET sourceFolderUri = NULL WHERE sourceFolderUri IS NOT NULL")
suspend fun detachAllFolderBooks() suspend fun detachAllFolderBooks()
@Query("UPDATE recent_files SET highlights = :highlightsJson, lastModifiedTimestamp = :timestamp WHERE bookId = :bookId")
suspend fun updateHighlights(bookId: String, highlightsJson: String, timestamp: Long)
} }

View file

@ -49,5 +49,6 @@ data class RecentFileEntity(
val bookmarks: String?, val bookmarks: String?,
@ColumnInfo(defaultValue = "NULL") val sourceFolderUri: String?, @ColumnInfo(defaultValue = "NULL") val sourceFolderUri: String?,
@ColumnInfo(defaultValue = "0") val isReflowPreferred: Boolean, @ColumnInfo(defaultValue = "0") val isReflowPreferred: Boolean,
@ColumnInfo(defaultValue = "NULL") val customName: String? @ColumnInfo(defaultValue = "NULL") val customName: String?,
@ColumnInfo(defaultValue = "NULL") val highlights: String?
) )

View file

@ -45,7 +45,8 @@ data class RecentFileItem(
val bookmarksJson: String? = null, val bookmarksJson: String? = null,
val sourceFolderUri: String? = null, val sourceFolderUri: String? = null,
val isReflowPreferred: Boolean = false, val isReflowPreferred: Boolean = false,
val customName: String? = null val customName: String? = null,
val highlightsJson: String? = null
) { ) {
fun getUri(): Uri? = uriString?.toUri() fun getUri(): Uri? = uriString?.toUri()
} }
@ -73,7 +74,8 @@ fun RecentFileEntity.toRecentFileItem(): RecentFileItem {
bookmarksJson = this.bookmarks, bookmarksJson = this.bookmarks,
sourceFolderUri = this.sourceFolderUri, sourceFolderUri = this.sourceFolderUri,
isReflowPreferred = this.isReflowPreferred, isReflowPreferred = this.isReflowPreferred,
customName = this.customName customName = this.customName,
highlightsJson = this.highlights
) )
} }
@ -100,7 +102,8 @@ fun RecentFileItem.toRecentFileEntity(): RecentFileEntity {
bookmarks = this.bookmarksJson, bookmarks = this.bookmarksJson,
sourceFolderUri = this.sourceFolderUri, sourceFolderUri = this.sourceFolderUri,
isReflowPreferred = this.isReflowPreferred, isReflowPreferred = this.isReflowPreferred,
customName = this.customName customName = this.customName,
highlights = this.highlightsJson
) )
} }
@ -122,7 +125,8 @@ fun RecentFileItem.toBookMetadata(): BookMetadata {
lastModifiedTimestamp = this.lastModifiedTimestamp, lastModifiedTimestamp = this.lastModifiedTimestamp,
bookmarksJson = this.bookmarksJson, bookmarksJson = this.bookmarksJson,
hasAnnotations = false, hasAnnotations = false,
customName = this.customName customName = this.customName,
highlightsJson = this.highlightsJson
) )
} }
@ -147,6 +151,7 @@ fun BookMetadata.toRecentFileItem(): RecentFileItem {
lastModifiedTimestamp = this.lastModifiedTimestamp, lastModifiedTimestamp = this.lastModifiedTimestamp,
isDeleted = this.isDeleted, isDeleted = this.isDeleted,
bookmarksJson = this.bookmarksJson, bookmarksJson = this.bookmarksJson,
customName = this.customName customName = this.customName,
highlightsJson = this.highlightsJson
) )
} }

View file

@ -117,7 +117,8 @@ class RecentFilesRepository(private val context: Context) {
progressPercentage = item.progressPercentage ?: existingItem.progressPercentage, progressPercentage = item.progressPercentage ?: existingItem.progressPercentage,
isRecent = item.isRecent, isRecent = item.isRecent,
isDeleted = item.isDeleted, isDeleted = item.isDeleted,
sourceFolderUri = item.sourceFolderUri ?: existingItem.sourceFolderUri sourceFolderUri = item.sourceFolderUri ?: existingItem.sourceFolderUri,
highlights = item.highlightsJson ?: existingItem.highlights
) )
} else { } else {
item.toRecentFileEntity() item.toRecentFileEntity()
@ -128,6 +129,12 @@ class RecentFilesRepository(private val context: Context) {
Timber.d("Added/Updated recent file in DB: ${item.displayName}") Timber.d("Added/Updated recent file in DB: ${item.displayName}")
} }
suspend fun updateHighlights(bookId: String, highlightsJson: String) = withContext(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
recentFileDao.updateHighlights(bookId, highlightsJson, currentTime)
Timber.d("Updated highlights for $bookId")
}
suspend fun syncLocalMetadataToFolder(bookId: String) = withContext(Dispatchers.IO) { suspend fun syncLocalMetadataToFolder(bookId: String) = withContext(Dispatchers.IO) {
val entity = recentFileDao.getFileByBookId(bookId) ?: return@withContext val entity = recentFileDao.getFileByBookId(bookId) ?: return@withContext
val folderUriString = entity.sourceFolderUri val folderUriString = entity.sourceFolderUri
@ -135,7 +142,8 @@ class RecentFilesRepository(private val context: Context) {
if (folderUriString != null) { if (folderUriString != null) {
val hasProgress = (entity.progressPercentage != null && entity.progressPercentage > 0f) val hasProgress = (entity.progressPercentage != null && entity.progressPercentage > 0f)
val hasBookmarks = !entity.bookmarks.isNullOrEmpty() && entity.bookmarks != "[]" val hasBookmarks = !entity.bookmarks.isNullOrEmpty() && entity.bookmarks != "[]"
val isDirty = entity.isRecent || hasProgress || hasBookmarks val hasHighlights = !entity.highlights.isNullOrEmpty() && entity.highlights != "[]"
val isDirty = entity.isRecent || hasProgress || hasBookmarks || hasHighlights
if (!isDirty) { if (!isDirty) {
Timber.d("SyncDebug: Book $bookId is 'Clean' (Unread/Not Recent). Skipping JSON creation.") Timber.d("SyncDebug: Book $bookId is 'Clean' (Unread/Not Recent). Skipping JSON creation.")
@ -159,7 +167,8 @@ class RecentFilesRepository(private val context: Context) {
bookmarksJson = entity.bookmarks, bookmarksJson = entity.bookmarks,
locatorBlockIndex = entity.locatorBlockIndex, locatorBlockIndex = entity.locatorBlockIndex,
locatorCharOffset = entity.locatorCharOffset, locatorCharOffset = entity.locatorCharOffset,
customName = entity.customName customName = entity.customName,
highlightsJson = entity.highlights
) )
LocalSyncUtils.saveMetadataToFolder( LocalSyncUtils.saveMetadataToFolder(

View file

@ -215,6 +215,53 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List<UserHighl
return list return list
} }
fun parseHighlightsJson(jsonString: String?): List<UserHighlight> {
if (jsonString.isNullOrBlank()) return emptyList()
val list = mutableListOf<UserHighlight>()
try {
val jsonArray = JSONArray(jsonString)
for (i in 0 until jsonArray.length()) {
val obj = jsonArray.getJSONObject(i)
val colorId = obj.getString("colorId")
val color = HighlightColor.entries.find { it.id == colorId } ?: HighlightColor.YELLOW
list.add(
UserHighlight(
id = obj.optString("id", java.util.UUID.randomUUID().toString()),
cfi = obj.getString("cfi"),
text = obj.getString("text"),
color = color,
chapterIndex = obj.getInt("chapterIndex")
)
)
}
} catch (e: Exception) {
Timber.e(e, "Error parsing highlights JSON")
}
return list
}
fun highlightsToJson(highlights: List<UserHighlight>): String {
val jsonArray = JSONArray()
highlights.forEach { h ->
val obj = JSONObject().apply {
put("id", h.id)
put("cfi", h.cfi)
put("text", h.text)
put("colorId", h.color.id)
put("chapterIndex", h.chapterIndex)
}
jsonArray.put(obj)
}
return jsonArray.toString()
}
fun clearHighlightsFromPrefs(context: Context, bookTitle: String) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
val sanitizedTitle = bookTitle.replace("[^a-zA-Z0-9]".toRegex(), "")
val key = "highlights_data_$sanitizedTitle"
prefs.edit { remove(key) }
}
// --- Logic Helpers --- // --- Logic Helpers ---
fun processAndAddHighlight( fun processAndAddHighlight(

View file

@ -142,6 +142,8 @@ fun EpubReaderTopBar(
volumeScrollEnabled: Boolean, volumeScrollEnabled: Boolean,
isPageTurnAnimationEnabled: Boolean, isPageTurnAnimationEnabled: Boolean,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
isKeepScreenOn: Boolean,
onToggleKeepScreenOn: (Boolean) -> Unit,
onCloseSearch: () -> Unit, onCloseSearch: () -> Unit,
onChangeRenderMode: (RenderMode) -> Unit, onChangeRenderMode: (RenderMode) -> Unit,
onToggleBookmark: () -> Unit, onToggleBookmark: () -> Unit,
@ -334,6 +336,16 @@ fun EpubReaderTopBar(
) )
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(
text = { Text("Keep Screen On") },
onClick = {
onToggleKeepScreenOn(!isKeepScreenOn)
showMoreMenu = false
},
trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = "Enabled") }
)
HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Auto Scroll") }, text = { Text("Auto Scroll") },
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL, enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,

View file

@ -206,6 +206,17 @@ private const val AUTO_SCROLL_LOCAL_SPEED_PREFIX = "auto_scroll_local_speed_"
private const val AUTO_SCROLL_LOCAL_MIN_PREFIX = "auto_scroll_local_min_" private const val AUTO_SCROLL_LOCAL_MIN_PREFIX = "auto_scroll_local_min_"
private const val AUTO_SCROLL_LOCAL_MAX_PREFIX = "auto_scroll_local_max_" private const val AUTO_SCROLL_LOCAL_MAX_PREFIX = "auto_scroll_local_max_"
private const val MUSICIAN_MODE_KEY = "musician_mode_enabled" private const val MUSICIAN_MODE_KEY = "musician_mode_enabled"
private const val KEEP_SCREEN_ON_KEY = "keep_screen_on_enabled"
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
prefs.edit { putBoolean(KEEP_SCREEN_ON_KEY, isEnabled) }
}
private fun loadKeepScreenOn(context: Context): Boolean {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
return prefs.getBoolean(KEEP_SCREEN_ON_KEY, false)
}
private fun saveMusicianMode(context: Context, isEnabled: Boolean) { private fun saveMusicianMode(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE) val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
@ -385,10 +396,16 @@ fun EpubReaderScreen(
initialLocator = initialLocator, initialLocator = initialLocator,
initialCfi = initialCfi, initialCfi = initialCfi,
initialBookmarksJson = initialBookmarksJson, initialBookmarksJson = initialBookmarksJson,
initialHighlightsJson = uiState.initialHighlightsJson,
isProUser = isProUser, isProUser = isProUser,
onNavigateBack = onNavigateBack, onNavigateBack = onNavigateBack,
onSavePosition = onSavePosition, onSavePosition = onSavePosition,
onBookmarksChanged = onBookmarksChanged, onBookmarksChanged = onBookmarksChanged,
onHighlightsChanged = { json ->
uiState.selectedBookId?.let { id ->
viewModel.saveHighlights(id, json)
}
},
onNavigateToPro = onNavigateToPro, onNavigateToPro = onNavigateToPro,
coverImagePath = coverImagePath, coverImagePath = coverImagePath,
onRenderModeChange = onRenderModeChange, onRenderModeChange = onRenderModeChange,
@ -419,10 +436,12 @@ fun EpubReaderHost(
initialLocator: Locator?, initialLocator: Locator?,
initialCfi: String?, initialCfi: String?,
initialBookmarksJson: String?, initialBookmarksJson: String?,
initialHighlightsJson: String?,
isProUser: Boolean, isProUser: Boolean,
onNavigateBack: () -> Unit, onNavigateBack: () -> Unit,
onSavePosition: (locator: Locator, cfiForWebView: String?, progress: Float) -> Unit, onSavePosition: (locator: Locator, cfiForWebView: String?, progress: Float) -> Unit,
onBookmarksChanged: (bookmarksJson: String) -> Unit, onBookmarksChanged: (bookmarksJson: String) -> Unit,
onHighlightsChanged: (highlightsJson: String) -> Unit,
onNavigateToPro: () -> Unit, onNavigateToPro: () -> Unit,
coverImagePath: String?, coverImagePath: String?,
onRenderModeChange: (RenderMode) -> Unit, onRenderModeChange: (RenderMode) -> Unit,
@ -479,11 +498,24 @@ fun EpubReaderHost(
) )
} }
val userHighlights = remember { val userHighlights = remember(epubBook.title) {
mutableStateListOf<UserHighlight>().apply { mutableStateListOf<UserHighlight>().apply {
if (initialHighlightsJson != null) {
addAll(parseHighlightsJson(initialHighlightsJson))
} else {
addAll(loadHighlightsFromPrefs(context, epubBook.title)) addAll(loadHighlightsFromPrefs(context, epubBook.title))
} }
} }
}
LaunchedEffect(userHighlights.size, userHighlights.toList()) {
val json = highlightsToJson(userHighlights)
onHighlightsChanged(json)
if (initialHighlightsJson == null && userHighlights.isNotEmpty()) {
clearHighlightsFromPrefs(context, epubBook.title)
}
}
var isAutoScrollCollapsed by remember { mutableStateOf(false) } var isAutoScrollCollapsed by remember { mutableStateOf(false) }
@ -586,10 +618,6 @@ fun EpubReaderHost(
} }
} }
LaunchedEffect(userHighlights.size, userHighlights.toList()) {
saveHighlightsToPrefs(context, epubBook.title, userHighlights)
}
// Dictionary // Dictionary
var showAiDefinitionPopup by remember { mutableStateOf(false) } var showAiDefinitionPopup by remember { mutableStateOf(false) }
var selectedTextForAi by remember { mutableStateOf<String?>(null) } var selectedTextForAi by remember { mutableStateOf<String?>(null) }
@ -735,6 +763,7 @@ fun EpubReaderHost(
var searchHighlightTarget by remember { mutableStateOf<SearchResult?>(null) } var searchHighlightTarget by remember { mutableStateOf<SearchResult?>(null) }
var lastHighlightClickTime by remember { mutableLongStateOf(0L) } var lastHighlightClickTime by remember { mutableLongStateOf(0L) }
var lastScrollHideTime by remember { mutableLongStateOf(0L) }
var webViewRefForTts by remember { mutableStateOf<WebView?>(null) } var webViewRefForTts by remember { mutableStateOf<WebView?>(null) }
@ -932,6 +961,15 @@ fun EpubReaderHost(
var isMusicianMode by remember { mutableStateOf(loadMusicianMode(context)) } var isMusicianMode by remember { mutableStateOf(loadMusicianMode(context)) }
var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) } var autoScrollUseSlider by remember { mutableStateOf(loadAutoScrollUseSlider(context)) }
var isKeepScreenOn by remember { mutableStateOf(loadKeepScreenOn(context)) }
DisposableEffect(isKeepScreenOn) {
view.keepScreenOn = isKeepScreenOn
onDispose {
view.keepScreenOn = false
}
}
DisposableEffect(Unit) { DisposableEffect(Unit) {
onDispose { onDispose {
Timber.d("Disposing sample MediaPlayer.") Timber.d("Disposing sample MediaPlayer.")
@ -998,6 +1036,15 @@ fun EpubReaderHost(
} }
val activeTextureId = activeTheme.textureId val activeTextureId = activeTheme.textureId
val infoBarBgColor = remember(effectiveBg, isDarkTheme) {
val overlayAlpha = if (isDarkTheme) 0.08f else 0.06f
val overlayColor = if (isDarkTheme) Color.White else Color.Black
val outR = overlayColor.red * overlayAlpha + effectiveBg.red * (1 - overlayAlpha)
val outG = overlayColor.green * overlayAlpha + effectiveBg.green * (1 - overlayAlpha)
val outB = overlayColor.blue * overlayAlpha + effectiveBg.blue * (1 - overlayAlpha)
Color(outR, outG, outB).copy(alpha = 0.95f)
}
val currentChapterInPaginatedMode by remember { val currentChapterInPaginatedMode by remember {
derivedStateOf { derivedStateOf {
if (currentRenderMode == RenderMode.PAGINATED) { if (currentRenderMode == RenderMode.PAGINATED) {
@ -2201,6 +2248,10 @@ fun EpubReaderHost(
if (volumeScrollEnabled && !searchState.isSearchActive) { if (volumeScrollEnabled && !searchState.isSearchActive) {
containerFocusRequester.requestFocus() containerFocusRequester.requestFocus()
} }
if (System.currentTimeMillis() - lastScrollHideTime < 250) {
Timber.d("Ignoring tap toggle because bars were just hidden by scroll (sloppy tap).")
} else {
if (showBars || showFormatAdjustmentBars) { if (showBars || showFormatAdjustmentBars) {
showBars = false showBars = false
showFormatAdjustmentBars = false showFormatAdjustmentBars = false
@ -2210,11 +2261,13 @@ fun EpubReaderHost(
Timber.d("Chapter tapped, showing main bars.") Timber.d("Chapter tapped, showing main bars.")
} }
} }
}
}, },
onPotentialScroll = { onPotentialScroll = {
if (showBars) { if (showBars || showFormatAdjustmentBars) {
showBars = false showBars = false
showFormatAdjustmentBars = false showFormatAdjustmentBars = false
lastScrollHideTime = System.currentTimeMillis() // Added
Timber.d("Scroll/Drag detected, hiding bars.") Timber.d("Scroll/Drag detected, hiding bars.")
} }
if (isAutoScrollModeActive && isAutoScrollPlaying) { if (isAutoScrollModeActive && isAutoScrollPlaying) {
@ -2947,7 +3000,7 @@ fun EpubReaderHost(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT) .height(PAGE_INFO_BAR_HEIGHT)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f)) .background(infoBarBgColor)
.padding(bottom = bottomPadding) .padding(bottom = bottomPadding)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@ -2958,7 +3011,7 @@ fun EpubReaderHost(
Text( Text(
text = "$chapterTitle ($currentPageInChapter/$totalPagesInCurrentChapter)", text = "$chapterTitle ($currentPageInChapter/$totalPagesInCurrentChapter)",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -2971,7 +3024,7 @@ fun EpubReaderHost(
Text( Text(
text = "%.1f%%".format(currentBookProgress), text = "%.1f%%".format(currentBookProgress),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier.align(Alignment.CenterEnd)
) )
@ -2990,7 +3043,7 @@ fun EpubReaderHost(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT) .height(PAGE_INFO_BAR_HEIGHT)
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f)) .background(infoBarBgColor)
.padding(bottom = bottomPadding) .padding(bottom = bottomPadding)
.padding(horizontal = 16.dp), .padding(horizontal = 16.dp),
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
@ -3019,7 +3072,7 @@ fun EpubReaderHost(
Text( Text(
text = textToShow, text = textToShow,
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
@ -3053,7 +3106,7 @@ fun EpubReaderHost(
Text( Text(
text = "%.1f%%".format(displayProgress), text = "%.1f%%".format(displayProgress),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier.align(Alignment.CenterEnd)
) )
@ -3068,7 +3121,7 @@ fun EpubReaderHost(
Text( Text(
text = "%.1f%%".format(percentage), text = "%.1f%%".format(percentage),
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant, color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End, textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd) modifier = Modifier.align(Alignment.CenterEnd)
) )
@ -3283,6 +3336,11 @@ fun EpubReaderHost(
volumeScrollEnabled = volumeScrollEnabled, volumeScrollEnabled = volumeScrollEnabled,
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled, isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
onNavigateBack = { triggerSaveAndExit() }, onNavigateBack = { triggerSaveAndExit() },
isKeepScreenOn = isKeepScreenOn,
onToggleKeepScreenOn = { enabled ->
isKeepScreenOn = enabled
saveKeepScreenOn(context, enabled)
},
onCloseSearch = { onCloseSearch = {
searchState.isSearchActive = false searchState.isSearchActive = false
searchState.onQueryChange("") searchState.onQueryChange("")

View file

@ -5,9 +5,7 @@ import android.content.Context
import android.graphics.Bitmap import android.graphics.Bitmap
import android.net.Uri import android.net.Uri
import android.util.Base64 import android.util.Base64
import io.legere.pdfiumandroid.PdfiumCore
import io.legere.pdfiumandroid.suspend.PdfDocumentKt import io.legere.pdfiumandroid.suspend.PdfDocumentKt
import io.legere.pdfiumandroid.suspend.PdfiumCoreKt
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
@ -219,9 +217,17 @@ object PdfToHtmlGenerator {
val c = rawText[i] val c = rawText[i]
val code = c.code val code = c.code
if (code == 0 || code == 13) continue if (code == 0) continue
if (c == '\r') {
commitLine()
continue
}
if (c == '\n') { if (c == '\n') {
if (i > 0 && rawText[i - 1] == '\r') {
continue
}
commitLine() commitLine()
continue continue
} }
@ -292,7 +298,7 @@ object PdfToHtmlGenerator {
} }
buildPageHtml(pageNumber, finalElements, headerFooterStrings) buildPageHtml(pageNumber, finalElements, headerFooterStrings)
} ?: buildEmptyPageSection(pageNumber) }
} ?: buildEmptyPageSection(pageNumber) } ?: buildEmptyPageSection(pageNumber)
} catch (e: Exception) { } catch (e: Exception) {
Timber.tag(TAG).w(e, "Error extracting page $pageIdx") Timber.tag(TAG).w(e, "Error extracting page $pageIdx")
@ -339,9 +345,12 @@ object PdfToHtmlGenerator {
var inParagraph = false var inParagraph = false
var inUl = false var inUl = false
var inOl = false var inOl = false
var inLi = false
fun closeParagraph() { if (inParagraph) { sb.append("</p>\n"); inParagraph = false } } fun closeParagraph() { if (inParagraph) { sb.append("</p>\n"); inParagraph = false } }
fun closeLi() { if (inLi) { sb.append("</li>\n"); inLi = false } }
fun closeList() { fun closeList() {
closeLi()
if (inUl) { sb.append("</ul>\n"); inUl = false } if (inUl) { sb.append("</ul>\n"); inUl = false }
if (inOl) { sb.append("</ol>\n"); inOl = false } if (inOl) { sb.append("</ol>\n"); inOl = false }
} }
@ -404,26 +413,36 @@ object PdfToHtmlGenerator {
sb.append("<$tag>${renderSpans(line.spans, insideHeading = true)}</$tag>\n") sb.append("<$tag>${renderSpans(line.spans, insideHeading = true)}</$tag>\n")
} }
isBullet -> { isBullet -> {
closeParagraph() closeParagraph(); closeLi()
if (inOl) { sb.append("</ol>\n"); inOl = false } if (inOl) { sb.append("</ol>\n"); inOl = false }
if (!inUl) { sb.append("<ul>\n"); inUl = true } if (!inUl) { sb.append("<ul>\n"); inUl = true }
val content = trimmed.removePrefix("").removePrefix("").removePrefix("").removePrefix("").removePrefix("- ").trim() val content = trimmed.removePrefix("").removePrefix("").removePrefix("").removePrefix("").removePrefix("- ").trim()
sb.append("<li>${content.escapeHtml()}</li>\n") sb.append("<li>${content.escapeHtml()}")
inLi = true
} }
numberedMatch -> { numberedMatch -> {
closeParagraph() closeParagraph(); closeLi()
if (inUl) { sb.append("</ul>\n"); inUl = false } if (inUl) { sb.append("</ul>\n"); inUl = false }
if (!inOl) { sb.append("<ol>\n"); inOl = true } if (!inOl) { sb.append("<ol>\n"); inOl = true }
val content = trimmed.substringAfter(" ").trim() val content = trimmed.substringAfter(" ").trim()
sb.append("<li>${content.escapeHtml()}</li>\n") sb.append("<li>${content.escapeHtml()}")
inLi = true
} }
shouldBreakParagraph -> { shouldBreakParagraph -> {
if (inLi) {
sb.append(" ").append(renderSpans(line.spans))
closeLi()
} else {
closeList() closeList()
if (!inParagraph) { sb.append("<p>"); inParagraph = true } if (!inParagraph) { sb.append("<p>"); inParagraph = true }
sb.append(renderSpans(line.spans)) sb.append(renderSpans(line.spans))
closeParagraph() closeParagraph()
} }
}
else -> { else -> {
if (inLi) {
sb.append(" ").append(renderSpans(line.spans))
} else {
closeList() closeList()
if (!inParagraph) { sb.append("<p>"); inParagraph = true } else sb.append(" ") if (!inParagraph) { sb.append("<p>"); inParagraph = true } else sb.append(" ")
sb.append(renderSpans(line.spans)) sb.append(renderSpans(line.spans))
@ -432,6 +451,7 @@ object PdfToHtmlGenerator {
} }
} }
} }
}
closeParagraph() closeParagraph()
closeList() closeList()

View file

@ -347,6 +347,17 @@ private const val PREF_EXTERNAL_DICT_PKG = "external_dictionary_package"
private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package" private const val PREF_EXTERNAL_TRANSLATE_PKG = "external_translate_package"
private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package" private const val PREF_EXTERNAL_SEARCH_PKG = "external_search_package"
private const val PDF_THEME_KEY = "pdf_reader_theme" private const val PDF_THEME_KEY = "pdf_reader_theme"
private const val PDF_KEEP_SCREEN_ON_KEY = "pdf_keep_screen_on_enabled"
private fun saveKeepScreenOn(context: Context, isEnabled: Boolean) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit { putBoolean(PDF_KEEP_SCREEN_ON_KEY, isEnabled) }
}
private fun loadKeepScreenOn(context: Context): Boolean {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
return prefs.getBoolean(PDF_KEEP_SCREEN_ON_KEY, false)
}
private fun savePdfThemeId(context: Context, themeId: String) { private fun savePdfThemeId(context: Context, themeId: String) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE) val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
@ -1152,7 +1163,7 @@ fun PdfViewerScreen(
?: pdfUri.lastPathSegment ?: "Document.pdf" ?: pdfUri.lastPathSegment ?: "Document.pdf"
} }
} }
val view = LocalView.current
var isDockDragging by remember { mutableStateOf(false) } var isDockDragging by remember { mutableStateOf(false) }
var initialScrollDone by remember { mutableStateOf(false) } var initialScrollDone by remember { mutableStateOf(false) }
@ -1167,6 +1178,14 @@ fun PdfViewerScreen(
var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) } var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) }
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) } var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
var showTtsSettingsSheet by remember { mutableStateOf(false) } var showTtsSettingsSheet by remember { mutableStateOf(false) }
var isKeepScreenOn by remember { mutableStateOf(loadKeepScreenOn(context)) }
DisposableEffect(isKeepScreenOn) {
view.keepScreenOn = isKeepScreenOn
onDispose {
view.keepScreenOn = false
}
}
var showDictionarySettingsSheet by remember { mutableStateOf(false) } var showDictionarySettingsSheet by remember { mutableStateOf(false) }
var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) } var useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
@ -1338,7 +1357,6 @@ fun PdfViewerScreen(
} }
} }
val view = LocalView.current
val window = (view.context as? Activity)?.window val window = (view.context as? Activity)?.window
LaunchedEffect(isFullScreen) { LaunchedEffect(isFullScreen) {
if (window != null) { if (window != null) {
@ -5423,6 +5441,23 @@ fun PdfViewerScreen(
} }
}) })
HorizontalDivider() HorizontalDivider()
DropdownMenuItem(
text = { Text("Keep Screen On") },
onClick = {
isKeepScreenOn = !isKeepScreenOn
saveKeepScreenOn(context, isKeepScreenOn)
showMoreMenu = false
},
trailingIcon = {
if (isKeepScreenOn) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Selected"
)
}
}
)
HorizontalDivider()
DropdownMenuItem( DropdownMenuItem(
text = { Text("Auto Scroll") }, text = { Text("Auto Scroll") },
enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL, enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL,

View file

@ -23,7 +23,8 @@ data class BookMetadata(
val lastModifiedTimestamp: Long = 0L, val lastModifiedTimestamp: Long = 0L,
val bookmarksJson: String? = null, val bookmarksJson: String? = null,
val hasAnnotations: Boolean = false, val hasAnnotations: Boolean = false,
val customName: String? = null val customName: String? = null,
val highlightsJson: String? = null
) )
data class DeviceItem( data class DeviceItem(