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:
parent
60dacf9c12
commit
4c5cd20b21
17 changed files with 404 additions and 84 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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?
|
||||||
)
|
)
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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("")
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue