/* * Episteme Reader - A native Android document reader. * Copyright (C) 2026 Episteme * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . * * mail: epistemereader@gmail.com */ package com.aryan.reader.epubreader import timber.log.Timber import android.graphics.Color import android.view.KeyEvent import android.view.View import android.view.Window import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.onPreviewKeyEvent import androidx.compose.ui.input.key.type import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.aryan.reader.RenderMode @Composable fun EpubReaderSystemUiController( window: Window?, view: View, showBars: Boolean, initialIsAppearanceLightStatusBars: Boolean, initialSystemBarsBehavior: Int, isDarkTheme: Boolean, systemUiMode: SystemUiMode ) { DisposableEffect(window, view, initialIsAppearanceLightStatusBars, initialSystemBarsBehavior) { if (window == null) { Timber.w("Window is null, cannot control system UI.") return@DisposableEffect onDispose {} } val insetsController = WindowCompat.getInsetsController(window, view) val originalStatusBarColor = window.statusBarColor val originalNavigationBarColor = window.navigationBarColor Timber.d("Applying immersive mode.") WindowCompat.setDecorFitsSystemWindows(window, false) window.statusBarColor = Color.TRANSPARENT window.navigationBarColor = Color.TRANSPARENT insetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE onDispose { Timber.d("Restoring system UI.") WindowCompat.setDecorFitsSystemWindows(window, true) window.statusBarColor = originalStatusBarColor window.navigationBarColor = originalNavigationBarColor insetsController.show(WindowInsetsCompat.Type.navigationBars() or WindowInsetsCompat.Type.statusBars()) insetsController.isAppearanceLightStatusBars = initialIsAppearanceLightStatusBars insetsController.systemBarsBehavior = initialSystemBarsBehavior } } LaunchedEffect(window, view, isDarkTheme) { if (window != null) { val insetsController = WindowCompat.getInsetsController(window, view) insetsController.isAppearanceLightStatusBars = !isDarkTheme } } LaunchedEffect(showBars, systemUiMode, window, view) { if (window != null) { val insetsController = WindowCompat.getInsetsController(window, view) when (systemUiMode) { SystemUiMode.DEFAULT -> { insetsController.show(WindowInsetsCompat.Type.statusBars()) if (showBars) insetsController.show(WindowInsetsCompat.Type.navigationBars()) else insetsController.hide(WindowInsetsCompat.Type.navigationBars()) } SystemUiMode.SYNC -> { if (showBars) { insetsController.show(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) } else { insetsController.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) } } SystemUiMode.HIDDEN -> { insetsController.hide(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars()) } } } } } fun Modifier.volumeScrollHandler( volumeScrollEnabled: Boolean, renderMode: RenderMode, isTtsActive: Boolean, isMusicActive: Boolean, currentScrollY: Int, currentScrollHeight: Int, currentClientHeight: Int, currentChapterIndex: Int, totalChapters: Int, onScrollBy: (Int) -> Unit, onNavigateChapter: (offset: Int, scrollTarget: ChapterScrollPosition) -> Unit, onNextPage: () -> Unit = {}, onPrevPage: () -> Unit = {} ): Modifier = this.onPreviewKeyEvent { keyEvent -> val shouldHandle = volumeScrollEnabled && !isTtsActive && !isMusicActive if (!shouldHandle) return@onPreviewKeyEvent false val isVolumeKey = keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_UP if (!isVolumeKey) return@onPreviewKeyEvent false if (keyEvent.type == KeyEventType.KeyDown) { val isNext = keyEvent.nativeKeyEvent.keyCode == KeyEvent.KEYCODE_VOLUME_DOWN if (renderMode == RenderMode.VERTICAL_SCROLL) { val direction = if (isNext) 1 else -1 val isAtBottom = (currentScrollY + currentClientHeight) >= (currentScrollHeight - 2) if (direction == -1 && currentScrollY == 0) { if (currentChapterIndex > 0) onNavigateChapter(-1, ChapterScrollPosition.END) } else if (direction == 1 && isAtBottom) { if (currentChapterIndex < totalChapters - 1) onNavigateChapter(1, ChapterScrollPosition.START) } else { val scrollAmount = (currentClientHeight * 0.25).toInt() * direction onScrollBy(scrollAmount) } } else { if (isNext) onNextPage() else onPrevPage() } } true }