Migrate top reader app bar to Compose

This commit is contained in:
arkon 2023-10-14 12:30:17 -04:00
parent 5619a4c0d9
commit d18022c259
12 changed files with 149 additions and 231 deletions

View file

@ -1,5 +1,6 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
@ -60,6 +61,7 @@ const val SEARCH_DEBOUNCE_MILLIS = 250L
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Text
title: String?,
subtitle: String? = null,
@ -81,6 +83,7 @@ fun AppBar(
AppBar(
modifier = modifier,
backgroundColor = backgroundColor,
titleContent = {
if (isActionMode) {
AppBarTitle(actionModeCounter.toString())
@ -106,6 +109,7 @@ fun AppBar(
@Composable
fun AppBar(
modifier: Modifier = Modifier,
backgroundColor: Color? = null,
// Title
titleContent: @Composable () -> Unit,
// Up button
@ -142,7 +146,7 @@ fun AppBar(
title = titleContent,
actions = actions,
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(
containerColor = backgroundColor ?: MaterialTheme.colorScheme.surfaceColorAtElevation(
elevation = if (isActionMode) 3.dp else 0.dp,
),
),
@ -170,6 +174,9 @@ fun AppBarTitle(
style = MaterialTheme.typography.bodyMedium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.basicMarquee(
delayMillis = 2_000,
),
)
}
}

View file

@ -1,7 +1,6 @@
package eu.kanade.presentation.reader.appbars
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
@ -10,11 +9,10 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
@ -24,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
@Composable
fun BottomReaderBar(
backgroundColor: Color,
readingMode: ReadingModeType,
onClickReadingMode: () -> Unit,
orientationMode: OrientationType,
@ -32,11 +31,6 @@ fun BottomReaderBar(
onClickCropBorder: () -> Unit,
onClickSettings: () -> Unit,
) {
// Match with toolbar background color set in ReaderActivity
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
Row(
modifier = Modifier
.fillMaxWidth()

View file

@ -4,14 +4,28 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
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.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Bookmark
import androidx.compose.material.icons.outlined.BookmarkBorder
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
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.tachiyomi.R
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
@ -22,8 +36,18 @@ private val animationSpec = tween<IntOffset>(200)
@Composable
fun ReaderAppBars(
visible: Boolean,
viewer: Viewer?,
fullscreen: Boolean,
mangaTitle: String?,
chapterTitle: String?,
navigateUp: () -> Unit,
onClickTopAppBar: () -> Unit,
bookmarked: Boolean,
onToggleBookmarked: () -> Unit,
onOpenInWebView: (() -> Unit)?,
onShare: (() -> Unit)?,
viewer: Viewer?,
onNextChapter: () -> Unit,
enabledNext: Boolean,
onPreviousChapter: () -> Unit,
@ -41,11 +65,64 @@ fun ReaderAppBars(
onClickSettings: () -> Unit,
) {
val isRtl = viewer is R2LPagerViewer
val backgroundColor = MaterialTheme.colorScheme
.surfaceColorAtElevation(3.dp)
.copy(alpha = if (isSystemInDarkTheme()) 0.9f else 0.95f)
val appBarModifier = if (fullscreen) {
Modifier.windowInsetsPadding(WindowInsets.systemBars)
} else {
Modifier
}
Column(
modifier = Modifier.fillMaxHeight(),
verticalArrangement = Arrangement.SpaceBetween,
) {
AnimatedVisibility(
visible = visible,
enter = slideInVertically(
initialOffsetY = { -it },
animationSpec = animationSpec,
),
exit = slideOutVertically(
targetOffsetY = { -it },
animationSpec = animationSpec,
),
) {
AppBar(
modifier = appBarModifier
.clickable(onClick = onClickTopAppBar),
backgroundColor = backgroundColor,
title = mangaTitle,
subtitle = chapterTitle,
navigateUp = navigateUp,
actions = {
AppBarActions(
listOfNotNull(
AppBar.Action(
title = stringResource(if (bookmarked) R.string.action_remove_bookmark else R.string.action_bookmark),
icon = if (bookmarked) Icons.Outlined.Bookmark else Icons.Outlined.BookmarkBorder,
onClick = onToggleBookmarked,
),
onOpenInWebView?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_web_view),
onClick = it,
)
},
onShare?.let {
AppBar.OverflowAction(
title = stringResource(R.string.action_share),
onClick = it,
)
},
),
)
},
)
}
Spacer(modifier = Modifier.weight(1f))
AnimatedVisibility(
@ -74,6 +151,7 @@ fun ReaderAppBars(
)
BottomReaderBar(
backgroundColor = backgroundColor,
readingMode = readingMode,
onClickReadingMode = onClickReadingMode,
orientationMode = orientationMode,

View file

@ -13,15 +13,10 @@ import android.graphics.Paint
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.text.TextUtils
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View.LAYER_TYPE_HARDWARE
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.Toast
import androidx.activity.viewModels
import androidx.compose.foundation.layout.Arrangement
@ -44,8 +39,7 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isVisible
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.internal.ToolbarUtils
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.elevation.SurfaceColors
import com.google.android.material.transition.platform.MaterialContainerTransform
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.domain.base.BasePreferences
@ -74,13 +68,11 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
import eu.kanade.tachiyomi.util.system.hasDisplayCutout
import eu.kanade.tachiyomi.util.system.isNightMode
import eu.kanade.tachiyomi.util.system.toShareIntent
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.setComposeContent
import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filterNotNull
@ -265,47 +257,6 @@ class ReaderActivity : BaseActivity() {
assistUrl?.let { outContent.webUri = it.toUri() }
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.reader, menu)
val isChapterBookmarked = viewModel.getCurrentChapter()?.chapter?.bookmark ?: false
menu.findItem(R.id.action_bookmark).isVisible = !isChapterBookmarked
menu.findItem(R.id.action_remove_bookmark).isVisible = isChapterBookmarked
val isHttpSource = viewModel.getSource() is HttpSource
menu.findItem(R.id.action_open_in_web_view).isVisible = isHttpSource
menu.findItem(R.id.action_share).isVisible = isHttpSource
return true
}
/**
* Called when an item of the options menu was clicked. Used to handle clicks on our menu
* entries.
*/
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_open_in_web_view -> {
openChapterInWebView()
}
R.id.action_bookmark -> {
viewModel.bookmarkCurrentChapter(true)
invalidateOptionsMenu()
}
R.id.action_remove_bookmark -> {
viewModel.bookmarkCurrentChapter(false)
invalidateOptionsMenu()
}
R.id.action_share -> {
assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain")
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
}
}
return super.onOptionsItemSelected(item)
}
/**
* Called when the user clicks the back key or the button on the toolbar. The call is
* delegated to the presenter.
@ -348,35 +299,12 @@ class ReaderActivity : BaseActivity() {
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
onBackPressedDispatcher.onBackPressed()
}
binding.toolbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
margin(top = true, horizontal = true)
}
}
binding.dialogRoot.applyInsetter {
type(navigationBars = true) {
margin(vertical = true, horizontal = true)
}
}
binding.toolbar.setOnClickListener {
viewModel.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = Constants.SHORTCUT_MANGA
putExtra(Constants.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
},
)
}
}
binding.pageNumber.setComposeContent {
val state by viewModel.state.collectAsState()
val showPageNumber by viewModel.readerPreferences.showPageNumber().collectAsState()
@ -400,6 +328,9 @@ class ReaderActivity : BaseActivity() {
)
}
val isHttpSource = viewModel.getSource() is HttpSource
val isFullscreen by readerPreferences.fullscreen().collectAsState()
val cropBorderPaged by readerPreferences.cropBorders().collectAsState()
val cropBorderWebtoon by readerPreferences.cropBordersWebtoon().collectAsState()
val isPagerType = ReadingModeType.isPagerType(viewModel.getMangaReadingMode())
@ -407,8 +338,18 @@ class ReaderActivity : BaseActivity() {
ReaderAppBars(
visible = state.menuVisible,
viewer = state.viewer,
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 },
onShare = ::shareChapter.takeIf { isHttpSource },
viewer = state.viewer,
onNextChapter = ::loadNextChapter,
enabledNext = state.viewerChapters?.nextChapter != null,
onPreviousChapter = ::loadPreviousChapter,
@ -435,15 +376,8 @@ class ReaderActivity : BaseActivity() {
cropEnabled = cropEnabled,
onClickCropBorder = {
val enabled = viewModel.toggleCropBorders()
menuToggleToast?.cancel()
menuToggleToast = toast(
if (enabled) {
R.string.on
} else {
R.string.off
},
)
menuToggleToast = toast(if (enabled) R.string.on else R.string.off)
},
onClickSettings = viewModel::openSettingsDialog,
)
@ -507,13 +441,9 @@ class ReaderActivity : BaseActivity() {
}
}
val toolbarBackground = (binding.toolbar.background as MaterialShapeDrawable).apply {
elevation = resources.getDimension(R.dimen.m3_sys_elevation_level2)
alpha = if (isNightMode()) 230 else 242 // 90% dark 95% light
}
val toolbarColor = ColorUtils.setAlphaComponent(
toolbarBackground.resolvedTintColor,
toolbarBackground.alpha,
SurfaceColors.SURFACE_2.getColor(this),
if (isNightMode()) 230 else 242, // 90% dark 95% light
)
window.statusBarColor = toolbarColor
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
@ -531,35 +461,12 @@ class ReaderActivity : BaseActivity() {
viewModel.showMenus(visible)
if (visible) {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
binding.readerMenu.isVisible = true
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.enter_from_top)
toolbarAnimation.applySystemAnimatorScale(this)
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationStart(animation: Animation) {
// Fix status bar being translucent the first time it's opened.
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
},
)
binding.toolbar.startAnimation(toolbarAnimation)
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
}
val toolbarAnimation = AnimationUtils.loadAnimation(this, R.anim.exit_to_top)
toolbarAnimation.applySystemAnimatorScale(this)
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationEnd(animation: Animation) {
binding.readerMenu.isVisible = false
}
},
)
binding.toolbar.startAnimation(toolbarAnimation)
}
}
@ -593,14 +500,24 @@ class ReaderActivity : BaseActivity() {
showReadingModeToast(viewModel.getMangaReadingMode())
}
supportActionBar?.title = manga.title
loadingIndicator = ReaderProgressIndicator(this)
binding.readerContainer.addView(loadingIndicator)
startPostponedEnterTransition()
}
private fun openMangaScreen() {
viewModel.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = Constants.SHORTCUT_MANGA
putExtra(Constants.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
},
)
}
}
private fun openChapterInWebView() {
val manga = viewModel.manga ?: return
val source = viewModel.getSource() ?: return
@ -610,6 +527,13 @@ class ReaderActivity : BaseActivity() {
}
}
private fun shareChapter() {
assistUrl?.let {
val intent = it.toUri().toShareIntent(this, type = "text/plain")
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
}
private fun showReadingModeToast(mode: Int) {
try {
readingModeToast?.cancel()
@ -629,15 +553,6 @@ class ReaderActivity : BaseActivity() {
binding.readerContainer.removeView(loadingIndicator)
viewModel.state.value.viewer?.setChapters(viewerChapters)
binding.toolbar.subtitle = viewerChapters.currChapter.chapter.name
ToolbarUtils.getSubtitleTextView(binding.toolbar)?.let {
it.ellipsize = TextUtils.TruncateAt.MARQUEE
it.isSelected = true
}
// Invalidate menu to show proper chapter bookmark state
invalidateOptionsMenu()
lifecycleScope.launchIO {
viewModel.getChapterUrl()?.let { url ->
assistUrl = url
@ -675,7 +590,7 @@ class ReaderActivity : BaseActivity() {
*/
private fun moveToPageIndex(index: Int) {
val viewer = viewModel.state.value.viewer ?: return
val currentChapter = viewModel.getCurrentChapter() ?: return
val currentChapter = viewModel.state.value.currentChapter ?: return
val page = currentChapter.pages?.getOrNull(index) ?: return
viewer.moveToPage(page)
}

View file

@ -298,7 +298,10 @@ class ReaderViewModel @JvmOverloads constructor(
it.viewerChapters?.unref()
chapterToDownload = cancelQueuedDownloads(newChapters.currChapter)
it.copy(viewerChapters = newChapters)
it.copy(
viewerChapters = newChapters,
bookmarked = newChapters.currChapter.chapter.bookmark,
)
}
}
return newChapters
@ -567,8 +570,8 @@ class ReaderViewModel @JvmOverloads constructor(
/**
* Returns the currently active chapter.
*/
fun getCurrentChapter(): ReaderChapter? {
return state.value.viewerChapters?.currChapter
private fun getCurrentChapter(): ReaderChapter? {
return state.value.currentChapter
}
fun getSource() = manga?.source?.let { sourceManager.getOrStub(it) } as? HttpSource
@ -588,9 +591,11 @@ class ReaderViewModel @JvmOverloads constructor(
/**
* Bookmarks the currently active chapter.
*/
fun bookmarkCurrentChapter(bookmarked: Boolean) {
fun toggleChapterBookmark() {
val chapter = getCurrentChapter()?.chapter ?: return
chapter.bookmark = bookmarked // Otherwise the bookmark icon doesn't update
val bookmarked = !chapter.bookmark
chapter.bookmark = bookmarked
viewModelScope.launchNonCancellable {
updateChapter.await(
ChapterUpdate(
@ -599,6 +604,12 @@ class ReaderViewModel @JvmOverloads constructor(
),
)
}
mutableState.update {
it.copy(
bookmarked = bookmarked,
)
}
}
/**
@ -873,6 +884,7 @@ class ReaderViewModel @JvmOverloads constructor(
data class State(
val manga: Manga? = null,
val viewerChapters: ViewerChapters? = null,
val bookmarked: Boolean = false,
val isLoadingAdjacentChapter: Boolean = false,
val currentPage: Int = -1,
@ -883,8 +895,11 @@ class ReaderViewModel @JvmOverloads constructor(
val dialog: Dialog? = null,
val menuVisible: Boolean = false,
) {
val currentChapter: ReaderChapter?
get() = viewerChapters?.currChapter
val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1
get() = currentChapter?.pages?.size ?: -1
}
sealed interface Dialog {

View file

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.widget.listener
import android.view.animation.Animation
open class SimpleAnimationListener : Animation.AnimationListener {
override fun onAnimationRepeat(animation: Animation) {}
override fun onAnimationEnd(animation: Animation) {}
override fun onAnimationStart(animation: Animation) {}
}

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="-100%"
android:toYDelta="0%" />
</set>

View file

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:shareInterpolator="false">
<translate
android:duration="200"
android:fromYDelta="0%"
android:toYDelta="-100%" />
</set>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17,3H7c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3V5c0,-1.1 -0.9,-2 -2,-2z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000"
android:pathData="M17,3L7,3c-1.1,0 -1.99,0.9 -1.99,2L5,21l7,-3 7,3L19,5c0,-1.1 -0.9,-2 -2,-2zM17,18l-5,-2.18L7,18L7,5h10v13z" />
</vector>

View file

@ -1,5 +1,4 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
@ -42,22 +41,6 @@
android:layout_height="match_parent"
android:visibility="gone" />
<FrameLayout
android:id="@+id/reader_menu"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:theme="?attr/actionBarTheme"
android:visibility="invisible"
tools:visibility="visible">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="?attr/actionBarSize" />
</FrameLayout>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"

View file

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_bookmark"
android:icon="@drawable/ic_bookmark_border_24dp"
android:title="@string/action_bookmark"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_remove_bookmark"
android:icon="@drawable/ic_bookmark_24dp"
android:title="@string/action_remove_bookmark"
app:iconTint="?attr/colorOnSurface"
app:showAsAction="ifRoom" />
<item
android:id="@+id/action_open_in_web_view"
android:title="@string/action_open_in_web_view"
app:showAsAction="never" />
<item
android:id="@+id/action_share"
android:title="@string/action_share"
app:showAsAction="never" />
</menu>