diff --git a/komelia-domain/core/src/androidMain/kotlin/snd/komelia/AndroidWindowState.kt b/komelia-domain/core/src/androidMain/kotlin/snd/komelia/AndroidWindowState.kt index 69337b347..3cd95cfa1 100644 --- a/komelia-domain/core/src/androidMain/kotlin/snd/komelia/AndroidWindowState.kt +++ b/komelia-domain/core/src/androidMain/kotlin/snd/komelia/AndroidWindowState.kt @@ -25,7 +25,6 @@ class AndroidWindowState( insetsController.show(statusBars() or navigationBars()) insetsController.systemBarsBehavior = BEHAVIOR_DEFAULT isFullscreen.value = false - } } } \ No newline at end of file diff --git a/komelia-domain/core/src/commonMain/kotlin/snd/komelia/settings/EpubReaderSettingsRepository.kt b/komelia-domain/core/src/commonMain/kotlin/snd/komelia/settings/EpubReaderSettingsRepository.kt index 69300a3df..85e834bc1 100644 --- a/komelia-domain/core/src/commonMain/kotlin/snd/komelia/settings/EpubReaderSettingsRepository.kt +++ b/komelia-domain/core/src/commonMain/kotlin/snd/komelia/settings/EpubReaderSettingsRepository.kt @@ -14,4 +14,7 @@ interface EpubReaderSettingsRepository { suspend fun getTtsuReaderSettings(): TtsuReaderSettings suspend fun putTtsuReaderSettings(settings: TtsuReaderSettings) + + fun getFullscreenEnabled(): Flow + fun putFullscreenEnabled(enabled: Boolean) } \ No newline at end of file diff --git a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/EpubReaderSettings.kt b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/EpubReaderSettings.kt index f1f268a8b..80f294650 100644 --- a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/EpubReaderSettings.kt +++ b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/EpubReaderSettings.kt @@ -11,4 +11,5 @@ data class EpubReaderSettings( val readerType: EpubReaderType = EpubReaderType.TTSU_EPUB, val komgaReaderSettings: JsonObject = buildJsonObject { }, val ttsuReaderSettings: TtsuReaderSettings = TtsuReaderSettings(), + val fullscreenEnabled: Boolean = true, ) \ No newline at end of file diff --git a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/SettingsStateWrapper.kt b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/SettingsStateWrapper.kt index b3a923500..40f0b09fc 100644 --- a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/SettingsStateWrapper.kt +++ b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/SettingsStateWrapper.kt @@ -1,10 +1,14 @@ package snd.komelia.db +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch class SettingsStateWrapper( settings: T, @@ -12,14 +16,23 @@ class SettingsStateWrapper( ) { private val _state: MutableStateFlow = MutableStateFlow(settings) val state = _state.asStateFlow() + private val persistScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) inline fun mapState(crossinline transform: suspend (value: T) -> R): Flow { return state.map(transform).distinctUntilChanged() } + // Updates in-memory state immediately and fires a background DB save. + // Use this when the caller cannot wait for the save (e.g. non-suspend context). + fun update(transform: (T) -> T) { + val transformed = transform(_state.value) + _state.value = transformed + persistScope.launch { saveSettings(transformed) } + } + suspend fun transform(transform: suspend (settings: T) -> T) { val transformed = transform(_state.value) - saveSettings(transformed) _state.value = transformed + saveSettings(transformed) } } diff --git a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/repository/EpubReaderSettingsRepositoryWrapper.kt b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/repository/EpubReaderSettingsRepositoryWrapper.kt index 4d119f937..7132103c0 100644 --- a/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/repository/EpubReaderSettingsRepositoryWrapper.kt +++ b/komelia-infra/database/shared/src/commonMain/kotlin/snd/komelia/db/repository/EpubReaderSettingsRepositoryWrapper.kt @@ -35,4 +35,12 @@ class EpubReaderSettingsRepositoryWrapper( override suspend fun putTtsuReaderSettings(settings: TtsuReaderSettings) { return wrapper.transform { it.copy(ttsuReaderSettings = settings) } } + + override fun getFullscreenEnabled(): Flow { + return wrapper.mapState { it.fullscreenEnabled } + } + + override fun putFullscreenEnabled(enabled: Boolean) { + wrapper.update { it.copy(fullscreenEnabled = enabled) } + } } \ No newline at end of file diff --git a/komelia-infra/database/sqlite/build.gradle.kts b/komelia-infra/database/sqlite/build.gradle.kts index c8d629795..d71289a80 100644 --- a/komelia-infra/database/sqlite/build.gradle.kts +++ b/komelia-infra/database/sqlite/build.gradle.kts @@ -100,3 +100,15 @@ tasks.register("android-x86-ExtractSqliteLib") { from(file) into("$projectDir/src/androidMain/jniLibs/x86") } + +afterEvaluate { + val extractTasks = listOf( + "android-arm64-ExtractSqliteLib", + "android-armv7a-ExtractSqliteLib", + "android-x86_64-ExtractSqliteLib", + "android-x86-ExtractSqliteLib" + ) + tasks.named("preBuild") { + dependsOn(extractTasks) + } +} diff --git a/komelia-infra/database/sqlite/src/commonMain/composeResources/files/migrations/app/V13__epub_fullscreen_setting.sql b/komelia-infra/database/sqlite/src/commonMain/composeResources/files/migrations/app/V13__epub_fullscreen_setting.sql new file mode 100644 index 000000000..2738e91cd --- /dev/null +++ b/komelia-infra/database/sqlite/src/commonMain/composeResources/files/migrations/app/V13__epub_fullscreen_setting.sql @@ -0,0 +1,2 @@ +ALTER TABLE EpubReaderSettings + ADD COLUMN fullscreen_enabled BOOLEAN DEFAULT 1 NOT NULL; diff --git a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/migrations/AppMigrations.kt b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/migrations/AppMigrations.kt index 625ba6a9e..e4742e0d9 100644 --- a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/migrations/AppMigrations.kt +++ b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/migrations/AppMigrations.kt @@ -19,6 +19,7 @@ class AppMigrations : MigrationResourcesProvider() { "V10__komf_settings.sql", "V11__home_filters.sql", "V12__offline_mode.sql", + "V13__epub_fullscreen_setting.sql", ) override suspend fun getMigration(name: String): ByteArray? { diff --git a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/settings/ExposedEpubReaderSettingsRepository.kt b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/settings/ExposedEpubReaderSettingsRepository.kt index c1a0c16ef..f680bd9b9 100644 --- a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/settings/ExposedEpubReaderSettingsRepository.kt +++ b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/settings/ExposedEpubReaderSettingsRepository.kt @@ -21,7 +21,8 @@ class ExposedEpubReaderSettingsRepository(database: Database) : ExposedRepositor EpubReaderSettings( readerType = EpubReaderType.valueOf(it[EpubReaderSettingsTable.readerType]), komgaReaderSettings = it[EpubReaderSettingsTable.komgaSettingsJson], - ttsuReaderSettings = it[EpubReaderSettingsTable.ttsuSettingsJson] + ttsuReaderSettings = it[EpubReaderSettingsTable.ttsuSettingsJson], + fullscreenEnabled = it[EpubReaderSettingsTable.fullscreenEnabled], ) } } @@ -34,6 +35,7 @@ class ExposedEpubReaderSettingsRepository(database: Database) : ExposedRepositor it[readerType] = settings.readerType.name it[komgaSettingsJson] = settings.komgaReaderSettings it[ttsuSettingsJson] = settings.ttsuReaderSettings + it[fullscreenEnabled] = settings.fullscreenEnabled } } } diff --git a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/tables/EpubReaderSettingsTable.kt b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/tables/EpubReaderSettingsTable.kt index 76d1ebac4..df2d373d4 100644 --- a/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/tables/EpubReaderSettingsTable.kt +++ b/komelia-infra/database/sqlite/src/commonMain/kotlin/snd/komelia/db/tables/EpubReaderSettingsTable.kt @@ -11,6 +11,7 @@ object EpubReaderSettingsTable : Table("EpubReaderSettings") { val readerType = text("reader_type") val komgaSettingsJson = json("komga_settings_json", JsonDbDefault) val ttsuSettingsJson = json("ttsu_settings_json", JsonDbDefault) + val fullscreenEnabled = bool("fullscreen_enabled").default(true) override val primaryKey = PrimaryKey(bookId) } diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/EpubScreen.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/EpubScreen.kt index 745be5b6c..26b0dbbc1 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/EpubScreen.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/EpubScreen.kt @@ -1,9 +1,13 @@ package snd.komelia.ui.reader +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier import cafe.adriel.voyager.core.model.rememberScreenModel import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.ScreenKey @@ -60,10 +64,10 @@ class EpubScreen( } val state = vm.state.collectAsState().value + val isFullscreen = LocalWindowState.current.isFullscreen.collectAsState(false) Column { PlatformTitleBar(applyInsets = false) { if (canIntegrateWithSystemBar()) { - val isFullscreen = LocalWindowState.current.isFullscreen.collectAsState(false) if (state is LoadState.Success && !isFullscreen.value) { val book = state.value.book.collectAsState().value TitleBarContent( @@ -85,10 +89,19 @@ class EpubScreen( } ) - is LoadState.Success -> EpubContent( - onWebviewCreated = { state.value.onWebviewCreated(it) }, - onBackButtonPress = state.value::onBackButtonPress - ) + is LoadState.Success -> { + // On mobile (edge-to-edge, no integrated title bar), pad below status bar when not fullscreen + val modifier = if (!isFullscreen.value && !canIntegrateWithSystemBar()) + Modifier.statusBarsPadding().fillMaxSize() + else + Modifier.fillMaxSize() + Box(modifier = modifier) { + EpubContent( + onWebviewCreated = { state.value.onWebviewCreated(it) }, + onBackButtonPress = state.value::onBackButtonPress + ) + } + } } } } diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/KomgaEpubReaderState.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/KomgaEpubReaderState.kt index 2a8cfea96..9301ac81c 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/KomgaEpubReaderState.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/KomgaEpubReaderState.kt @@ -65,7 +65,7 @@ class KomgaEpubReaderState( @OptIn(ExperimentalResourceApi::class) override suspend fun initialize(navigator: Navigator) { this.navigator.value = navigator - if (platformType == PlatformType.MOBILE) windowState.setFullscreen(true) + if (platformType == PlatformType.MOBILE && epubSettingsRepository.getFullscreenEnabled().first()) windowState.setFullscreen(true) if (state.value !is Uninitialized) return state.value = LoadState.Loading diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/TtsuReaderState.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/TtsuReaderState.kt index e8f62bbd9..b1d30c549 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/TtsuReaderState.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/reader/epub/TtsuReaderState.kt @@ -89,7 +89,6 @@ class TtsuReaderState( @OptIn(ExperimentalResourceApi::class) override suspend fun initialize(navigator: Navigator) { this.navigator.value = navigator - if (platformType == PlatformType.MOBILE) windowState.setFullscreen(true) if (state.value !is LoadState.Uninitialized) return state.value = LoadState.Loading @@ -123,8 +122,6 @@ class TtsuReaderState( override fun closeWebview() { webview.value?.close() - if (platformType == PlatformType.MOBILE) windowState.setFullscreen(false) - navigator.value?.let { nav -> if (nav.canPop) nav.pop() else { diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsContent.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsContent.kt index 9a849d1ce..0a3f68b91 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsContent.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsContent.kt @@ -21,12 +21,15 @@ import snd.komelia.settings.model.EpubReaderType.TTSU_EPUB import snd.komelia.ui.LocalStrings import snd.komelia.ui.common.components.DropdownChoiceMenu import snd.komelia.ui.common.components.LabeledEntry +import snd.komelia.ui.common.components.SwitchWithLabel import snd.komelia.ui.platform.cursorForHand @Composable fun EpubReaderSettingsContent( readerType: EpubReaderType, onReaderChange: (EpubReaderType) -> Unit, + fullscreenEnabled: Boolean, + onFullscreenEnabledChange: (Boolean) -> Unit, ) { val strings = LocalStrings.current.settings Column( @@ -70,5 +73,14 @@ fun EpubReaderSettingsContent( KOMGA_EPUB -> Text("Komga webui epub reader adapted for use in Komelia") } + + if (readerType == KOMGA_EPUB) { + SwitchWithLabel( + checked = fullscreenEnabled, + onCheckedChange = onFullscreenEnabledChange, + label = { Text("Fullscreen mode") }, + supportingText = { Text("Automatically enter fullscreen when opening a book on mobile") }, + ) + } } } \ No newline at end of file diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsScreen.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsScreen.kt index 9d029ce2b..396d8cd70 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsScreen.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsScreen.kt @@ -25,8 +25,10 @@ class EpubReaderSettingsScreen : Screen { is LoadState.Error -> ErrorContent(result.exception) LoadState.Uninitialized, LoadState.Loading -> LoadingMaxSizeIndicator() is LoadState.Success -> EpubReaderSettingsContent( - vm.selectedEpubReader.collectAsState().value, - vm::onSelectedTypeChange + readerType = vm.selectedEpubReader.collectAsState().value, + onReaderChange = vm::onSelectedTypeChange, + fullscreenEnabled = vm.fullscreenEnabled.collectAsState().value, + onFullscreenEnabledChange = vm::onFullscreenEnabledChange, ) } diff --git a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsViewModel.kt b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsViewModel.kt index c9c60b2ef..ac448d58d 100644 --- a/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsViewModel.kt +++ b/komelia-ui/src/commonMain/kotlin/snd/komelia/ui/settings/epub/EpubReaderSettingsViewModel.kt @@ -14,10 +14,12 @@ class EpubReaderSettingsViewModel( private val settingsRepository: EpubReaderSettingsRepository ) : StateScreenModel>(LoadState.Uninitialized) { val selectedEpubReader = MutableStateFlow(TTSU_EPUB) + val fullscreenEnabled = MutableStateFlow(true) suspend fun initialize() { if (state.value !is LoadState.Uninitialized) return selectedEpubReader.value = settingsRepository.getReaderType().first() + fullscreenEnabled.value = settingsRepository.getFullscreenEnabled().first() mutableState.value = LoadState.Success(Unit) } @@ -25,4 +27,9 @@ class EpubReaderSettingsViewModel( selectedEpubReader.value = type screenModelScope.launch { settingsRepository.putReaderType(type) } } + + fun onFullscreenEnabledChange(enabled: Boolean) { + fullscreenEnabled.value = enabled + settingsRepository.putFullscreenEnabled(enabled) + } }