added visual settings (font/spacing/weight), imprpved linux "show file", added "share/show" to a library card
This commit is contained in:
parent
3e83185e4f
commit
2fddacb7e7
@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl
|
|||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
val appVersionName = "1.0"
|
val appVersionName = "1.0"
|
||||||
val appVersionCode = 4
|
val appVersionCode = 5
|
||||||
val appVersionDisplay = "$appVersionName.$appVersionCode"
|
val appVersionDisplay = "$appVersionName.$appVersionCode"
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
|||||||
@ -372,7 +372,7 @@ actual suspend fun shareLibraryBookFile(fileId: String): Boolean = withContext(D
|
|||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
|
actual suspend fun viewLibraryBookFile(folder: String, fileName: String): Boolean = false
|
||||||
|
|
||||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
@ -432,6 +432,18 @@ actual suspend fun saveThemeMode(mode: ThemeMode) = withContext(Dispatchers.IO)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFontSettings(): ReaderFontSettings = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
readerFontSettingsFromStorageValue(db.getAppFlag(ReaderFontSettingsFlag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(ReaderFontSettingsFlag, settings.toStorageValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
||||||
@ -803,6 +815,7 @@ private val SearchPrefixRegex = Regex("""[\p{L}\p{N}]+""")
|
|||||||
|
|
||||||
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
||||||
private const val ThemeModeFlag = "theme_mode"
|
private const val ThemeModeFlag = "theme_mode"
|
||||||
|
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
||||||
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
||||||
private const val AppLocaleTagFlag = "app_locale_tag"
|
private const val AppLocaleTagFlag = "app_locale_tag"
|
||||||
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
||||||
|
|||||||
@ -3,6 +3,7 @@ package net.sergeych.toread
|
|||||||
import net.sergeych.toread.fb2.Fb2Binary
|
import net.sergeych.toread.fb2.Fb2Binary
|
||||||
import net.sergeych.toread.fb2.Fb2Book
|
import net.sergeych.toread.fb2.Fb2Book
|
||||||
import net.sergeych.toread.storage.BookReadingStatus
|
import net.sergeych.toread.storage.BookReadingStatus
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
data class LibraryItem(
|
data class LibraryItem(
|
||||||
val fileId: String,
|
val fileId: String,
|
||||||
@ -63,6 +64,13 @@ data class ReadingPosition(
|
|||||||
val readAloudSentenceIndex: Int? = null,
|
val readAloudSentenceIndex: Int? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class ReaderFontSettings(
|
||||||
|
val fontSizeSp: Float = defaultReaderFontSizeSp(),
|
||||||
|
val lineHeightSp: Float = DefaultReaderLineHeightSp,
|
||||||
|
val fontWeight: Int = defaultReaderFontWeight(),
|
||||||
|
val fontName: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
data class BookInfoExtras(
|
data class BookInfoExtras(
|
||||||
val sourceFileName: String? = null,
|
val sourceFileName: String? = null,
|
||||||
val sourceFilePath: String? = null,
|
val sourceFilePath: String? = null,
|
||||||
@ -132,7 +140,7 @@ expect suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boole
|
|||||||
|
|
||||||
expect suspend fun shareLibraryBookFile(fileId: String): Boolean
|
expect suspend fun shareLibraryBookFile(fileId: String): Boolean
|
||||||
|
|
||||||
expect suspend fun viewLibraryBookFile(fileId: String): Boolean
|
expect suspend fun viewLibraryBookFile(folder: String, fileName: String): Boolean
|
||||||
|
|
||||||
expect suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras
|
expect suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras
|
||||||
|
|
||||||
@ -144,6 +152,10 @@ expect suspend fun loadThemeMode(): ThemeMode
|
|||||||
|
|
||||||
expect suspend fun saveThemeMode(mode: ThemeMode)
|
expect suspend fun saveThemeMode(mode: ThemeMode)
|
||||||
|
|
||||||
|
expect suspend fun loadReaderFontSettings(): ReaderFontSettings
|
||||||
|
|
||||||
|
expect suspend fun saveReaderFontSettings(settings: ReaderFontSettings)
|
||||||
|
|
||||||
expect suspend fun loadScanDownloadsAutomatically(): Boolean
|
expect suspend fun loadScanDownloadsAutomatically(): Boolean
|
||||||
|
|
||||||
expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)
|
expect suspend fun saveScanDownloadsAutomatically(enabled: Boolean)
|
||||||
@ -172,3 +184,116 @@ internal fun Fb2Book.libraryCoverBinary(): Fb2Binary? {
|
|||||||
val image = coverImages.firstOrNull() ?: bodyImages.firstOrNull()
|
val image = coverImages.firstOrNull() ?: bodyImages.firstOrNull()
|
||||||
return image?.let(::binaryFor)
|
return image?.let(::binaryFor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun canShareLibraryBookFile(): Boolean =
|
||||||
|
getPlatform().name.startsWith("Android")
|
||||||
|
|
||||||
|
internal suspend fun shareLibraryBook(fileId: String): String =
|
||||||
|
if (shareLibraryBookFile(fileId)) strings.shareOpened() else strings.couldNotShareBook()
|
||||||
|
|
||||||
|
internal fun canViewLibraryBookFile(): Boolean =
|
||||||
|
getPlatform().name.startsWith("Java")
|
||||||
|
|
||||||
|
internal suspend fun viewLibraryBook(fileId: String): String {
|
||||||
|
val location = loadLibraryItem(fileId)?.storageUri?.toLibraryBookFileLocation()
|
||||||
|
?: return strings.couldNotOpenFileLocation()
|
||||||
|
return if (viewLibraryBookFile(location.folder, location.fileName)) {
|
||||||
|
strings.openedFileLocation()
|
||||||
|
} else {
|
||||||
|
strings.couldNotOpenFileLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class LibraryBookFileLocation(
|
||||||
|
val folder: String,
|
||||||
|
val fileName: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun String.toLibraryBookFileLocation(): LibraryBookFileLocation? {
|
||||||
|
val separatorIndex = maxOf(lastIndexOf('/'), lastIndexOf('\\'))
|
||||||
|
if (separatorIndex < 0 || separatorIndex >= lastIndex) return null
|
||||||
|
return LibraryBookFileLocation(
|
||||||
|
folder = if (separatorIndex == 0) substring(0, 1) else substring(0, separatorIndex),
|
||||||
|
fileName = substring(separatorIndex + 1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal const val DefaultReaderLineHeightSp = 26f
|
||||||
|
internal const val ReaderFontSizeStepSp = 1f
|
||||||
|
internal const val ReaderLineHeightStepSp = 1f
|
||||||
|
|
||||||
|
private const val MinReaderFontSizeSp = 14f
|
||||||
|
private const val MaxReaderFontSizeSp = 34f
|
||||||
|
private const val MinReaderLineHeightSp = 18f
|
||||||
|
private const val MaxReaderLineHeightSp = 48f
|
||||||
|
private const val MinReaderFontWeight = 100
|
||||||
|
private const val MaxReaderFontWeight = 900
|
||||||
|
private val ReaderFontWeightSteps = listOf(100, 200, 300, 400, 500, 600, 700, 800, 900)
|
||||||
|
|
||||||
|
internal fun defaultReaderFontSettings(): ReaderFontSettings = ReaderFontSettings()
|
||||||
|
|
||||||
|
internal fun defaultReaderFontSizeSp(): Float =
|
||||||
|
if (getPlatform().name.startsWith("Android")) 20f else 18f
|
||||||
|
|
||||||
|
internal fun defaultReaderFontWeight(): Int =
|
||||||
|
if (getPlatform().name.startsWith("Android")) 350 else 400
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.coerced(): ReaderFontSettings =
|
||||||
|
copy(
|
||||||
|
fontSizeSp = fontSizeSp.coerceIn(MinReaderFontSizeSp, MaxReaderFontSizeSp),
|
||||||
|
lineHeightSp = lineHeightSp.coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
|
||||||
|
fontWeight = fontWeight.coerceIn(MinReaderFontWeight, MaxReaderFontWeight),
|
||||||
|
fontName = fontName?.takeIf { it.isNotBlank() },
|
||||||
|
)
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.adjustFontSize(steps: Int): ReaderFontSettings {
|
||||||
|
val current = coerced()
|
||||||
|
val nextFontSize = (current.fontSizeSp + steps * ReaderFontSizeStepSp)
|
||||||
|
.coerceIn(MinReaderFontSizeSp, MaxReaderFontSizeSp)
|
||||||
|
val scale = if (current.fontSizeSp > 0f) nextFontSize / current.fontSizeSp else 1f
|
||||||
|
return current.copy(
|
||||||
|
fontSizeSp = nextFontSize,
|
||||||
|
lineHeightSp = (current.lineHeightSp * scale).coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.adjustLineHeight(steps: Int): ReaderFontSettings =
|
||||||
|
coerced().let { current ->
|
||||||
|
current.copy(
|
||||||
|
lineHeightSp = (current.lineHeightSp + steps * ReaderLineHeightStepSp)
|
||||||
|
.coerceIn(MinReaderLineHeightSp, MaxReaderLineHeightSp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.adjustFontWeight(steps: Int): ReaderFontSettings {
|
||||||
|
val current = coerced()
|
||||||
|
if (steps == 0) return current
|
||||||
|
|
||||||
|
var nextWeight = current.fontWeight
|
||||||
|
repeat(abs(steps)) {
|
||||||
|
nextWeight = current.fontWeight + if (steps > 0) 50 else -50
|
||||||
|
if( nextWeight == 100 || nextWeight == 900 ) return@repeat
|
||||||
|
}
|
||||||
|
return current.copy(fontWeight = nextWeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun ReaderFontSettings.toStorageValue(): String =
|
||||||
|
coerced().let { current ->
|
||||||
|
listOf(
|
||||||
|
current.fontSizeSp.toString(),
|
||||||
|
current.lineHeightSp.toString(),
|
||||||
|
current.fontWeight.toString(),
|
||||||
|
current.fontName.orEmpty(),
|
||||||
|
).joinToString("|")
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun readerFontSettingsFromStorageValue(value: String?): ReaderFontSettings {
|
||||||
|
val defaults = defaultReaderFontSettings()
|
||||||
|
val parts = value?.split("|") ?: return defaults
|
||||||
|
return ReaderFontSettings(
|
||||||
|
fontSizeSp = parts.getOrNull(0)?.toFloatOrNull() ?: defaults.fontSizeSp,
|
||||||
|
lineHeightSp = parts.getOrNull(1)?.toFloatOrNull() ?: defaults.lineHeightSp,
|
||||||
|
fontWeight = parts.getOrNull(2)?.toIntOrNull() ?: defaults.fontWeight,
|
||||||
|
fontName = parts.getOrNull(3)?.takeIf { it.isNotBlank() } ?: defaults.fontName,
|
||||||
|
).coerced()
|
||||||
|
}
|
||||||
|
|||||||
@ -120,6 +120,8 @@ internal fun LibraryScreen(
|
|||||||
val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds }
|
val recentlyAdded = recentlyAddedItems.filterNot { it.fileId in hiddenFileIds }
|
||||||
val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive)
|
val visibleItems = selectedFilter.apply(sourceItems, recentlyAdded, searchActive)
|
||||||
.withoutDuplicateFileIds()
|
.withoutDuplicateFileIds()
|
||||||
|
val showShareAction = canShareLibraryBookFile()
|
||||||
|
val showViewFileAction = canViewLibraryBookFile()
|
||||||
|
|
||||||
suspend fun loadLibrary() {
|
suspend fun loadLibrary() {
|
||||||
if (loadingLibrary) return
|
if (loadingLibrary) return
|
||||||
@ -605,6 +607,26 @@ internal fun LibraryScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onShare = {
|
||||||
|
scope.launch {
|
||||||
|
busy = true
|
||||||
|
try {
|
||||||
|
message = shareLibraryBook(item.fileId)
|
||||||
|
} finally {
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onViewFile = {
|
||||||
|
scope.launch {
|
||||||
|
busy = true
|
||||||
|
try {
|
||||||
|
message = viewLibraryBook(item.fileId)
|
||||||
|
} finally {
|
||||||
|
busy = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
onDelete = {
|
onDelete = {
|
||||||
val previousItems = items
|
val previousItems = items
|
||||||
val previousSearchResults = searchResults
|
val previousSearchResults = searchResults
|
||||||
@ -638,6 +660,8 @@ internal fun LibraryScreen(
|
|||||||
item = item,
|
item = item,
|
||||||
coverCache = coverCache,
|
coverCache = coverCache,
|
||||||
enabled = !busy,
|
enabled = !busy,
|
||||||
|
showShareAction = showShareAction,
|
||||||
|
showViewFileAction = showViewFileAction,
|
||||||
actions = itemActions(item),
|
actions = itemActions(item),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -843,6 +867,8 @@ private fun LibraryRow(
|
|||||||
item: LibraryItem,
|
item: LibraryItem,
|
||||||
coverCache: MutableMap<String, LibraryCover?>,
|
coverCache: MutableMap<String, LibraryCover?>,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
|
showShareAction: Boolean,
|
||||||
|
showViewFileAction: Boolean,
|
||||||
actions: LibraryItemActions,
|
actions: LibraryItemActions,
|
||||||
) {
|
) {
|
||||||
var menuOpen by remember { mutableStateOf(false) }
|
var menuOpen by remember { mutableStateOf(false) }
|
||||||
@ -958,6 +984,27 @@ private fun LibraryRow(
|
|||||||
actions.onNotInterested()
|
actions.onNotInterested()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
if (showShareAction || showViewFileAction) {
|
||||||
|
HorizontalDivider()
|
||||||
|
}
|
||||||
|
if (showShareAction) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(strings.share) },
|
||||||
|
onClick = {
|
||||||
|
menuOpen = false
|
||||||
|
actions.onShare()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showViewFileAction) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(strings.viewFile) },
|
||||||
|
onClick = {
|
||||||
|
menuOpen = false
|
||||||
|
actions.onViewFile()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(strings.delete) },
|
text = { Text(strings.delete) },
|
||||||
@ -980,6 +1027,8 @@ private data class LibraryItemActions(
|
|||||||
val onMarkToRead: () -> Unit,
|
val onMarkToRead: () -> Unit,
|
||||||
val onNotInterested: () -> Unit,
|
val onNotInterested: () -> Unit,
|
||||||
val onFavoriteChange: (Boolean) -> Unit,
|
val onFavoriteChange: (Boolean) -> Unit,
|
||||||
|
val onShare: () -> Unit,
|
||||||
|
val onViewFile: () -> Unit,
|
||||||
val onDelete: () -> Unit,
|
val onDelete: () -> Unit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -136,6 +136,15 @@ internal open class AppStrings {
|
|||||||
open val readAloud = "Read aloud"
|
open val readAloud = "Read aloud"
|
||||||
open val readerMenu = "Book reader menu"
|
open val readerMenu = "Book reader menu"
|
||||||
open val info = "Info..."
|
open val info = "Info..."
|
||||||
|
open val readerSettings = "Settings"
|
||||||
|
open val readerFontSizeIncrease = "Increase font size"
|
||||||
|
open val readerFontSizeDecrease = "Decrease font size"
|
||||||
|
open val readerLineHeightIncrease = "Increase line spacing"
|
||||||
|
open val readerLineHeightDecrease = "Decrease line spacing"
|
||||||
|
open val readerFontWeightIncrease = "Increase font weight"
|
||||||
|
open val readerFontWeightDecrease = "Decrease font weight"
|
||||||
|
open val resetReaderFontSettings = "Reset font 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"
|
||||||
open val previousSentence = "Previous sentence"
|
open val previousSentence = "Previous sentence"
|
||||||
@ -331,6 +340,15 @@ internal object RussianStrings : AppStrings() {
|
|||||||
override val readAloud = "Читать вслух"
|
override val readAloud = "Читать вслух"
|
||||||
override val readerMenu = "Меню чтения"
|
override val readerMenu = "Меню чтения"
|
||||||
override val info = "Информация..."
|
override val info = "Информация..."
|
||||||
|
override val readerSettings = "Настройки"
|
||||||
|
override val readerFontSizeIncrease = "Увеличить шрифт"
|
||||||
|
override val readerFontSizeDecrease = "Уменьшить шрифт"
|
||||||
|
override val readerLineHeightIncrease = "Увеличить межстрочный интервал"
|
||||||
|
override val readerLineHeightDecrease = "Уменьшить межстрочный интервал"
|
||||||
|
override val readerFontWeightIncrease = "Увеличить насыщенность шрифта"
|
||||||
|
override val readerFontWeightDecrease = "Уменьшить насыщенность шрифта"
|
||||||
|
override val resetReaderFontSettings = "Сбросить настройки шрифта"
|
||||||
|
override val closeReaderSettings = "Закрыть настройки чтения"
|
||||||
override val share = "Поделиться"
|
override val share = "Поделиться"
|
||||||
override val viewFile = "Показать файл"
|
override val viewFile = "Показать файл"
|
||||||
override val previousSentence = "Предыдущее предложение"
|
override val previousSentence = "Предыдущее предложение"
|
||||||
|
|||||||
@ -102,6 +102,7 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
import kotlin.math.min
|
import kotlin.math.min
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
import kotlin.math.sqrt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun ContinuousBookReader(
|
internal fun ContinuousBookReader(
|
||||||
@ -111,13 +112,15 @@ internal fun ContinuousBookReader(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
contentPlan: ReaderContentPlan = remember(book) { buildReaderContentPlan(book) },
|
contentPlan: ReaderContentPlan = remember(book) { buildReaderContentPlan(book) },
|
||||||
highlightedSentence: ReadAloudSentence? = null,
|
highlightedSentence: ReadAloudSentence? = null,
|
||||||
|
readerFontSettings: ReaderFontSettings = defaultReaderFontSettings(),
|
||||||
|
onReaderFontZoom: (Int) -> Unit = {},
|
||||||
onUserScroll: () -> Unit = {},
|
onUserScroll: () -> Unit = {},
|
||||||
onImageOpen: (ViewedBookImage) -> Unit = {},
|
onImageOpen: (ViewedBookImage) -> Unit = {},
|
||||||
onNoteOpen: (String) -> Unit = {},
|
onNoteOpen: (String) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val hyphenation = remember { HyphenationRegistry() }
|
val hyphenation = remember { HyphenationRegistry() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val textLineMetricsByItem = remember(contentPlan) { mutableStateMapOf<Int, TextLineMetrics>() }
|
val textLineMetricsByItem = remember(contentPlan, readerFontSettings) { mutableStateMapOf<Int, TextLineMetrics>() }
|
||||||
val contentPadding = PaddingValues(top=6.dp, bottom = 6.dp, start = 4.dp, end = 6.dp)
|
val contentPadding = PaddingValues(top=6.dp, bottom = 6.dp, start = 4.dp, end = 6.dp)
|
||||||
val userScrollConnection = remember(onUserScroll) {
|
val userScrollConnection = remember(onUserScroll) {
|
||||||
object : NestedScrollConnection {
|
object : NestedScrollConnection {
|
||||||
@ -135,6 +138,7 @@ internal fun ContinuousBookReader(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
.nestedScroll(userScrollConnection)
|
.nestedScroll(userScrollConnection)
|
||||||
|
.readerFontZoomGesture(onReaderFontZoom)
|
||||||
.pageTurnOnTouchTap(
|
.pageTurnOnTouchTap(
|
||||||
onPageDown = {
|
onPageDown = {
|
||||||
onUserScroll()
|
onUserScroll()
|
||||||
@ -206,7 +210,7 @@ internal fun ContinuousBookReader(
|
|||||||
text = element.text,
|
text = element.text,
|
||||||
language = book.language,
|
language = book.language,
|
||||||
hyphenation = hyphenation,
|
hyphenation = hyphenation,
|
||||||
style = readerParagraphTextStyle(book.language),
|
style = readerParagraphTextStyle(book.language, readerFontSettings),
|
||||||
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),
|
||||||
@ -253,6 +257,50 @@ internal fun ContinuousBookReader(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Modifier.readerFontZoomGesture(
|
||||||
|
onZoom: (Int) -> Unit,
|
||||||
|
): Modifier = pointerInput(onZoom) {
|
||||||
|
awaitEachGesture {
|
||||||
|
var baselineDistance: Float? = null
|
||||||
|
while (true) {
|
||||||
|
val event = awaitPointerEvent(pass = PointerEventPass.Main)
|
||||||
|
val touches = event.changes.filter { it.pressed && it.type == PointerType.Touch }
|
||||||
|
if (touches.isEmpty()) break
|
||||||
|
if (touches.size < 2) {
|
||||||
|
baselineDistance = null
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val distance = touches.firstTwoDistance()
|
||||||
|
val baseline = baselineDistance
|
||||||
|
if (baseline == null || baseline <= 0f) {
|
||||||
|
baselineDistance = distance
|
||||||
|
} else {
|
||||||
|
val zoom = distance / baseline
|
||||||
|
when {
|
||||||
|
zoom >= ReaderZoomGestureStep -> {
|
||||||
|
onZoom(1)
|
||||||
|
baselineDistance = distance
|
||||||
|
}
|
||||||
|
zoom <= 1f / ReaderZoomGestureStep -> {
|
||||||
|
onZoom(-1)
|
||||||
|
baselineDistance = distance
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
touches.forEach { it.consume() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<androidx.compose.ui.input.pointer.PointerInputChange>.firstTwoDistance(): Float {
|
||||||
|
val first = this[0].position
|
||||||
|
val second = this[1].position
|
||||||
|
val dx = second.x - first.x
|
||||||
|
val dy = second.y - first.y
|
||||||
|
return sqrt(dx * dx + dy * dy)
|
||||||
|
}
|
||||||
|
|
||||||
private fun Modifier.pageTurnOnTouchTap(
|
private fun Modifier.pageTurnOnTouchTap(
|
||||||
onPageDown: () -> Unit,
|
onPageDown: () -> Unit,
|
||||||
onPageUp: () -> Unit,
|
onPageUp: () -> Unit,
|
||||||
@ -927,16 +975,31 @@ private fun epigraphAuthorTextStyle(language: String?): TextStyle =
|
|||||||
epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold)
|
epigraphTextStyle(language).copy(fontWeight = FontWeight.SemiBold)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun readerParagraphTextStyle(language: String?): TextStyle =
|
private fun readerParagraphTextStyle(
|
||||||
MaterialTheme.typography.bodyLarge.copy(
|
language: String?,
|
||||||
fontWeight = if( isAndroidPlatform) FontWeight(350) else FontWeight.Normal,
|
settings: ReaderFontSettings = defaultReaderFontSettings(),
|
||||||
fontSize = if( isAndroidPlatform) 20.sp else 18.sp,
|
): TextStyle {
|
||||||
lineHeight = 26.sp,
|
val coerced = settings.coerced()
|
||||||
|
return MaterialTheme.typography.bodyLarge.copy(
|
||||||
|
fontWeight = FontWeight(coerced.fontWeight),
|
||||||
|
fontSize = coerced.fontSizeSp.sp,
|
||||||
|
lineHeight = coerced.lineHeightSp.sp,
|
||||||
|
fontFamily = coerced.readerFontFamily(),
|
||||||
letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing,
|
letterSpacing = if (isAndroidPlatform) 0.sp else MaterialTheme.typography.bodyLarge.letterSpacing,
|
||||||
hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified,
|
hyphens = if (isAndroidPlatform) Hyphens.Auto else Hyphens.Unspecified,
|
||||||
lineBreak = if (isAndroidPlatform) LineBreak.Paragraph else LineBreak.Unspecified,
|
lineBreak = if (isAndroidPlatform) LineBreak.Paragraph else LineBreak.Unspecified,
|
||||||
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
|
localeList = language?.takeIf(String::isNotBlank)?.let { LocaleList(Locale(it)) },
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReaderFontSettings.readerFontFamily(): FontFamily? =
|
||||||
|
when (fontName?.lowercase()) {
|
||||||
|
"serif" -> FontFamily.Serif
|
||||||
|
"sans", "sansserif", "sans-serif" -> FontFamily.SansSerif
|
||||||
|
"monospace", "mono" -> FontFamily.Monospace
|
||||||
|
"cursive" -> FontFamily.Cursive
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
private val isAndroidPlatform: Boolean by lazy {
|
private val isAndroidPlatform: Boolean by lazy {
|
||||||
getPlatform().name.startsWith("Android")
|
getPlatform().name.startsWith("Android")
|
||||||
@ -946,6 +1009,8 @@ private val isDesktopPlatform: Boolean by lazy {
|
|||||||
getPlatform().name.startsWith("Java")
|
getPlatform().name.startsWith("Java")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val ReaderZoomGestureStep = 1.12f
|
||||||
|
|
||||||
private val MinimumBookImageMaxDimension = 800.dp
|
private val MinimumBookImageMaxDimension = 800.dp
|
||||||
|
|
||||||
private fun minimumBookImageMaxDimension(availableWidth: Dp): Dp {
|
private fun minimumBookImageMaxDimension(availableWidth: Dp): Dp {
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
package net.sergeych.toread
|
package net.sergeych.toread
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
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
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -11,16 +12,22 @@ 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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
import androidx.compose.material.icons.automirrored.filled.VolumeUp
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.FastForward
|
import androidx.compose.material.icons.filled.FastForward
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.Replay
|
import androidx.compose.material.icons.filled.Replay
|
||||||
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldLess
|
||||||
|
import androidx.compose.material.icons.filled.UnfoldMore
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
@ -48,7 +55,11 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import net.sergeych.toread.fb2.Fb2Book
|
import net.sergeych.toread.fb2.Fb2Book
|
||||||
@ -58,6 +69,7 @@ import kotlinx.coroutines.flow.debounce
|
|||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.filter
|
import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
@OptIn(ExperimentalMaterial3Api::class, FlowPreview::class)
|
||||||
@ -86,14 +98,15 @@ internal fun BookView(
|
|||||||
var libraryItem by remember(fileId) { mutableStateOf<LibraryItem?>(null) }
|
var libraryItem by remember(fileId) { mutableStateOf<LibraryItem?>(null) }
|
||||||
var readAloudPanelVisible by remember(fileId) { mutableStateOf(false) }
|
var readAloudPanelVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
var readAloudSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
var readAloudSettingsVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
|
var readerSettingsPanelVisible by remember(fileId) { mutableStateOf(false) }
|
||||||
|
var readerFontSettings by remember { mutableStateOf(defaultReaderFontSettings()) }
|
||||||
var readAloudResumeSentenceIndex by remember(fileId) { mutableStateOf<Int?>(null) }
|
var readAloudResumeSentenceIndex by remember(fileId) { mutableStateOf<Int?>(null) }
|
||||||
var userScrollGeneration by remember(fileId) { mutableStateOf(0) }
|
var userScrollGeneration by remember(fileId) { mutableStateOf(0) }
|
||||||
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 platformName = getPlatform().name
|
val showShareAction = canShareLibraryBookFile()
|
||||||
val showShareAction = platformName.startsWith("Android")
|
val showViewFileAction = canViewLibraryBookFile()
|
||||||
val showViewFileAction = platformName.startsWith("Java")
|
|
||||||
val showReadAloudAction = ReadAloudPlatform.isSupported && contentPlan.sentences.isNotEmpty()
|
val showReadAloudAction = ReadAloudPlatform.isSupported && contentPlan.sentences.isNotEmpty()
|
||||||
val activeReadAloudSentence = readAloudState.sentenceIndex
|
val activeReadAloudSentence = readAloudState.sentenceIndex
|
||||||
?.let { index -> contentPlan.sentences.getOrNull(index) }
|
?.let { index -> contentPlan.sentences.getOrNull(index) }
|
||||||
@ -107,6 +120,15 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateReaderFontSettings(transform: (ReaderFontSettings) -> ReaderFontSettings) {
|
||||||
|
val next = transform(readerFontSettings).coerced()
|
||||||
|
if (next == readerFontSettings) return
|
||||||
|
readerFontSettings = next
|
||||||
|
scope.launch {
|
||||||
|
saveReaderFontSettings(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun setReadingStatus(status: BookReadingStatus, successMessage: String) {
|
fun setReadingStatus(status: BookReadingStatus, successMessage: String) {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
if (markLibraryReadingStatus(fileId, status)) {
|
if (markLibraryReadingStatus(fileId, status)) {
|
||||||
@ -140,6 +162,10 @@ internal fun BookView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
readerFontSettings = loadReaderFontSettings()
|
||||||
|
}
|
||||||
|
|
||||||
DisposableEffect(fileId) {
|
DisposableEffect(fileId) {
|
||||||
onDispose {
|
onDispose {
|
||||||
ReadAloudPlatform.stop()
|
ReadAloudPlatform.stop()
|
||||||
@ -244,17 +270,18 @@ internal fun BookView(
|
|||||||
showShareAction = showShareAction,
|
showShareAction = showShareAction,
|
||||||
onShare = {
|
onShare = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val shared = shareLibraryBookFile(fileId)
|
showMessage(shareLibraryBook(fileId))
|
||||||
showMessage(if (shared) strings.shareOpened() else strings.couldNotShareBook())
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showViewFileAction = showViewFileAction,
|
showViewFileAction = showViewFileAction,
|
||||||
onViewFile = {
|
onViewFile = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val opened = viewLibraryBookFile(fileId)
|
showMessage(viewLibraryBook(fileId))
|
||||||
showMessage(if (opened) strings.openedFileLocation() else strings.couldNotOpenFileLocation())
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onReaderSettings = {
|
||||||
|
readerSettingsPanelVisible = true
|
||||||
|
},
|
||||||
showReadAloudAction = showReadAloudAction,
|
showReadAloudAction = showReadAloudAction,
|
||||||
onReadAloud = {
|
onReadAloud = {
|
||||||
val position = ReadingPosition(
|
val position = ReadingPosition(
|
||||||
@ -310,10 +337,41 @@ internal fun BookView(
|
|||||||
listState = listState,
|
listState = listState,
|
||||||
contentPlan = contentPlan,
|
contentPlan = contentPlan,
|
||||||
highlightedSentence = highlightedSentence,
|
highlightedSentence = highlightedSentence,
|
||||||
|
readerFontSettings = readerFontSettings,
|
||||||
|
onReaderFontZoom = { steps ->
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustFontSize(steps) }
|
||||||
|
},
|
||||||
onUserScroll = { userScrollGeneration += 1 },
|
onUserScroll = { userScrollGeneration += 1 },
|
||||||
onImageOpen = onImageOpen,
|
onImageOpen = onImageOpen,
|
||||||
onNoteOpen = { href -> selectedNoteId = href.removePrefix("#") },
|
onNoteOpen = { href -> selectedNoteId = href.removePrefix("#") },
|
||||||
)
|
)
|
||||||
|
if (readerSettingsPanelVisible) {
|
||||||
|
ReaderFontSettingsPanel(
|
||||||
|
settings = readerFontSettings,
|
||||||
|
onFontSizeDecrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustFontSize(-1) }
|
||||||
|
},
|
||||||
|
onFontSizeIncrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustFontSize(1) }
|
||||||
|
},
|
||||||
|
onLineHeightDecrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustLineHeight(-1) }
|
||||||
|
},
|
||||||
|
onLineHeightIncrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustLineHeight(1) }
|
||||||
|
},
|
||||||
|
onFontWeightDecrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustFontWeight(-1) }
|
||||||
|
},
|
||||||
|
onFontWeightIncrease = {
|
||||||
|
updateReaderFontSettings { settings -> settings.adjustFontWeight(1) }
|
||||||
|
},
|
||||||
|
onReset = {
|
||||||
|
updateReaderFontSettings { defaultReaderFontSettings() }
|
||||||
|
},
|
||||||
|
onClose = { readerSettingsPanelVisible = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
if (readAloudPanelVisible && readAloudState.active) {
|
if (readAloudPanelVisible && readAloudState.active) {
|
||||||
ReadAloudPanel(
|
ReadAloudPanel(
|
||||||
playing = readAloudState.playing,
|
playing = readAloudState.playing,
|
||||||
@ -375,6 +433,7 @@ private fun CompactReaderTopBar(
|
|||||||
onViewFile: () -> Unit,
|
onViewFile: () -> Unit,
|
||||||
showReadAloudAction: Boolean,
|
showReadAloudAction: Boolean,
|
||||||
onReadAloud: () -> Unit,
|
onReadAloud: () -> Unit,
|
||||||
|
onReaderSettings: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@ -415,6 +474,13 @@ private fun CompactReaderTopBar(
|
|||||||
onBookInfo()
|
onBookInfo()
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text(strings.readerSettings) },
|
||||||
|
onClick = {
|
||||||
|
menuOpen = false
|
||||||
|
onReaderSettings()
|
||||||
|
},
|
||||||
|
)
|
||||||
HorizontalDivider()
|
HorizontalDivider()
|
||||||
DropdownMenuItem(
|
DropdownMenuItem(
|
||||||
text = { Text(strings.markAsRead) },
|
text = { Text(strings.markAsRead) },
|
||||||
@ -496,6 +562,106 @@ private fun CompactReaderTopBar(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderFontSettingsPanel(
|
||||||
|
settings: ReaderFontSettings,
|
||||||
|
onFontSizeDecrease: () -> Unit,
|
||||||
|
onFontSizeIncrease: () -> Unit,
|
||||||
|
onLineHeightDecrease: () -> Unit,
|
||||||
|
onLineHeightIncrease: () -> Unit,
|
||||||
|
onFontWeightDecrease: () -> Unit,
|
||||||
|
onFontWeightIncrease: () -> Unit,
|
||||||
|
onReset: () -> Unit,
|
||||||
|
onClose: () -> Unit,
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
shadowElevation = 4.dp,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
.padding(bottom = 0.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp, Alignment.CenterHorizontally),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Text("A ${settings.fontSizeSp.roundToInt()}", style = MaterialTheme.typography.labelMedium)
|
||||||
|
IconButton(onClick = onFontSizeDecrease) {
|
||||||
|
ReaderLetterChangeIcon(
|
||||||
|
letter = "A",
|
||||||
|
increase = false,
|
||||||
|
contentDescription = strings.readerFontSizeDecrease,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onFontSizeIncrease) {
|
||||||
|
ReaderLetterChangeIcon(
|
||||||
|
letter = "A",
|
||||||
|
increase = true,
|
||||||
|
contentDescription = strings.readerFontSizeIncrease,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onLineHeightDecrease) {
|
||||||
|
Icon(Icons.Filled.UnfoldLess, contentDescription = strings.readerLineHeightDecrease)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onLineHeightIncrease) {
|
||||||
|
Icon(Icons.Filled.UnfoldMore, contentDescription = strings.readerLineHeightIncrease)
|
||||||
|
}
|
||||||
|
// Text("W ${settings.fontWeight}", style = MaterialTheme.typography.labelMedium)
|
||||||
|
IconButton(onClick = onFontWeightDecrease) {
|
||||||
|
ReaderLetterChangeIcon(
|
||||||
|
letter = "B",
|
||||||
|
increase = false,
|
||||||
|
contentDescription = strings.readerFontWeightDecrease,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onFontWeightIncrease) {
|
||||||
|
ReaderLetterChangeIcon(
|
||||||
|
letter = "B",
|
||||||
|
increase = true,
|
||||||
|
contentDescription = strings.readerFontWeightIncrease,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onReset) {
|
||||||
|
Icon(Icons.Filled.RestartAlt, contentDescription = strings.resetReaderFontSettings)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onClose) {
|
||||||
|
Icon(Icons.Filled.Close, contentDescription = strings.closeReaderSettings)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Row(Modifier.fillMaxWidth().padding(bottom = 2.dp, top = 0.dp), horizontalArrangement = Arrangement.Center) {
|
||||||
|
// Text(
|
||||||
|
// "size: ${settings.fontSizeSp.roundToInt()} height: ${settings.lineHeightSp.roundToInt()}, weight: ${settings.fontWeight}",
|
||||||
|
// style = MaterialTheme.typography.bodySmall,
|
||||||
|
// maxLines = 1,
|
||||||
|
// overflow = TextOverflow.Ellipsis,
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReaderLetterChangeIcon(
|
||||||
|
letter: String,
|
||||||
|
increase: Boolean,
|
||||||
|
contentDescription: String,
|
||||||
|
fontWeight: FontWeight = FontWeight.SemiBold,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = letter + if (increase) "⁺" else "⁻",
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = fontWeight,
|
||||||
|
modifier = Modifier.semantics {
|
||||||
|
this.contentDescription = contentDescription
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ReadAloudPanel(
|
private fun ReadAloudPanel(
|
||||||
playing: Boolean,
|
playing: Boolean,
|
||||||
|
|||||||
@ -293,22 +293,55 @@ actual suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boole
|
|||||||
|
|
||||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||||
|
|
||||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = withContext(Dispatchers.IO) {
|
actual suspend fun viewLibraryBookFile(folder: String, fileName: String): Boolean = withContext(Dispatchers.IO) {
|
||||||
runCatching {
|
runCatching {
|
||||||
if (!Desktop.isDesktopSupported()) return@withContext false
|
val directory = File(folder).takeIf { it.isDirectory } ?: return@withContext false
|
||||||
val file = openLibraryDatabase().useLibrary { db ->
|
val file = File(directory, fileName).takeIf { it.isFile } ?: return@withContext false
|
||||||
db.files.get(fileId)?.storageUri?.let(::File)?.takeIf { it.isFile }
|
selectOrOpenBookFile(directory, file)
|
||||||
} ?: return@withContext false
|
|
||||||
val desktop = Desktop.getDesktop()
|
|
||||||
if (desktop.isSupported(Desktop.Action.BROWSE_FILE_DIR)) {
|
|
||||||
desktop.browseFileDirectory(file)
|
|
||||||
} else {
|
|
||||||
desktop.open(file.parentFile ?: file)
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}.getOrDefault(false)
|
}.getOrDefault(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun selectOrOpenBookFile(directory: File, file: File): Boolean {
|
||||||
|
if (isLinuxDesktop() && trySelectLinuxFile(file)) return true
|
||||||
|
if (!Desktop.isDesktopSupported()) return false
|
||||||
|
|
||||||
|
val desktop = Desktop.getDesktop()
|
||||||
|
return when {
|
||||||
|
desktop.isSupported(Desktop.Action.BROWSE_FILE_DIR) -> {
|
||||||
|
desktop.browseFileDirectory(file)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
desktop.isSupported(Desktop.Action.OPEN) -> {
|
||||||
|
desktop.open(directory)
|
||||||
|
true
|
||||||
|
}
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isLinuxDesktop(): Boolean =
|
||||||
|
System.getProperty("os.name").orEmpty().lowercase(Locale.ROOT).contains("linux")
|
||||||
|
|
||||||
|
private fun trySelectLinuxFile(file: File): Boolean =
|
||||||
|
runCatching {
|
||||||
|
val process = ProcessBuilder(
|
||||||
|
"dbus-send",
|
||||||
|
"--session",
|
||||||
|
"--dest=org.freedesktop.FileManager1",
|
||||||
|
"--type=method_call",
|
||||||
|
"/org/freedesktop/FileManager1",
|
||||||
|
"org.freedesktop.FileManager1.ShowItems",
|
||||||
|
"array:string:${file.toURI()}",
|
||||||
|
"string:",
|
||||||
|
).start()
|
||||||
|
if (process.waitFor(2, TimeUnit.SECONDS)) {
|
||||||
|
process.exitValue() == 0
|
||||||
|
} else {
|
||||||
|
process.destroyForcibly()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}.getOrDefault(false)
|
||||||
|
|
||||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
|
val file = db.files.get(fileId) ?: return@useLibrary BookInfoExtras()
|
||||||
@ -367,6 +400,18 @@ actual suspend fun saveThemeMode(mode: ThemeMode) = withContext(Dispatchers.IO)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFontSettings(): ReaderFontSettings = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
readerFontSettingsFromStorageValue(db.getAppFlag(ReaderFontSettingsFlag))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) = withContext(Dispatchers.IO) {
|
||||||
|
openLibraryDatabase().useLibrary { db ->
|
||||||
|
db.setAppFlag(ReaderFontSettingsFlag, settings.toStorageValue())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = withContext(Dispatchers.IO) {
|
||||||
openLibraryDatabase().useLibrary { db ->
|
openLibraryDatabase().useLibrary { db ->
|
||||||
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
db.getAppFlag(ScanDownloadsAutomaticallyFlag)?.toBooleanStrictOrNull() ?: true
|
||||||
@ -611,6 +656,7 @@ private fun runCommand(vararg command: String): String? =
|
|||||||
|
|
||||||
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
private const val ActiveReadingFileIdFlag = "active_reading_file_id"
|
||||||
private const val ThemeModeFlag = "theme_mode"
|
private const val ThemeModeFlag = "theme_mode"
|
||||||
|
private const val ReaderFontSettingsFlag = "reader_font_settings"
|
||||||
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
private const val ScanDownloadsAutomaticallyFlag = "scan_downloads_automatically"
|
||||||
private const val AppLocaleTagFlag = "app_locale_tag"
|
private const val AppLocaleTagFlag = "app_locale_tag"
|
||||||
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
private const val DownloadsWasScannedFlag = "downloads_was_scanned"
|
||||||
|
|||||||
@ -61,7 +61,7 @@ actual suspend fun markLibraryFavorite(fileId: String, favorite: Boolean): Boole
|
|||||||
|
|
||||||
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
actual suspend fun shareLibraryBookFile(fileId: String): Boolean = false
|
||||||
|
|
||||||
actual suspend fun viewLibraryBookFile(fileId: String): Boolean = false
|
actual suspend fun viewLibraryBookFile(folder: String, fileName: String): Boolean = false
|
||||||
|
|
||||||
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = BookInfoExtras()
|
actual suspend fun loadBookInfoExtras(fileId: String): BookInfoExtras = BookInfoExtras()
|
||||||
|
|
||||||
@ -73,6 +73,13 @@ actual suspend fun loadThemeMode(): ThemeMode = ThemeMode.SYSTEM
|
|||||||
|
|
||||||
actual suspend fun saveThemeMode(mode: ThemeMode) = Unit
|
actual suspend fun saveThemeMode(mode: ThemeMode) = Unit
|
||||||
|
|
||||||
|
actual suspend fun loadReaderFontSettings(): ReaderFontSettings =
|
||||||
|
readerFontSettingsFromStorageValue(window.localStorage.getItem(ReaderFontSettingsStorageKey))
|
||||||
|
|
||||||
|
actual suspend fun saveReaderFontSettings(settings: ReaderFontSettings) {
|
||||||
|
window.localStorage.setItem(ReaderFontSettingsStorageKey, settings.toStorageValue())
|
||||||
|
}
|
||||||
|
|
||||||
actual suspend fun loadScanDownloadsAutomatically(): Boolean = true
|
actual suspend fun loadScanDownloadsAutomatically(): Boolean = true
|
||||||
|
|
||||||
actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = Unit
|
actual suspend fun saveScanDownloadsAutomatically(enabled: Boolean) = Unit
|
||||||
@ -111,6 +118,7 @@ actual fun watchPlatformDarkTheme(onChange: (Boolean) -> Unit): () -> Unit {
|
|||||||
actual fun libraryLogPath(): String? = null
|
actual fun libraryLogPath(): String? = null
|
||||||
|
|
||||||
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
private const val AppLocaleStorageKey = "toread.appLocaleTag"
|
||||||
|
private const val ReaderFontSettingsStorageKey = "toread.readerFontSettings"
|
||||||
|
|
||||||
actual fun formatLibraryLastReadTime(millis: Long): String {
|
actual fun formatLibraryLastReadTime(millis: Long): String {
|
||||||
val totalMinutes = millis / 60_000L
|
val totalMinutes = millis / 60_000L
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user