Reading progress, persist library mode and improve scan feedback
This commit is contained in:
parent
e8b18ec472
commit
dbf63c351c
@ -473,6 +473,18 @@ actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) = withCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFullScreen(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.getAppFlag(ReaderFullScreenFlag)?.toBooleanStrictOrNull() ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFullScreen(fullScreen: Boolean) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(ReaderFullScreenFlag, fullScreen.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
||||||
@ -485,6 +497,20 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.getAppFlag(LibraryFilterFlag)
|
||||||
|
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
||||||
|
?: LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual suspend fun saveLibraryFilter(filter: LibraryFilter) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(LibraryFilterFlag, filter.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
|
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
|
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
|
||||||
@ -868,6 +894,8 @@ private val SearchPrefixRegex = Regex("""[\p{L}\p{N}]+""")
|
|||||||
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
||||||
private const val ThemeModeFlag = "theme_mode"
|
private const val ThemeModeFlag = "theme_mode"
|
||||||
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
||||||
|
private const val ReaderFullScreenFlag = "reader_full_screen"
|
||||||
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
||||||
|
private const val LibraryFilterFlag = "library_filter"
|
||||||
private const val AppLocaleTagFlag = "app_locale_tag"
|
private const val AppLocaleTagFlag = "app_locale_tag"
|
||||||
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
||||||
|
|||||||
@ -27,13 +27,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.LifecycleEventObserver
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||||
import net.sergeych.toread.fb2.Fb2Format
|
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
private const val DefaultToastDurationMillis = 1_600L
|
private const val DefaultToastDurationMillis = 1_600L
|
||||||
private const val DeleteUndoDurationMillis = 5_000L
|
private const val DeleteUndoDurationMillis = 5_000L
|
||||||
|
private const val ScanResultToastDurationMillis = 5_000L
|
||||||
private const val BackgroundLibraryPageSize = 50
|
private const val BackgroundLibraryPageSize = 50
|
||||||
|
|
||||||
private data class AppToastData(
|
private data class AppToastData(
|
||||||
@ -220,7 +220,12 @@ private fun BookReaderApp(
|
|||||||
is AppState.BookInfo -> current.scanPath
|
is AppState.BookInfo -> current.scanPath
|
||||||
is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
|
is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
|
||||||
}
|
}
|
||||||
state = AppState.Library(emptyList(), scanPath, requestResult.exceptionOrNull()?.message ?: strings.couldNotOpenLibrary)
|
state = AppState.Library(
|
||||||
|
emptyList(),
|
||||||
|
scanPath,
|
||||||
|
requestResult.exceptionOrNull()?.message ?: strings.couldNotOpenLibrary,
|
||||||
|
loadLibraryFilter(),
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
val request = requestResult.getOrNull() ?: continue
|
val request = requestResult.getOrNull() ?: continue
|
||||||
@ -232,7 +237,7 @@ private fun BookReaderApp(
|
|||||||
is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
|
is AppState.Error, AppState.LoadingStartup -> defaultLibraryScanPath().orEmpty()
|
||||||
}
|
}
|
||||||
val nextState = runCatching {
|
val nextState = runCatching {
|
||||||
val book = Fb2Format.parse(request.bytes, request.displayName)
|
val book = parseBookInBackground(request.bytes, request.displayName)
|
||||||
saveActiveReadingFileId(request.id)
|
saveActiveReadingFileId(request.id)
|
||||||
AppState.Reader(
|
AppState.Reader(
|
||||||
fileId = request.id,
|
fileId = request.id,
|
||||||
@ -241,11 +246,11 @@ private fun BookReaderApp(
|
|||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName))
|
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), loadLibraryFilter())
|
||||||
}
|
}
|
||||||
libraryBackState = when (val current = state) {
|
libraryBackState = when (val current = state) {
|
||||||
is AppState.Library -> current
|
is AppState.Library -> current
|
||||||
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message)
|
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message, current.selectedFilter)
|
||||||
is AppState.Reader, is AppState.BookInfo -> libraryBackState
|
is AppState.Reader, is AppState.BookInfo -> libraryBackState
|
||||||
is AppState.Error, AppState.LoadingStartup -> null
|
is AppState.Error, AppState.LoadingStartup -> null
|
||||||
}
|
}
|
||||||
@ -362,7 +367,7 @@ private fun BookReaderApp(
|
|||||||
downloadsRescanRequestGeneration += 1
|
downloadsRescanRequestGeneration += 1
|
||||||
backState
|
backState
|
||||||
}
|
}
|
||||||
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message)
|
is AppState.Scan -> AppState.Library(current.items, current.scanPath, current.message, current.selectedFilter)
|
||||||
is AppState.Error -> AppState.LoadingStartup
|
is AppState.Error -> AppState.LoadingStartup
|
||||||
is AppState.Library, AppState.LoadingStartup -> current
|
is AppState.Library, AppState.LoadingStartup -> current
|
||||||
}
|
}
|
||||||
@ -380,7 +385,23 @@ private fun BookReaderApp(
|
|||||||
onBack = ::navigateBack,
|
onBack = ::navigateBack,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun startScan(path: String) {
|
fun setLibraryFilter(filter: LibraryFilter) {
|
||||||
|
state = when (val current = state) {
|
||||||
|
is AppState.Library -> current.copy(selectedFilter = filter)
|
||||||
|
is AppState.Scan -> current.copy(selectedFilter = filter)
|
||||||
|
else -> current
|
||||||
|
}
|
||||||
|
libraryBackState = libraryBackState?.copy(selectedFilter = filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveAndSetLibraryFilter(filter: LibraryFilter) {
|
||||||
|
setLibraryFilter(filter)
|
||||||
|
scope.launch {
|
||||||
|
saveLibraryFilter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startScan(path: String, userInitiated: Boolean) {
|
||||||
if (scanJob?.isActive == true) return
|
if (scanJob?.isActive == true) return
|
||||||
activeScan = LibraryScanProgress(0, 0, 0, 0)
|
activeScan = LibraryScanProgress(0, 0, 0, 0)
|
||||||
scanJob = scope.launch {
|
scanJob = scope.launch {
|
||||||
@ -400,19 +421,34 @@ private fun BookReaderApp(
|
|||||||
},
|
},
|
||||||
onFailure = { it.message ?: strings.scanFailed },
|
onFailure = { it.message ?: strings.scanFailed },
|
||||||
)
|
)
|
||||||
|
val importedFiles = report.getOrNull()?.importedFiles
|
||||||
|
when {
|
||||||
|
importedFiles == null -> Unit
|
||||||
|
importedFiles > 0 -> {
|
||||||
|
onShowToast(
|
||||||
|
strings.newBooksAdded(importedFiles),
|
||||||
|
strings.show,
|
||||||
|
{ saveAndSetLibraryFilter(LibraryFilter.RecentlyAdded) },
|
||||||
|
ScanResultToastDurationMillis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
userInitiated -> {
|
||||||
|
onShowToast(strings.noNewBooksFound, null, null, DefaultToastDurationMillis)
|
||||||
|
}
|
||||||
|
}
|
||||||
scanJob = null
|
scanJob = null
|
||||||
activeScan = null
|
activeScan = null
|
||||||
state = when (val current = state) {
|
state = when (val current = state) {
|
||||||
is AppState.Library -> if (report.getOrNull()?.hasLibraryChanges() == true) {
|
is AppState.Library -> current.copy(
|
||||||
loadLibraryState(message, path)
|
scanPath = path,
|
||||||
} else {
|
message = message,
|
||||||
current.copy(scanPath = path, message = message)
|
)
|
||||||
}
|
is AppState.Scan -> AppState.Library(
|
||||||
is AppState.Scan -> if (report.getOrNull()?.hasLibraryChanges() == true) {
|
current.items,
|
||||||
loadLibraryState(message, path)
|
path,
|
||||||
} else {
|
message,
|
||||||
AppState.Library(current.items, path, message)
|
selectedFilter = current.selectedFilter,
|
||||||
}
|
)
|
||||||
AppState.LoadingStartup -> loadLibraryState(message, path)
|
AppState.LoadingStartup -> loadLibraryState(message, path)
|
||||||
is AppState.Reader -> current.copy(message = message)
|
is AppState.Reader -> current.copy(message = message)
|
||||||
is AppState.BookInfo -> current.copy(message = message)
|
is AppState.BookInfo -> current.copy(message = message)
|
||||||
@ -426,7 +462,7 @@ private fun BookReaderApp(
|
|||||||
if (!loadScanDownloadsAutomatically() || !loadDownloadsWasScanned()) return@LaunchedEffect
|
if (!loadScanDownloadsAutomatically() || !loadDownloadsWasScanned()) return@LaunchedEffect
|
||||||
val path = downloadsScanPath() ?: return@LaunchedEffect
|
val path = downloadsScanPath() ?: return@LaunchedEffect
|
||||||
state = state.withMessage(strings.scanningDownloads)
|
state = state.withMessage(strings.scanningDownloads)
|
||||||
startScan(path)
|
startScan(path, userInitiated = false)
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(Modifier.fillMaxSize()) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
@ -448,9 +484,15 @@ private fun BookReaderApp(
|
|||||||
}
|
}
|
||||||
state = next
|
state = next
|
||||||
},
|
},
|
||||||
|
onLibraryFilterChange = ::setLibraryFilter,
|
||||||
onNavigateToScan = {
|
onNavigateToScan = {
|
||||||
libraryBackState = null
|
libraryBackState = null
|
||||||
state = AppState.Scan(libraryState.items, libraryState.scanPath, libraryState.message)
|
state = AppState.Scan(
|
||||||
|
libraryState.items,
|
||||||
|
libraryState.scanPath,
|
||||||
|
libraryState.message,
|
||||||
|
libraryState.selectedFilter,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
onDeleteRequested = ::requestDelete,
|
onDeleteRequested = ::requestDelete,
|
||||||
)
|
)
|
||||||
@ -464,8 +506,8 @@ private fun BookReaderApp(
|
|||||||
activeScan = activeScan,
|
activeScan = activeScan,
|
||||||
onStateChange = { state = it },
|
onStateChange = { state = it },
|
||||||
onStartScan = { path ->
|
onStartScan = { path ->
|
||||||
startScan(path)
|
startScan(path, userInitiated = true)
|
||||||
state = AppState.Library(current.items, path, strings.scanning)
|
state = AppState.Library(current.items, path, strings.scanning, current.selectedFilter)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
is AppState.Reader -> BookView(
|
is AppState.Reader -> BookView(
|
||||||
@ -516,9 +558,6 @@ internal data class ViewedBookImage(
|
|||||||
val title: String,
|
val title: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun LibraryScanReport.hasLibraryChanges(): Boolean =
|
|
||||||
importedFiles > 0
|
|
||||||
|
|
||||||
private fun AppState.withMessage(message: String): AppState =
|
private fun AppState.withMessage(message: String): AppState =
|
||||||
when (this) {
|
when (this) {
|
||||||
is AppState.Library -> copy(message = message)
|
is AppState.Library -> copy(message = message)
|
||||||
|
|||||||
@ -2,6 +2,8 @@ package net.sergeych.toread
|
|||||||
|
|
||||||
import net.sergeych.toread.fb2.Fb2Book
|
import net.sergeych.toread.fb2.Fb2Book
|
||||||
import net.sergeych.toread.fb2.Fb2Format
|
import net.sergeych.toread.fb2.Fb2Format
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
internal sealed interface AppState {
|
internal sealed interface AppState {
|
||||||
data object LoadingStartup : AppState
|
data object LoadingStartup : AppState
|
||||||
@ -9,12 +11,14 @@ internal sealed interface AppState {
|
|||||||
val items: List<LibraryItem>,
|
val items: List<LibraryItem>,
|
||||||
val scanPath: String,
|
val scanPath: String,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
|
val selectedFilter: LibraryFilter = LibraryFilter.MyLibrary,
|
||||||
) : AppState
|
) : AppState
|
||||||
|
|
||||||
data class Scan(
|
data class Scan(
|
||||||
val items: List<LibraryItem>,
|
val items: List<LibraryItem>,
|
||||||
val scanPath: String,
|
val scanPath: String,
|
||||||
val message: String? = null,
|
val message: String? = null,
|
||||||
|
val selectedFilter: LibraryFilter = LibraryFilter.MyLibrary,
|
||||||
) : AppState
|
) : AppState
|
||||||
|
|
||||||
data class Reader(
|
data class Reader(
|
||||||
@ -42,12 +46,13 @@ internal suspend fun loadStartupState(): AppState {
|
|||||||
} catch (t: Throwable) {
|
} catch (t: Throwable) {
|
||||||
return AppState.Error(t.message ?: strings.couldNotOpenLibrary)
|
return AppState.Error(t.message ?: strings.couldNotOpenLibrary)
|
||||||
}
|
}
|
||||||
|
val libraryFilter = loadLibraryFilter()
|
||||||
val platformRequest = runCatching { loadPlatformOpenBookRequest() }.getOrElse {
|
val platformRequest = runCatching { loadPlatformOpenBookRequest() }.getOrElse {
|
||||||
return AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpenLibrary)
|
return AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpenLibrary, libraryFilter)
|
||||||
}
|
}
|
||||||
platformRequest?.let { request ->
|
platformRequest?.let { request ->
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val book = Fb2Format.parse(request.bytes, request.displayName)
|
val book = parseBookInBackground(request.bytes, request.displayName)
|
||||||
saveActiveReadingFileId(request.id)
|
saveActiveReadingFileId(request.id)
|
||||||
AppState.Reader(
|
AppState.Reader(
|
||||||
fileId = request.id,
|
fileId = request.id,
|
||||||
@ -56,36 +61,46 @@ internal suspend fun loadStartupState(): AppState {
|
|||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName))
|
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), libraryFilter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val activeFileId = loadActiveReadingFileId() ?: return AppState.Library(emptyList(), scanPath)
|
val activeFileId = loadActiveReadingFileId() ?: return AppState.Library(emptyList(), scanPath, selectedFilter = libraryFilter)
|
||||||
val item = loadLibraryItem(activeFileId)
|
val item = loadLibraryItem(activeFileId)
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
saveActiveReadingFileId(null)
|
saveActiveReadingFileId(null)
|
||||||
return AppState.Library(emptyList(), scanPath)
|
return AppState.Library(emptyList(), scanPath, selectedFilter = libraryFilter)
|
||||||
}
|
}
|
||||||
return runCatching {
|
return runCatching {
|
||||||
val bytes = openLibraryBook(activeFileId) ?: error(strings.bookFileNotAvailable)
|
val bytes = openLibraryBook(activeFileId) ?: error(strings.bookFileNotAvailable)
|
||||||
AppState.Reader(
|
AppState.Reader(
|
||||||
fileId = activeFileId,
|
fileId = activeFileId,
|
||||||
book = Fb2Format.parse(bytes, item.storageUri ?: item.title),
|
book = parseBookInBackground(bytes, item.storageUri ?: item.title),
|
||||||
libraryItems = emptyList(),
|
libraryItems = emptyList(),
|
||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
saveActiveReadingFileId(null)
|
saveActiveReadingFileId(null)
|
||||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotReopenLastBook())
|
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotReopenLastBook(), libraryFilter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun loadLibraryState(message: String? = null, scanPath: String? = null): AppState =
|
internal suspend fun loadLibraryState(
|
||||||
|
message: String? = null,
|
||||||
|
scanPath: String? = null,
|
||||||
|
selectedFilter: LibraryFilter? = null,
|
||||||
|
): AppState =
|
||||||
runCatching {
|
runCatching {
|
||||||
AppState.Library(
|
AppState.Library(
|
||||||
items = emptyList(),
|
items = emptyList(),
|
||||||
scanPath = scanPath ?: defaultLibraryScanPath().orEmpty(),
|
scanPath = scanPath ?: defaultLibraryScanPath().orEmpty(),
|
||||||
message = message,
|
message = message,
|
||||||
|
selectedFilter = selectedFilter ?: loadLibraryFilter(),
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Error(it.message ?: strings.couldNotOpenLibrary)
|
AppState.Error(it.message ?: strings.couldNotOpenLibrary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal suspend fun parseBookInBackground(input: ByteArray, fileName: String? = null): Fb2Book =
|
||||||
|
withContext(Dispatchers.Default) {
|
||||||
|
Fb2Format.parse(input, fileName)
|
||||||
|
}
|
||||||
|
|||||||
@ -159,10 +159,18 @@ expect suspend fun loadReaderFontSettings(): ReaderFontSettings
|
|||||||
|
|
||||||
expect suspend fun saveReaderFontSettings(settings: ReaderFontSettings)
|
expect suspend fun saveReaderFontSettings(settings: ReaderFontSettings)
|
||||||
|
|
||||||
|
expect suspend fun loadReaderFullScreen(): Boolean
|
||||||
|
|
||||||
|
expect suspend fun saveReaderFullScreen(fullScreen: Boolean)
|
||||||
|
|
||||||
expect suspend fun loadScanDownloadsAutomatically(): Boolean
|
expect suspend fun loadScanDownloadsAutomatically(): Boolean
|
||||||
|
|
||||||
expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)
|
expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)
|
||||||
|
|
||||||
|
internal expect suspend fun loadLibraryFilter(): LibraryFilter
|
||||||
|
|
||||||
|
internal expect suspend fun saveLibraryFilter(filter: LibraryFilter)
|
||||||
|
|
||||||
expect suspend fun loadAppLocaleTag(): String?
|
expect suspend fun loadAppLocaleTag(): String?
|
||||||
|
|
||||||
expect suspend fun saveAppLocaleTag(localeTag: String?)
|
expect suspend fun saveAppLocaleTag(localeTag: String?)
|
||||||
|
|||||||
@ -83,7 +83,6 @@ import androidx.compose.ui.platform.LocalFocusManager
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.sergeych.toread.fb2.Fb2Format
|
|
||||||
import net.sergeych.toread.storage.BookReadingStatus
|
import net.sergeych.toread.storage.BookReadingStatus
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
@ -97,6 +96,7 @@ internal fun LibraryScreen(
|
|||||||
itemRefreshRequest: LibraryItemRefreshRequest?,
|
itemRefreshRequest: LibraryItemRefreshRequest?,
|
||||||
hiddenFileIds: Set<String>,
|
hiddenFileIds: Set<String>,
|
||||||
onStateChange: (AppState) -> Unit,
|
onStateChange: (AppState) -> Unit,
|
||||||
|
onLibraryFilterChange: (LibraryFilter) -> Unit,
|
||||||
onNavigateToScan: () -> Unit,
|
onNavigateToScan: () -> Unit,
|
||||||
onDeleteRequested: (
|
onDeleteRequested: (
|
||||||
request: LibraryDeleteRequest,
|
request: LibraryDeleteRequest,
|
||||||
@ -107,6 +107,7 @@ internal fun LibraryScreen(
|
|||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
var busy by remember { mutableStateOf(false) }
|
var busy by remember { mutableStateOf(false) }
|
||||||
|
var openingBook by remember { mutableStateOf(false) }
|
||||||
var message by remember(state.message) { mutableStateOf(state.message) }
|
var message by remember(state.message) { mutableStateOf(state.message) }
|
||||||
var items by remember(state.items) { mutableStateOf(state.items) }
|
var items by remember(state.items) { mutableStateOf(state.items) }
|
||||||
var loadingLibrary by remember(state.items) { mutableStateOf(false) }
|
var loadingLibrary by remember(state.items) { mutableStateOf(false) }
|
||||||
@ -119,8 +120,7 @@ internal fun LibraryScreen(
|
|||||||
var searchFocused by remember { mutableStateOf(false) }
|
var searchFocused by remember { mutableStateOf(false) }
|
||||||
var searchResults by remember { mutableStateOf<List<LibraryItem>>(emptyList()) }
|
var searchResults by remember { mutableStateOf<List<LibraryItem>>(emptyList()) }
|
||||||
var searching by remember { mutableStateOf(false) }
|
var searching by remember { mutableStateOf(false) }
|
||||||
var selectedFilter by remember(state.scanPath) { mutableStateOf(LibraryFilter.ReadingNow) }
|
var selectedFilter by remember(state.scanPath, state.selectedFilter) { mutableStateOf(state.selectedFilter) }
|
||||||
var filterChosenByUser by remember(state.scanPath) { mutableStateOf(false) }
|
|
||||||
val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
|
val coverCache = remember { mutableStateMapOf<String, LibraryCover?>() }
|
||||||
val searchActive = searchText.isNotBlank()
|
val searchActive = searchText.isNotBlank()
|
||||||
val libraryItems = items.filterNot { it.fileId in hiddenFileIds }
|
val libraryItems = items.filterNot { it.fileId in hiddenFileIds }
|
||||||
@ -302,13 +302,6 @@ internal fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(searchActive, loadingLibrary, libraryItems, recentlyAdded) {
|
|
||||||
val libraryDataLoaded = libraryItems.isNotEmpty() || recentlyAdded.isNotEmpty()
|
|
||||||
if (!filterChosenByUser && !searchActive && !loadingLibrary && libraryDataLoaded) {
|
|
||||||
selectedFilter = defaultLibraryFilter(libraryItems, recentlyAdded)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
autoScanDownloads = loadScanDownloadsAutomatically()
|
autoScanDownloads = loadScanDownloadsAutomatically()
|
||||||
}
|
}
|
||||||
@ -328,6 +321,7 @@ internal fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@ -341,8 +335,11 @@ internal fun LibraryScreen(
|
|||||||
LibraryFilterSelect(
|
LibraryFilterSelect(
|
||||||
selected = selectedFilter,
|
selected = selectedFilter,
|
||||||
onSelected = {
|
onSelected = {
|
||||||
filterChosenByUser = true
|
|
||||||
selectedFilter = it
|
selectedFilter = it
|
||||||
|
onLibraryFilterChange(it)
|
||||||
|
scope.launch {
|
||||||
|
saveLibraryFilter(it)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -505,10 +502,11 @@ internal fun LibraryScreen(
|
|||||||
onOpen = {
|
onOpen = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
busy = true
|
busy = true
|
||||||
|
openingBook = true
|
||||||
try {
|
try {
|
||||||
val next = runCatching {
|
val next = runCatching {
|
||||||
val bytes = openLibraryBook(item.fileId) ?: error(strings.bookFileNotAvailable)
|
val bytes = openLibraryBook(item.fileId) ?: error(strings.bookFileNotAvailable)
|
||||||
val book = Fb2Format.parse(bytes, item.storageUri ?: item.title)
|
val book = parseBookInBackground(bytes, item.storageUri ?: item.title)
|
||||||
var readerLibraryItems = visibleItems
|
var readerLibraryItems = visibleItems
|
||||||
refreshLibraryItemFromParsedBook(item.fileId, book)?.let { updatedItem ->
|
refreshLibraryItemFromParsedBook(item.fileId, book)?.let { updatedItem ->
|
||||||
items = items.replaceLibraryItem(updatedItem)
|
items = items.replaceLibraryItem(updatedItem)
|
||||||
@ -534,10 +532,16 @@ internal fun LibraryScreen(
|
|||||||
message = message,
|
message = message,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(visibleItems, state.scanPath, it.message ?: strings.couldNotOpenBook)
|
AppState.Library(
|
||||||
|
visibleItems,
|
||||||
|
state.scanPath,
|
||||||
|
it.message ?: strings.couldNotOpenBook,
|
||||||
|
state.selectedFilter,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
onStateChange(next)
|
onStateChange(next)
|
||||||
} finally {
|
} finally {
|
||||||
|
openingBook = false
|
||||||
busy = false
|
busy = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -676,6 +680,10 @@ internal fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (openingBook) {
|
||||||
|
LoadingOverlay(strings.loadingOpeningBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -1061,7 +1069,7 @@ private data class LibraryItemActions(
|
|||||||
val onDelete: () -> Unit,
|
val onDelete: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
private enum class LibraryFilter {
|
internal enum class LibraryFilter {
|
||||||
ReadingNow,
|
ReadingNow,
|
||||||
RecentlyAdded,
|
RecentlyAdded,
|
||||||
MyLibrary,
|
MyLibrary,
|
||||||
@ -1178,16 +1186,6 @@ private val LibraryFilter.label: String
|
|||||||
LibraryFilter.NotInterested -> strings.notInterested
|
LibraryFilter.NotInterested -> strings.notInterested
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun defaultLibraryFilter(
|
|
||||||
libraryItems: List<LibraryItem>,
|
|
||||||
recentlyAddedItems: List<LibraryItem>,
|
|
||||||
): LibraryFilter =
|
|
||||||
when {
|
|
||||||
libraryItems.any { it.readingStatus == BookReadingStatus.READING } -> LibraryFilter.ReadingNow
|
|
||||||
recentlyAddedItems.any { it.readingStatus == BookReadingStatus.NEW } -> LibraryFilter.RecentlyAdded
|
|
||||||
else -> LibraryFilter.MyLibrary
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LibraryCover(
|
private fun LibraryCover(
|
||||||
item: LibraryItem,
|
item: LibraryItem,
|
||||||
|
|||||||
@ -139,6 +139,7 @@ internal open class AppStrings {
|
|||||||
open val readerMenu = "Book reader menu"
|
open val readerMenu = "Book reader menu"
|
||||||
open val info = "Info..."
|
open val info = "Info..."
|
||||||
open val readerSettings = "Settings"
|
open val readerSettings = "Settings"
|
||||||
|
open val fullScreen = "Full screen"
|
||||||
open val readerFontSizeIncrease = "Increase font size"
|
open val readerFontSizeIncrease = "Increase font size"
|
||||||
open val readerFontSizeDecrease = "Decrease font size"
|
open val readerFontSizeDecrease = "Decrease font size"
|
||||||
open val readerLineHeightIncrease = "Increase line spacing"
|
open val readerLineHeightIncrease = "Increase line spacing"
|
||||||
@ -187,6 +188,10 @@ internal open class AppStrings {
|
|||||||
open fun couldNotReopenLastBook(): String = "Could not reopen last book."
|
open fun couldNotReopenLastBook(): String = "Could not reopen last book."
|
||||||
open fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
open fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||||
"Scanned $scanned, imported $imported, skipped $skipped, failed $failed."
|
"Scanned $scanned, imported $imported, skipped $skipped, failed $failed."
|
||||||
|
open val noNewBooksFound = "Nothing new has been found."
|
||||||
|
open val show = "Show"
|
||||||
|
open fun newBooksAdded(count: Int): String =
|
||||||
|
if (count == 1) "1 new book added." else "$count new books added."
|
||||||
open fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
open fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
||||||
"Rescanned $scanned, updated $updated, failed $failed."
|
"Rescanned $scanned, updated $updated, failed $failed."
|
||||||
open fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
open fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||||
@ -345,6 +350,7 @@ internal object RussianStrings : AppStrings() {
|
|||||||
override val readerMenu = "Меню чтения"
|
override val readerMenu = "Меню чтения"
|
||||||
override val info = "Информация..."
|
override val info = "Информация..."
|
||||||
override val readerSettings = "Настройки"
|
override val readerSettings = "Настройки"
|
||||||
|
override val fullScreen = "На весь экран"
|
||||||
override val readerFontSizeIncrease = "Увеличить шрифт"
|
override val readerFontSizeIncrease = "Увеличить шрифт"
|
||||||
override val readerFontSizeDecrease = "Уменьшить шрифт"
|
override val readerFontSizeDecrease = "Уменьшить шрифт"
|
||||||
override val readerLineHeightIncrease = "Увеличить межстрочный интервал"
|
override val readerLineHeightIncrease = "Увеличить межстрочный интервал"
|
||||||
@ -393,6 +399,14 @@ internal object RussianStrings : AppStrings() {
|
|||||||
override fun couldNotReopenLastBook(): String = "Не удалось открыть последнюю книгу."
|
override fun couldNotReopenLastBook(): String = "Не удалось открыть последнюю книгу."
|
||||||
override fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
override fun scanReport(scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||||
"Проверено: $scanned, импортировано: $imported, пропущено: $skipped, ошибок: $failed."
|
"Проверено: $scanned, импортировано: $imported, пропущено: $skipped, ошибок: $failed."
|
||||||
|
override val noNewBooksFound = "Ничего нового не найдено."
|
||||||
|
override val show = "Показать"
|
||||||
|
override fun newBooksAdded(count: Int): String =
|
||||||
|
when {
|
||||||
|
count % 10 == 1 && count % 100 != 11 -> "$count новая книга добавлена."
|
||||||
|
count % 10 in 2..4 && count % 100 !in 12..14 -> "$count новые книги добавлены."
|
||||||
|
else -> "$count новых книг добавлено."
|
||||||
|
}
|
||||||
override fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
override fun rescanReport(scanned: Int, updated: Int, failed: Int): String =
|
||||||
"Пересканировано: $scanned, обновлено: $updated, ошибок: $failed."
|
"Пересканировано: $scanned, обновлено: $updated, ошибок: $failed."
|
||||||
override fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
override fun checkingFile(currentFile: String?, scanned: Int, imported: Int, skipped: Int, failed: Int): String =
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateMapOf
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
@ -52,6 +53,7 @@ import androidx.compose.ui.graphics.BlendMode
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.CompositingStrategy
|
import androidx.compose.ui.graphics.CompositingStrategy
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||||
@ -98,12 +100,19 @@ import net.sergeych.toread.fb2.Fb2TextSpan
|
|||||||
import net.sergeych.toread.fb2.Fb2TextStyle
|
import net.sergeych.toread.fb2.Fb2TextStyle
|
||||||
import net.sergeych.toread.text.HyphenationRegistry
|
import net.sergeych.toread.text.HyphenationRegistry
|
||||||
import net.sergeych.toread.text.SoftHyphen
|
import net.sergeych.toread.text.SoftHyphen
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
import kotlin.math.sqrt
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
|
private data class DecodedBookImage(
|
||||||
|
val bitmap: ImageBitmap,
|
||||||
|
val bytes: ByteArray,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinuousBookReader(
|
internal fun ContinuousBookReader(
|
||||||
book: Fb2Book,
|
book: Fb2Book,
|
||||||
@ -1035,9 +1044,18 @@ private fun BookImage(
|
|||||||
val binary = remember(book, image) {
|
val binary = remember(book, image) {
|
||||||
image?.let(book::binaryFor)
|
image?.let(book::binaryFor)
|
||||||
}
|
}
|
||||||
val bitmap = remember(binary) {
|
var decodedImage by remember(binary) { mutableStateOf<DecodedBookImage?>(null) }
|
||||||
binary?.let { decodeBookImage(it) }
|
LaunchedEffect(binary) {
|
||||||
|
decodedImage = withContext(Dispatchers.Default) {
|
||||||
|
binary?.let {
|
||||||
|
val bytes = it.imageBytes()
|
||||||
|
val bitmap = decodeImageBytes(bytes) ?: return@let null
|
||||||
|
DecodedBookImage(bitmap, bytes)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val currentDecodedImage = decodedImage
|
||||||
|
val bitmap = currentDecodedImage?.bitmap
|
||||||
val imageTitle = image?.alt?.ifBlank { null } ?: book.title
|
val imageTitle = image?.alt?.ifBlank { null } ?: book.title
|
||||||
val imageBackgroundColor = readerImageBackgroundColor()
|
val imageBackgroundColor = readerImageBackgroundColor()
|
||||||
val imageShape = RoundedCornerShape(8.dp)
|
val imageShape = RoundedCornerShape(8.dp)
|
||||||
@ -1048,12 +1066,12 @@ private fun BookImage(
|
|||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.then(
|
.then(
|
||||||
if (bitmap != null && binary != null) {
|
if (currentDecodedImage != null && binary != null) {
|
||||||
Modifier.clickable {
|
Modifier.clickable {
|
||||||
onOpen(
|
onOpen(
|
||||||
ViewedBookImage(
|
ViewedBookImage(
|
||||||
bitmap = bitmap,
|
bitmap = currentDecodedImage.bitmap,
|
||||||
bytes = binary.imageBytes(),
|
bytes = currentDecodedImage.bytes,
|
||||||
mimeType = binary.contentType,
|
mimeType = binary.contentType,
|
||||||
title = imageTitle,
|
title = imageTitle,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
package net.sergeych.toread
|
package net.sergeych.toread
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@ -11,6 +18,7 @@ import androidx.compose.foundation.lazy.LazyListState
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -45,6 +53,7 @@ import androidx.compose.material.icons.filled.UnfoldLess
|
|||||||
import androidx.compose.material.icons.filled.UnfoldMore
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.material.icons.filled.VisibilityOff
|
import androidx.compose.material.icons.filled.VisibilityOff
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
@ -64,6 +73,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@ -74,20 +84,32 @@ import androidx.compose.runtime.snapshotFlow
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.sergeych.toread.fb2.Fb2Book
|
import net.sergeych.toread.fb2.Fb2Book
|
||||||
import net.sergeych.toread.storage.BookReadingStatus
|
import net.sergeych.toread.storage.BookReadingStatus
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.FlowPreview
|
import kotlinx.coroutines.FlowPreview
|
||||||
import kotlinx.coroutines.flow.debounce
|
import kotlinx.coroutines.flow.debounce
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
private data class ReaderBookPreparation(
|
||||||
|
val stats: BookStats,
|
||||||
|
val contentPlan: ReaderContentPlan,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||||
internal fun BookView(
|
internal fun BookView(
|
||||||
@ -105,17 +127,17 @@ internal fun BookView(
|
|||||||
) -> Unit,
|
) -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val stats = remember(book) { BookStats.from(book) }
|
|
||||||
val contentPlan = remember(book) { buildReaderContentPlan(book) }
|
|
||||||
val listState = remember(fileId) { LazyListState() }
|
val listState = remember(fileId) { LazyListState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
var preparation by remember(book) { mutableStateOf<ReaderBookPreparation?>(null) }
|
||||||
var restored by remember(fileId) { mutableStateOf(false) }
|
var restored by remember(fileId) { mutableStateOf(false) }
|
||||||
var markedRead by remember(fileId) { mutableStateOf(false) }
|
var markedRead by remember(fileId) { mutableStateOf(false) }
|
||||||
var libraryItem by remember(fileId) { mutableStateOf<LibraryItem?>(null) }
|
var libraryItem by remember(fileId) { mutableStateOf<LibraryItem?>(null) }
|
||||||
var readAloudPanelVisible by remember(fileId) { mutableStateOf(false) }
|
var readAloudPanelVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
var readAloudSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
var readAloudSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
var readerSettingsPanelVisible by remember(fileId) { mutableStateOf(false) }
|
var readerSettingsPanelVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
|
var fullScreenReader by remember { mutableStateOf(false) }
|
||||||
var tableOfContentsVisible by remember(fileId) { mutableStateOf(false) }
|
var tableOfContentsVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
var tableOfContentsBackStack by remember(fileId) { mutableStateOf<List<ReadingPosition>>(emptyList()) }
|
var tableOfContentsBackStack by remember(fileId) { mutableStateOf<List<ReadingPosition>>(emptyList()) }
|
||||||
var tableOfContentsBackPosition by remember(fileId) { mutableStateOf<ReadingPosition?>(null) }
|
var tableOfContentsBackPosition by remember(fileId) { mutableStateOf<ReadingPosition?>(null) }
|
||||||
@ -125,9 +147,31 @@ internal fun BookView(
|
|||||||
var selectedNoteId by remember(fileId) { mutableStateOf<String?>(null) }
|
var selectedNoteId by remember(fileId) { mutableStateOf<String?>(null) }
|
||||||
val readAloudState by ReadAloudPlatform.state.collectAsState()
|
val readAloudState by ReadAloudPlatform.state.collectAsState()
|
||||||
val readAloudSettings by ReadAloudPlatform.settingsState.collectAsState()
|
val readAloudSettings by ReadAloudPlatform.settingsState.collectAsState()
|
||||||
|
|
||||||
|
LaunchedEffect(book) {
|
||||||
|
preparation = withContext(Dispatchers.Default) {
|
||||||
|
ReaderBookPreparation(
|
||||||
|
stats = BookStats.from(book),
|
||||||
|
contentPlan = buildReaderContentPlan(book),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val preparedBook = preparation
|
||||||
|
if (preparedBook == null) {
|
||||||
|
LoadingScreen(strings.loadingOpeningBook)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val stats = preparedBook.stats
|
||||||
|
val contentPlan = preparedBook.contentPlan
|
||||||
val showShareAction = canShareLibraryBookFile()
|
val showShareAction = canShareLibraryBookFile()
|
||||||
val showViewFileAction = canViewLibraryBookFile()
|
val showViewFileAction = canViewLibraryBookFile()
|
||||||
val showReadAloudAction = ReadAloudPlatform.isSupported && contentPlan.sentences.isNotEmpty()
|
val showReadAloudAction = ReadAloudPlatform.isSupported && contentPlan.sentences.isNotEmpty()
|
||||||
|
val fullScreenProgressTitle = remember(book) { book.fullScreenProgressTitle() }
|
||||||
|
val readingProgress by remember(listState) {
|
||||||
|
derivedStateOf { listState.readerProgress() }
|
||||||
|
}
|
||||||
val activeReadAloudSentence = readAloudState.sentenceIndex
|
val activeReadAloudSentence = readAloudState.sentenceIndex
|
||||||
?.let { index -> contentPlan.sentences.getOrNull(index) }
|
?.let { index -> contentPlan.sentences.getOrNull(index) }
|
||||||
?.takeIf { readAloudState.active }
|
?.takeIf { readAloudState.active }
|
||||||
@ -157,6 +201,14 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateReaderFullScreen(fullScreen: Boolean) {
|
||||||
|
if (fullScreen == fullScreenReader) return
|
||||||
|
fullScreenReader = fullScreen
|
||||||
|
scope.launch {
|
||||||
|
saveReaderFullScreen(fullScreen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun currentReadingPosition(backStack: List<ReadingPosition> = tableOfContentsBackStack): ReadingPosition =
|
fun currentReadingPosition(backStack: List<ReadingPosition> = tableOfContentsBackStack): ReadingPosition =
|
||||||
ReadingPosition(
|
ReadingPosition(
|
||||||
listState.firstVisibleItemIndex,
|
listState.firstVisibleItemIndex,
|
||||||
@ -225,6 +277,7 @@ internal fun BookView(
|
|||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
readerFontSettings = loadReaderFontSettings()
|
readerFontSettings = loadReaderFontSettings()
|
||||||
|
fullScreenReader = loadReaderFullScreen()
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(fileId) {
|
DisposableEffect(fileId) {
|
||||||
@ -294,6 +347,11 @@ internal fun BookView(
|
|||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
topBar = {
|
topBar = {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = !fullScreenReader,
|
||||||
|
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||||
|
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||||
|
) {
|
||||||
CompactReaderTopBar(
|
CompactReaderTopBar(
|
||||||
title = book.title,
|
title = book.title,
|
||||||
onThemeToggle = onThemeToggle,
|
onThemeToggle = onThemeToggle,
|
||||||
@ -332,6 +390,8 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
fullScreen = fullScreenReader,
|
||||||
|
onFullScreenChange = ::updateReaderFullScreen,
|
||||||
showShareAction = showShareAction,
|
showShareAction = showShareAction,
|
||||||
onShare = {
|
onShare = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@ -382,6 +442,7 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
@ -452,6 +513,12 @@ internal fun BookView(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
ReaderProgressPane(
|
||||||
|
progress = readingProgress,
|
||||||
|
fullScreen = fullScreenReader,
|
||||||
|
fullScreenTitle = fullScreenProgressTitle,
|
||||||
|
onFullScreenToggle = { updateReaderFullScreen(!fullScreenReader) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -505,6 +572,151 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderProgressPane(
|
||||||
|
progress: Float,
|
||||||
|
fullScreen: Boolean,
|
||||||
|
fullScreenTitle: String,
|
||||||
|
onFullScreenToggle: () -> Unit,
|
||||||
|
) {
|
||||||
|
val percent = (progress.coerceIn(0f, 1f) * 100f).roundToInt()
|
||||||
|
val trackColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.45f)
|
||||||
|
val readColor = MaterialTheme.colorScheme.primary
|
||||||
|
val fullScreenFillColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.72f)
|
||||||
|
val contentDescription = strings.progressLabel(progress.toDouble())
|
||||||
|
val height by animateDpAsState(if (fullScreen) 36.dp else 24.dp)
|
||||||
|
val progressRowHeight by animateDpAsState(24.dp)
|
||||||
|
val canvasHeight by animateDpAsState(if (fullScreen) 12.dp else 8.dp)
|
||||||
|
val trackStroke by animateDpAsState(1.dp)
|
||||||
|
val readStroke by animateDpAsState(3.dp)
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 2.dp,
|
||||||
|
shadowElevation = 2.dp,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onFullScreenToggle),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(height)
|
||||||
|
.then(
|
||||||
|
if (fullScreen) {
|
||||||
|
Modifier.drawBehind {
|
||||||
|
drawRect(
|
||||||
|
color = fullScreenFillColor,
|
||||||
|
size = Size(size.width * progress.coerceIn(0f, 1f), size.height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.semantics { this.contentDescription = contentDescription },
|
||||||
|
) {
|
||||||
|
if (fullScreen) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(progressRowHeight)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.padding(end = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = fullScreenTitle,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.size(10.dp))
|
||||||
|
Text(
|
||||||
|
text = "$percent%",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(progressRowHeight)
|
||||||
|
.padding(start = 12.dp, end = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Canvas(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(canvasHeight),
|
||||||
|
) {
|
||||||
|
val centerY = size.height / 2f
|
||||||
|
drawLine(
|
||||||
|
color = trackColor,
|
||||||
|
start = Offset(0f, centerY),
|
||||||
|
end = Offset(size.width, centerY),
|
||||||
|
strokeWidth = trackStroke.toPx(),
|
||||||
|
cap = StrokeCap.Square,
|
||||||
|
)
|
||||||
|
if (progress > 0f) {
|
||||||
|
drawLine(
|
||||||
|
color = readColor,
|
||||||
|
start = Offset(0f, centerY),
|
||||||
|
end = Offset(size.width * progress.coerceIn(0f, 1f), centerY),
|
||||||
|
strokeWidth = readStroke.toPx(),
|
||||||
|
cap = StrokeCap.Square,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.size(10.dp))
|
||||||
|
Text(
|
||||||
|
text = "$percent%",
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Fb2Book.fullScreenProgressTitle(): String {
|
||||||
|
val author = authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor }
|
||||||
|
return "$author. $title"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun LazyListState.readerProgress(): Float {
|
||||||
|
val layoutInfo = layoutInfo
|
||||||
|
val totalItems = layoutInfo.totalItemsCount
|
||||||
|
if (totalItems <= 1) return 0f
|
||||||
|
|
||||||
|
val visibleItems = layoutInfo.visibleItemsInfo
|
||||||
|
val lastVisibleItem = visibleItems.lastOrNull()
|
||||||
|
if (
|
||||||
|
lastVisibleItem?.index == totalItems - 1 &&
|
||||||
|
lastVisibleItem.offset + lastVisibleItem.size <= layoutInfo.viewportEndOffset
|
||||||
|
) {
|
||||||
|
return 1f
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstIndex = firstVisibleItemIndex.coerceIn(0, totalItems - 1)
|
||||||
|
val firstVisibleItemSize = visibleItems.firstOrNull { it.index == firstIndex }?.size
|
||||||
|
val itemFraction = firstVisibleItemSize
|
||||||
|
?.takeIf { it > 0 }
|
||||||
|
?.let { (firstVisibleItemScrollOffset.toFloat() / it.toFloat()).coerceIn(0f, 1f) }
|
||||||
|
?: 0f
|
||||||
|
return ((firstIndex + itemFraction) / (totalItems - 1).toFloat()).coerceIn(0f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun CompactReaderTopBar(
|
private fun CompactReaderTopBar(
|
||||||
title: String,
|
title: String,
|
||||||
@ -518,6 +730,8 @@ private fun CompactReaderTopBar(
|
|||||||
readingStatus: BookReadingStatus?,
|
readingStatus: BookReadingStatus?,
|
||||||
favorite: Boolean,
|
favorite: Boolean,
|
||||||
onFavoriteChange: (Boolean) -> Unit,
|
onFavoriteChange: (Boolean) -> Unit,
|
||||||
|
fullScreen: Boolean,
|
||||||
|
onFullScreenChange: (Boolean) -> Unit,
|
||||||
showShareAction: Boolean,
|
showShareAction: Boolean,
|
||||||
onShare: () -> Unit,
|
onShare: () -> Unit,
|
||||||
showViewFileAction: Boolean,
|
showViewFileAction: Boolean,
|
||||||
@ -581,6 +795,16 @@ private fun CompactReaderTopBar(
|
|||||||
onReaderSettings()
|
onReaderSettings()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Checkbox(checked = fullScreen, onCheckedChange = null)
|
||||||
|
},
|
||||||
|
text = { Text(strings.fullScreen) },
|
||||||
|
onClick = {
|
||||||
|
menuOpen = false
|
||||||
|
onFullScreenChange(!fullScreen)
|
||||||
|
},
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
|
|||||||
@ -57,7 +57,7 @@ internal fun ScanScreen(
|
|||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
title = { Text(strings.scan) },
|
title = { Text(strings.scan) },
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = { onStateChange(AppState.Library(state.items, scanPath, message)) }) {
|
IconButton(onClick = { onStateChange(AppState.Library(state.items, scanPath, message, state.selectedFilter)) }) {
|
||||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.backToLibrary)
|
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = strings.backToLibrary)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package net.sergeych.toread
|
package net.sergeych.toread
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -31,8 +32,39 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LoadingScreen(message: String) {
|
internal fun LoadingScreen(message: String) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(
|
||||||
Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(14.dp)) {
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
LoadingPanel(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LoadingOverlay(message: String, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.58f)),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
LoadingPanel(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun LoadingPanel(message: String) {
|
||||||
|
Card(
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(horizontal = 20.dp, vertical = 18.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
Text(message, style = MaterialTheme.typography.bodyMedium)
|
Text(message, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -449,6 +449,18 @@ actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) = withCo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFullScreen(): Boolean = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.getAppFlag(ReaderFullScreenFlag)?.toBooleanStrictOrNull() ?: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFullScreen(fullScreen: Boolean) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(ReaderFullScreenFlag, fullScreen.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
||||||
@ -461,6 +473,20 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.getAppFlag(LibraryFilterFlag)
|
||||||
|
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
||||||
|
?: LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal actual suspend fun saveLibraryFilter(filter: LibraryFilter) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(LibraryFilterFlag, filter.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
|
actual suspend fun loadAppLocaleTag(): String? = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
|
db.getAppFlag(AppLocaleTagFlag)?.takeIf { it.isNotBlank() }
|
||||||
@ -715,6 +741,8 @@ private fun String.isSupportedBookFile(): Boolean =
|
|||||||
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
||||||
private const val ThemeModeFlag = "theme_mode"
|
private const val ThemeModeFlag = "theme_mode"
|
||||||
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
||||||
|
private const val ReaderFullScreenFlag = "reader_full_screen"
|
||||||
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
||||||
|
private const val LibraryFilterFlag = "library_filter"
|
||||||
private const val AppLocaleTagFlag = "app_locale_tag"
|
private const val AppLocaleTagFlag = "app_locale_tag"
|
||||||
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
||||||
|
|||||||
@ -83,10 +83,26 @@ actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) {
|
|||||||
window.localStorage.setItem(ReaderFontSettingsStorageKey, settings.toStorageValue())
|
window.localStorage.setItem(ReaderFontSettingsStorageKey, settings.toStorageValue())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFullScreen(): Boolean =
|
||||||
|
window.localStorage.getItem(ReaderFullScreenStorageKey)?.toBooleanStrictOrNull() ?: false
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFullScreen(fullScreen: Boolean) {
|
||||||
|
window.localStorage.setItem(ReaderFullScreenStorageKey, fullScreen.toString())
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = true
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = true
|
||||||
|
|
||||||
actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = Unit
|
actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = Unit
|
||||||
|
|
||||||
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter =
|
||||||
|
window.localStorage.getItem(LibraryFilterStorageKey)
|
||||||
|
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
||||||
|
?: LibraryFilter.MyLibrary
|
||||||
|
|
||||||
|
internal actual suspend fun saveLibraryFilter(filter: LibraryFilter) {
|
||||||
|
window.localStorage.setItem(LibraryFilterStorageKey, filter.name)
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadAppLocaleTag(): String? =
|
actual suspend fun loadAppLocaleTag(): String? =
|
||||||
window.localStorage.getItem(AppLocaleStorageKey)?.takeIf { it.isNotBlank() }
|
window.localStorage.getItem(AppLocaleStorageKey)?.takeIf { it.isNotBlank() }
|
||||||
|
|
||||||
@ -121,7 +137,9 @@ actual fun watchPlatformDarkTheme(onChange: (Boolean) -> Unit): () -> Unit {
|
|||||||
actual fun libraryLogPath(): String? = null
|
actual fun libraryLogPath(): String? = null
|
||||||
|
|
||||||
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
||||||
|
private const val LibraryFilterStorageKey = "toread.libraryFilter"
|
||||||
private const val ReaderFontSettingsStorageKey = "toread.readerFontSettings"
|
private const val ReaderFontSettingsStorageKey = "toread.readerFontSettings"
|
||||||
|
private const val ReaderFullScreenStorageKey = "toread.readerFullScreen"
|
||||||
|
|
||||||
actual fun formatLibraryLastReadTime(millis: Long): String {
|
actual fun formatLibraryLastReadTime(millis: Long): String {
|
||||||
val totalMinutes = millis / 60_000L
|
val totalMinutes = millis / 60_000L
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
[versions]
|
[versions]
|
||||||
agp = "8.11.2"
|
agp = "8.13.2"
|
||||||
android-compileSdk = "36"
|
android-compileSdk = "36"
|
||||||
android-minSdk = "26"
|
android-minSdk = "26"
|
||||||
android-targetSdk = "36"
|
android-targetSdk = "36"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user