Make reader edge-to-edge (#1908)

This commit is contained in:
AntsyLich
2025-11-02 19:41:33 +06:00
committed by GitHub
parent ac28b6c80c
commit 5f0c460668
13 changed files with 344 additions and 328 deletions

View File

@@ -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))

View File

@@ -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)

View File

@@ -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(),

View File

@@ -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)
}
}
}

View File

@@ -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<IntOffset>(200)
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
private val readerBarsFadeAnimationSpec = tween<Float>(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<AppBar.AppBarAction>().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,
),
) {
Column(
modifier = modifierWithInsetsPadding,
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
enter = slideInVertically(initialOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeIn(animationSpec = readerBarsFadeAnimationSpec),
exit = slideOutVertically(targetOffsetY = { it }, animationSpec = readerBarsSlideAnimationSpec) +
fadeOut(animationSpec = readerBarsFadeAnimationSpec),
) {
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,

View File

@@ -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,
) {

View File

@@ -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<AppBar.AppBarAction>().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(),
)
},
)
}

View File

@@ -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(),

View File

@@ -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<ReaderViewModel>()
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,195 +249,32 @@ class ReaderActivity : BaseActivity() {
.launchIn(lifecycleScope)
}
/**
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
*/
override fun onDestroy() {
super.onDestroy()
viewModel.state.value.viewer?.destroy()
config = null
menuToggleToast?.cancel()
readingModeToast?.cancel()
}
override fun onPause() {
viewModel.flushReadTimer()
super.onPause()
}
/**
* Set menu visibility again on activity resume to apply immersive mode again if needed.
* Helps with rotations.
*/
override fun onResume() {
super.onResume()
viewModel.restartReadTimer()
setMenuVisibility(viewModel.state.value.menuVisible)
}
/**
* Called when the window focus changes. It sets the menu visibility to the last known state
* to apply immersive mode again if needed.
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
setMenuVisibility(viewModel.state.value.menuVisible)
}
}
override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent)
assistUrl?.let { outContent.webUri = it.toUri() }
}
/**
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
*/
override fun finish() {
viewModel.onActivityFinish()
super.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
Activity.OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_N) {
loadNextChapter()
return true
} else if (keyCode == KeyEvent.KEYCODE_P) {
loadPreviousChapter()
return true
}
return super.onKeyUp(keyCode, event)
}
/**
* Dispatches a key event. If the viewer doesn't handle it, call the default implementation.
*/
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val handled = viewModel.state.value.viewer?.handleKeyEvent(event) ?: false
return handled || super.dispatchKeyEvent(event)
}
/**
* Dispatches a generic motion event. If the viewer doesn't handle it, call the default
* implementation.
*/
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val handled = viewModel.state.value.viewer?.handleGenericMotionEvent(event) ?: false
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()
if (!state.menuVisible && showPageNumber) {
PageIndicatorText(
currentPage = state.currentPage,
totalPages = state.totalPages,
)
}
}
binding.dialogRoot.setComposeContent {
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,
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,
Box(modifier = Modifier.fillMaxSize()) {
if (!state.menuVisible && showPageNumber) {
ReaderPageIndicator(
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,
modifier = Modifier
.align(Alignment.BottomCenter)
.then(if (isFullscreen) Modifier else Modifier.navigationBarsPadding()),
)
}
if (flashOnPageChange) {
DisplayRefreshHost(
hostState = displayRefreshHost,
)
ContentOverlay(state = state)
AppBars(state = state)
}
val onDismissRequest = viewModel::closeDialog
@@ -488,21 +336,173 @@ class ReaderActivity : BaseActivity() {
}
}
val toolbarColor = ColorUtils.setAlphaComponent(
SurfaceColors.SURFACE_2.getColor(this),
if (isNightMode()) 230 else 242, // 90% dark 95% light
)
@Suppress("DEPRECATION")
window.statusBarColor = toolbarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@Suppress("DEPRECATION")
window.navigationBarColor = toolbarColor
/**
* Called when the activity is destroyed. Cleans up the viewer, configuration and any view.
*/
override fun onDestroy() {
super.onDestroy()
viewModel.state.value.viewer?.destroy()
config = null
menuToggleToast?.cancel()
readingModeToast?.cancel()
}
// Set initial visibility
override fun onPause() {
viewModel.flushReadTimer()
super.onPause()
}
/**
* Set menu visibility again on activity resume to apply immersive mode again if needed.
* Helps with rotations.
*/
override fun onResume() {
super.onResume()
viewModel.restartReadTimer()
setMenuVisibility(viewModel.state.value.menuVisible)
}
/**
* Called when the window focus changes. It sets the menu visibility to the last known state
* to apply immersive mode again if needed.
*/
override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (hasFocus) {
setMenuVisibility(viewModel.state.value.menuVisible)
}
}
override fun onProvideAssistContent(outContent: AssistContent) {
super.onProvideAssistContent(outContent)
assistUrl?.let { outContent.webUri = it.toUri() }
}
/**
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
*/
override fun finish() {
viewModel.onActivityFinish()
super.finish()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
overrideActivityTransition(
OVERRIDE_TRANSITION_CLOSE,
R.anim.shared_axis_x_pop_enter,
R.anim.shared_axis_x_pop_exit,
)
} else {
@Suppress("DEPRECATION")
overridePendingTransition(R.anim.shared_axis_x_pop_enter, R.anim.shared_axis_x_pop_exit)
}
}
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
if (keyCode == KeyEvent.KEYCODE_N) {
loadNextChapter()
return true
} else if (keyCode == KeyEvent.KEYCODE_P) {
loadPreviousChapter()
return true
}
return super.onKeyUp(keyCode, event)
}
/**
* Dispatches a key event. If the viewer doesn't handle it, call the default implementation.
*/
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
val handled = viewModel.state.value.viewer?.handleKeyEvent(event) ?: false
return handled || super.dispatchKeyEvent(event)
}
/**
* Dispatches a generic motion event. If the viewer doesn't handle it, call the default
* implementation.
*/
override fun dispatchGenericMotionEvent(event: MotionEvent): Boolean {
val handled = viewModel.state.value.viewer?.handleGenericMotionEvent(event) ?: false
return handled || super.dispatchGenericMotionEvent(event)
}
@Composable
private fun ContentOverlay(state: ReaderViewModel.State) {
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
}
ReaderContentOverlay(
brightness = state.brightnessOverlayValue,
color = colorOverlay.takeIf { colorOverlayEnabled },
colorBlendMode = colorOverlayBlendMode,
)
if (flashOnPageChange) {
DisplayRefreshHost(hostState = displayRefreshHost)
}
}
@Composable
fun AppBars(state: ReaderViewModel.State) {
if (!ifSourcesLoaded()) {
return
}
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,
)
}
/**
* Sets the visibility of the menu according to [visible].
*/
@@ -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()) {
} else if (readerPreferences.fullscreen().get()) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
}
}
@@ -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,13 +781,22 @@ 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)
}
/**
@@ -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)
}
/**

View File

@@ -13,7 +13,6 @@ import uy.kohesive.injekt.api.get
class ReaderSettingsScreenModel(
readerState: StateFlow<ReaderViewModel.State>,
val hasDisplayCutout: Boolean,
val onChangeReadingMode: (ReadingMode) -> Unit,
val onChangeOrientation: (ReaderOrientation) -> Unit,
val preferences: ReaderPreferences = Injekt.get(),

View File

@@ -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
}
/**

View File

@@ -13,12 +13,6 @@
android:layout_height="match_parent"
android:descendantFocusability="blocksDescendants" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/page_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|center_horizontal" />
</FrameLayout>
<eu.kanade.tachiyomi.ui.reader.ReaderNavigationOverlayView
@@ -30,7 +24,7 @@
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:id="@+id/compose_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@@ -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"