From 4c5cd20b21c33f3c6774d82164165d479126046b Mon Sep 17 00:00:00 2001
From: Aryan
Date: Thu, 26 Mar 2026 13:22:05 +0530
Subject: [PATCH] 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
---
app/src/main/AndroidManifest.xml | 111 ++++++++++++++----
.../java/com/aryan/reader/FolderSyncWorker.kt | 4 +
.../java/com/aryan/reader/LibraryScreen.kt | 5 +-
.../java/com/aryan/reader/MainViewModel.kt | 20 +++-
.../com/aryan/reader/SharedComposables.kt | 43 +++++--
.../java/com/aryan/reader/data/AppDatabase.kt | 10 +-
.../aryan/reader/data/FolderBookMetadata.kt | 10 +-
.../com/aryan/reader/data/RecentFileDao.kt | 3 +
.../com/aryan/reader/data/RecentFileEntity.kt | 3 +-
.../com/aryan/reader/data/RecentFileItem.kt | 15 ++-
.../reader/data/RecentFilesRepository.kt | 15 ++-
.../epubreader/EpubReaderAnnotations.kt | 47 ++++++++
.../reader/epubreader/EpubReaderControls.kt | 12 ++
.../reader/epubreader/EpubReaderScreen.kt | 98 ++++++++++++----
.../aryan/reader/pdf/PdfToHtmlGenerator.kt | 50 +++++---
.../com/aryan/reader/pdf/PdfViewerScreen.kt | 39 +++++-
.../aryan/reader/data/FirestoreRepository.kt | 3 +-
17 files changed, 404 insertions(+), 84 deletions(-)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index da02503..b74f8bd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -54,7 +54,7 @@
-
+
@@ -64,7 +64,7 @@
-
+
@@ -74,7 +74,7 @@
-
+
@@ -84,13 +84,9 @@
-
-
-
-
-
+
@@ -99,14 +95,10 @@
-
-
-
-
-
+
@@ -114,14 +106,28 @@
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -131,7 +137,72 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
index 7caf244..29225d2 100644
--- a/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
+++ b/app/src/main/java/com/aryan/reader/FolderSyncWorker.kt
@@ -152,6 +152,8 @@ class FolderSyncWorker(
lastPositionCfi = remoteMeta.lastPositionCfi,
progressPercentage = remoteMeta.progressPercentage,
bookmarksJson = remoteMeta.bookmarksJson,
+ highlightsJson = remoteMeta.highlightsJson,
+ customName = remoteMeta.customName,
locatorBlockIndex = remoteMeta.locatorBlockIndex,
locatorCharOffset = remoteMeta.locatorCharOffset,
lastModifiedTimestamp = remoteMeta.lastModifiedTimestamp,
@@ -247,6 +249,8 @@ class FolderSyncWorker(
lastPositionCfi = remoteMeta?.lastPositionCfi,
progressPercentage = remoteMeta?.progressPercentage,
bookmarksJson = remoteMeta?.bookmarksJson,
+ highlightsJson = remoteMeta?.highlightsJson,
+ customName = remoteMeta?.customName,
locatorBlockIndex = remoteMeta?.locatorBlockIndex,
locatorCharOffset = remoteMeta?.locatorCharOffset
)
diff --git a/app/src/main/java/com/aryan/reader/LibraryScreen.kt b/app/src/main/java/com/aryan/reader/LibraryScreen.kt
index 68fc298..4f3195f 100644
--- a/app/src/main/java/com/aryan/reader/LibraryScreen.kt
+++ b/app/src/main/java/com/aryan/reader/LibraryScreen.kt
@@ -134,6 +134,7 @@ fun LibraryScreen(
val isShelfContextualModeActive = selectedShelves.isNotEmpty()
val sortOrder = uiState.sortOrder
val shelves = uiState.shelves
+ val rawLibraryFiles = uiState.rawLibraryFiles
val pagerState = rememberPagerState(
initialPage = uiState.libraryScreenStartPage,
pageCount = { 3 }
@@ -234,6 +235,7 @@ fun LibraryScreen(
Box(modifier = Modifier.fillMaxSize()) {
LibraryScreenContent(
recentFiles = uiState.allRecentFiles,
+ rawLibraryFiles = rawLibraryFiles,
shelves = shelves,
selectedItems = selectedItems,
selectedShelves = selectedShelves,
@@ -460,6 +462,7 @@ fun ShelfScreen(
@Composable
fun LibraryScreenContent(
recentFiles: List,
+ rawLibraryFiles: List,
shelves: List,
selectedItems: Set,
selectedShelves: Set,
@@ -759,7 +762,7 @@ fun LibraryScreenContent(
2 -> {
FolderSyncScreen(
syncedFolders = syncedFolders,
- allRecentFiles = recentFiles,
+ allRecentFiles = rawLibraryFiles,
onAddFolderClick = onAddFolderClick,
onRemoveFolderClick = onRemoveFolderClick,
onEditFolderFiltersClick = onEditFolderFiltersClick,
diff --git a/app/src/main/java/com/aryan/reader/MainViewModel.kt b/app/src/main/java/com/aryan/reader/MainViewModel.kt
index 515f02e..0ee38ba 100644
--- a/app/src/main/java/com/aryan/reader/MainViewModel.kt
+++ b/app/src/main/java/com/aryan/reader/MainViewModel.kt
@@ -192,6 +192,7 @@ data class ReaderScreenState(
val initialLocator: Locator? = null,
val initialCfi: String? = null,
val initialBookmarksJson: String? = null,
+ val initialHighlightsJson: String? = null,
val initialPageInBook: Int? = null,
val shelves: List = emptyList(),
val viewingShelfName: String? = null,
@@ -225,6 +226,7 @@ data class ReaderScreenState(
val reflowProgress: Float? = null,
val recentFiles: List = emptyList(),
val allRecentFiles: List = emptyList(),
+ val rawLibraryFiles: List = emptyList(),
val pinnedHomeBookIds: Set = emptySet(),
val pinnedLibraryBookIds: Set = emptySet(),
val libraryFilters: LibraryFilters = LibraryFilters(),
@@ -421,6 +423,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
internalState.copy(
recentFiles = visibleRecentFiles,
allRecentFiles = sortedLibraryFiles,
+ rawLibraryFiles = baseVisibleFiles,
contextualActionItems = validContextualItems,
shelves = allShelves,
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 =
WorkManager.getInstance(appContext).getWorkInfosByTagFlow(ReflowWorker.WORK_NAME)
.map { list ->
@@ -2646,6 +2662,7 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
),
initialCfi = null,
initialBookmarksJson = item.bookmarksJson,
+ initialHighlightsJson = item.highlightsJson,
isLoading = false
)
}
@@ -2847,7 +2864,8 @@ open class MainViewModel(application: Application) : AndroidViewModel(applicatio
selectedEpubUri = uri,
initialLocator = locator,
initialCfi = recentItem?.lastPositionCfi,
- initialBookmarksJson = recentItem?.bookmarksJson
+ initialBookmarksJson = recentItem?.bookmarksJson,
+ initialHighlightsJson = recentItem?.highlightsJson,
)
}
diff --git a/app/src/main/java/com/aryan/reader/SharedComposables.kt b/app/src/main/java/com/aryan/reader/SharedComposables.kt
index a13c953..30a88bc 100644
--- a/app/src/main/java/com/aryan/reader/SharedComposables.kt
+++ b/app/src/main/java/com/aryan/reader/SharedComposables.kt
@@ -48,6 +48,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.text.ClickableText
+import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
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))
}
- val pathText = remember(item.sourceFolderUri, item.displayName) {
- if (item.sourceFolderUri != null) {
+ val context = LocalContext.current
+ val pathText = remember(item.sourceFolderUri, item.uriString, item.displayName, context) {
+ if (item.sourceFolderUri != null && item.uriString != null) {
try {
- val uri = item.sourceFolderUri.toUri()
- val docId = android.provider.DocumentsContract.getTreeDocumentId(uri)
+ val uri = item.uriString.toUri()
+ 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 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("/")
} else ""
+ if (!relativePath.endsWith(item.displayName)) {
+ relativePath = if (relativePath.isEmpty()) item.displayName else "$relativePath/${item.displayName}"
+ }
+
val leadingSlash = if (relativePath.isNotEmpty() && !relativePath.startsWith("/")) "/" else ""
- "/$storageName$leadingSlash$relativePath/${item.displayName}"
+ "/$storageName$leadingSlash$relativePath"
} catch (_: Exception) {
- val decoded = Uri.decode(item.sourceFolderUri)
+ val decoded = Uri.decode(item.uriString)
if (decoded.contains("primary:")) {
- "/Internal storage/${decoded.substringAfter("primary:").removeSuffix("/")}/${item.displayName}"
+ "/Internal storage/${decoded.substringAfter("primary:").substringBeforeLast("/")}/${item.displayName}"
} else {
item.displayName
}
@@ -435,6 +448,7 @@ fun FileInfoDialog(item: RecentFileItem, onDismiss: () -> Unit, onUpdateName: (S
label = "Location",
value = pathText,
maxLines = 4,
+ isScrollable = true,
onCopy = {
clipboardManager.setText(AnnotatedString(pathText))
}
@@ -471,6 +485,7 @@ private fun InfoRowDetailed(
label: String,
value: String,
maxLines: Int = 1,
+ isScrollable: Boolean = false, // ADD THIS
onCopy: (() -> Unit)? = null
) {
Row(
@@ -486,15 +501,23 @@ private fun InfoRowDetailed(
.width(85.dp)
.padding(top = 2.dp)
)
+
+ val scrollModifier = if (isScrollable) {
+ Modifier
+ .heightIn(max = 66.dp)
+ .verticalScroll(androidx.compose.foundation.rememberScrollState())
+ } else Modifier
+
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurface,
- maxLines = maxLines,
- overflow = TextOverflow.Ellipsis,
+ maxLines = if (isScrollable) Int.MAX_VALUE else maxLines,
+ overflow = if (isScrollable) TextOverflow.Clip else TextOverflow.Ellipsis,
modifier = Modifier
.weight(1f)
.padding(top = 2.dp)
+ .then(scrollModifier)
)
if (onCopy != null) {
IconButton(
diff --git a/app/src/main/java/com/aryan/reader/data/AppDatabase.kt b/app/src/main/java/com/aryan/reader/data/AppDatabase.kt
index 146a9b2..d716bfa 100644
--- a/app/src/main/java/com/aryan/reader/data/AppDatabase.kt
+++ b/app/src/main/java/com/aryan/reader/data/AppDatabase.kt
@@ -27,7 +27,7 @@ import androidx.room.TypeConverters
import androidx.room.migration.Migration
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)
abstract class AppDatabase : RoomDatabase() {
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 {
return INSTANCE ?: synchronized(this) {
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_5_6, MIGRATION_6_7, MIGRATION_7_8, MIGRATION_8_9,
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)
.build()
diff --git a/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
index 4358f19..81b3d19 100644
--- a/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
+++ b/app/src/main/java/com/aryan/reader/data/FolderBookMetadata.kt
@@ -19,7 +19,8 @@ data class FolderBookMetadata(
val bookmarksJson: String?,
val locatorBlockIndex: Int?,
val locatorCharOffset: Int?,
- val customName: String?
+ val customName: String?,
+ val highlightsJson: String?
) {
fun toJsonString(): String {
val json = JSONObject()
@@ -38,6 +39,7 @@ data class FolderBookMetadata(
json.put("locatorBlockIndex", locatorBlockIndex ?: -1)
json.put("locatorCharOffset", locatorCharOffset ?: -1)
json.put("customName", customName)
+ json.put("highlightsJson", highlightsJson)
return json.toString()
}
@@ -69,7 +71,8 @@ data class FolderBookMetadata(
bookmarksJson = json.optStringNull("bookmarksJson"),
locatorBlockIndex = json.optIntNull("locatorBlockIndex"),
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,
bookmarksJson = this.bookmarksJson,
sourceFolderUri = sourceFolderUri,
- customName = this.customName
+ customName = this.customName,
+ highlightsJson = this.highlightsJson
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
index 8e7a1b7..2ecbf18 100644
--- a/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileDao.kt
@@ -93,4 +93,7 @@ interface RecentFileDao {
@Query("UPDATE recent_files SET sourceFolderUri = NULL WHERE sourceFolderUri IS NOT NULL")
suspend fun detachAllFolderBooks()
+
+ @Query("UPDATE recent_files SET highlights = :highlightsJson, lastModifiedTimestamp = :timestamp WHERE bookId = :bookId")
+ suspend fun updateHighlights(bookId: String, highlightsJson: String, timestamp: Long)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt b/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
index 0ca11a3..5cfea27 100644
--- a/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileEntity.kt
@@ -49,5 +49,6 @@ data class RecentFileEntity(
val bookmarks: String?,
@ColumnInfo(defaultValue = "NULL") val sourceFolderUri: String?,
@ColumnInfo(defaultValue = "0") val isReflowPreferred: Boolean,
- @ColumnInfo(defaultValue = "NULL") val customName: String?
+ @ColumnInfo(defaultValue = "NULL") val customName: String?,
+ @ColumnInfo(defaultValue = "NULL") val highlights: String?
)
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt b/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
index 65afffc..66c9e13 100644
--- a/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFileItem.kt
@@ -45,7 +45,8 @@ data class RecentFileItem(
val bookmarksJson: String? = null,
val sourceFolderUri: String? = null,
val isReflowPreferred: Boolean = false,
- val customName: String? = null
+ val customName: String? = null,
+ val highlightsJson: String? = null
) {
fun getUri(): Uri? = uriString?.toUri()
}
@@ -73,7 +74,8 @@ fun RecentFileEntity.toRecentFileItem(): RecentFileItem {
bookmarksJson = this.bookmarks,
sourceFolderUri = this.sourceFolderUri,
isReflowPreferred = this.isReflowPreferred,
- customName = this.customName
+ customName = this.customName,
+ highlightsJson = this.highlights
)
}
@@ -100,7 +102,8 @@ fun RecentFileItem.toRecentFileEntity(): RecentFileEntity {
bookmarks = this.bookmarksJson,
sourceFolderUri = this.sourceFolderUri,
isReflowPreferred = this.isReflowPreferred,
- customName = this.customName
+ customName = this.customName,
+ highlights = this.highlightsJson
)
}
@@ -122,7 +125,8 @@ fun RecentFileItem.toBookMetadata(): BookMetadata {
lastModifiedTimestamp = this.lastModifiedTimestamp,
bookmarksJson = this.bookmarksJson,
hasAnnotations = false,
- customName = this.customName
+ customName = this.customName,
+ highlightsJson = this.highlightsJson
)
}
@@ -147,6 +151,7 @@ fun BookMetadata.toRecentFileItem(): RecentFileItem {
lastModifiedTimestamp = this.lastModifiedTimestamp,
isDeleted = this.isDeleted,
bookmarksJson = this.bookmarksJson,
- customName = this.customName
+ customName = this.customName,
+ highlightsJson = this.highlightsJson
)
}
\ No newline at end of file
diff --git a/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
index a8e04d7..1e8367f 100644
--- a/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
+++ b/app/src/main/java/com/aryan/reader/data/RecentFilesRepository.kt
@@ -117,7 +117,8 @@ class RecentFilesRepository(private val context: Context) {
progressPercentage = item.progressPercentage ?: existingItem.progressPercentage,
isRecent = item.isRecent,
isDeleted = item.isDeleted,
- sourceFolderUri = item.sourceFolderUri ?: existingItem.sourceFolderUri
+ sourceFolderUri = item.sourceFolderUri ?: existingItem.sourceFolderUri,
+ highlights = item.highlightsJson ?: existingItem.highlights
)
} else {
item.toRecentFileEntity()
@@ -128,6 +129,12 @@ class RecentFilesRepository(private val context: Context) {
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) {
val entity = recentFileDao.getFileByBookId(bookId) ?: return@withContext
val folderUriString = entity.sourceFolderUri
@@ -135,7 +142,8 @@ class RecentFilesRepository(private val context: Context) {
if (folderUriString != null) {
val hasProgress = (entity.progressPercentage != null && entity.progressPercentage > 0f)
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) {
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,
locatorBlockIndex = entity.locatorBlockIndex,
locatorCharOffset = entity.locatorCharOffset,
- customName = entity.customName
+ customName = entity.customName,
+ highlightsJson = entity.highlights
)
LocalSyncUtils.saveMetadataToFolder(
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
index b7766e0..a4da7f5 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderAnnotations.kt
@@ -215,6 +215,53 @@ fun loadHighlightsFromPrefs(context: Context, bookTitle: String): List {
+ if (jsonString.isNullOrBlank()) return emptyList()
+ val list = mutableListOf()
+ 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): 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 ---
fun processAndAddHighlight(
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
index 9f6ddd3..03ef3f5 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderControls.kt
@@ -142,6 +142,8 @@ fun EpubReaderTopBar(
volumeScrollEnabled: Boolean,
isPageTurnAnimationEnabled: Boolean,
onNavigateBack: () -> Unit,
+ isKeepScreenOn: Boolean,
+ onToggleKeepScreenOn: (Boolean) -> Unit,
onCloseSearch: () -> Unit,
onChangeRenderMode: (RenderMode) -> Unit,
onToggleBookmark: () -> Unit,
@@ -334,6 +336,16 @@ fun EpubReaderTopBar(
)
HorizontalDivider()
+ DropdownMenuItem(
+ text = { Text("Keep Screen On") },
+ onClick = {
+ onToggleKeepScreenOn(!isKeepScreenOn)
+ showMoreMenu = false
+ },
+ trailingIcon = { if (isKeepScreenOn) Icon(Icons.Default.Check, contentDescription = "Enabled") }
+ )
+ HorizontalDivider()
+
DropdownMenuItem(
text = { Text("Auto Scroll") },
enabled = !isTtsActive && currentRenderMode == RenderMode.VERTICAL_SCROLL,
diff --git a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
index 3ac9c1d..880d21d 100644
--- a/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
+++ b/app/src/main/java/com/aryan/reader/epubreader/EpubReaderScreen.kt
@@ -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_MAX_PREFIX = "auto_scroll_local_max_"
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) {
val prefs = context.getSharedPreferences("reader_prefs", Context.MODE_PRIVATE)
@@ -385,10 +396,16 @@ fun EpubReaderScreen(
initialLocator = initialLocator,
initialCfi = initialCfi,
initialBookmarksJson = initialBookmarksJson,
+ initialHighlightsJson = uiState.initialHighlightsJson,
isProUser = isProUser,
onNavigateBack = onNavigateBack,
onSavePosition = onSavePosition,
onBookmarksChanged = onBookmarksChanged,
+ onHighlightsChanged = { json ->
+ uiState.selectedBookId?.let { id ->
+ viewModel.saveHighlights(id, json)
+ }
+ },
onNavigateToPro = onNavigateToPro,
coverImagePath = coverImagePath,
onRenderModeChange = onRenderModeChange,
@@ -419,10 +436,12 @@ fun EpubReaderHost(
initialLocator: Locator?,
initialCfi: String?,
initialBookmarksJson: String?,
+ initialHighlightsJson: String?,
isProUser: Boolean,
onNavigateBack: () -> Unit,
onSavePosition: (locator: Locator, cfiForWebView: String?, progress: Float) -> Unit,
onBookmarksChanged: (bookmarksJson: String) -> Unit,
+ onHighlightsChanged: (highlightsJson: String) -> Unit,
onNavigateToPro: () -> Unit,
coverImagePath: String?,
onRenderModeChange: (RenderMode) -> Unit,
@@ -479,9 +498,22 @@ fun EpubReaderHost(
)
}
- val userHighlights = remember {
+ val userHighlights = remember(epubBook.title) {
mutableStateListOf().apply {
- addAll(loadHighlightsFromPrefs(context, epubBook.title))
+ if (initialHighlightsJson != null) {
+ addAll(parseHighlightsJson(initialHighlightsJson))
+ } else {
+ 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)
}
}
@@ -586,10 +618,6 @@ fun EpubReaderHost(
}
}
- LaunchedEffect(userHighlights.size, userHighlights.toList()) {
- saveHighlightsToPrefs(context, epubBook.title, userHighlights)
- }
-
// Dictionary
var showAiDefinitionPopup by remember { mutableStateOf(false) }
var selectedTextForAi by remember { mutableStateOf(null) }
@@ -735,6 +763,7 @@ fun EpubReaderHost(
var searchHighlightTarget by remember { mutableStateOf(null) }
var lastHighlightClickTime by remember { mutableLongStateOf(0L) }
+ var lastScrollHideTime by remember { mutableLongStateOf(0L) }
var webViewRefForTts by remember { mutableStateOf(null) }
@@ -932,6 +961,15 @@ fun EpubReaderHost(
var isMusicianMode by remember { mutableStateOf(loadMusicianMode(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) {
onDispose {
Timber.d("Disposing sample MediaPlayer.")
@@ -998,6 +1036,15 @@ fun EpubReaderHost(
}
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 {
derivedStateOf {
if (currentRenderMode == RenderMode.PAGINATED) {
@@ -2201,20 +2248,26 @@ fun EpubReaderHost(
if (volumeScrollEnabled && !searchState.isSearchActive) {
containerFocusRequester.requestFocus()
}
- if (showBars || showFormatAdjustmentBars) {
- showBars = false
- showFormatAdjustmentBars = false
- Timber.d("Chapter tapped, hiding all bars.")
+
+ if (System.currentTimeMillis() - lastScrollHideTime < 250) {
+ Timber.d("Ignoring tap toggle because bars were just hidden by scroll (sloppy tap).")
} else {
- showBars = true
- Timber.d("Chapter tapped, showing main bars.")
+ if (showBars || showFormatAdjustmentBars) {
+ showBars = false
+ showFormatAdjustmentBars = false
+ Timber.d("Chapter tapped, hiding all bars.")
+ } else {
+ showBars = true
+ Timber.d("Chapter tapped, showing main bars.")
+ }
}
}
},
onPotentialScroll = {
- if (showBars) {
+ if (showBars || showFormatAdjustmentBars) {
showBars = false
showFormatAdjustmentBars = false
+ lastScrollHideTime = System.currentTimeMillis() // Added
Timber.d("Scroll/Drag detected, hiding bars.")
}
if (isAutoScrollModeActive && isAutoScrollPlaying) {
@@ -2947,7 +3000,7 @@ fun EpubReaderHost(
modifier = Modifier
.fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT)
- .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f))
+ .background(infoBarBgColor)
.padding(bottom = bottomPadding)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
@@ -2958,7 +3011,7 @@ fun EpubReaderHost(
Text(
text = "$chapterTitle ($currentPageInChapter/$totalPagesInCurrentChapter)",
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -2971,7 +3024,7 @@ fun EpubReaderHost(
Text(
text = "%.1f%%".format(currentBookProgress),
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd)
)
@@ -2990,7 +3043,7 @@ fun EpubReaderHost(
modifier = Modifier
.fillMaxWidth()
.height(PAGE_INFO_BAR_HEIGHT)
- .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.85f))
+ .background(infoBarBgColor)
.padding(bottom = bottomPadding)
.padding(horizontal = 16.dp),
contentAlignment = Alignment.Center
@@ -3019,7 +3072,7 @@ fun EpubReaderHost(
Text(
text = textToShow,
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -3053,7 +3106,7 @@ fun EpubReaderHost(
Text(
text = "%.1f%%".format(displayProgress),
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd)
)
@@ -3068,7 +3121,7 @@ fun EpubReaderHost(
Text(
text = "%.1f%%".format(percentage),
style = MaterialTheme.typography.bodySmall,
- color = MaterialTheme.colorScheme.onSurfaceVariant,
+ color = effectiveText.copy(alpha = 0.8f),
textAlign = TextAlign.End,
modifier = Modifier.align(Alignment.CenterEnd)
)
@@ -3283,6 +3336,11 @@ fun EpubReaderHost(
volumeScrollEnabled = volumeScrollEnabled,
isPageTurnAnimationEnabled = isPageTurnAnimationEnabled,
onNavigateBack = { triggerSaveAndExit() },
+ isKeepScreenOn = isKeepScreenOn,
+ onToggleKeepScreenOn = { enabled ->
+ isKeepScreenOn = enabled
+ saveKeepScreenOn(context, enabled)
+ },
onCloseSearch = {
searchState.isSearchActive = false
searchState.onQueryChange("")
diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt b/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt
index d556cff..b10cbe9 100644
--- a/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt
+++ b/app/src/main/java/com/aryan/reader/pdf/PdfToHtmlGenerator.kt
@@ -5,9 +5,7 @@ import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
-import io.legere.pdfiumandroid.PdfiumCore
import io.legere.pdfiumandroid.suspend.PdfDocumentKt
-import io.legere.pdfiumandroid.suspend.PdfiumCoreKt
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import timber.log.Timber
@@ -219,9 +217,17 @@ object PdfToHtmlGenerator {
val c = rawText[i]
val code = c.code
- if (code == 0 || code == 13) continue
+ if (code == 0) continue
+
+ if (c == '\r') {
+ commitLine()
+ continue
+ }
if (c == '\n') {
+ if (i > 0 && rawText[i - 1] == '\r') {
+ continue
+ }
commitLine()
continue
}
@@ -292,7 +298,7 @@ object PdfToHtmlGenerator {
}
buildPageHtml(pageNumber, finalElements, headerFooterStrings)
- } ?: buildEmptyPageSection(pageNumber)
+ }
} ?: buildEmptyPageSection(pageNumber)
} catch (e: Exception) {
Timber.tag(TAG).w(e, "Error extracting page $pageIdx")
@@ -339,9 +345,12 @@ object PdfToHtmlGenerator {
var inParagraph = false
var inUl = false
var inOl = false
+ var inLi = false
fun closeParagraph() { if (inParagraph) { sb.append("
\n"); inParagraph = false } }
+ fun closeLi() { if (inLi) { sb.append("\n"); inLi = false } }
fun closeList() {
+ closeLi()
if (inUl) { sb.append("\n"); inUl = false }
if (inOl) { sb.append("\n"); inOl = false }
}
@@ -404,29 +413,40 @@ object PdfToHtmlGenerator {
sb.append("<$tag>${renderSpans(line.spans, insideHeading = true)}$tag>\n")
}
isBullet -> {
- closeParagraph()
+ closeParagraph(); closeLi()
if (inOl) { sb.append("\n"); inOl = false }
if (!inUl) { sb.append("\n"); inUl = true }
val content = trimmed.removePrefix("•").removePrefix("▪").removePrefix("◦").removePrefix("–").removePrefix("- ").trim()
- sb.append("- ${content.escapeHtml()}
\n")
+ sb.append("- ${content.escapeHtml()}")
+ inLi = true
}
numberedMatch -> {
- closeParagraph()
+ closeParagraph(); closeLi()
if (inUl) { sb.append("
\n"); inUl = false }
if (!inOl) { sb.append("\n"); inOl = true }
val content = trimmed.substringAfter(" ").trim()
- sb.append("- ${content.escapeHtml()}
\n")
+ sb.append("- ${content.escapeHtml()}")
+ inLi = true
}
shouldBreakParagraph -> {
- closeList()
- if (!inParagraph) { sb.append("
"); inParagraph = true }
- sb.append(renderSpans(line.spans))
- closeParagraph()
+ if (inLi) {
+ sb.append(" ").append(renderSpans(line.spans))
+ closeLi()
+ } else {
+ closeList()
+ if (!inParagraph) { sb.append("
"); inParagraph = true }
+ sb.append(renderSpans(line.spans))
+ closeParagraph()
+ }
}
else -> {
- closeList()
- if (!inParagraph) { sb.append("
"); inParagraph = true } else sb.append(" ")
- sb.append(renderSpans(line.spans))
+ if (inLi) {
+ sb.append(" ").append(renderSpans(line.spans))
+ } else {
+ closeList()
+ if (!inParagraph) { sb.append("
"); inParagraph = true } else sb.append(" ")
+ sb.append(renderSpans(line.spans))
+ }
}
}
}
diff --git a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt
index 27ef2cc..2f90416 100644
--- a/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt
+++ b/app/src/main/java/com/aryan/reader/pdf/PdfViewerScreen.kt
@@ -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_SEARCH_PKG = "external_search_package"
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) {
val prefs = context.getSharedPreferences(SETTINGS_PREFS_NAME, Context.MODE_PRIVATE)
@@ -1152,7 +1163,7 @@ fun PdfViewerScreen(
?: pdfUri.lastPathSegment ?: "Document.pdf"
}
}
-
+ val view = LocalView.current
var isDockDragging by remember { mutableStateOf(false) }
var initialScrollDone by remember { mutableStateOf(false) }
@@ -1167,6 +1178,14 @@ fun PdfViewerScreen(
var isStylusOnlyMode by remember { mutableStateOf(loadStylusOnlyMode(context)) }
var currentTtsMode by remember { mutableStateOf(loadTtsMode(context)) }
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 useOnlineDictionary by remember { mutableStateOf(loadUseOnlineDict(context)) }
@@ -1338,7 +1357,6 @@ fun PdfViewerScreen(
}
}
- val view = LocalView.current
val window = (view.context as? Activity)?.window
LaunchedEffect(isFullScreen) {
if (window != null) {
@@ -5423,6 +5441,23 @@ fun PdfViewerScreen(
}
})
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(
text = { Text("Auto Scroll") },
enabled = !isTtsSessionActive && displayMode == DisplayMode.VERTICAL_SCROLL,
diff --git a/app/src/oss/java/com/aryan/reader/data/FirestoreRepository.kt b/app/src/oss/java/com/aryan/reader/data/FirestoreRepository.kt
index 437bbb6..717ed29 100644
--- a/app/src/oss/java/com/aryan/reader/data/FirestoreRepository.kt
+++ b/app/src/oss/java/com/aryan/reader/data/FirestoreRepository.kt
@@ -23,7 +23,8 @@ data class BookMetadata(
val lastModifiedTimestamp: Long = 0L,
val bookmarksJson: String? = null,
val hasAnnotations: Boolean = false,
- val customName: String? = null
+ val customName: String? = null,
+ val highlightsJson: String? = null
)
data class DeviceItem(