diff --git a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt index 945b800b22..ddd5927734 100644 --- a/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/manga/MangaRepositoryImpl.kt @@ -18,6 +18,10 @@ class MangaRepositoryImpl( return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) } } + override suspend fun subscribeMangaById(id: Long): Flow { + return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) } + } + override fun getFavoritesBySourceId(sourceId: Long): Flow> { return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) } } diff --git a/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt b/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt index 9513c0c471..a641fceb93 100644 --- a/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt +++ b/app/src/main/java/eu/kanade/domain/manga/interactor/GetMangaById.kt @@ -3,6 +3,7 @@ package eu.kanade.domain.manga.interactor import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.repository.MangaRepository import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.flow.Flow import logcat.LogPriority class GetMangaById( @@ -17,4 +18,8 @@ class GetMangaById( null } } + + suspend fun subscribe(id: Long): Flow { + return mangaRepository.subscribeMangaById(id) + } } diff --git a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt index 98f897b322..e0ec74b0b4 100644 --- a/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt +++ b/app/src/main/java/eu/kanade/domain/manga/repository/MangaRepository.kt @@ -8,6 +8,8 @@ interface MangaRepository { suspend fun getMangaById(id: Long): Manga + suspend fun subscribeMangaById(id: Long): Flow + fun getFavoritesBySourceId(sourceId: Long): Flow> suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga? diff --git a/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt b/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt new file mode 100644 index 0000000000..e534801099 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/Scaffold.kt @@ -0,0 +1,296 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.SmallTopAppBar +import androidx.compose.material3.Snackbar +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Surface +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.SubcomposeLayout +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp + +/** + * Material Design layout. + * + * Scaffold implements the basic material design visual layout structure. + * + * This component provides API to put together several material components to construct your + * screen, by ensuring proper layout strategy for them and collecting necessary data so these + * components will work together correctly. + * + * Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]: + * + * @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar + * + * To show a [Snackbar], use [SnackbarHostState.showSnackbar]. + * + * @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar + * + * Tachiyomi changes: + * * Remove height constraint for expanded app bar + * * Also take account of fab height when providing inner padding + * + * @param modifier the [Modifier] to be applied to this scaffold + * @param topBar top app bar of the screen, typically a [SmallTopAppBar] + * @param bottomBar bottom bar of the screen, typically a [NavigationBar] + * @param snackbarHost component to host [Snackbar]s that are pushed to be shown via + * [SnackbarHostState.showSnackbar], typically a [SnackbarHost] + * @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton] + * @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition]. + * @param containerColor the color used for the background of this scaffold. Use [Color.Transparent] + * to have no color. + * @param contentColor the preferred color for content inside this scaffold. Defaults to either the + * matching content color for [containerColor], or to the current [LocalContentColor] if + * [containerColor] is not a color from the theme. + * @param content content of the screen. The lambda receives a [PaddingValues] that should be + * applied to the content root via [Modifier.padding] to properly offset top and bottom bars. If + * using [Modifier.verticalScroll], apply this modifier to the child of the scroll, and not on + * the scroll itself. + */ +@ExperimentalMaterial3Api +@Composable +fun Scaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + snackbarHost: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + floatingActionButtonPosition: FabPosition = FabPosition.End, + containerColor: Color = MaterialTheme.colorScheme.background, + contentColor: Color = contentColorFor(containerColor), + content: @Composable (PaddingValues) -> Unit, +) { + Surface(modifier = modifier, color = containerColor, contentColor = contentColor) { + ScaffoldLayout( + fabPosition = floatingActionButtonPosition, + topBar = topBar, + bottomBar = bottomBar, + content = content, + snackbar = snackbarHost, + fab = floatingActionButton, + ) + } +} + +/** + * Layout for a [Scaffold]'s content. + * + * @param fabPosition [FabPosition] for the FAB (if present) + * @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar] + * @param content the main 'body' of the [Scaffold] + * @param snackbar the [Snackbar] displayed on top of the [content] + * @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar] + * and above the [bottomBar] + * @param bottomBar the content to place at the bottom of the [Scaffold], on top of the + * [content], typically a [NavigationBar]. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ScaffoldLayout( + fabPosition: FabPosition, + topBar: @Composable () -> Unit, + content: @Composable (PaddingValues) -> Unit, + snackbar: @Composable () -> Unit, + fab: @Composable () -> Unit, + bottomBar: @Composable () -> Unit, + +) { + SubcomposeLayout { constraints -> + val layoutWidth = constraints.maxWidth + val layoutHeight = constraints.maxHeight + + val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0) + + /** + * Tachiyomi: Remove height constraint for expanded app bar + */ + val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity) + + layout(layoutWidth, layoutHeight) { + val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).map { + it.measure(topBarConstraints) + } + + val topBarHeight = topBarPlaceables.maxByOrNull { it.height }?.height ?: 0 + + val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).map { + it.measure(looseConstraints) + } + + val snackbarHeight = snackbarPlaceables.maxByOrNull { it.height }?.height ?: 0 + val snackbarWidth = snackbarPlaceables.maxByOrNull { it.width }?.width ?: 0 + + val fabPlaceables = + subcompose(ScaffoldLayoutContent.Fab, fab).mapNotNull { measurable -> + measurable.measure(looseConstraints).takeIf { it.height != 0 && it.width != 0 } + } + + val fabHeight = fabPlaceables.maxByOrNull { it.height }?.height ?: 0 + + val fabPlacement = if (fabPlaceables.isNotEmpty()) { + val fabWidth = fabPlaceables.maxByOrNull { it.width }!!.width + // FAB distance from the left of the layout, taking into account LTR / RTL + val fabLeftOffset = if (fabPosition == FabPosition.End) { + if (layoutDirection == LayoutDirection.Ltr) { + layoutWidth - FabSpacing.roundToPx() - fabWidth + } else { + FabSpacing.roundToPx() + } + } else { + (layoutWidth - fabWidth) / 2 + } + + FabPlacement( + left = fabLeftOffset, + width = fabWidth, + height = fabHeight, + ) + } else { + null + } + + val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) { + CompositionLocalProvider( + LocalFabPlacement provides fabPlacement, + content = bottomBar, + ) + }.map { it.measure(looseConstraints) } + + val bottomBarHeight = bottomBarPlaceables.maxByOrNull { it.height }?.height ?: 0 + val fabOffsetFromBottom = fabPlacement?.let { + if (bottomBarHeight == 0) { + it.height + FabSpacing.roundToPx() + } else { + // Total height is the bottom bar height + the FAB height + the padding + // between the FAB and bottom bar + bottomBarHeight + it.height + FabSpacing.roundToPx() + } + } + + val snackbarOffsetFromBottom = if (snackbarHeight != 0) { + snackbarHeight + (fabOffsetFromBottom ?: bottomBarHeight) + } else { + 0 + } + + /** + * Tachiyomi: Also take account of fab height when providing inner padding + */ + val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) { + val innerPadding = PaddingValues( + top = topBarHeight.toDp(), + bottom = bottomBarHeight.toDp() + fabHeight.toDp(), + ) + content(innerPadding) + }.map { it.measure(looseConstraints) } + + // Placing to control drawing order to match default elevation of each placeable + + bodyContentPlaceables.forEach { + it.place(0, 0) + } + topBarPlaceables.forEach { + it.place(0, 0) + } + snackbarPlaceables.forEach { + it.place( + (layoutWidth - snackbarWidth) / 2, + layoutHeight - snackbarOffsetFromBottom, + ) + } + // The bottom bar is always at the bottom of the layout + bottomBarPlaceables.forEach { + it.place(0, layoutHeight - bottomBarHeight) + } + // Explicitly not using placeRelative here as `leftOffset` already accounts for RTL + fabPlacement?.let { placement -> + fabPlaceables.forEach { + it.place(placement.left, layoutHeight - fabOffsetFromBottom!!) + } + } + } + } +} + +/** + * The possible positions for a [FloatingActionButton] attached to a [Scaffold]. + */ +@ExperimentalMaterial3Api +@JvmInline +value class FabPosition internal constructor(@Suppress("unused") private val value: Int) { + companion object { + /** + * Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it + * exists) + */ + val Center = FabPosition(0) + + /** + * Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it + * exists) + */ + val End = FabPosition(1) + } + + override fun toString(): String { + return when (this) { + Center -> "FabPosition.Center" + else -> "FabPosition.End" + } + } +} + +/** + * Placement information for a [FloatingActionButton] inside a [Scaffold]. + * + * @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL + * support + * @property width the width of the FAB + * @property height the height of the FAB + */ +@Immutable +internal class FabPlacement( + val left: Int, + val width: Int, + val height: Int, +) + +/** + * CompositionLocal containing a [FabPlacement] that is used to calculate the FAB bottom offset. + */ +internal val LocalFabPlacement = staticCompositionLocalOf { null } + +// FAB spacing above the bottom bar / bottom of the Scaffold +private val FabSpacing = 16.dp + +private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar } diff --git a/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt new file mode 100644 index 0000000000..ec8eebc76b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/MangaScreenConstants.kt @@ -0,0 +1,6 @@ +package eu.kanade.presentation.manga + +enum class EditCoverAction { + EDIT, + DELETE, +} diff --git a/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt new file mode 100644 index 0000000000..aee0bc481c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/manga/components/MangaCoverDialog.kt @@ -0,0 +1,163 @@ +package eu.kanade.presentation.manga.components + +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import android.os.Build +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Save +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView +import androidx.core.view.updatePadding +import coil.imageLoader +import coil.request.ImageRequest +import coil.size.Size +import eu.kanade.domain.manga.model.Manga +import eu.kanade.presentation.components.DropdownMenu +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.manga.EditCoverAction +import eu.kanade.presentation.util.clickableNoIndication +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView + +@Composable +fun MangaCoverDialog( + coverDataProvider: () -> Manga, + isCustomCover: Boolean, + onShareClick: () -> Unit, + onSaveClick: () -> Unit, + onEditClick: ((EditCoverAction) -> Unit)?, + onDismissRequest: () -> Unit, +) { + Scaffold( + bottomBar = { + Row( + modifier = Modifier + .fillMaxWidth() + .background(color = MaterialTheme.colorScheme.background.copy(alpha = 0.9f)) + .padding(horizontal = 4.dp, vertical = 4.dp) + .navigationBarsPadding(), + ) { + IconButton(onClick = onDismissRequest) { + Icon( + imageVector = Icons.Default.Close, + contentDescription = stringResource(id = R.string.action_close), + ) + } + Spacer(modifier = Modifier.weight(1f)) + IconButton(onClick = onShareClick) { + Icon( + imageVector = Icons.Default.Share, + contentDescription = stringResource(id = R.string.action_share), + ) + } + IconButton(onClick = onSaveClick) { + Icon( + imageVector = Icons.Default.Save, + contentDescription = stringResource(id = R.string.action_save), + ) + } + if (onEditClick != null) { + Box { + val (expanded, onExpand) = remember { mutableStateOf(false) } + IconButton( + onClick = { if (isCustomCover) onExpand(true) else onEditClick(EditCoverAction.EDIT) }, + ) { + Icon( + imageVector = Icons.Default.Edit, + contentDescription = stringResource(id = R.string.action_edit_cover), + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { onExpand(false) }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_edit)) }, + onClick = { + onEditClick(EditCoverAction.EDIT) + onExpand(false) + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_delete)) }, + onClick = { + onEditClick(EditCoverAction.DELETE) + onExpand(false) + }, + ) + } + } + } + } + }, + ) { contentPadding -> + val statusBarPaddingPx = WindowInsets.systemBars.getTop(LocalDensity.current) + val bottomPaddingPx = with(LocalDensity.current) { contentPadding.calculateBottomPadding().roundToPx() } + Box( + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.background) + .clickableNoIndication(onClick = onDismissRequest), + ) { + AndroidView( + factory = { + ReaderPageImageView(it).apply { + onViewClicked = onDismissRequest + clipToPadding = false + clipChildren = false + } + }, + update = { view -> + val request = ImageRequest.Builder(view.context) + .data(coverDataProvider()) + .size(Size.ORIGINAL) + .target { drawable -> + // Copy bitmap in case it came from memory cache + // Because SSIV needs to thoroughly read the image + val copy = (drawable as? BitmapDrawable)?.let { + val config = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Bitmap.Config.HARDWARE + } else { + Bitmap.Config.ARGB_8888 + } + BitmapDrawable( + view.context.resources, + it.bitmap.copy(config, false), + ) + } ?: drawable + view.setImage(copy, ReaderPageImageView.Config(zoomDuration = 500)) + } + .build() + view.context.imageLoader.enqueue(request) + + view.updatePadding(top = statusBarPaddingPx, bottom = bottomPaddingPx) + }, + modifier = Modifier.fillMaxSize(), + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/util/Modifier.kt b/app/src/main/java/eu/kanade/presentation/util/Modifier.kt new file mode 100644 index 0000000000..3eefaa4618 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/Modifier.kt @@ -0,0 +1,76 @@ +package eu.kanade.presentation.util + +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.material3.LocalMinimumTouchTargetEnforcement +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.layout.LayoutModifier +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.layout.MeasureResult +import androidx.compose.ui.layout.MeasureScope +import androidx.compose.ui.platform.LocalViewConfiguration +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.DpSize +import kotlin.math.roundToInt + +fun Modifier.secondaryItemAlpha(): Modifier = this.alpha(.78f) + +fun Modifier.clickableNoIndication( + onLongClick: (() -> Unit)? = null, + onClick: () -> Unit, +): Modifier = composed { + this.combinedClickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onLongClick = onLongClick, + onClick = onClick, + ) +} + +@Suppress("ModifierInspectorInfo") +fun Modifier.minimumTouchTargetSize(): Modifier = composed( + inspectorInfo = debugInspectorInfo { + name = "minimumTouchTargetSize" + properties["README"] = "Adds outer padding to measure at least 48.dp (default) in " + + "size to disambiguate touch interactions if the element would measure smaller" + }, +) { + if (LocalMinimumTouchTargetEnforcement.current) { + val size = LocalViewConfiguration.current.minimumTouchTargetSize + MinimumTouchTargetModifier(size) + } else { + Modifier + } +} + +private class MinimumTouchTargetModifier(val size: DpSize) : LayoutModifier { + override fun MeasureScope.measure( + measurable: Measurable, + constraints: Constraints, + ): MeasureResult { + val placeable = measurable.measure(constraints) + + // Be at least as big as the minimum dimension in both dimensions + val width = maxOf(placeable.width, size.width.roundToPx()) + val height = maxOf(placeable.height, size.height.roundToPx()) + + return layout(width, height) { + val centerX = ((width - placeable.width) / 2f).roundToInt() + val centerY = ((height - placeable.height) / 2f).roundToInt() + placeable.place(centerX, centerY) + } + } + + override fun equals(other: Any?): Boolean { + val otherModifier = other as? MinimumTouchTargetModifier ?: return false + return size == otherModifier.size + } + + override fun hashCode(): Int { + return size.hashCode() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt index 15f562bec8..ac7ce1080e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt @@ -16,6 +16,30 @@ import eu.kanade.tachiyomi.databinding.ComposeControllerBinding import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import nucleus.presenter.Presenter +abstract class FullComposeController

>(bundle: Bundle? = null) : + NucleusController(bundle), + FullComposeContentController { + + override fun createBinding(inflater: LayoutInflater) = + ComposeControllerBinding.inflate(inflater) + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + binding.root.apply { + consumeWindowInsets = false + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + setContent { + TachiyomiTheme { + CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { + ComposeContent() + } + } + } + } + } +} + /** * Compose controller with a Nucleus presenter. */ @@ -97,6 +121,10 @@ abstract class SearchableComposeController

>(bundle: Bundle? } } +interface FullComposeContentController { + @Composable fun ComposeContent() +} + interface ComposeContentController { @Composable fun ComposeContent(nestedScrollInterop: NestedScrollConnection) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 4764b9a927..6097ec9365 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -43,6 +43,7 @@ import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.ui.base.activity.BaseActivity import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.FabController +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController import eu.kanade.tachiyomi.ui.base.controller.NoAppBarElevationController import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.TabbedController @@ -599,6 +600,7 @@ class MainActivity : BaseActivity() { binding.fabLayout.rootFab.hide() } + val isFullComposeController = internalTo is FullComposeController<*> if (!isTablet()) { // Save lift state if (isPush) { @@ -622,8 +624,16 @@ class MainActivity : BaseActivity() { binding.root.isLiftAppBarOnScroll = internalTo !is NoAppBarElevationController - binding.appbar.isTransparentWhenNotLifted = internalTo is MangaController - binding.controllerContainer.overlapHeader = internalTo is MangaController + binding.appbar.isVisible = !isFullComposeController + binding.controllerContainer.enableScrollingBehavior(!isFullComposeController) + + // TODO: Remove when MangaController is full compose + if (!isFullComposeController) { + binding.appbar.isTransparentWhenNotLifted = internalTo is MangaController + binding.controllerContainer.overlapHeader = internalTo is MangaController + } + } else { + binding.appbar.isVisible = !isFullComposeController } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index 48812da25d..f6ea48b3ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -1,11 +1,7 @@ package eu.kanade.tachiyomi.ui.manga -import android.app.Activity import android.app.ActivityOptions -import android.content.Context import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.BitmapDrawable import android.os.Bundle import android.view.LayoutInflater import android.view.Menu @@ -24,8 +20,6 @@ import androidx.recyclerview.widget.ConcatAdapter import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import coil.imageLoader -import coil.request.ImageRequest import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton @@ -45,8 +39,6 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.saver.Image -import eu.kanade.tachiyomi.data.saver.Location import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch @@ -61,12 +53,12 @@ import eu.kanade.tachiyomi.ui.base.controller.FabController import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.getMainAppBarHeight import eu.kanade.tachiyomi.ui.base.controller.pushController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog -import eu.kanade.tachiyomi.ui.library.ChangeMangaCoverDialog import eu.kanade.tachiyomi.ui.library.LibraryController import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem @@ -85,12 +77,9 @@ import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.recent.history.HistoryController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.webview.WebViewActivity -import eu.kanade.tachiyomi.util.hasCustomCover import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.logcat -import eu.kanade.tachiyomi.util.system.toShareIntent import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.view.shrinkOnScroll import eu.kanade.tachiyomi.util.view.snack @@ -115,7 +104,6 @@ class MangaController : FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemLongClickListener, BaseChaptersAdapter.OnChapterClickListener, - ChangeMangaCoverDialog.Listener, ChangeMangaCategoriesDialog.Listener, DownloadCustomChaptersDialog.Listener, DeleteChaptersDialog.Listener { @@ -724,128 +712,9 @@ class MangaController : } } - /** - * Fetches the cover with Coil, turns it into Bitmap and does something with it (asynchronous) - * @param context The context for building and executing the ImageRequest - * @param coverHandler A function that describes what should be done with the Bitmap - */ - private fun useCoverAsBitmap(context: Context, coverHandler: (Bitmap) -> Unit) { - val req = ImageRequest.Builder(context) - .data(manga) - .target { result -> - val coverBitmap = (result as BitmapDrawable).bitmap - coverHandler(coverBitmap) - } - .build() - context.imageLoader.enqueue(req) - } - fun showFullCoverDialog() { - val manga = manga ?: return - MangaFullCoverDialog(this, manga) - .showDialog(router) - } - - fun shareCover() { - try { - val manga = manga!! - val activity = activity!! - useCoverAsBitmap(activity) { coverBitmap -> - viewScope.launchIO { - val uri = presenter.saveImage( - image = Image.Cover( - bitmap = coverBitmap, - name = manga.title, - location = Location.Cache, - ), - ) - launchUI { - startActivity(uri.toShareIntent(activity)) - } - } - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - activity?.toast(R.string.error_sharing_cover) - } - } - - fun saveCover() { - try { - val manga = manga!! - val activity = activity!! - useCoverAsBitmap(activity) { coverBitmap -> - viewScope.launchIO { - presenter.saveImage( - image = Image.Cover( - bitmap = coverBitmap, - name = manga.title, - location = Location.Pictures.create(), - ), - ) - launchUI { - activity.toast(R.string.cover_saved) - } - } - } - } catch (e: Throwable) { - logcat(LogPriority.ERROR, e) - activity?.toast(R.string.error_saving_cover) - } - } - - fun changeCover() { - val manga = manga ?: return - if (manga.hasCustomCover(coverCache)) { - ChangeMangaCoverDialog(this, manga).showDialog(router) - } else { - openMangaCoverPicker(manga) - } - } - - override fun openMangaCoverPicker(manga: Manga) { - if (manga.favorite) { - val intent = Intent(Intent.ACTION_GET_CONTENT).apply { - type = "image/*" - } - startActivityForResult( - Intent.createChooser( - intent, - resources?.getString(R.string.file_select_cover), - ), - REQUEST_IMAGE_OPEN, - ) - } else { - activity?.toast(R.string.notification_first_add_to_library) - } - - destroyActionModeIfNeeded() - } - - override fun deleteMangaCover(manga: Manga) { - presenter.deleteCustomCover(manga) - mangaInfoAdapter?.notifyItemChanged(0, manga) - destroyActionModeIfNeeded() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == REQUEST_IMAGE_OPEN) { - val dataUri = data?.data - if (dataUri == null || resultCode != Activity.RESULT_OK) return - val activity = activity ?: return - presenter.editCover(activity, dataUri) - } - } - - fun onSetCoverSuccess() { - mangaInfoAdapter?.notifyItemChanged(0, this) - (router.backstack.lastOrNull()?.controller as? MangaFullCoverDialog)?.setImage(manga) - activity?.toast(R.string.cover_updated) - } - - fun onSetCoverError(error: Throwable) { - activity?.toast(R.string.notification_cover_update_failed) - logcat(LogPriority.ERROR, error) + val mangaId = manga?.id ?: return + router.pushController(MangaFullCoverDialog(mangaId).withFadeTransaction()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 6119648ca1..5768f68465 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -1,7 +1,5 @@ package eu.kanade.tachiyomi.ui.manga -import android.content.Context -import android.net.Uri import android.os.Bundle import com.jakewharton.rxrelay.PublishRelay import eu.kanade.domain.category.interactor.GetCategories @@ -16,13 +14,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.models.toDomainManga import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.preference.PreferencesHelper -import eu.kanade.tachiyomi.data.saver.Image -import eu.kanade.tachiyomi.data.saver.ImageSaver import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService @@ -36,17 +31,14 @@ import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.getChapterSort import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay -import eu.kanade.tachiyomi.util.editCover import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.lang.launchIO -import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.updateCoverLastModified import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import kotlinx.coroutines.Job import kotlinx.coroutines.async @@ -61,7 +53,6 @@ import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import uy.kohesive.injekt.injectLazy import java.util.Date import eu.kanade.domain.category.model.Category as DomainCategory @@ -115,8 +106,6 @@ class MangaPresenter( private val loggedServices by lazy { trackManager.services.filter { it.isLogged } } - private val imageSaver: ImageSaver by injectLazy() - private var trackSubscription: Subscription? = null private var searchTrackerJob: Job? = null private var refreshTrackersJob: Job? = null @@ -295,49 +284,6 @@ class MangaPresenter( moveMangaToCategories(manga, listOfNotNull(category)) } - /** - * Save manga cover Bitmap to picture or temporary share directory. - * - * @param image the image with specified location - * @return flow Flow which emits the Uri which specifies where the image is saved when - */ - fun saveImage(image: Image): Uri { - return imageSaver.save(image) - } - - /** - * Update cover with local file. - * - * @param context Context. - * @param data uri of the cover resource. - */ - fun editCover(context: Context, data: Uri) { - presenterScope.launchIO { - context.contentResolver.openInputStream(data)?.use { - try { - val result = manga.toDomainManga()!!.editCover(context, it) - launchUI { if (result) view?.onSetCoverSuccess() } - } catch (e: Exception) { - launchUI { view?.onSetCoverError(e) } - } - } - } - } - - fun deleteCustomCover(manga: Manga) { - Observable - .fromCallable { - coverCache.deleteCustomCover(manga.id) - manga.updateCoverLastModified(db) - } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeFirst( - { view, _ -> view.onSetCoverSuccess() }, - { view, e -> view.onSetCoverError(e) }, - ) - } - // Manga info - end // Chapters list - start diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt index 3ffe68d13e..6d6452d5e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaFullCoverDialog.kt @@ -1,118 +1,255 @@ package eu.kanade.tachiyomi.ui.manga.info -import android.app.Dialog -import android.graphics.drawable.ColorDrawable +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.graphics.drawable.BitmapDrawable +import android.net.Uri import android.os.Bundle -import android.util.TypedValue -import android.view.View -import androidx.core.graphics.ColorUtils +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier import androidx.core.os.bundleOf -import androidx.core.view.WindowCompat import coil.imageLoader -import coil.request.Disposable import coil.request.ImageRequest -import dev.chrisbanes.insetter.applyInsetter +import coil.size.Size +import eu.kanade.domain.manga.interactor.GetMangaById +import eu.kanade.domain.manga.interactor.UpdateManga +import eu.kanade.domain.manga.model.Manga +import eu.kanade.domain.manga.model.hasCustomCover +import eu.kanade.presentation.manga.EditCoverAction +import eu.kanade.presentation.manga.components.MangaCoverDialog import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.MangaFullCoverDialogBinding -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.manga.MangaController -import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView -import eu.kanade.tachiyomi.util.view.setNavigationBarTransparentCompat -import eu.kanade.tachiyomi.widget.TachiyomiFullscreenDialog +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.saver.Image +import eu.kanade.tachiyomi.data.saver.ImageSaver +import eu.kanade.tachiyomi.data.saver.Location +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController +import eu.kanade.tachiyomi.util.editCover +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.system.logcat +import eu.kanade.tachiyomi.util.system.toShareIntent +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy -class MangaFullCoverDialog : DialogController { +class MangaFullCoverDialog : FullComposeController { - private var manga: Manga? = null - - private var binding: MangaFullCoverDialogBinding? = null - - private var disposable: Disposable? = null - - private val mangaController - get() = targetController as MangaController? - - constructor(targetController: MangaController, manga: Manga) : super(bundleOf("mangaId" to manga.id)) { - this.targetController = targetController - this.manga = manga - } + private val mangaId: Long @Suppress("unused") - constructor(bundle: Bundle) : super(bundle) { - val db = Injekt.get() - manga = db.getManga(bundle.getLong("mangaId")).executeAsBlocking() + constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA)) + + constructor( + mangaId: Long, + ) : super(bundleOf(MANGA_EXTRA to mangaId)) { + this.mangaId = mangaId } - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - binding = MangaFullCoverDialogBinding.inflate(activity!!.layoutInflater) + override fun createPresenter() = Presenter(mangaId) - binding?.toolbar?.apply { - setNavigationOnClickListener { dialog?.dismiss() } - setOnMenuItemClickListener { - when (it.itemId) { - R.id.action_share_cover -> mangaController?.shareCover() - R.id.action_save_cover -> mangaController?.saveCover() - R.id.action_edit_cover -> mangaController?.changeCover() + @Composable + override fun ComposeContent() { + val manga = presenter.manga.collectAsState().value + if (manga != null) { + MangaCoverDialog( + coverDataProvider = { manga }, + isCustomCover = remember(manga) { manga.hasCustomCover() }, + onShareClick = this::shareCover, + onSaveClick = this::saveCover, + onEditClick = this::changeCover, + onDismissRequest = router::popCurrentController, + ) + } else { + Box( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.background), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } + } + } + + private fun shareCover() { + val activity = activity ?: return + viewScope.launchIO { + try { + val uri = presenter.saveCover(activity, temp = true) ?: return@launchIO + launchUI { + startActivity(uri.toShareIntent(activity)) + } + } catch (e: Throwable) { + launchUI { + logcat(LogPriority.ERROR, e) + activity.toast(R.string.error_saving_cover) } - true } - menu?.findItem(R.id.action_edit_cover)?.isVisible = manga?.favorite ?: false - } - - setImage(manga) - - binding?.appbar?.applyInsetter { - type(navigationBars = true, statusBars = true) { - padding(left = true, top = true, right = true) - } - } - - binding?.container?.onViewClicked = { dialog?.dismiss() } - binding?.container?.applyInsetter { - type(navigationBars = true) { - padding(bottom = true) - } - } - - return TachiyomiFullscreenDialog(activity!!, binding!!.root).apply { - val typedValue = TypedValue() - val theme = context.theme - theme.resolveAttribute(android.R.attr.colorBackground, typedValue, true) - window?.setBackgroundDrawable(ColorDrawable(ColorUtils.setAlphaComponent(typedValue.data, 230))) } } - override fun onAttach(view: View) { - super.onAttach(view) - dialog?.window?.let { window -> - window.setNavigationBarTransparentCompat(window.context) - WindowCompat.setDecorFitsSystemWindows(window, false) + private fun saveCover() { + val activity = activity ?: return + viewScope.launchIO { + try { + presenter.saveCover(activity, temp = false) + launchUI { + activity.toast(R.string.cover_saved) + } + } catch (e: Throwable) { + launchUI { + logcat(LogPriority.ERROR, e) + activity.toast(R.string.error_saving_cover) + } + } } } - override fun onDetach(view: View) { - super.onDetach(view) - disposable?.dispose() - disposable = null - } - - fun setImage(manga: Manga?) { - if (manga == null) return - val request = ImageRequest.Builder(applicationContext!!) - .data(manga) - .target { - binding?.container?.setImage( - it, - ReaderPageImageView.Config( - zoomDuration = 500, + private fun changeCover(action: EditCoverAction) { + when (action) { + EditCoverAction.EDIT -> { + val intent = Intent(Intent.ACTION_GET_CONTENT).apply { + type = "image/*" + } + startActivityForResult( + Intent.createChooser( + intent, + resources?.getString(R.string.file_select_cover), ), + REQUEST_IMAGE_OPEN, ) } - .build() + EditCoverAction.DELETE -> presenter.deleteCustomCover() + } + } - disposable = applicationContext?.imageLoader?.enqueue(request) + private fun onSetCoverSuccess() { + activity?.toast(R.string.cover_updated) + } + + private fun onSetCoverError(error: Throwable) { + activity?.toast(R.string.notification_cover_update_failed) + logcat(LogPriority.ERROR, error) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_IMAGE_OPEN) { + val dataUri = data?.data + if (dataUri == null || resultCode != Activity.RESULT_OK) return + val activity = activity ?: return + presenter.editCover(activity, dataUri) + } + } + + class Presenter( + private val mangaId: Long, + private val getMangaById: GetMangaById = Injekt.get(), + ) : nucleus.presenter.Presenter() { + + private var presenterScope: CoroutineScope = MainScope() + + private val _mangaFlow = MutableStateFlow(null) + val manga = _mangaFlow.asStateFlow() + + private val imageSaver by injectLazy() + private val coverCache by injectLazy() + private val updateManga by injectLazy() + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + presenterScope.launchIO { + getMangaById.subscribe(mangaId) + .collect { _mangaFlow.value = it } + } + } + + override fun onDestroy() { + super.onDestroy() + presenterScope.cancel() + } + + /** + * Save manga cover Bitmap to picture or temporary share directory. + * + * @param context The context for building and executing the ImageRequest + * @return the uri to saved file + */ + suspend fun saveCover(context: Context, temp: Boolean): Uri? { + val manga = manga.value ?: return null + val req = ImageRequest.Builder(context) + .data(manga) + .size(Size.ORIGINAL) + .build() + val result = context.imageLoader.execute(req).drawable + + // TODO: Handle animated cover + val bitmap = (result as? BitmapDrawable)?.bitmap ?: return null + return imageSaver.save( + Image.Cover( + bitmap = bitmap, + name = manga.title, + location = if (temp) Location.Cache else Location.Pictures.create(), + ), + ) + } + + /** + * Update cover with local file. + * + * @param context Context. + * @param data uri of the cover resource. + */ + fun editCover(context: Context, data: Uri) { + val manga = manga.value ?: return + presenterScope.launchIO { + context.contentResolver.openInputStream(data)?.use { + val result = try { + manga.editCover(context, it, updateManga, coverCache) + } catch (e: Exception) { + view?.onSetCoverError(e) + false + } + launchUI { if (result) view?.onSetCoverSuccess() } + } + } + } + + fun deleteCustomCover() { + val mangaId = manga.value?.id ?: return + presenterScope.launchIO { + try { + coverCache.deleteCustomCover(mangaId) + updateManga.awaitUpdateCoverLastModified(mangaId) + launchUI { view?.onSetCoverSuccess() } + } catch (e: Exception) { + launchUI { view?.onSetCoverError(e) } + } + } + } + } + + companion object { + private const val MANGA_EXTRA = "mangaId" + + /** + * Key to change the cover of a manga in [onActivityResult]. + */ + private const val REQUEST_IMAGE_OPEN = 101 } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt index e6cfa5a33b..839f2fd794 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoHeaderAdapter.kt @@ -6,7 +6,6 @@ import android.view.ViewGroup import androidx.core.view.isVisible import androidx.core.view.updateLayoutParams import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.dialog.MaterialAlertDialogBuilder import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -191,36 +190,9 @@ class MangaInfoHeaderAdapter( } .launchIn(controller.viewScope) - binding.mangaCover.longClicks() - .onEach { - showCoverOptionsDialog() - } - .launchIn(controller.viewScope) - setMangaInfo() } - private fun showCoverOptionsDialog() { - val options = listOfNotNull( - R.string.action_share, - R.string.action_save, - // Can only edit cover for library manga - if (manga.favorite) R.string.action_edit else null, - ).map(controller.activity!!::getString).toTypedArray() - - MaterialAlertDialogBuilder(controller.activity!!) - .setTitle(R.string.manga_cover) - .setItems(options) { _, item -> - when (item) { - 0 -> controller.shareCover() - 1 -> controller.saveCover() - 2 -> controller.changeCover() - } - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } - /** * Update the view with manga information. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt index 2667b56590..9c8d123e39 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiChangeHandlerFrameLayout.kt @@ -34,5 +34,18 @@ class TachiyomiChangeHandlerFrameLayout( } } + fun enableScrollingBehavior(enable: Boolean) { + (layoutParams as? CoordinatorLayout.LayoutParams)?.behavior = if (enable) { + behavior.apply { + shouldHeaderOverlap = overlapHeader + } + } else null + if (!enable) { + // The behavior doesn't reset translationY when shouldHeaderOverlap is false + translationY = 0F + } + forceLayout() + } + override fun getBehavior() = TachiyomiScrollingViewBehavior() }