From 5f0c4606681cd59b38ae0855c7827e149fa5488c Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Sun, 2 Nov 2025 19:41:33 +0600 Subject: [PATCH] Make reader edge-to-edge (#1908) --- CHANGELOG.md | 4 + app/build.gradle.kts | 1 - .../settings/screen/SettingsReaderScreen.kt | 6 +- ...ndicatorText.kt => ReaderPageIndicator.kt} | 9 +- .../reader/appbars/ReaderAppBars.kt | 133 ++---- ...{BottomReaderBar.kt => ReaderBottomBar.kt} | 16 +- .../reader/appbars/ReaderTopBar.kt | 83 ++++ .../reader/settings/GeneralSettingsPage.kt | 5 +- .../tachiyomi/ui/reader/ReaderActivity.kt | 395 +++++++++--------- .../setting/ReaderSettingsScreenModel.kt | 1 - .../util/system/DisplayExtensions.kt | 10 +- app/src/main/res/layout/reader_activity.xml | 8 +- gradle/libs.versions.toml | 1 - 13 files changed, 344 insertions(+), 328 deletions(-) rename app/src/main/java/eu/kanade/presentation/reader/{PageIndicatorText.kt => ReaderPageIndicator.kt} (85%) rename app/src/main/java/eu/kanade/presentation/reader/appbars/{BottomReaderBar.kt => ReaderBottomBar.kt} (83%) create mode 100644 app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderTopBar.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index b76c98e12..ca5b85949 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co ### Changed - Increased default concurrent page downloads to 5 ([@AntsyLich](https://github.com/AntsyLich)) ([#2637](https://github.com/mihonapp/mihon/pull/2637)) +- Hide "Show content in cutout area" reader setting on Android 15+ as it's not supported ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908)) ### Improved - Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491)) @@ -37,6 +38,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co - Fix scrollbar not showing when animator duration scale animation is turned off ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2398](https://github.com/mihonapp/mihon/pull/2398)) - Fix date picker not allowing the same start and finish date in negative time zones ([@AntsyLich](https://github.com/AntsyLich), [@kashish-aggarwal21](https://github.com/kashish-aggarwal21)) ([#2617](https://github.com/mihonapp/mihon/pull/2617)) - Fix reader tap zones triggering after scrolling was stopped by the user ([@Naputt1](https://github.com/Naputt1), [@AntsyLich](https://github.com/AntsyLich)) ([#2518](https://github.com/mihonapp/mihon/pull/2518)) +- Fix reader page indicator being partially visible on some devices ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908)) +- Fix inconsistent system bar and reader app bar background ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908)) +- Fix transparent system bar background in reader on Android 15+ ([@AntsyLich](https://github.com/AntsyLich)) ([#1908](https://github.com/mihonapp/mihon/pull/1908)) ### Other - Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476)) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7e4cf78a4..6dfd2325b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -261,7 +261,6 @@ dependencies { implementation(libs.directionalviewpager) { exclude(group = "androidx.viewpager", module = "viewpager") } - implementation(libs.insetter) implementation(libs.richeditor.compose) implementation(libs.aboutLibraries.compose) implementation(libs.bundles.voyager) diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt index 48d649cd5..3ec853d5f 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsReaderScreen.kt @@ -1,6 +1,5 @@ package eu.kanade.presentation.more.settings.screen -import android.os.Build import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.getValue @@ -10,6 +9,7 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode +import eu.kanade.tachiyomi.util.system.hasDisplayCutout import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentMapOf import kotlinx.collections.immutable.toImmutableMap @@ -103,9 +103,7 @@ object SettingsReaderScreen : SearchableSettings { Preference.PreferenceItem.SwitchPreference( preference = readerPreferences.cutoutShort(), title = stringResource(MR.strings.pref_cutout_short), - enabled = fullscreen && - Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - LocalView.current.rootWindowInsets?.displayCutout != null, // has cutout + enabled = LocalView.current.hasDisplayCutout() && fullscreen, ), Preference.PreferenceItem.SwitchPreference( preference = readerPreferences.keepScreenOn(), diff --git a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageIndicator.kt similarity index 85% rename from app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt rename to app/src/main/java/eu/kanade/presentation/reader/ReaderPageIndicator.kt index 3dd057a56..4befd4828 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/PageIndicatorText.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/ReaderPageIndicator.kt @@ -6,6 +6,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.text.TextStyle @@ -15,9 +16,10 @@ import androidx.compose.ui.unit.sp import eu.kanade.presentation.theme.TachiyomiPreviewTheme @Composable -fun PageIndicatorText( +fun ReaderPageIndicator( currentPage: Int, totalPages: Int, + modifier: Modifier = Modifier, ) { if (currentPage <= 0 || totalPages <= 0) return @@ -36,6 +38,7 @@ fun PageIndicatorText( Box( contentAlignment = Alignment.Center, + modifier = modifier, ) { Text( text = text, @@ -50,10 +53,10 @@ fun PageIndicatorText( @PreviewLightDark @Composable -private fun PageIndicatorTextPreview() { +private fun ReaderPageIndicatorPreview() { TachiyomiPreviewTheme { Surface { - PageIndicatorText(currentPage = 10, totalPages = 69) + ReaderPageIndicator(currentPage = 10, totalPages = 69) } } } diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt index 8a1bd3c28..fe2b2de0c 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderAppBars.kt @@ -2,42 +2,41 @@ package eu.kanade.presentation.reader.appbars import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.systemBarsPadding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.Bookmark -import androidx.compose.material.icons.outlined.BookmarkBorder +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBars +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp -import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.components.AppBarActions import eu.kanade.presentation.reader.components.ChapterNavigator import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode import eu.kanade.tachiyomi.ui.reader.viewer.Viewer import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer -import kotlinx.collections.immutable.persistentListOf -import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.i18n.stringResource -private val animationSpec = tween(200) +private val readerBarsSlideAnimationSpec = tween(200) +private val readerBarsFadeAnimationSpec = tween(150) @Composable fun ReaderAppBars( visible: Boolean, - fullscreen: Boolean, mangaTitle: String?, chapterTitle: String?, @@ -71,83 +70,26 @@ fun ReaderAppBars( .surfaceColorAtElevation(3.dp) .copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f) - val modifierWithInsetsPadding = if (fullscreen) { - Modifier.systemBarsPadding() - } else { - Modifier - } - - Column( - modifier = Modifier.fillMaxHeight(), - verticalArrangement = Arrangement.SpaceBetween, - ) { + Column(modifier = Modifier.fillMaxHeight()) { AnimatedVisibility( visible = visible, - enter = slideInVertically( - initialOffsetY = { -it }, - animationSpec = animationSpec, - ), - exit = slideOutVertically( - targetOffsetY = { -it }, - animationSpec = animationSpec, - ), + enter = slideInVertically(initialOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) + + fadeIn(animationSpec = readerBarsFadeAnimationSpec), + exit = slideOutVertically(targetOffsetY = { -it }, animationSpec = readerBarsSlideAnimationSpec) + + fadeOut(animationSpec = readerBarsFadeAnimationSpec), ) { - AppBar( - modifier = modifierWithInsetsPadding + ReaderTopBar( + modifier = Modifier + .background(backgroundColor) .clickable(onClick = onClickTopAppBar), - backgroundColor = backgroundColor, - title = mangaTitle, - subtitle = chapterTitle, + mangaTitle = mangaTitle, + chapterTitle = chapterTitle, navigateUp = navigateUp, - actions = { - AppBarActions( - actions = persistentListOf().builder() - .apply { - add( - AppBar.Action( - title = stringResource( - if (bookmarked) { - MR.strings.action_remove_bookmark - } else { - MR.strings.action_bookmark - }, - ), - icon = if (bookmarked) { - Icons.Outlined.Bookmark - } else { - Icons.Outlined.BookmarkBorder - }, - onClick = onToggleBookmarked, - ), - ) - onOpenInWebView?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_open_in_web_view), - onClick = it, - ), - ) - } - onOpenInBrowser?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_open_in_browser), - onClick = it, - ), - ) - } - onShare?.let { - add( - AppBar.OverflowAction( - title = stringResource(MR.strings.action_share), - onClick = it, - ), - ) - } - } - .build(), - ) - }, + bookmarked = bookmarked, + onToggleBookmarked = onToggleBookmarked, + onOpenInWebView = onOpenInWebView, + onOpenInBrowser = onOpenInBrowser, + onShare = onShare, ) } @@ -155,19 +97,12 @@ fun ReaderAppBars( AnimatedVisibility( visible = visible, - enter = slideInVertically( - initialOffsetY = { it }, - animationSpec = animationSpec, - ), - exit = slideOutVertically( - targetOffsetY = { it }, - animationSpec = animationSpec, - ), + enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) + + fadeIn(animationSpec = readerBarsFadeAnimationSpec), + exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) + + fadeOut(animationSpec = readerBarsFadeAnimationSpec), ) { - Column( - modifier = modifierWithInsetsPadding, - verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), - ) { + Column(verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) { ChapterNavigator( isRtl = isRtl, onNextChapter = onNextChapter, @@ -178,8 +113,12 @@ fun ReaderAppBars( totalPages = totalPages, onPageIndexChange = onPageIndexChange, ) - BottomReaderBar( - backgroundColor = backgroundColor, + ReaderBottomBar( + modifier = Modifier + .fillMaxWidth() + .background(backgroundColor) + .padding(horizontal = MaterialTheme.padding.small) + .windowInsetsPadding(WindowInsets.navigationBars), readingMode = readingMode, onClickReadingMode = onClickReadingMode, orientation = orientation, diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderBottomBar.kt similarity index 83% rename from app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt rename to app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderBottomBar.kt index 8d0cae57b..f730dceb4 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/appbars/BottomReaderBar.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderBottomBar.kt @@ -1,10 +1,7 @@ package eu.kanade.presentation.reader.appbars -import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Settings import androidx.compose.material3.Icon @@ -12,9 +9,8 @@ import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.res.painterResource -import androidx.compose.ui.unit.dp import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode @@ -22,8 +18,7 @@ import tachiyomi.i18n.MR import tachiyomi.presentation.core.i18n.stringResource @Composable -fun BottomReaderBar( - backgroundColor: Color, +fun ReaderBottomBar( readingMode: ReadingMode, onClickReadingMode: () -> Unit, orientation: ReaderOrientation, @@ -31,12 +26,11 @@ fun BottomReaderBar( cropEnabled: Boolean, onClickCropBorder: () -> Unit, onClickSettings: () -> Unit, + modifier: Modifier = Modifier, ) { Row( - modifier = Modifier - .fillMaxWidth() - .background(backgroundColor) - .padding(8.dp), + modifier = modifier + .pointerInput(Unit) {}, horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderTopBar.kt b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderTopBar.kt new file mode 100644 index 000000000..9d783af7b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/reader/appbars/ReaderTopBar.kt @@ -0,0 +1,83 @@ +package eu.kanade.presentation.reader.appbars + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Bookmark +import androidx.compose.material.icons.outlined.BookmarkBorder +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.AppBarActions +import kotlinx.collections.immutable.persistentListOf +import tachiyomi.i18n.MR +import tachiyomi.presentation.core.i18n.stringResource + +@Composable +fun ReaderTopBar( + mangaTitle: String?, + chapterTitle: String?, + navigateUp: () -> Unit, + bookmarked: Boolean, + onToggleBookmarked: () -> Unit, + onOpenInWebView: (() -> Unit)?, + onOpenInBrowser: (() -> Unit)?, + onShare: (() -> Unit)?, + modifier: Modifier = Modifier, +) { + AppBar( + modifier = modifier, + backgroundColor = Color.Transparent, + title = mangaTitle, + subtitle = chapterTitle, + navigateUp = navigateUp, + actions = { + AppBarActions( + actions = persistentListOf().builder() + .apply { + add( + AppBar.Action( + title = stringResource( + if (bookmarked) { + MR.strings.action_remove_bookmark + } else { + MR.strings.action_bookmark + }, + ), + icon = if (bookmarked) { + Icons.Outlined.Bookmark + } else { + Icons.Outlined.BookmarkBorder + }, + onClick = onToggleBookmarked, + ), + ) + onOpenInWebView?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_open_in_web_view), + onClick = it, + ), + ) + } + onOpenInBrowser?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_open_in_browser), + onClick = it, + ), + ) + } + onShare?.let { + add( + AppBar.OverflowAction( + title = stringResource(MR.strings.action_share), + onClick = it, + ), + ) + } + } + .build(), + ) + }, + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt index 33f5ab4a0..c6da9fbc7 100644 --- a/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt +++ b/app/src/main/java/eu/kanade/presentation/reader/settings/GeneralSettingsPage.kt @@ -6,8 +6,10 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.platform.LocalView import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel +import eu.kanade.tachiyomi.util.system.hasDisplayCutout import tachiyomi.i18n.MR import tachiyomi.presentation.core.components.CheckboxItem import tachiyomi.presentation.core.components.SettingsChipRow @@ -64,7 +66,8 @@ internal fun ColumnScope.GeneralPage(screenModel: ReaderSettingsScreenModel) { pref = screenModel.preferences.fullscreen(), ) - if (screenModel.hasDisplayCutout && screenModel.preferences.fullscreen().get()) { + val isFullscreen by screenModel.preferences.fullscreen().collectAsState() + if (LocalView.current.hasDisplayCutout() && isFullscreen) { CheckboxItem( label = stringResource(MR.strings.pref_cutout_short), pref = screenModel.preferences.cutoutShort(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 6ed97ec2b..c2039127a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.ui.reader import android.annotation.SuppressLint -import android.app.Activity import android.app.assist.AssistContent import android.content.ClipData import android.content.ClipboardManager @@ -16,40 +15,47 @@ import android.os.Build import android.os.Bundle import android.view.KeyEvent import android.view.MotionEvent +import android.view.View import android.view.View.LAYER_TYPE_HARDWARE import android.view.WindowManager import android.widget.Toast +import androidx.activity.SystemBarStyle +import androidx.activity.enableEdgeToEdge import androidx.activity.viewModels import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.material3.AlertDialog import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.core.content.getSystemService -import androidx.core.graphics.ColorUtils +import androidx.core.graphics.Insets import androidx.core.net.toUri import androidx.core.transition.doOnEnd +import androidx.core.view.ViewCompat import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.lifecycleScope import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView -import com.google.android.material.elevation.SurfaceColors import com.google.android.material.transition.platform.MaterialContainerTransform import com.hippo.unifile.UniFile -import dev.chrisbanes.insetter.applyInsetter import eu.kanade.core.util.ifSourcesLoaded import eu.kanade.domain.base.BasePreferences import eu.kanade.presentation.reader.DisplayRefreshHost import eu.kanade.presentation.reader.OrientationSelectDialog -import eu.kanade.presentation.reader.PageIndicatorText import eu.kanade.presentation.reader.ReaderContentOverlay import eu.kanade.presentation.reader.ReaderPageActionsDialog +import eu.kanade.presentation.reader.ReaderPageIndicator import eu.kanade.presentation.reader.ReadingModeSelectDialog import eu.kanade.presentation.reader.appbars.ReaderAppBars import eu.kanade.presentation.reader.settings.ReaderSettingsDialog @@ -121,8 +127,6 @@ class ReaderActivity : BaseActivity() { val viewModel by viewModels() private var assistUrl: String? = null - private val hasCutout by lazy { hasDisplayCutout() } - /** * Configuration at reader level, like background color or forced orientation. */ @@ -132,7 +136,7 @@ class ReaderActivity : BaseActivity() { private var readingModeToast: Toast? = null private val displayRefreshHost = DisplayRefreshHost() - private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, binding.root) } + private val windowInsetsController by lazy { WindowInsetsControllerCompat(window, window.decorView) } private var loadingIndicator: ReaderProgressIndicator? = null @@ -146,7 +150,7 @@ class ReaderActivity : BaseActivity() { registerSecureActivity(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { overrideActivityTransition( - Activity.OVERRIDE_TRANSITION_OPEN, + OVERRIDE_TRANSITION_OPEN, R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit, ) @@ -155,10 +159,17 @@ class ReaderActivity : BaseActivity() { overridePendingTransition(R.anim.shared_axis_x_push_enter, R.anim.shared_axis_x_push_exit) } + enableEdgeToEdge(navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT)) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + window.isNavigationBarContrastEnforced = false + } + windowInsetsController.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + super.onCreate(savedInstanceState) binding = ReaderActivityBinding.inflate(layoutInflater) setContentView(binding.root) + binding.setComposeOverlay() if (viewModel.needsInit()) { val manga = intent.extras?.getLong("manga", -1) ?: -1L @@ -181,7 +192,7 @@ class ReaderActivity : BaseActivity() { } config = ReaderConfig() - initializeMenu() + setMenuVisibility(viewModel.state.value.menuVisible) // Finish when incognito mode is disabled preferences.incognitoMode().changes() @@ -238,6 +249,93 @@ class ReaderActivity : BaseActivity() { .launchIn(lifecycleScope) } + private fun ReaderActivityBinding.setComposeOverlay(): Unit = composeOverlay.setComposeContent { + val state by viewModel.state.collectAsState() + val showPageNumber by readerPreferences.showPageNumber().collectAsState() + val isFullscreen by readerPreferences.fullscreen().collectAsState() + val settingsScreenModel = remember { + ReaderSettingsScreenModel( + readerState = viewModel.state, + onChangeReadingMode = viewModel::setMangaReadingMode, + onChangeOrientation = viewModel::setMangaOrientationType, + ) + } + + Box(modifier = Modifier.fillMaxSize()) { + if (!state.menuVisible && showPageNumber) { + ReaderPageIndicator( + currentPage = state.currentPage, + totalPages = state.totalPages, + modifier = Modifier + .align(Alignment.BottomCenter) + .then(if (isFullscreen) Modifier else Modifier.navigationBarsPadding()), + ) + } + + ContentOverlay(state = state) + + AppBars(state = state) + } + + val onDismissRequest = viewModel::closeDialog + when (state.dialog) { + is ReaderViewModel.Dialog.Loading -> { + AlertDialog( + onDismissRequest = {}, + confirmButton = {}, + text = { + Row( + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CircularProgressIndicator() + Text(stringResource(MR.strings.loading)) + } + }, + ) + } + is ReaderViewModel.Dialog.Settings -> { + ReaderSettingsDialog( + onDismissRequest = onDismissRequest, + onShowMenus = { setMenuVisibility(true) }, + onHideMenus = { setMenuVisibility(false) }, + screenModel = settingsScreenModel, + ) + } + is ReaderViewModel.Dialog.ReadingModeSelect -> { + ReadingModeSelectDialog( + onDismissRequest = onDismissRequest, + screenModel = settingsScreenModel, + onChange = { stringRes -> + menuToggleToast?.cancel() + if (!readerPreferences.showReadingMode().get()) { + menuToggleToast = toast(stringRes) + } + }, + ) + } + is ReaderViewModel.Dialog.OrientationModeSelect -> { + OrientationSelectDialog( + onDismissRequest = onDismissRequest, + screenModel = settingsScreenModel, + onChange = { stringRes -> + menuToggleToast?.cancel() + menuToggleToast = toast(stringRes) + }, + ) + } + is ReaderViewModel.Dialog.PageActions -> { + ReaderPageActionsDialog( + onDismissRequest = onDismissRequest, + onSetAsCover = viewModel::setAsCover, + onShare = viewModel::shareImage, + onSave = viewModel::saveImage, + ) + } + null -> {} + } + } + /** * Called when the activity is destroyed. Cleans up the viewer, configuration and any view. */ @@ -289,7 +387,7 @@ class ReaderActivity : BaseActivity() { super.finish() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { overrideActivityTransition( - Activity.OVERRIDE_TRANSITION_CLOSE, + OVERRIDE_TRANSITION_CLOSE, R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit, ) @@ -327,180 +425,82 @@ class ReaderActivity : BaseActivity() { return handled || super.dispatchGenericMotionEvent(event) } - /** - * Initializes the reader menu. It sets up click listeners and the initial visibility. - */ - private fun initializeMenu() { - binding.pageNumber.setComposeContent { - val state by viewModel.state.collectAsState() - val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState() + @Composable + private fun ContentOverlay(state: ReaderViewModel.State) { + val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState() - if (!state.menuVisible && showPageNumber) { - PageIndicatorText( - currentPage = state.currentPage, - totalPages = state.totalPages, - ) - } + val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState() + val colorOverlay by readerPreferences.colorFilterValue().collectAsState() + val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState() + val colorOverlayBlendMode = remember(colorOverlayMode) { + ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second } - binding.dialogRoot.setComposeContent { - val state by viewModel.state.collectAsState() - val settingsScreenModel = remember { - ReaderSettingsScreenModel( - readerState = viewModel.state, - hasDisplayCutout = hasCutout, - onChangeReadingMode = viewModel::setMangaReadingMode, - onChangeOrientation = viewModel::setMangaOrientationType, - ) - } - - if (!ifSourcesLoaded()) { - return@setComposeContent - } - - val isHttpSource = viewModel.getSource() is HttpSource - val isFullscreen by readerPreferences.fullscreen().collectAsState() - val flashOnPageChange by readerPreferences.flashOnPageChange().collectAsState() - - val colorOverlayEnabled by readerPreferences.colorFilter().collectAsState() - val colorOverlay by readerPreferences.colorFilterValue().collectAsState() - val colorOverlayMode by readerPreferences.colorFilterMode().collectAsState() - val colorOverlayBlendMode = remember(colorOverlayMode) { - ReaderPreferences.ColorFilterMode.getOrNull(colorOverlayMode)?.second - } - - val cropBorderPaged by readerPreferences.cropBorders().collectAsState() - val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState() - val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode()) - val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon - - ReaderContentOverlay( - brightness = state.brightnessOverlayValue, - color = colorOverlay.takeIf { colorOverlayEnabled }, - colorBlendMode = colorOverlayBlendMode, - ) - - ReaderAppBars( - visible = state.menuVisible, - fullscreen = isFullscreen, - - mangaTitle = state.manga?.title, - chapterTitle = state.currentChapter?.chapter?.name, - navigateUp = onBackPressedDispatcher::onBackPressed, - onClickTopAppBar = ::openMangaScreen, - bookmarked = state.bookmarked, - onToggleBookmarked = viewModel::toggleChapterBookmark, - onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource }, - onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource }, - onShare = ::shareChapter.takeIf { isHttpSource }, - - viewer = state.viewer, - onNextChapter = ::loadNextChapter, - enabledNext = state.viewerChapters?.nextChapter != null, - onPreviousChapter = ::loadPreviousChapter, - enabledPrevious = state.viewerChapters?.prevChapter != null, - currentPage = state.currentPage, - totalPages = state.totalPages, - onPageIndexChange = { - isScrollingThroughPages = true - moveToPageIndex(it) - }, - - readingMode = ReadingMode.fromPreference( - viewModel.getMangaReadingMode(resolveDefault = false), - ), - onClickReadingMode = viewModel::openReadingModeSelectDialog, - orientation = ReaderOrientation.fromPreference( - viewModel.getMangaOrientation(resolveDefault = false), - ), - onClickOrientation = viewModel::openOrientationModeSelectDialog, - cropEnabled = cropEnabled, - onClickCropBorder = { - val enabled = viewModel.toggleCropBorders() - menuToggleToast?.cancel() - menuToggleToast = toast(if (enabled) MR.strings.on else MR.strings.off) - }, - onClickSettings = viewModel::openSettingsDialog, - ) - - if (flashOnPageChange) { - DisplayRefreshHost( - hostState = displayRefreshHost, - ) - } - - val onDismissRequest = viewModel::closeDialog - when (state.dialog) { - is ReaderViewModel.Dialog.Loading -> { - AlertDialog( - onDismissRequest = {}, - confirmButton = {}, - text = { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CircularProgressIndicator() - Text(stringResource(MR.strings.loading)) - } - }, - ) - } - is ReaderViewModel.Dialog.Settings -> { - ReaderSettingsDialog( - onDismissRequest = onDismissRequest, - onShowMenus = { setMenuVisibility(true) }, - onHideMenus = { setMenuVisibility(false) }, - screenModel = settingsScreenModel, - ) - } - is ReaderViewModel.Dialog.ReadingModeSelect -> { - ReadingModeSelectDialog( - onDismissRequest = onDismissRequest, - screenModel = settingsScreenModel, - onChange = { stringRes -> - menuToggleToast?.cancel() - if (!readerPreferences.showReadingMode().get()) { - menuToggleToast = toast(stringRes) - } - }, - ) - } - is ReaderViewModel.Dialog.OrientationModeSelect -> { - OrientationSelectDialog( - onDismissRequest = onDismissRequest, - screenModel = settingsScreenModel, - onChange = { stringRes -> - menuToggleToast?.cancel() - menuToggleToast = toast(stringRes) - }, - ) - } - is ReaderViewModel.Dialog.PageActions -> { - ReaderPageActionsDialog( - onDismissRequest = onDismissRequest, - onSetAsCover = viewModel::setAsCover, - onShare = viewModel::shareImage, - onSave = viewModel::saveImage, - ) - } - null -> {} - } - } - - val toolbarColor = ColorUtils.setAlphaComponent( - SurfaceColors.SURFACE_2.getColor(this), - if (isNightMode()) 230 else 242, // 90% dark 95% light + ReaderContentOverlay( + brightness = state.brightnessOverlayValue, + color = colorOverlay.takeIf { colorOverlayEnabled }, + colorBlendMode = colorOverlayBlendMode, ) - @Suppress("DEPRECATION") - window.statusBarColor = toolbarColor - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - @Suppress("DEPRECATION") - window.navigationBarColor = toolbarColor + + if (flashOnPageChange) { + DisplayRefreshHost(hostState = displayRefreshHost) + } + } + + @Composable + fun AppBars(state: ReaderViewModel.State) { + if (!ifSourcesLoaded()) { + return } - // Set initial visibility - setMenuVisibility(viewModel.state.value.menuVisible) + val isHttpSource = viewModel.getSource() is HttpSource + + val cropBorderPaged by readerPreferences.cropBorders().collectAsState() + val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState() + val isPagerType = ReadingMode.isPagerType(viewModel.getMangaReadingMode()) + val cropEnabled = if (isPagerType) cropBorderPaged else cropBorderWebtoon + + ReaderAppBars( + visible = state.menuVisible, + + mangaTitle = state.manga?.title, + chapterTitle = state.currentChapter?.chapter?.name, + navigateUp = onBackPressedDispatcher::onBackPressed, + onClickTopAppBar = ::openMangaScreen, + bookmarked = state.bookmarked, + onToggleBookmarked = viewModel::toggleChapterBookmark, + onOpenInWebView = ::openChapterInWebView.takeIf { isHttpSource }, + onOpenInBrowser = ::openChapterInBrowser.takeIf { isHttpSource }, + onShare = ::shareChapter.takeIf { isHttpSource }, + + viewer = state.viewer, + onNextChapter = ::loadNextChapter, + enabledNext = state.viewerChapters?.nextChapter != null, + onPreviousChapter = ::loadPreviousChapter, + enabledPrevious = state.viewerChapters?.prevChapter != null, + currentPage = state.currentPage, + totalPages = state.totalPages, + onPageIndexChange = { + isScrollingThroughPages = true + moveToPageIndex(it) + }, + + readingMode = ReadingMode.fromPreference( + viewModel.getMangaReadingMode(resolveDefault = false), + ), + onClickReadingMode = viewModel::openReadingModeSelectDialog, + orientation = ReaderOrientation.fromPreference( + viewModel.getMangaOrientation(resolveDefault = false), + ), + onClickOrientation = viewModel::openOrientationModeSelectDialog, + cropEnabled = cropEnabled, + onClickCropBorder = { + val enabled = viewModel.toggleCropBorders() + menuToggleToast?.cancel() + menuToggleToast = toast(if (enabled) MR.strings.on else MR.strings.off) + }, + onClickSettings = viewModel::openSettingsDialog, + ) } /** @@ -510,13 +510,8 @@ class ReaderActivity : BaseActivity() { viewModel.showMenus(visible) if (visible) { windowInsetsController.show(WindowInsetsCompat.Type.systemBars()) - window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS) - } else { - if (readerPreferences.fullscreen().get()) { - windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) - windowInsetsController.systemBarsBehavior = - WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE - } + } else if (readerPreferences.fullscreen().get()) { + windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) } } @@ -593,7 +588,7 @@ class ReaderActivity : BaseActivity() { try { readingModeToast?.cancel() readingModeToast = toast(ReadingMode.fromPreference(mode).stringRes) - } catch (e: ArrayIndexOutOfBoundsException) { + } catch (_: ArrayIndexOutOfBoundsException) { logcat(LogPriority.ERROR) { "Unknown reading mode: $mode" } } } @@ -786,15 +781,24 @@ class ReaderActivity : BaseActivity() { * Updates viewer inset depending on fullscreen reader preferences. */ private fun updateViewerInset(fullscreen: Boolean) { - viewModel.state.value.viewer?.getView()?.applyInsetter { - if (!fullscreen) { - type(navigationBars = true, statusBars = true) { - padding() - } - } + val view = viewModel.state.value.viewer?.getView() ?: return + + view.applyInsetsPadding(ViewCompat.getRootWindowInsets(view), fullscreen) + ViewCompat.setOnApplyWindowInsetsListener(view) { view, windowInsets -> + view.applyInsetsPadding(windowInsets, fullscreen) + windowInsets } } + private fun View.applyInsetsPadding(windowInsets: WindowInsetsCompat?, fullscreen: Boolean) { + val insets = if (!fullscreen) { + windowInsets?.getInsets(WindowInsetsCompat.Type.systemBars()) ?: Insets.NONE + } else { + Insets.NONE + } + setPadding(insets.left, insets.top, insets.right, insets.bottom) + } + /** * Class that handles the user preferences of the reader. */ @@ -902,15 +906,12 @@ class ReaderActivity : BaseActivity() { } private fun setCutoutShort(enabled: Boolean) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) return + if (!window.decorView.hasDisplayCutout()) return window.attributes.layoutInDisplayCutoutMode = when (enabled) { true -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES false -> WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER } - - // Trigger relayout - setMenuVisibility(viewModel.state.value.menuVisible) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt index 5f107c388..790984bea 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsScreenModel.kt @@ -13,7 +13,6 @@ import uy.kohesive.injekt.api.get class ReaderSettingsScreenModel( readerState: StateFlow, - val hasDisplayCutout: Boolean, val onChangeReadingMode: (ReadingMode) -> Unit, val onChangeOrientation: (ReaderOrientation) -> Unit, val preferences: ReaderPreferences = Injekt.get(), diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt index f051ccbc8..fa5d6c2d8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/DisplayExtensions.kt @@ -1,9 +1,9 @@ package eu.kanade.tachiyomi.util.system -import android.app.Activity import android.content.Context import android.content.res.Configuration import android.os.Build +import android.view.View import eu.kanade.domain.ui.UiPreferences import eu.kanade.domain.ui.model.TabletUiMode import uy.kohesive.injekt.Injekt @@ -57,11 +57,11 @@ fun Context.isNightMode(): Boolean { /** * Checks whether if the device has a display cutout (i.e. notch, camera cutout, etc.). * - * Only works in Android 9+. + * Only relevant from Android 9 to Android 14. */ -fun Activity.hasDisplayCutout(): Boolean { - return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && - window.decorView.rootWindowInsets?.displayCutout != null +fun View.hasDisplayCutout(): Boolean { + return Build.VERSION.SDK_INT in Build.VERSION_CODES.P..Build.VERSION_CODES.UPSIDE_DOWN_CAKE && + rootWindowInsets?.displayCutout != null } /** diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index ed289767a..e759ba454 100644 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -13,12 +13,6 @@ android:layout_height="match_parent" android:descendantFocusability="blocksDescendants" /> - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5f2b3ef03..a0703d1fa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -60,7 +60,6 @@ material = "com.google.android.material:material:1.12.0" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" -insetter = "dev.chrisbanes.insetter:insetter:0.6.1" compose-materialmotion = "io.github.fornewid:material-motion-compose-core:2.0.1" compose-webview = "io.github.kevinnzou:compose-webview:0.33.6" compose-grid = "io.woong.compose.grid:grid:1.2.2"