+better notes click processing
+android & universal UI tweaks +settings to change text and background color
This commit is contained in:
parent
f344befbdd
commit
85d97d5a90
@ -499,9 +499,18 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
|
|||||||
|
|
||||||
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(LibraryFilterFlag)
|
val saved = db.getAppFlag(LibraryFilterFlag)
|
||||||
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
when {
|
||||||
?: LibraryFilter.MyLibrary
|
saved == null -> {
|
||||||
|
appendLibraryLog("warning library filter was not saved; using ${LibraryFilter.MyLibrary.name}")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
else -> runCatching { LibraryFilter.valueOf(saved) }
|
||||||
|
.getOrElse {
|
||||||
|
appendLibraryLog("warning invalid saved library filter '$saved'; using ${LibraryFilter.MyLibrary.name}")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import android.Manifest
|
|||||||
import android.app.AlertDialog
|
import android.app.AlertDialog
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@ -23,6 +24,8 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import kotlinx.coroutines.CompletableDeferred
|
import kotlinx.coroutines.CompletableDeferred
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@ -55,7 +58,7 @@ class MainActivity : ComponentActivity(), AndroidLibraryDirectoryChooser {
|
|||||||
}
|
}
|
||||||
notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
|
notificationPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {}
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, true)
|
configureEdgeToEdgeBookWindow()
|
||||||
initToreadPlatform(this, this)
|
initToreadPlatform(this, this)
|
||||||
rememberPlatformOpenBookIntent(intent)
|
rememberPlatformOpenBookIntent(intent)
|
||||||
|
|
||||||
@ -67,6 +70,11 @@ class MainActivity : ComponentActivity(), AndroidLibraryDirectoryChooser {
|
|||||||
requestNotificationPermissionIfNeeded()
|
requestNotificationPermissionIfNeeded()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onWindowFocusChanged(hasFocus: Boolean) {
|
||||||
|
super.onWindowFocusChanged(hasFocus)
|
||||||
|
if (hasFocus) hideNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
override suspend fun chooseDirectory(): String? {
|
override suspend fun chooseDirectory(): String? {
|
||||||
val result = CompletableDeferred<String?>()
|
val result = CompletableDeferred<String?>()
|
||||||
runOnUiThread {
|
runOnUiThread {
|
||||||
@ -175,6 +183,27 @@ class MainActivity : ComponentActivity(), AndroidLibraryDirectoryChooser {
|
|||||||
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
notificationPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun configureEdgeToEdgeBookWindow() {
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
window.navigationBarColor = Color.TRANSPARENT
|
||||||
|
window.statusBarColor = Color.TRANSPARENT
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||||
|
window.navigationBarDividerColor = Color.TRANSPARENT
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
window.isStatusBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
hideNavigationBar()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun hideNavigationBar() {
|
||||||
|
WindowCompat.getInsetsController(window, window.decorView).apply {
|
||||||
|
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun downloadsPath(): String =
|
private fun downloadsPath(): String =
|
||||||
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath
|
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath
|
||||||
?: Environment.getExternalStorageDirectory().absolutePath
|
?: Environment.getExternalStorageDirectory().absolutePath
|
||||||
@ -185,7 +214,7 @@ private fun AndroidAppWindow(content: @Composable () -> Unit) {
|
|||||||
androidx.compose.foundation.layout.Box(
|
androidx.compose.foundation.layout.Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Vertical)),
|
.windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Top)),
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import androidx.compose.material3.Surface
|
|||||||
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.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@ -104,25 +105,27 @@ fun App() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MaterialTheme(colorScheme = if (useDark) darkReaderColorScheme() else lightReaderColorScheme()) {
|
MaterialTheme(colorScheme = if (useDark) darkReaderColorScheme() else lightReaderColorScheme()) {
|
||||||
Box(Modifier.fillMaxSize()) {
|
CompositionLocalProvider(LocalReaderThemeIsDark provides useDark) {
|
||||||
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
Box(Modifier.fillMaxSize()) {
|
||||||
if (localePreferenceLoaded) {
|
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.background) {
|
||||||
BookReaderApp(
|
if (localePreferenceLoaded) {
|
||||||
onThemeToggle = {
|
BookReaderApp(
|
||||||
val next = themeMode.next()
|
onThemeToggle = {
|
||||||
themeMode = next
|
val next = themeMode.next()
|
||||||
showToast(strings.themeChanged(next))
|
themeMode = next
|
||||||
scope.launch {
|
showToast(strings.themeChanged(next))
|
||||||
saveThemeMode(next)
|
scope.launch {
|
||||||
}
|
saveThemeMode(next)
|
||||||
},
|
}
|
||||||
onShowToast = ::showToast,
|
},
|
||||||
)
|
onShowToast = ::showToast,
|
||||||
} else {
|
)
|
||||||
LoadingScreen(strings.loadingOpeningBook)
|
} else {
|
||||||
|
LoadingScreen(strings.loadingOpeningBook)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
AppToast(toast, modifier = Modifier.align(Alignment.BottomCenter))
|
||||||
}
|
}
|
||||||
AppToast(toast, modifier = Modifier.align(Alignment.BottomCenter))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,6 +247,13 @@ private fun BookReaderApp(
|
|||||||
book = book,
|
book = book,
|
||||||
libraryItems = emptyList(),
|
libraryItems = emptyList(),
|
||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
|
selectedFilter = when (val current = state) {
|
||||||
|
is AppState.Library -> current.selectedFilter
|
||||||
|
is AppState.Scan -> current.selectedFilter
|
||||||
|
is AppState.Reader -> current.selectedFilter
|
||||||
|
is AppState.BookInfo -> current.selectedFilter
|
||||||
|
is AppState.Error, AppState.LoadingStartup -> loadLibraryFilter()
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), loadLibraryFilter())
|
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), loadLibraryFilter())
|
||||||
@ -357,12 +367,18 @@ private fun BookReaderApp(
|
|||||||
libraryItems = current.libraryItems,
|
libraryItems = current.libraryItems,
|
||||||
scanPath = current.scanPath,
|
scanPath = current.scanPath,
|
||||||
message = current.message,
|
message = current.message,
|
||||||
|
selectedFilter = current.selectedFilter,
|
||||||
)
|
)
|
||||||
is AppState.Reader -> {
|
is AppState.Reader -> {
|
||||||
scope.launch { saveActiveReadingFileId(null) }
|
scope.launch { saveActiveReadingFileId(null) }
|
||||||
refreshLibraryItem(current.fileId)
|
refreshLibraryItem(current.fileId)
|
||||||
val backState = libraryBackState?.copy(message = current.message)
|
val backState = libraryBackState?.copy(message = current.message)
|
||||||
?: AppState.Library(current.libraryItems, current.scanPath, current.message)
|
?: AppState.Library(
|
||||||
|
current.libraryItems,
|
||||||
|
current.scanPath,
|
||||||
|
current.message,
|
||||||
|
current.selectedFilter,
|
||||||
|
)
|
||||||
libraryBackState = null
|
libraryBackState = null
|
||||||
downloadsRescanRequestGeneration += 1
|
downloadsRescanRequestGeneration += 1
|
||||||
backState
|
backState
|
||||||
@ -523,11 +539,12 @@ private fun BookReaderApp(
|
|||||||
libraryItems = current.libraryItems,
|
libraryItems = current.libraryItems,
|
||||||
scanPath = current.scanPath,
|
scanPath = current.scanPath,
|
||||||
message = current.message,
|
message = current.message,
|
||||||
|
selectedFilter = current.selectedFilter,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onDeleted = { message ->
|
onDeleted = { message ->
|
||||||
state = libraryBackState?.copy(message = message)
|
state = libraryBackState?.copy(message = message)
|
||||||
?: AppState.Library(emptyList(), current.scanPath, message)
|
?: AppState.Library(emptyList(), current.scanPath, message, current.selectedFilter)
|
||||||
libraryBackState = null
|
libraryBackState = null
|
||||||
},
|
},
|
||||||
onDeleteRequested = ::requestDelete,
|
onDeleteRequested = ::requestDelete,
|
||||||
|
|||||||
@ -27,6 +27,7 @@ internal sealed interface AppState {
|
|||||||
val libraryItems: List<LibraryItem>,
|
val libraryItems: 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 BookInfo(
|
data class BookInfo(
|
||||||
@ -35,6 +36,7 @@ internal sealed interface AppState {
|
|||||||
val libraryItems: List<LibraryItem>,
|
val libraryItems: 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 Error(val message: String) : AppState
|
data class Error(val message: String) : AppState
|
||||||
@ -59,6 +61,7 @@ internal suspend fun loadStartupState(): AppState {
|
|||||||
book = book,
|
book = book,
|
||||||
libraryItems = emptyList(),
|
libraryItems = emptyList(),
|
||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
|
selectedFilter = libraryFilter,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), libraryFilter)
|
AppState.Library(emptyList(), scanPath, it.message ?: strings.couldNotOpen(request.displayName), libraryFilter)
|
||||||
@ -77,6 +80,7 @@ internal suspend fun loadStartupState(): AppState {
|
|||||||
book = parseBookInBackground(bytes, item.storageUri ?: item.title),
|
book = parseBookInBackground(bytes, item.storageUri ?: item.title),
|
||||||
libraryItems = emptyList(),
|
libraryItems = emptyList(),
|
||||||
scanPath = scanPath,
|
scanPath = scanPath,
|
||||||
|
selectedFilter = libraryFilter,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
saveActiveReadingFileId(null)
|
saveActiveReadingFileId(null)
|
||||||
|
|||||||
@ -70,6 +70,14 @@ data class ReaderFontSettings(
|
|||||||
val lineHeightSp: Float = DefaultReaderLineHeightSp,
|
val lineHeightSp: Float = DefaultReaderLineHeightSp,
|
||||||
val fontWeight: Int = defaultReaderFontWeight(),
|
val fontWeight: Int = defaultReaderFontWeight(),
|
||||||
val fontName: String? = null,
|
val fontName: String? = null,
|
||||||
|
val lightBackgroundArgb: Long? = null,
|
||||||
|
val lightTextArgb: Long? = null,
|
||||||
|
val darkBackgroundArgb: Long? = null,
|
||||||
|
val darkTextArgb: Long? = null,
|
||||||
|
val lightCustomBackgroundArgb: Long? = null,
|
||||||
|
val lightCustomTextArgb: Long? = null,
|
||||||
|
val darkCustomBackgroundArgb: Long? = null,
|
||||||
|
val darkCustomTextArgb: Long? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class BookInfoExtras(
|
data class BookInfoExtras(
|
||||||
@ -255,6 +263,14 @@ internal fun ReaderFontSettings.coerced(): ReaderFontSettings =
|
|||||||
lineHeightSp = lineHeightSp.coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
|
lineHeightSp = lineHeightSp.coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
|
||||||
fontWeight = fontWeight.coerceIn(MinReaderFontWeight, MaxReaderFontWeight),
|
fontWeight = fontWeight.coerceIn(MinReaderFontWeight, MaxReaderFontWeight),
|
||||||
fontName = fontName?.takeIf { it.isNotBlank() },
|
fontName = fontName?.takeIf { it.isNotBlank() },
|
||||||
|
lightBackgroundArgb = lightBackgroundArgb?.coerceArgb(),
|
||||||
|
lightTextArgb = lightTextArgb?.coerceArgb(),
|
||||||
|
darkBackgroundArgb = darkBackgroundArgb?.coerceArgb(),
|
||||||
|
darkTextArgb = darkTextArgb?.coerceArgb(),
|
||||||
|
lightCustomBackgroundArgb = lightCustomBackgroundArgb?.coerceArgb(),
|
||||||
|
lightCustomTextArgb = lightCustomTextArgb?.coerceArgb(),
|
||||||
|
darkCustomBackgroundArgb = darkCustomBackgroundArgb?.coerceArgb(),
|
||||||
|
darkCustomTextArgb = darkCustomTextArgb?.coerceArgb(),
|
||||||
)
|
)
|
||||||
|
|
||||||
internal fun ReaderFontSettings.adjustFontSize(steps: Int): ReaderFontSettings {
|
internal fun ReaderFontSettings.adjustFontSize(steps: Int): ReaderFontSettings {
|
||||||
@ -295,6 +311,14 @@ internal fun ReaderFontSettings.toStorageValue(): String =
|
|||||||
current.lineHeightSp.toString(),
|
current.lineHeightSp.toString(),
|
||||||
current.fontWeight.toString(),
|
current.fontWeight.toString(),
|
||||||
current.fontName.orEmpty(),
|
current.fontName.orEmpty(),
|
||||||
|
current.lightBackgroundArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.lightTextArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.darkBackgroundArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.darkTextArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.lightCustomBackgroundArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.lightCustomTextArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.darkCustomBackgroundArgb?.toStorageArgb().orEmpty(),
|
||||||
|
current.darkCustomTextArgb?.toStorageArgb().orEmpty(),
|
||||||
).joinToString("|")
|
).joinToString("|")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -306,5 +330,71 @@ internal fun readerFontSettingsFromStorageValue(value: String?): ReaderFontSetti
|
|||||||
lineHeightSp = parts.getOrNull(1)?.toFloatOrNull() ?: defaults.lineHeightSp,
|
lineHeightSp = parts.getOrNull(1)?.toFloatOrNull() ?: defaults.lineHeightSp,
|
||||||
fontWeight = parts.getOrNull(2)?.toIntOrNull() ?: defaults.fontWeight,
|
fontWeight = parts.getOrNull(2)?.toIntOrNull() ?: defaults.fontWeight,
|
||||||
fontName = parts.getOrNull(3)?.takeIf { it.isNotBlank() } ?: defaults.fontName,
|
fontName = parts.getOrNull(3)?.takeIf { it.isNotBlank() } ?: defaults.fontName,
|
||||||
|
lightBackgroundArgb = parts.getOrNull(4)?.storageArgbOrNull(),
|
||||||
|
lightTextArgb = parts.getOrNull(5)?.storageArgbOrNull(),
|
||||||
|
darkBackgroundArgb = parts.getOrNull(6)?.storageArgbOrNull(),
|
||||||
|
darkTextArgb = parts.getOrNull(7)?.storageArgbOrNull(),
|
||||||
|
lightCustomBackgroundArgb = parts.getOrNull(8)?.storageArgbOrNull(),
|
||||||
|
lightCustomTextArgb = parts.getOrNull(9)?.storageArgbOrNull(),
|
||||||
|
darkCustomBackgroundArgb = parts.getOrNull(10)?.storageArgbOrNull(),
|
||||||
|
darkCustomTextArgb = parts.getOrNull(11)?.storageArgbOrNull(),
|
||||||
).coerced()
|
).coerced()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.withThemeBackgroundArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
|
||||||
|
if (useDarkTheme) copy(darkBackgroundArgb = argb?.coerceArgb()) else copy(lightBackgroundArgb = argb?.coerceArgb())
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.withThemeTextArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
|
||||||
|
if (useDarkTheme) copy(darkTextArgb = argb?.coerceArgb()) else copy(lightTextArgb = argb?.coerceArgb())
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.withThemeCustomBackgroundArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
|
||||||
|
if (useDarkTheme) {
|
||||||
|
copy(darkCustomBackgroundArgb = argb?.coerceArgb(), darkBackgroundArgb = argb?.coerceArgb())
|
||||||
|
} else {
|
||||||
|
copy(lightCustomBackgroundArgb = argb?.coerceArgb(), lightBackgroundArgb = argb?.coerceArgb())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.withThemeCustomTextArgb(useDarkTheme: Boolean, argb: Long?): ReaderFontSettings =
|
||||||
|
if (useDarkTheme) {
|
||||||
|
copy(darkCustomTextArgb = argb?.coerceArgb(), darkTextArgb = argb?.coerceArgb())
|
||||||
|
} else {
|
||||||
|
copy(lightCustomTextArgb = argb?.coerceArgb(), lightTextArgb = argb?.coerceArgb())
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.themeBackgroundArgb(useDarkTheme: Boolean): Long? =
|
||||||
|
if (useDarkTheme) darkBackgroundArgb else lightBackgroundArgb
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.themeTextArgb(useDarkTheme: Boolean): Long? =
|
||||||
|
if (useDarkTheme) darkTextArgb else lightTextArgb
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.themeCustomBackgroundArgb(useDarkTheme: Boolean): Long? =
|
||||||
|
if (useDarkTheme) darkCustomBackgroundArgb else lightCustomBackgroundArgb
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.themeCustomTextArgb(useDarkTheme: Boolean): Long? =
|
||||||
|
if (useDarkTheme) darkCustomTextArgb else lightCustomTextArgb
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.resetThemeColorScheme(useDarkTheme: Boolean): ReaderFontSettings =
|
||||||
|
if (useDarkTheme) {
|
||||||
|
copy(
|
||||||
|
darkBackgroundArgb = null,
|
||||||
|
darkTextArgb = null,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
copy(
|
||||||
|
lightBackgroundArgb = null,
|
||||||
|
lightTextArgb = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Long.coerceArgb(): Long =
|
||||||
|
coerceIn(0x00000000L, 0xFFFFFFFFL)
|
||||||
|
|
||||||
|
private fun Long.toStorageArgb(): String =
|
||||||
|
coerceArgb().toString(16).padStart(8, '0').uppercase()
|
||||||
|
|
||||||
|
private fun String.storageArgbOrNull(): Long? =
|
||||||
|
trim()
|
||||||
|
.removePrefix("#")
|
||||||
|
.takeIf { it.length == 8 }
|
||||||
|
?.toLongOrNull(16)
|
||||||
|
?.coerceArgb()
|
||||||
|
|||||||
@ -530,6 +530,7 @@ internal fun LibraryScreen(
|
|||||||
libraryItems = readerLibraryItems,
|
libraryItems = readerLibraryItems,
|
||||||
scanPath = state.scanPath,
|
scanPath = state.scanPath,
|
||||||
message = message,
|
message = message,
|
||||||
|
selectedFilter = state.selectedFilter,
|
||||||
)
|
)
|
||||||
}.getOrElse {
|
}.getOrElse {
|
||||||
AppState.Library(
|
AppState.Library(
|
||||||
|
|||||||
@ -44,6 +44,7 @@ internal open class AppStrings {
|
|||||||
open val undo = "Undo"
|
open val undo = "Undo"
|
||||||
open val retry = "Retry"
|
open val retry = "Retry"
|
||||||
open val done = "Done"
|
open val done = "Done"
|
||||||
|
open val cancel = "Cancel"
|
||||||
|
|
||||||
open val libraryError = "Library error"
|
open val libraryError = "Library error"
|
||||||
open val couldNotOpenLibrary = "Could not open library."
|
open val couldNotOpenLibrary = "Could not open library."
|
||||||
@ -147,6 +148,19 @@ internal open class AppStrings {
|
|||||||
open val readerFontWeightIncrease = "Increase font weight"
|
open val readerFontWeightIncrease = "Increase font weight"
|
||||||
open val readerFontWeightDecrease = "Decrease font weight"
|
open val readerFontWeightDecrease = "Decrease font weight"
|
||||||
open val resetReaderFontSettings = "Reset font settings"
|
open val resetReaderFontSettings = "Reset font settings"
|
||||||
|
open val revertColorScheme = "Revert color scheme"
|
||||||
|
open val readerBackgroundColor = "Reader background color"
|
||||||
|
open val readerTextColor = "Reader text color"
|
||||||
|
open val extraReaderSettings = "Extra reader settings"
|
||||||
|
open val customColor = "Custom color"
|
||||||
|
open val red = "R"
|
||||||
|
open val green = "G"
|
||||||
|
open val blue = "B"
|
||||||
|
open val readerColors = "Colors"
|
||||||
|
open val readerTypography = "Typography"
|
||||||
|
open val readerLayout = "Layout"
|
||||||
|
open val readerNavigation = "Navigation"
|
||||||
|
open val readerProgress = "Progress"
|
||||||
open val closeReaderSettings = "Close reader settings"
|
open val closeReaderSettings = "Close reader settings"
|
||||||
open val share = "Share"
|
open val share = "Share"
|
||||||
open val viewFile = "View file"
|
open val viewFile = "View file"
|
||||||
@ -255,6 +269,7 @@ internal object RussianStrings : AppStrings() {
|
|||||||
override val undo = "Отменить"
|
override val undo = "Отменить"
|
||||||
override val retry = "Повторить"
|
override val retry = "Повторить"
|
||||||
override val done = "Готово"
|
override val done = "Готово"
|
||||||
|
override val cancel = "Отмена"
|
||||||
|
|
||||||
override val libraryError = "Ошибка библиотеки"
|
override val libraryError = "Ошибка библиотеки"
|
||||||
override val couldNotOpenLibrary = "Не удалось открыть библиотеку."
|
override val couldNotOpenLibrary = "Не удалось открыть библиотеку."
|
||||||
@ -358,6 +373,19 @@ internal object RussianStrings : AppStrings() {
|
|||||||
override val readerFontWeightIncrease = "Увеличить насыщенность шрифта"
|
override val readerFontWeightIncrease = "Увеличить насыщенность шрифта"
|
||||||
override val readerFontWeightDecrease = "Уменьшить насыщенность шрифта"
|
override val readerFontWeightDecrease = "Уменьшить насыщенность шрифта"
|
||||||
override val resetReaderFontSettings = "Сбросить настройки шрифта"
|
override val resetReaderFontSettings = "Сбросить настройки шрифта"
|
||||||
|
override val revertColorScheme = "Вернуть цветовую схему"
|
||||||
|
override val readerBackgroundColor = "Цвет фона чтения"
|
||||||
|
override val readerTextColor = "Цвет текста чтения"
|
||||||
|
override val extraReaderSettings = "Дополнительные настройки"
|
||||||
|
override val customColor = "Свой цвет"
|
||||||
|
override val red = "К"
|
||||||
|
override val green = "З"
|
||||||
|
override val blue = "С"
|
||||||
|
override val readerColors = "Цвета"
|
||||||
|
override val readerTypography = "Типографика"
|
||||||
|
override val readerLayout = "Макет"
|
||||||
|
override val readerNavigation = "Навигация"
|
||||||
|
override val readerProgress = "Прогресс"
|
||||||
override val closeReaderSettings = "Закрыть настройки чтения"
|
override val closeReaderSettings = "Закрыть настройки чтения"
|
||||||
override val share = "Поделиться"
|
override val share = "Поделиться"
|
||||||
override val viewFile = "Показать файл"
|
override val viewFile = "Показать файл"
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import androidx.compose.foundation.gestures.awaitFirstDown
|
|||||||
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
import androidx.compose.foundation.gestures.waitForUpOrCancellation
|
||||||
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.BoxScope
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
@ -21,6 +22,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
@ -57,10 +59,8 @@ 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
|
||||||
import androidx.compose.ui.input.pointer.PointerIcon
|
|
||||||
import androidx.compose.ui.input.pointer.PointerType
|
import androidx.compose.ui.input.pointer.PointerType
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
import androidx.compose.ui.input.pointer.pointerHoverIcon
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
@ -81,8 +81,10 @@ import androidx.compose.ui.text.style.Hyphens
|
|||||||
import androidx.compose.ui.text.style.LineBreak
|
import androidx.compose.ui.text.style.LineBreak
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextDecoration
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.withAnnotation
|
||||||
import androidx.compose.ui.text.withStyle
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.isSpecified
|
import androidx.compose.ui.unit.isSpecified
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@ -122,6 +124,8 @@ internal fun ContinuousBookReader(
|
|||||||
contentPlan: ReaderContentPlan = remember(book) { buildReaderContentPlan(book) },
|
contentPlan: ReaderContentPlan = remember(book) { buildReaderContentPlan(book) },
|
||||||
highlightedSentence: ReadAloudSentence? = null,
|
highlightedSentence: ReadAloudSentence? = null,
|
||||||
readerFontSettings: ReaderFontSettings = defaultReaderFontSettings(),
|
readerFontSettings: ReaderFontSettings = defaultReaderFontSettings(),
|
||||||
|
readerBackgroundColor: Color = MaterialTheme.colorScheme.surface,
|
||||||
|
readerTextColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
onReaderFontZoom: (Int) -> Unit = {},
|
onReaderFontZoom: (Int) -> Unit = {},
|
||||||
onUserScroll: () -> Unit = {},
|
onUserScroll: () -> Unit = {},
|
||||||
onImageOpen: (ViewedBookImage) -> Unit = {},
|
onImageOpen: (ViewedBookImage) -> Unit = {},
|
||||||
@ -145,7 +149,7 @@ internal fun ContinuousBookReader(
|
|||||||
LazyColumn(
|
LazyColumn(
|
||||||
state = listState,
|
state = listState,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(readerBackgroundColor)
|
||||||
.nestedScroll(userScrollConnection)
|
.nestedScroll(userScrollConnection)
|
||||||
.readerFontZoomGesture(onReaderFontZoom)
|
.readerFontZoomGesture(onReaderFontZoom)
|
||||||
.pageTurnOnTouchTap(
|
.pageTurnOnTouchTap(
|
||||||
@ -202,7 +206,7 @@ internal fun ContinuousBookReader(
|
|||||||
0 -> MaterialTheme.typography.headlineMedium
|
0 -> MaterialTheme.typography.headlineMedium
|
||||||
1 -> MaterialTheme.typography.titleLarge
|
1 -> MaterialTheme.typography.titleLarge
|
||||||
else -> MaterialTheme.typography.titleMedium
|
else -> MaterialTheme.typography.titleMedium
|
||||||
},
|
}.copy(color = readerTextColor),
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
lineHeight = if (element.depth == 0) 36.sp else 28.sp,
|
lineHeight = if (element.depth == 0) 36.sp else 28.sp,
|
||||||
modifier = titleModifier,
|
modifier = titleModifier,
|
||||||
@ -219,7 +223,7 @@ internal fun ContinuousBookReader(
|
|||||||
text = element.text,
|
text = element.text,
|
||||||
language = book.language,
|
language = book.language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = readerParagraphTextStyle(book.language, readerFontSettings),
|
style = readerParagraphTextStyle(book.language, readerFontSettings).copy(color = readerTextColor),
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
textAlign = TextAlign.Justify,
|
textAlign = TextAlign.Justify,
|
||||||
modifier = Modifier.padding(start = (element.depth * 8).dp, end = 0.dp),
|
modifier = Modifier.padding(start = (element.depth * 8).dp, end = 0.dp),
|
||||||
@ -230,7 +234,7 @@ internal fun ContinuousBookReader(
|
|||||||
text = element.text,
|
text = element.text,
|
||||||
language = book.language,
|
language = book.language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold, color = readerTextColor),
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 18.dp, bottom = 8.dp),
|
||||||
@ -243,6 +247,7 @@ internal fun ContinuousBookReader(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = element.depth,
|
depth = element.depth,
|
||||||
|
textColor = readerTextColor,
|
||||||
onLinkOpen = onNoteOpen,
|
onLinkOpen = onNoteOpen,
|
||||||
)
|
)
|
||||||
is ReaderElement.Cite -> ReaderCite(
|
is ReaderElement.Cite -> ReaderCite(
|
||||||
@ -251,6 +256,7 @@ internal fun ContinuousBookReader(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = element.depth,
|
depth = element.depth,
|
||||||
|
textColor = readerTextColor,
|
||||||
onLinkOpen = onNoteOpen,
|
onLinkOpen = onNoteOpen,
|
||||||
)
|
)
|
||||||
is ReaderElement.Poem -> ReaderPoem(
|
is ReaderElement.Poem -> ReaderPoem(
|
||||||
@ -259,6 +265,7 @@ internal fun ContinuousBookReader(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = element.depth,
|
depth = element.depth,
|
||||||
|
textColor = readerTextColor,
|
||||||
onLinkOpen = onNoteOpen,
|
onLinkOpen = onNoteOpen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -332,6 +339,8 @@ private fun Modifier.pageTurnOnTouchTap(
|
|||||||
val change = event.changes.firstOrNull { it.id == down.id }
|
val change = event.changes.firstOrNull { it.id == down.id }
|
||||||
if (change == null) {
|
if (change == null) {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
|
} else if (change.isConsumed) {
|
||||||
|
cancelled = true
|
||||||
} else if (change.pressed) {
|
} else if (change.pressed) {
|
||||||
val dx = change.position.x - down.position.x
|
val dx = change.position.x - down.position.x
|
||||||
val dy = change.position.y - down.position.y
|
val dy = change.position.y - down.position.y
|
||||||
@ -699,27 +708,19 @@ private fun ReaderText(
|
|||||||
onLinkOpen: (String) -> Unit = {},
|
onLinkOpen: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val highlightColor = MaterialTheme.colorScheme.secondaryContainer
|
val highlightColor = MaterialTheme.colorScheme.secondaryContainer
|
||||||
val annotatedText = text.toAnnotatedString(language, hyphenation, highlightedRange, highlightColor)
|
val annotatedText = text.toAnnotatedString(
|
||||||
|
language = language,
|
||||||
|
hyphenation = hyphenation,
|
||||||
|
highlightedRange = highlightedRange,
|
||||||
|
highlightColor = highlightColor,
|
||||||
|
)
|
||||||
val hasLinks = annotatedText.hasReaderLinks()
|
val hasLinks = annotatedText.hasReaderLinks()
|
||||||
val needsSoftHyphenPaintWorkaround = isDesktopPlatform
|
val needsSoftHyphenPaintWorkaround = isDesktopPlatform
|
||||||
var textLayout by remember(annotatedText) { mutableStateOf<TextLayoutResult?>(null) }
|
var textLayout by remember(annotatedText) { mutableStateOf<TextLayoutResult?>(null) }
|
||||||
val desktopHyphenColor = MaterialTheme.colorScheme.onSurface
|
val desktopHyphenColor = if (style.color == Color.Unspecified) MaterialTheme.colorScheme.onSurface else style.color
|
||||||
val desktopHyphenGutter = 8.dp
|
val desktopHyphenGutter = 8.dp
|
||||||
val textModifier = modifier
|
val textModifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.then(
|
|
||||||
if (hasLinks) {
|
|
||||||
Modifier
|
|
||||||
.pointerHoverIcon(PointerIcon.Hand)
|
|
||||||
.readerLinkTapHandler(
|
|
||||||
annotatedText = annotatedText,
|
|
||||||
textLayout = textLayout,
|
|
||||||
onLinkOpen = onLinkOpen,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
Modifier
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.then(
|
.then(
|
||||||
if (needsSoftHyphenPaintWorkaround) {
|
if (needsSoftHyphenPaintWorkaround) {
|
||||||
Modifier.drawWithContent {
|
Modifier.drawWithContent {
|
||||||
@ -751,16 +752,26 @@ private fun ReaderText(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
Text(
|
Box(modifier = textModifier) {
|
||||||
text = annotatedText,
|
Text(
|
||||||
style = style,
|
text = annotatedText,
|
||||||
textAlign = textAlign,
|
style = style,
|
||||||
modifier = textModifier,
|
textAlign = textAlign,
|
||||||
onTextLayout = {
|
modifier = Modifier.fillMaxWidth(),
|
||||||
textLayout = it
|
onTextLayout = {
|
||||||
onTextLayout(it)
|
textLayout = it
|
||||||
},
|
onTextLayout(it)
|
||||||
)
|
},
|
||||||
|
)
|
||||||
|
val layout = textLayout
|
||||||
|
if (hasLinks && layout != null) {
|
||||||
|
ReaderLinkTouchOverlays(
|
||||||
|
annotatedText = annotatedText,
|
||||||
|
textLayout = layout,
|
||||||
|
onLinkOpen = onLinkOpen,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -770,6 +781,7 @@ private fun ReaderPoem(
|
|||||||
hyphenation: HyphenationRegistry,
|
hyphenation: HyphenationRegistry,
|
||||||
highlightedRange: ReaderSentenceRange?,
|
highlightedRange: ReaderSentenceRange?,
|
||||||
depth: Int,
|
depth: Int,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLinkOpen: (String) -> Unit = {},
|
onLinkOpen: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@ -786,6 +798,7 @@ private fun ReaderPoem(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
|
textColor = textColor,
|
||||||
modifier = Modifier.padding(top = if (index == 0) 0.dp else 8.dp, bottom = 8.dp),
|
modifier = Modifier.padding(top = if (index == 0) 0.dp else 8.dp, bottom = 8.dp),
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
)
|
)
|
||||||
@ -798,7 +811,7 @@ private fun ReaderPoem(
|
|||||||
text = segment.text,
|
text = segment.text,
|
||||||
language = language,
|
language = language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = poemTextStyle(segment.kind, language),
|
style = poemTextStyle(segment.kind, language).copy(color = textColor),
|
||||||
highlightedRange = highlightedRange?.forSegment(segment),
|
highlightedRange = highlightedRange?.forSegment(segment),
|
||||||
textAlign = when (segment.kind) {
|
textAlign = when (segment.kind) {
|
||||||
ReaderPoemSegmentKind.Title,
|
ReaderPoemSegmentKind.Title,
|
||||||
@ -824,6 +837,7 @@ private fun ReaderEpigraph(
|
|||||||
hyphenation: HyphenationRegistry,
|
hyphenation: HyphenationRegistry,
|
||||||
highlightedRange: ReaderSentenceRange?,
|
highlightedRange: ReaderSentenceRange?,
|
||||||
depth: Int,
|
depth: Int,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLinkOpen: (String) -> Unit = {},
|
onLinkOpen: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@ -840,6 +854,7 @@ private fun ReaderEpigraph(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = null,
|
highlightedRange = null,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
|
textColor = textColor,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -848,7 +863,7 @@ private fun ReaderEpigraph(
|
|||||||
text = author,
|
text = author,
|
||||||
language = language,
|
language = language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = epigraphAuthorTextStyle(language),
|
style = epigraphAuthorTextStyle(language).copy(color = textColor),
|
||||||
highlightedRange = null,
|
highlightedRange = null,
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
|
||||||
@ -865,6 +880,7 @@ private fun ReaderCite(
|
|||||||
hyphenation: HyphenationRegistry,
|
hyphenation: HyphenationRegistry,
|
||||||
highlightedRange: ReaderSentenceRange?,
|
highlightedRange: ReaderSentenceRange?,
|
||||||
depth: Int,
|
depth: Int,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onLinkOpen: (String) -> Unit = {},
|
onLinkOpen: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
@ -881,6 +897,7 @@ private fun ReaderCite(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = null,
|
highlightedRange = null,
|
||||||
depth = 0,
|
depth = 0,
|
||||||
|
textColor = textColor,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -889,7 +906,7 @@ private fun ReaderCite(
|
|||||||
text = author,
|
text = author,
|
||||||
language = language,
|
language = language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = epigraphAuthorTextStyle(language),
|
style = epigraphAuthorTextStyle(language).copy(color = textColor),
|
||||||
highlightedRange = null,
|
highlightedRange = null,
|
||||||
textAlign = TextAlign.End,
|
textAlign = TextAlign.End,
|
||||||
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
|
modifier = Modifier.fillMaxWidth().padding(top = 2.dp),
|
||||||
@ -906,6 +923,7 @@ private fun ReaderEpigraphBlock(
|
|||||||
hyphenation: HyphenationRegistry,
|
hyphenation: HyphenationRegistry,
|
||||||
highlightedRange: ReaderSentenceRange?,
|
highlightedRange: ReaderSentenceRange?,
|
||||||
depth: Int,
|
depth: Int,
|
||||||
|
textColor: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
onLinkOpen: (String) -> Unit,
|
onLinkOpen: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
when (block) {
|
when (block) {
|
||||||
@ -914,7 +932,7 @@ private fun ReaderEpigraphBlock(
|
|||||||
text = block.content,
|
text = block.content,
|
||||||
language = language,
|
language = language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = epigraphTextStyle(language),
|
style = epigraphTextStyle(language).copy(color = textColor),
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
textAlign = TextAlign.Start,
|
textAlign = TextAlign.Start,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
@ -923,7 +941,7 @@ private fun ReaderEpigraphBlock(
|
|||||||
text = block.content,
|
text = block.content,
|
||||||
language = language,
|
language = language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold),
|
style = epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold, color = textColor),
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
@ -934,6 +952,7 @@ private fun ReaderEpigraphBlock(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = depth,
|
depth = depth,
|
||||||
|
textColor = textColor,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
)
|
)
|
||||||
is Fb2EpigraphBlock.Cite -> ReaderCite(
|
is Fb2EpigraphBlock.Cite -> ReaderCite(
|
||||||
@ -942,6 +961,7 @@ private fun ReaderEpigraphBlock(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
highlightedRange = highlightedRange,
|
highlightedRange = highlightedRange,
|
||||||
depth = depth,
|
depth = depth,
|
||||||
|
textColor = textColor,
|
||||||
onLinkOpen = onLinkOpen,
|
onLinkOpen = onLinkOpen,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -1174,80 +1194,120 @@ private fun Modifier.softImageEdgeFade(): Modifier =
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Modifier.readerLinkTapHandler(
|
@Composable
|
||||||
|
private fun BoxScope.ReaderLinkTouchOverlays(
|
||||||
annotatedText: AnnotatedString,
|
annotatedText: AnnotatedString,
|
||||||
textLayout: TextLayoutResult?,
|
textLayout: TextLayoutResult,
|
||||||
onLinkOpen: (String) -> Unit,
|
onLinkOpen: (String) -> Unit,
|
||||||
): Modifier = pointerInput(annotatedText, textLayout, onLinkOpen) {
|
) {
|
||||||
awaitEachGesture {
|
val density = LocalDensity.current
|
||||||
val down = awaitFirstDown(requireUnconsumed = false, pass = PointerEventPass.Main)
|
val touchAreas = remember(annotatedText, textLayout, density) {
|
||||||
val link = textLayout?.readerLinkAt(
|
with(density) {
|
||||||
text = annotatedText,
|
textLayout.readerLinkTouchAreas(
|
||||||
position = down.position,
|
text = annotatedText,
|
||||||
minimumTouchWidthPx = ReaderLinkMinimumTouchWidth.toPx(),
|
trimLeftPx = ReaderLinkTouchTrimLeft.toPx(),
|
||||||
minimumTouchHeightPx = ReaderLinkMinimumTouchHeight.toPx(),
|
extendRightPx = ReaderLinkTouchExtendRight.toPx(),
|
||||||
horizontalPaddingPx = ReaderLinkHorizontalTouchPadding.toPx(),
|
minimumHeightPx = ReaderLinkTouchMinimumHeight.toPx(),
|
||||||
verticalPaddingPx = ReaderLinkVerticalTouchPadding.toPx(),
|
verticalPaddingPx = ReaderLinkTouchVerticalPadding.toPx(),
|
||||||
)
|
)
|
||||||
if (link == null) {
|
|
||||||
waitForUpOrCancellation(pass = PointerEventPass.Main)
|
|
||||||
return@awaitEachGesture
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
down.consume()
|
Box(Modifier.matchParentSize()) {
|
||||||
val up = waitForUpOrCancellation(pass = PointerEventPass.Main)
|
touchAreas.forEach { area ->
|
||||||
if (up != null) {
|
Box(
|
||||||
up.consume()
|
Modifier
|
||||||
onLinkOpen(link)
|
.offset {
|
||||||
|
IntOffset(
|
||||||
|
x = area.rect.left.roundToInt(),
|
||||||
|
y = area.rect.top.roundToInt(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.width(with(density) { area.rect.width.toDp() })
|
||||||
|
.height(with(density) { area.rect.height.toDp() })
|
||||||
|
// Link hit-area debugging: uncomment this background to visualize the
|
||||||
|
// actual touch overlay, including the right-side extension.
|
||||||
|
// .background(ReaderLinkTouchDebugBackground)
|
||||||
|
.clickable { onLinkOpen(area.target) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun TextLayoutResult.readerLinkAt(
|
private data class ReaderLinkTouchArea(
|
||||||
|
val target: String,
|
||||||
|
val rect: Rect,
|
||||||
|
)
|
||||||
|
|
||||||
|
private data class ReaderLinkLineBounds(
|
||||||
|
val annotationIndex: Int,
|
||||||
|
val target: String,
|
||||||
|
val line: Int,
|
||||||
|
val rect: Rect,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun TextLayoutResult.readerLinkTouchAreas(
|
||||||
text: AnnotatedString,
|
text: AnnotatedString,
|
||||||
position: Offset,
|
trimLeftPx: Float,
|
||||||
minimumTouchWidthPx: Float,
|
extendRightPx: Float,
|
||||||
minimumTouchHeightPx: Float,
|
minimumHeightPx: Float,
|
||||||
horizontalPaddingPx: Float,
|
|
||||||
verticalPaddingPx: Float,
|
verticalPaddingPx: Float,
|
||||||
): String? =
|
): List<ReaderLinkTouchArea> {
|
||||||
text.getStringAnnotations(ReaderLinkAnnotationTag, 0, text.length)
|
val linkBounds = text.getStringAnnotations(ReaderLinkAnnotationTag, 0, text.length)
|
||||||
.firstOrNull { annotation ->
|
.sortedBy { it.start }
|
||||||
readerLinkTouchBounds(
|
.flatMapIndexed { annotationIndex, annotation ->
|
||||||
start = annotation.start,
|
val safeStart = min(annotation.start, layoutInput.text.length)
|
||||||
end = annotation.end,
|
val safeEnd = min(annotation.end, layoutInput.text.length)
|
||||||
minimumTouchWidthPx = minimumTouchWidthPx,
|
if (safeStart >= safeEnd) return@flatMapIndexed emptyList()
|
||||||
minimumTouchHeightPx = minimumTouchHeightPx,
|
|
||||||
horizontalPaddingPx = horizontalPaddingPx,
|
val startLine = getLineForOffset(safeStart)
|
||||||
verticalPaddingPx = verticalPaddingPx,
|
val endLine = getLineForOffset(safeEnd - 1)
|
||||||
).any { it.contains(position) }
|
|
||||||
|
(startLine..endLine).mapNotNull { line ->
|
||||||
|
val lineStart = max(safeStart, getLineStart(line))
|
||||||
|
val lineEnd = min(safeEnd, getLineEnd(line))
|
||||||
|
if (lineStart >= lineEnd) return@mapNotNull null
|
||||||
|
|
||||||
|
val rangeBounds = getPathForRange(lineStart, lineEnd).getBounds()
|
||||||
|
val horizontalBounds = Rect(
|
||||||
|
left = rangeBounds.left,
|
||||||
|
top = rangeBounds.top,
|
||||||
|
right = rangeBounds.right,
|
||||||
|
bottom = rangeBounds.bottom,
|
||||||
|
)
|
||||||
|
ReaderLinkLineBounds(
|
||||||
|
annotationIndex = annotationIndex,
|
||||||
|
target = annotation.item,
|
||||||
|
line = line,
|
||||||
|
rect = horizontalBounds,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
?.item
|
|
||||||
|
|
||||||
private fun TextLayoutResult.readerLinkTouchBounds(
|
return linkBounds.map { bounds ->
|
||||||
start: Int,
|
val nextLinkLeft = linkBounds
|
||||||
end: Int,
|
.asSequence()
|
||||||
minimumTouchWidthPx: Float,
|
.filter {
|
||||||
minimumTouchHeightPx: Float,
|
it.line == bounds.line &&
|
||||||
horizontalPaddingPx: Float,
|
it.annotationIndex != bounds.annotationIndex &&
|
||||||
verticalPaddingPx: Float,
|
it.rect.left >= bounds.rect.right
|
||||||
): List<Rect> {
|
}
|
||||||
if (start >= end) return emptyList()
|
.map { it.rect.left }
|
||||||
|
.minOrNull()
|
||||||
|
val rightLimit = nextLinkLeft ?: size.width.toFloat()
|
||||||
|
val right = minOf(bounds.rect.right + extendRightPx, rightLimit, size.width.toFloat())
|
||||||
|
.coerceAtLeast(bounds.rect.right)
|
||||||
|
val touchHeight = max(bounds.rect.height + verticalPaddingPx * 2f, minimumHeightPx)
|
||||||
|
val centerY = bounds.rect.center.y
|
||||||
|
|
||||||
val boundsByLine = linkedMapOf<Int, Rect>()
|
ReaderLinkTouchArea(
|
||||||
val safeEnd = min(end, layoutInput.text.length)
|
target = bounds.target,
|
||||||
for (offset in start until safeEnd) {
|
rect = Rect(
|
||||||
val line = getLineForOffset(offset)
|
left = min(bounds.rect.left + trimLeftPx, bounds.rect.right),
|
||||||
val bounds = getBoundingBox(offset)
|
top = max(0f, centerY - touchHeight / 2f),
|
||||||
boundsByLine[line] = boundsByLine[line]?.union(bounds) ?: bounds
|
right = right,
|
||||||
}
|
bottom = centerY + touchHeight / 2f,
|
||||||
|
),
|
||||||
return boundsByLine.values.map { bounds ->
|
|
||||||
bounds.withCenteredTouchPadding(
|
|
||||||
minimumWidth = minimumTouchWidthPx,
|
|
||||||
minimumHeight = minimumTouchHeightPx,
|
|
||||||
horizontalPadding = horizontalPaddingPx,
|
|
||||||
verticalPadding = verticalPaddingPx,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1260,23 +1320,6 @@ private fun Rect.union(other: Rect): Rect =
|
|||||||
bottom = max(bottom, other.bottom),
|
bottom = max(bottom, other.bottom),
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun Rect.withCenteredTouchPadding(
|
|
||||||
minimumWidth: Float,
|
|
||||||
minimumHeight: Float,
|
|
||||||
horizontalPadding: Float,
|
|
||||||
verticalPadding: Float,
|
|
||||||
): Rect {
|
|
||||||
val touchWidth = max(width + horizontalPadding * 2f, minimumWidth)
|
|
||||||
val touchHeight = max(height + verticalPadding * 2f, minimumHeight)
|
|
||||||
val center = center
|
|
||||||
return Rect(
|
|
||||||
left = center.x - touchWidth / 2f,
|
|
||||||
top = center.y - touchHeight / 2f,
|
|
||||||
right = center.x + touchWidth / 2f,
|
|
||||||
bottom = center.y + touchHeight / 2f,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun AnnotatedString.hasReaderLinks(): Boolean =
|
private fun AnnotatedString.hasReaderLinks(): Boolean =
|
||||||
getStringAnnotations(ReaderLinkAnnotationTag, 0, length).isNotEmpty()
|
getStringAnnotations(ReaderLinkAnnotationTag, 0, length).isNotEmpty()
|
||||||
|
|
||||||
@ -1295,6 +1338,9 @@ private fun Fb2Text.toAnnotatedString(
|
|||||||
fontWeight = if (Fb2TextStyle.Strong in span.styles) FontWeight.Bold else null,
|
fontWeight = if (Fb2TextStyle.Strong in span.styles) FontWeight.Bold else null,
|
||||||
fontFamily = if (Fb2TextStyle.Code in span.styles) FontFamily.Monospace else null,
|
fontFamily = if (Fb2TextStyle.Code in span.styles) FontFamily.Monospace else null,
|
||||||
color = if (isLink) LinkTextColor else Color.Unspecified,
|
color = if (isLink) LinkTextColor else Color.Unspecified,
|
||||||
|
// Link hit-area debugging: set this to ReaderLinkDebugBackground for links
|
||||||
|
// to compare the annotated text range with the touch overlay.
|
||||||
|
background = Color.Unspecified,
|
||||||
textDecoration = when {
|
textDecoration = when {
|
||||||
isLink && Fb2TextStyle.Strikethrough in span.styles ->
|
isLink && Fb2TextStyle.Strikethrough in span.styles ->
|
||||||
TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
|
TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
|
||||||
@ -1308,12 +1354,11 @@ private fun Fb2Text.toAnnotatedString(
|
|||||||
else -> null
|
else -> null
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
span.href?.takeIf { it.isNotBlank() }?.let {
|
|
||||||
pushStringAnnotation(ReaderLinkAnnotationTag, it)
|
|
||||||
}
|
|
||||||
if (isLink) {
|
if (isLink) {
|
||||||
withStyle(spanStyle) {
|
withAnnotation(ReaderLinkAnnotationTag, span.href!!) {
|
||||||
appendWithHighlight(span.text, plainOffset, highlightedRange, highlightColor, language, hyphenation)
|
withStyle(spanStyle) {
|
||||||
|
appendWithHighlight(span.text, plainOffset, highlightedRange, highlightColor, language, hyphenation)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
appendWithDetectedWebLinks(
|
appendWithDetectedWebLinks(
|
||||||
@ -1326,7 +1371,6 @@ private fun Fb2Text.toAnnotatedString(
|
|||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (isLink) pop()
|
|
||||||
plainOffset += span.text.length
|
plainOffset += span.text.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1356,18 +1400,19 @@ private fun AnnotatedString.Builder.appendWithDetectedWebLinks(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val url = text.substring(range)
|
val url = text.substring(range)
|
||||||
pushStringAnnotation(ReaderLinkAnnotationTag, url)
|
val linkStyle = spanStyle.readerLinkStyle()
|
||||||
withStyle(spanStyle.readerLinkStyle()) {
|
withAnnotation(ReaderLinkAnnotationTag, url) {
|
||||||
appendWithHighlight(
|
withStyle(linkStyle) {
|
||||||
text = url,
|
appendWithHighlight(
|
||||||
plainOffset = plainOffset + range.start,
|
text = url,
|
||||||
highlightedRange = highlightedRange,
|
plainOffset = plainOffset + range.start,
|
||||||
highlightColor = highlightColor,
|
highlightedRange = highlightedRange,
|
||||||
language = null,
|
highlightColor = highlightColor,
|
||||||
hyphenation = hyphenation,
|
language = null,
|
||||||
)
|
hyphenation = hyphenation,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pop()
|
|
||||||
cursor = range.endInclusive + 1
|
cursor = range.endInclusive + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1388,6 +1433,9 @@ private fun AnnotatedString.Builder.appendWithDetectedWebLinks(
|
|||||||
private fun SpanStyle.readerLinkStyle(): SpanStyle =
|
private fun SpanStyle.readerLinkStyle(): SpanStyle =
|
||||||
copy(
|
copy(
|
||||||
color = LinkTextColor,
|
color = LinkTextColor,
|
||||||
|
// Link hit-area debugging: set this to ReaderLinkDebugBackground to compare
|
||||||
|
// detected URL text ranges with the touch overlay.
|
||||||
|
background = Color.Unspecified,
|
||||||
textDecoration = when (textDecoration) {
|
textDecoration = when (textDecoration) {
|
||||||
TextDecoration.LineThrough ->
|
TextDecoration.LineThrough ->
|
||||||
TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
|
TextDecoration.combine(listOf(TextDecoration.Underline, TextDecoration.LineThrough))
|
||||||
@ -1798,14 +1846,18 @@ private const val StarBreakPauseMillis = 1_200L
|
|||||||
private const val EllipsisPauseAfterMillis = 350L
|
private const val EllipsisPauseAfterMillis = 350L
|
||||||
private const val CombiningAcuteAccent = '\u0301'
|
private const val CombiningAcuteAccent = '\u0301'
|
||||||
private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY"
|
private const val ReadAloudStressableLetters = "аеёиоуыэюяАЕЁИОУЫЭЮЯaeiouyAEIOUY"
|
||||||
private const val ReaderLinkAnnotationTag = "fb2-link"
|
|
||||||
private val WebUrlRegex = Regex("""https?://\S+""", RegexOption.IGNORE_CASE)
|
private val WebUrlRegex = Regex("""https?://\S+""", RegexOption.IGNORE_CASE)
|
||||||
private val WebUrlTrailingPunctuation = charArrayOf('.', ',', ';', ':', '!', '?', ')', ']', '}', '>', '"', '\'', '»', '”', '’')
|
private val WebUrlTrailingPunctuation = charArrayOf('.', ',', ';', ':', '!', '?', ')', ']', '}', '>', '"', '\'', '»', '”', '’')
|
||||||
private val LinkTextColor = Color(0xFF0B57D0)
|
private val LinkTextColor = Color(0xFF0B57D0)
|
||||||
private val ReaderLinkMinimumTouchWidth = 44.dp
|
// Link hit-area debugging colors. Kept here so the temporary backgrounds above can be
|
||||||
private val ReaderLinkMinimumTouchHeight = 40.dp
|
// re-enabled quickly when continuing touch overlay investigation.
|
||||||
private val ReaderLinkHorizontalTouchPadding = 10.dp
|
// private val ReaderLinkDebugBackground = Color(0x33FFB300)
|
||||||
private val ReaderLinkVerticalTouchPadding = 6.dp
|
// private val ReaderLinkTouchDebugBackground = Color(0x3300BCD4)
|
||||||
|
private const val ReaderLinkAnnotationTag = "fb2-link"
|
||||||
|
private val ReaderLinkTouchTrimLeft = 8.dp
|
||||||
|
private val ReaderLinkTouchExtendRight = 48.dp
|
||||||
|
private val ReaderLinkTouchMinimumHeight = 34.dp
|
||||||
|
private val ReaderLinkTouchVerticalPadding = 4.dp
|
||||||
|
|
||||||
private val ReadAloudHardcodedTextReplacements = listOf(
|
private val ReadAloudHardcodedTextReplacements = listOf(
|
||||||
ReadAloudTextReplacement(from = "Господа,", to = "Господ/а,", caseSensitive = true),
|
ReadAloudTextReplacement(from = "Господа,", to = "Господ/а,", caseSensitive = true),
|
||||||
|
|||||||
@ -6,9 +6,11 @@ import androidx.compose.animation.expandVertically
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.shrinkVertically
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.horizontalScroll
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@ -18,6 +20,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.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
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
|
||||||
@ -66,6 +69,7 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SnackbarDuration
|
import androidx.compose.material3.SnackbarDuration
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
@ -87,7 +91,9 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.lerp
|
||||||
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
|
||||||
@ -110,6 +116,11 @@ private data class ReaderBookPreparation(
|
|||||||
val contentPlan: ReaderContentPlan,
|
val contentPlan: ReaderContentPlan,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private enum class ReaderColorPickerTarget {
|
||||||
|
BACKGROUND,
|
||||||
|
TEXT,
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||||
internal fun BookView(
|
internal fun BookView(
|
||||||
@ -137,6 +148,8 @@ internal fun BookView(
|
|||||||
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 readerExtraSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
|
var readerColorPickerTarget by remember(fileId) { mutableStateOf<ReaderColorPickerTarget?>(null) }
|
||||||
var fullScreenReader by remember { 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()) }
|
||||||
@ -147,6 +160,7 @@ 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()
|
||||||
|
val useDarkReaderTheme = LocalReaderThemeIsDark.current
|
||||||
|
|
||||||
LaunchedEffect(book) {
|
LaunchedEffect(book) {
|
||||||
preparation = withContext(Dispatchers.Default) {
|
preparation = withContext(Dispatchers.Default) {
|
||||||
@ -172,6 +186,12 @@ internal fun BookView(
|
|||||||
val readingProgress by remember(listState) {
|
val readingProgress by remember(listState) {
|
||||||
derivedStateOf { listState.readerProgress() }
|
derivedStateOf { listState.readerProgress() }
|
||||||
}
|
}
|
||||||
|
val readerBackgroundColor = readerFontSettings.themeBackgroundArgb(useDarkReaderTheme)
|
||||||
|
?.toComposeColor()
|
||||||
|
?: MaterialTheme.colorScheme.surface
|
||||||
|
val readerTextColor = readerFontSettings.themeTextArgb(useDarkReaderTheme)
|
||||||
|
?.toComposeColor()
|
||||||
|
?: MaterialTheme.colorScheme.onSurface
|
||||||
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 }
|
||||||
@ -407,6 +427,9 @@ internal fun BookView(
|
|||||||
onReaderSettings = {
|
onReaderSettings = {
|
||||||
readerSettingsPanelVisible = true
|
readerSettingsPanelVisible = true
|
||||||
},
|
},
|
||||||
|
onReaderExtraSettings = {
|
||||||
|
readerExtraSettingsVisible = true
|
||||||
|
},
|
||||||
showReadAloudAction = showReadAloudAction,
|
showReadAloudAction = showReadAloudAction,
|
||||||
onReadAloud = {
|
onReadAloud = {
|
||||||
val position = currentReadingPosition()
|
val position = currentReadingPosition()
|
||||||
@ -449,7 +472,7 @@ internal fun BookView(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(it)
|
.padding(it)
|
||||||
.background(readerBackground()),
|
.background(readerBackgroundColor),
|
||||||
) {
|
) {
|
||||||
Column(Modifier.fillMaxSize()) {
|
Column(Modifier.fillMaxSize()) {
|
||||||
ContinuousBookReader(
|
ContinuousBookReader(
|
||||||
@ -460,6 +483,8 @@ internal fun BookView(
|
|||||||
contentPlan = contentPlan,
|
contentPlan = contentPlan,
|
||||||
highlightedSentence = highlightedSentence,
|
highlightedSentence = highlightedSentence,
|
||||||
readerFontSettings = readerFontSettings,
|
readerFontSettings = readerFontSettings,
|
||||||
|
readerBackgroundColor = readerBackgroundColor,
|
||||||
|
readerTextColor = readerTextColor,
|
||||||
onReaderFontZoom = { steps ->
|
onReaderFontZoom = { steps ->
|
||||||
updateReaderFontSettings { settings -> settings.adjustFontSize(steps) }
|
updateReaderFontSettings { settings -> settings.adjustFontSize(steps) }
|
||||||
},
|
},
|
||||||
@ -488,8 +513,35 @@ internal fun BookView(
|
|||||||
onFontWeightIncrease = {
|
onFontWeightIncrease = {
|
||||||
updateReaderFontSettings { settings -> settings.adjustFontWeight(1) }
|
updateReaderFontSettings { settings -> settings.adjustFontWeight(1) }
|
||||||
},
|
},
|
||||||
onReset = {
|
onBackgroundColorChange = {
|
||||||
updateReaderFontSettings { defaultReaderFontSettings() }
|
updateReaderFontSettings { settings ->
|
||||||
|
settings.withThemeBackgroundArgb(
|
||||||
|
useDarkReaderTheme,
|
||||||
|
nextReaderBackgroundColorArgb(
|
||||||
|
currentArgb = settings.themeBackgroundArgb(useDarkReaderTheme),
|
||||||
|
customArgb = settings.themeCustomBackgroundArgb(useDarkReaderTheme),
|
||||||
|
useDarkTheme = useDarkReaderTheme,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onBackgroundColorPick = {
|
||||||
|
readerColorPickerTarget = ReaderColorPickerTarget.BACKGROUND
|
||||||
|
},
|
||||||
|
onTextColorChange = {
|
||||||
|
updateReaderFontSettings { settings ->
|
||||||
|
settings.withThemeTextArgb(
|
||||||
|
useDarkReaderTheme,
|
||||||
|
nextReaderTextColorArgb(
|
||||||
|
currentArgb = settings.themeTextArgb(useDarkReaderTheme),
|
||||||
|
customArgb = settings.themeCustomTextArgb(useDarkReaderTheme),
|
||||||
|
useDarkTheme = useDarkReaderTheme,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTextColorPick = {
|
||||||
|
readerColorPickerTarget = ReaderColorPickerTarget.TEXT
|
||||||
},
|
},
|
||||||
onClose = { readerSettingsPanelVisible = false },
|
onClose = { readerSettingsPanelVisible = false },
|
||||||
)
|
)
|
||||||
@ -517,6 +569,8 @@ internal fun BookView(
|
|||||||
progress = readingProgress,
|
progress = readingProgress,
|
||||||
fullScreen = fullScreenReader,
|
fullScreen = fullScreenReader,
|
||||||
fullScreenTitle = fullScreenProgressTitle,
|
fullScreenTitle = fullScreenProgressTitle,
|
||||||
|
backgroundColor = readerBackgroundColor,
|
||||||
|
textColor = readerTextColor,
|
||||||
onFullScreenToggle = { updateReaderFullScreen(!fullScreenReader) },
|
onFullScreenToggle = { updateReaderFullScreen(!fullScreenReader) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -532,6 +586,50 @@ internal fun BookView(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (readerExtraSettingsVisible) {
|
||||||
|
ReaderExtraSettingsDialog(
|
||||||
|
onRevertColorScheme = {
|
||||||
|
updateReaderFontSettings { settings -> settings.resetThemeColorScheme(useDarkReaderTheme) }
|
||||||
|
},
|
||||||
|
onDismiss = { readerExtraSettingsVisible = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
readerColorPickerTarget?.let { target ->
|
||||||
|
ReaderColorPickerDialog(
|
||||||
|
title = when (target) {
|
||||||
|
ReaderColorPickerTarget.BACKGROUND -> strings.readerBackgroundColor
|
||||||
|
ReaderColorPickerTarget.TEXT -> strings.readerTextColor
|
||||||
|
},
|
||||||
|
initialArgb = when (target) {
|
||||||
|
ReaderColorPickerTarget.BACKGROUND ->
|
||||||
|
readerFontSettings.themeBackgroundArgb(useDarkReaderTheme) ?: readerBackgroundColor.toReaderArgb()
|
||||||
|
ReaderColorPickerTarget.TEXT ->
|
||||||
|
readerFontSettings.themeTextArgb(useDarkReaderTheme) ?: readerTextColor.toReaderArgb()
|
||||||
|
},
|
||||||
|
presets = when (target) {
|
||||||
|
ReaderColorPickerTarget.BACKGROUND -> readerBackgroundPalette(useDarkReaderTheme)
|
||||||
|
ReaderColorPickerTarget.TEXT -> readerTextPalette(useDarkReaderTheme)
|
||||||
|
},
|
||||||
|
customArgb = when (target) {
|
||||||
|
ReaderColorPickerTarget.BACKGROUND -> readerFontSettings.themeCustomBackgroundArgb(useDarkReaderTheme)
|
||||||
|
ReaderColorPickerTarget.TEXT -> readerFontSettings.themeCustomTextArgb(useDarkReaderTheme)
|
||||||
|
},
|
||||||
|
onDismiss = { readerColorPickerTarget = null },
|
||||||
|
onApply = { argb ->
|
||||||
|
updateReaderFontSettings { settings ->
|
||||||
|
when (target) {
|
||||||
|
ReaderColorPickerTarget.BACKGROUND ->
|
||||||
|
settings.withThemeCustomBackgroundArgb(useDarkReaderTheme, argb)
|
||||||
|
ReaderColorPickerTarget.TEXT ->
|
||||||
|
settings.withThemeCustomTextArgb(useDarkReaderTheme, argb)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
readerColorPickerTarget = null
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
selectedNoteId?.let { noteId ->
|
selectedNoteId?.let { noteId ->
|
||||||
Fb2NoteReferenceDialog(
|
Fb2NoteReferenceDialog(
|
||||||
book = book,
|
book = book,
|
||||||
@ -577,12 +675,14 @@ private fun ReaderProgressPane(
|
|||||||
progress: Float,
|
progress: Float,
|
||||||
fullScreen: Boolean,
|
fullScreen: Boolean,
|
||||||
fullScreenTitle: String,
|
fullScreenTitle: String,
|
||||||
|
backgroundColor: Color,
|
||||||
|
textColor: Color,
|
||||||
onFullScreenToggle: () -> Unit,
|
onFullScreenToggle: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val percent = (progress.coerceIn(0f, 1f) * 100f).roundToInt()
|
val percent = (progress.coerceIn(0f, 1f) * 100f).roundToInt()
|
||||||
val trackColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.45f)
|
val trackColor = textColor.copy(alpha = 0.34f)
|
||||||
val readColor = MaterialTheme.colorScheme.primary
|
val readColor = textColor.copy(alpha = 0.84f)
|
||||||
val fullScreenFillColor = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.72f)
|
val fullScreenFillColor = fullScreenProgressFillColor(textColor)
|
||||||
val contentDescription = strings.progressLabel(progress.toDouble())
|
val contentDescription = strings.progressLabel(progress.toDouble())
|
||||||
val height by animateDpAsState(if (fullScreen) 36.dp else 24.dp)
|
val height by animateDpAsState(if (fullScreen) 36.dp else 24.dp)
|
||||||
val progressRowHeight by animateDpAsState(24.dp)
|
val progressRowHeight by animateDpAsState(24.dp)
|
||||||
@ -593,7 +693,7 @@ private fun ReaderProgressPane(
|
|||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 2.dp,
|
tonalElevation = 2.dp,
|
||||||
shadowElevation = 2.dp,
|
shadowElevation = 2.dp,
|
||||||
color = MaterialTheme.colorScheme.surface,
|
color = backgroundColor,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable(onClick = onFullScreenToggle),
|
.clickable(onClick = onFullScreenToggle),
|
||||||
@ -628,7 +728,7 @@ private fun ReaderProgressPane(
|
|||||||
Text(
|
Text(
|
||||||
text = fullScreenTitle,
|
text = fullScreenTitle,
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = textColor,
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
@ -640,7 +740,7 @@ private fun ReaderProgressPane(
|
|||||||
text = "$percent%",
|
text = "$percent%",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -680,7 +780,7 @@ private fun ReaderProgressPane(
|
|||||||
text = "$percent%",
|
text = "$percent%",
|
||||||
style = MaterialTheme.typography.labelMedium,
|
style = MaterialTheme.typography.labelMedium,
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = textColor,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -689,9 +789,21 @@ private fun ReaderProgressPane(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun fullScreenProgressFillColor(textColor: Color): Color {
|
||||||
|
val luminance = textColor.readerLuminance()
|
||||||
|
return if (luminance < 0.55f) {
|
||||||
|
lerp(textColor, Color.White, 0.45f).copy(alpha = 0.22f)
|
||||||
|
} else {
|
||||||
|
textColor.copy(alpha = 0.12f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Color.readerLuminance(): Float =
|
||||||
|
0.2126f * red + 0.7152f * green + 0.0722f * blue
|
||||||
|
|
||||||
private fun Fb2Book.fullScreenProgressTitle(): String {
|
private fun Fb2Book.fullScreenProgressTitle(): String {
|
||||||
val author = authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor }
|
val author = authors.joinToString { it.displayName }.ifBlank { strings.unknownAuthor }
|
||||||
return "$author. $title"
|
return "$title. $author."
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun LazyListState.readerProgress(): Float {
|
private fun LazyListState.readerProgress(): Float {
|
||||||
@ -739,6 +851,7 @@ private fun CompactReaderTopBar(
|
|||||||
showReadAloudAction: Boolean,
|
showReadAloudAction: Boolean,
|
||||||
onReadAloud: () -> Unit,
|
onReadAloud: () -> Unit,
|
||||||
onReaderSettings: () -> Unit,
|
onReaderSettings: () -> Unit,
|
||||||
|
onReaderExtraSettings: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -795,6 +908,16 @@ private fun CompactReaderTopBar(
|
|||||||
onReaderSettings()
|
onReaderSettings()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(Icons.Filled.Settings, contentDescription = null)
|
||||||
|
},
|
||||||
|
text = { Text(strings.extraReaderSettings) },
|
||||||
|
onClick = {
|
||||||
|
menuOpen = false
|
||||||
|
onReaderExtraSettings()
|
||||||
|
},
|
||||||
|
)
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
Checkbox(checked = fullScreen, onCheckedChange = null)
|
Checkbox(checked = fullScreen, onCheckedChange = null)
|
||||||
@ -1062,9 +1185,16 @@ private fun ReaderFontSettingsPanel(
|
|||||||
onLineHeightIncrease: () -> Unit,
|
onLineHeightIncrease: () -> Unit,
|
||||||
onFontWeightDecrease: () -> Unit,
|
onFontWeightDecrease: () -> Unit,
|
||||||
onFontWeightIncrease: () -> Unit,
|
onFontWeightIncrease: () -> Unit,
|
||||||
onReset: () -> Unit,
|
onBackgroundColorChange: () -> Unit,
|
||||||
|
onBackgroundColorPick: () -> Unit,
|
||||||
|
onTextColorChange: () -> Unit,
|
||||||
|
onTextColorPick: () -> Unit,
|
||||||
onClose: () -> Unit,
|
onClose: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val useDarkTheme = LocalReaderThemeIsDark.current
|
||||||
|
val backgroundSwatch = settings.themeBackgroundArgb(useDarkTheme)?.toComposeColor() ?: MaterialTheme.colorScheme.surface
|
||||||
|
val textSwatch = settings.themeTextArgb(useDarkTheme)?.toComposeColor() ?: MaterialTheme.colorScheme.onSurface
|
||||||
|
|
||||||
Surface(
|
Surface(
|
||||||
tonalElevation = 3.dp,
|
tonalElevation = 3.dp,
|
||||||
shadowElevation = 4.dp,
|
shadowElevation = 4.dp,
|
||||||
@ -1074,34 +1204,48 @@ private fun ReaderFontSettingsPanel(
|
|||||||
Column {
|
Column {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
.horizontalScroll(rememberScrollState())
|
.padding(horizontal = 2.dp),
|
||||||
.padding(bottom = 0.dp),
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
// Text("A ${settings.fontSizeSp.roundToInt()}", style = MaterialTheme.typography.labelMedium)
|
// Text("A ${settings.fontSizeSp.roundToInt()}", style = MaterialTheme.typography.labelMedium)
|
||||||
IconButton(onClick = onFontSizeDecrease) {
|
ReaderSettingsButton(
|
||||||
|
contentDescription = strings.readerFontSizeDecrease,
|
||||||
|
onClick = onFontSizeDecrease,
|
||||||
|
) {
|
||||||
ReaderLetterChangeIcon(
|
ReaderLetterChangeIcon(
|
||||||
letter = "A",
|
letter = "A",
|
||||||
increase = false,
|
increase = false,
|
||||||
contentDescription = strings.readerFontSizeDecrease,
|
contentDescription = strings.readerFontSizeDecrease,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onFontSizeIncrease) {
|
ReaderSettingsButton(
|
||||||
|
contentDescription = strings.readerFontSizeIncrease,
|
||||||
|
onClick = onFontSizeIncrease,
|
||||||
|
) {
|
||||||
ReaderLetterChangeIcon(
|
ReaderLetterChangeIcon(
|
||||||
letter = "A",
|
letter = "A",
|
||||||
increase = true,
|
increase = true,
|
||||||
contentDescription = strings.readerFontSizeIncrease,
|
contentDescription = strings.readerFontSizeIncrease,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onLineHeightDecrease) {
|
ReaderSettingsButton(
|
||||||
Icon(Icons.Filled.UnfoldLess, contentDescription = strings.readerLineHeightDecrease)
|
contentDescription = strings.readerLineHeightDecrease,
|
||||||
|
onClick = onLineHeightDecrease,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.UnfoldLess, contentDescription = null)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onLineHeightIncrease) {
|
ReaderSettingsButton(
|
||||||
Icon(Icons.Filled.UnfoldMore, contentDescription = strings.readerLineHeightIncrease)
|
contentDescription = strings.readerLineHeightIncrease,
|
||||||
|
onClick = onLineHeightIncrease,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.UnfoldMore, contentDescription = null)
|
||||||
}
|
}
|
||||||
// Text("W ${settings.fontWeight}", style = MaterialTheme.typography.labelMedium)
|
// Text("W ${settings.fontWeight}", style = MaterialTheme.typography.labelMedium)
|
||||||
IconButton(onClick = onFontWeightDecrease) {
|
ReaderSettingsButton(
|
||||||
|
contentDescription = strings.readerFontWeightDecrease,
|
||||||
|
onClick = onFontWeightDecrease,
|
||||||
|
) {
|
||||||
ReaderLetterChangeIcon(
|
ReaderLetterChangeIcon(
|
||||||
letter = "B",
|
letter = "B",
|
||||||
increase = false,
|
increase = false,
|
||||||
@ -1109,7 +1253,10 @@ private fun ReaderFontSettingsPanel(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onFontWeightIncrease) {
|
ReaderSettingsButton(
|
||||||
|
contentDescription = strings.readerFontWeightIncrease,
|
||||||
|
onClick = onFontWeightIncrease,
|
||||||
|
) {
|
||||||
ReaderLetterChangeIcon(
|
ReaderLetterChangeIcon(
|
||||||
letter = "B",
|
letter = "B",
|
||||||
increase = true,
|
increase = true,
|
||||||
@ -1117,11 +1264,31 @@ private fun ReaderFontSettingsPanel(
|
|||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onReset) {
|
ColorSettingButton(
|
||||||
Icon(Icons.Filled.RestartAlt, contentDescription = strings.resetReaderFontSettings)
|
contentDescription = strings.readerBackgroundColor,
|
||||||
|
onClick = onBackgroundColorChange,
|
||||||
|
onLongClick = onBackgroundColorPick,
|
||||||
|
) {
|
||||||
|
ColorSwatchIcon(color = backgroundSwatch)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onClose) {
|
ColorSettingButton(
|
||||||
Icon(Icons.Filled.Close, contentDescription = strings.closeReaderSettings)
|
contentDescription = strings.readerTextColor,
|
||||||
|
onClick = onTextColorChange,
|
||||||
|
onLongClick = onTextColorPick,
|
||||||
|
) {
|
||||||
|
ReaderLetterChangeIcon(
|
||||||
|
letter = "T",
|
||||||
|
increase = true,
|
||||||
|
contentDescription = strings.readerTextColor,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = textSwatch,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ReaderSettingsButton(
|
||||||
|
contentDescription = strings.closeReaderSettings,
|
||||||
|
onClick = onClose,
|
||||||
|
) {
|
||||||
|
Icon(Icons.Filled.Close, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Row(Modifier.fillMaxWidth().padding(bottom = 2.dp, top = 0.dp), horizontalArrangement = Arrangement.Center) {
|
// Row(Modifier.fillMaxWidth().padding(bottom = 2.dp, top = 0.dp), horizontalArrangement = Arrangement.Center) {
|
||||||
@ -1142,17 +1309,320 @@ private fun ReaderLetterChangeIcon(
|
|||||||
increase: Boolean,
|
increase: Boolean,
|
||||||
contentDescription: String,
|
contentDescription: String,
|
||||||
fontWeight: FontWeight = FontWeight.SemiBold,
|
fontWeight: FontWeight = FontWeight.SemiBold,
|
||||||
|
color: Color = Color.Unspecified,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = letter + if (increase) "⁺" else "⁻",
|
text = letter + if (increase) "⁺" else "⁻",
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
fontWeight = fontWeight,
|
fontWeight = fontWeight,
|
||||||
|
color = color,
|
||||||
modifier = Modifier.semantics {
|
modifier = Modifier.semantics {
|
||||||
this.contentDescription = contentDescription
|
this.contentDescription = contentDescription
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.ReaderSettingsButton(
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.semantics {
|
||||||
|
this.contentDescription = contentDescription
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.ColorSettingButton(
|
||||||
|
contentDescription: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.height(48.dp)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
.semantics {
|
||||||
|
this.contentDescription = contentDescription
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColorSwatchIcon(color: Color) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(22.dp)
|
||||||
|
.background(color),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderExtraSettingsDialog(
|
||||||
|
onRevertColorScheme: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(strings.extraReaderSettings) },
|
||||||
|
text = {
|
||||||
|
Column(Modifier.fillMaxWidth()) {
|
||||||
|
ReaderExtraSettingsActionRow(
|
||||||
|
text = strings.revertColorScheme,
|
||||||
|
onClick = onRevertColorScheme,
|
||||||
|
)
|
||||||
|
HorizontalDivider()
|
||||||
|
readerExtraSettingOptions().forEach { option ->
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable { }
|
||||||
|
.padding(horizontal = 4.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = option,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = ">",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(strings.done)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderExtraSettingsActionRow(text: String, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(horizontal = 4.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderColorPickerDialog(
|
||||||
|
title: String,
|
||||||
|
initialArgb: Long,
|
||||||
|
presets: List<Long>,
|
||||||
|
customArgb: Long?,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
|
onApply: (Long) -> Unit,
|
||||||
|
) {
|
||||||
|
val initialRgb = remember(initialArgb) { initialArgb.toRgbComponents() }
|
||||||
|
var red by remember(initialArgb) { mutableStateOf(initialRgb.red) }
|
||||||
|
var green by remember(initialArgb) { mutableStateOf(initialRgb.green) }
|
||||||
|
var blue by remember(initialArgb) { mutableStateOf(initialRgb.blue) }
|
||||||
|
val selectedArgb = rgbToReaderArgb(red, green, blue)
|
||||||
|
val swatches = remember(presets, customArgb) { presets.withCustomColor(customArgb) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = { Text(title) },
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
color = selectedArgb.toComposeColor(),
|
||||||
|
modifier = Modifier.fillMaxWidth().height(48.dp),
|
||||||
|
) {}
|
||||||
|
if (customArgb != null) {
|
||||||
|
Text(strings.customColor, style = MaterialTheme.typography.labelMedium)
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
swatches.forEach { argb ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(36.dp)
|
||||||
|
.background(argb.toComposeColor())
|
||||||
|
.clickable {
|
||||||
|
val rgb = argb.toRgbComponents()
|
||||||
|
red = rgb.red
|
||||||
|
green = rgb.green
|
||||||
|
blue = rgb.blue
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ReaderColorSlider(label = strings.red, value = red, onValueChange = { red = it })
|
||||||
|
ReaderColorSlider(label = strings.green, value = green, onValueChange = { green = it })
|
||||||
|
ReaderColorSlider(label = strings.blue, value = blue, onValueChange = { blue = it })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(strings.cancel)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = { onApply(selectedArgb) }) {
|
||||||
|
Text(strings.done)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderColorSlider(label: String, value: Int, onValueChange: (Int) -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
value = value.toFloat(),
|
||||||
|
onValueChange = { onValueChange(it.roundToInt().coerceIn(0, 255)) },
|
||||||
|
valueRange = 0f..255f,
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value.toString(),
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
textAlign = TextAlign.End,
|
||||||
|
modifier = Modifier.size(36.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readerExtraSettingOptions(): List<String> =
|
||||||
|
listOf(
|
||||||
|
strings.readerColors,
|
||||||
|
strings.readerTypography,
|
||||||
|
strings.readerLayout,
|
||||||
|
strings.readerNavigation,
|
||||||
|
strings.readerProgress,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun nextReaderBackgroundColorArgb(currentArgb: Long?, customArgb: Long?, useDarkTheme: Boolean): Long? {
|
||||||
|
val palette = readerBackgroundPalette(useDarkTheme).withCustomColor(customArgb)
|
||||||
|
return nextReaderPaletteArgb(currentArgb, palette)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextReaderTextColorArgb(currentArgb: Long?, customArgb: Long?, useDarkTheme: Boolean): Long? {
|
||||||
|
val palette = readerTextPalette(useDarkTheme).withCustomColor(customArgb)
|
||||||
|
return nextReaderPaletteArgb(currentArgb, palette)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun nextReaderPaletteArgb(currentArgb: Long?, palette: List<Long>): Long? {
|
||||||
|
val index = palette.indexOf(currentArgb)
|
||||||
|
return when {
|
||||||
|
index < 0 -> palette.firstOrNull()
|
||||||
|
index < palette.lastIndex -> palette[index + 1]
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun readerBackgroundPalette(useDarkTheme: Boolean): List<Long> =
|
||||||
|
if (useDarkTheme) DarkReaderBackgroundPalette else LightReaderBackgroundPalette
|
||||||
|
|
||||||
|
private fun readerTextPalette(useDarkTheme: Boolean): List<Long> =
|
||||||
|
if (useDarkTheme) DarkReaderTextPalette else LightReaderTextPalette
|
||||||
|
|
||||||
|
private fun List<Long>.withCustomColor(customArgb: Long?): List<Long> =
|
||||||
|
if (customArgb == null || customArgb in this) this else this + customArgb
|
||||||
|
|
||||||
|
private data class RgbComponents(
|
||||||
|
val red: Int,
|
||||||
|
val green: Int,
|
||||||
|
val blue: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Long.toRgbComponents(): RgbComponents =
|
||||||
|
RgbComponents(
|
||||||
|
red = ((this shr 16) and 0xFF).toInt(),
|
||||||
|
green = ((this shr 8) and 0xFF).toInt(),
|
||||||
|
blue = (this and 0xFF).toInt(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Color.toReaderArgb(): Long {
|
||||||
|
fun Float.componentToByte(): Long = (coerceIn(0f, 1f) * 255f).roundToInt().coerceIn(0, 255).toLong()
|
||||||
|
return (alpha.componentToByte() shl 24) or
|
||||||
|
(red.componentToByte() shl 16) or
|
||||||
|
(green.componentToByte() shl 8) or
|
||||||
|
blue.componentToByte()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rgbToReaderArgb(red: Int, green: Int, blue: Int): Long =
|
||||||
|
(0xFFL shl 24) or
|
||||||
|
(red.coerceIn(0, 255).toLong() shl 16) or
|
||||||
|
(green.coerceIn(0, 255).toLong() shl 8) or
|
||||||
|
blue.coerceIn(0, 255).toLong()
|
||||||
|
|
||||||
|
private val LightReaderBackgroundPalette = listOf(
|
||||||
|
0xFFF7F2EAL,
|
||||||
|
0xFFF4F0D8L,
|
||||||
|
0xFFEAF3F0L,
|
||||||
|
0xFFECEFF5L,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkReaderBackgroundPalette = listOf(
|
||||||
|
0xFF171411L,
|
||||||
|
0xFF1B2421L,
|
||||||
|
0xFF181E29L,
|
||||||
|
0xFF24211DL,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LightReaderTextPalette = listOf(
|
||||||
|
0xFF005C51L,
|
||||||
|
0xFF7A1F1FL,
|
||||||
|
0xFF243A8FL,
|
||||||
|
0xFF6B2A6BL,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val DarkReaderTextPalette = listOf(
|
||||||
|
0xFFD8E8E2L,
|
||||||
|
0xFFE2C58FL,
|
||||||
|
0xFFECEFF5L,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReadAloudPanel(
|
private fun ReadAloudPanel(
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
|
|||||||
@ -141,6 +141,14 @@ internal fun themedTopAppBarColors(): TopAppBarColors =
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background)
|
internal fun readerBackground(): Brush = SolidColor(MaterialTheme.colorScheme.background)
|
||||||
|
|
||||||
|
internal fun Long.toComposeColor(): Color =
|
||||||
|
Color(
|
||||||
|
alpha = ((this shr 24) and 0xFF).toInt(),
|
||||||
|
red = ((this shr 16) and 0xFF).toInt(),
|
||||||
|
green = ((this shr 8) and 0xFF).toInt(),
|
||||||
|
blue = (this and 0xFF).toInt(),
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun readerImageBackgroundColor(): Color {
|
internal fun readerImageBackgroundColor(): Color {
|
||||||
val surface = MaterialTheme.colorScheme.surface
|
val surface = MaterialTheme.colorScheme.surface
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
package net.sergeych.toread
|
package net.sergeych.toread
|
||||||
|
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
internal val LocalReaderThemeIsDark = compositionLocalOf { false }
|
||||||
|
|
||||||
internal fun ThemeMode.next(): ThemeMode =
|
internal fun ThemeMode.next(): ThemeMode =
|
||||||
when (this) {
|
when (this) {
|
||||||
ThemeMode.SYSTEM -> ThemeMode.LIGHT
|
ThemeMode.SYSTEM -> ThemeMode.LIGHT
|
||||||
|
|||||||
@ -475,9 +475,18 @@ actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = withContex
|
|||||||
|
|
||||||
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(LibraryFilterFlag)
|
val saved = db.getAppFlag(LibraryFilterFlag)
|
||||||
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
when {
|
||||||
?: LibraryFilter.MyLibrary
|
saved == null -> {
|
||||||
|
appendLibraryLog("warning library filter was not saved; using ${LibraryFilter.MyLibrary.name}")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
else -> runCatching { LibraryFilter.valueOf(saved) }
|
||||||
|
.getOrElse {
|
||||||
|
appendLibraryLog("warning invalid saved library filter '$saved'; using ${LibraryFilter.MyLibrary.name}")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -95,9 +95,17 @@ 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 =
|
internal actual suspend fun loadLibraryFilter(): LibraryFilter =
|
||||||
window.localStorage.getItem(LibraryFilterStorageKey)
|
when (val saved = window.localStorage.getItem(LibraryFilterStorageKey)) {
|
||||||
?.let { runCatching { LibraryFilter.valueOf(it) }.getOrNull() }
|
null -> {
|
||||||
?: LibraryFilter.MyLibrary
|
warnLibraryFilterFallback("library filter was not saved")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
else -> runCatching { LibraryFilter.valueOf(saved) }
|
||||||
|
.getOrElse {
|
||||||
|
warnLibraryFilterFallback("invalid saved library filter '$saved'")
|
||||||
|
LibraryFilter.MyLibrary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal actual suspend fun saveLibraryFilter(filter: LibraryFilter) {
|
internal actual suspend fun saveLibraryFilter(filter: LibraryFilter) {
|
||||||
window.localStorage.setItem(LibraryFilterStorageKey, filter.name)
|
window.localStorage.setItem(LibraryFilterStorageKey, filter.name)
|
||||||
@ -136,6 +144,10 @@ actual fun watchPlatformDarkTheme(onChange: (Boolean) -> Unit): () -> Unit {
|
|||||||
|
|
||||||
actual fun libraryLogPath(): String? = null
|
actual fun libraryLogPath(): String? = null
|
||||||
|
|
||||||
|
private fun warnLibraryFilterFallback(reason: String) {
|
||||||
|
println("warning $reason; using ${LibraryFilter.MyLibrary.name}")
|
||||||
|
}
|
||||||
|
|
||||||
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
||||||
private const val LibraryFilterStorageKey = "toread.libraryFilter"
|
private const val LibraryFilterStorageKey = "toread.libraryFilter"
|
||||||
private const val ReaderFontSettingsStorageKey = "toread.readerFontSettings"
|
private const val ReaderFontSettingsStorageKey = "toread.readerFontSettings"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user