mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	Convert cover dialog view to compose (#7346)
This commit is contained in:
		@@ -18,6 +18,10 @@ class MangaRepositoryImpl(
 | 
			
		||||
        return handler.awaitOne { mangasQueries.getMangaById(id, mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun subscribeMangaById(id: Long): Flow<Manga> {
 | 
			
		||||
        return handler.subscribeToOne { mangasQueries.getMangaById(id, mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>> {
 | 
			
		||||
        return handler.subscribeToList { mangasQueries.getFavoriteBySourceId(sourceId, mangaMapper) }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -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<Manga> {
 | 
			
		||||
        return mangaRepository.subscribeMangaById(id)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ interface MangaRepository {
 | 
			
		||||
 | 
			
		||||
    suspend fun getMangaById(id: Long): Manga
 | 
			
		||||
 | 
			
		||||
    suspend fun subscribeMangaById(id: Long): Flow<Manga>
 | 
			
		||||
 | 
			
		||||
    fun getFavoritesBySourceId(sourceId: Long): Flow<List<Manga>>
 | 
			
		||||
 | 
			
		||||
    suspend fun getDuplicateLibraryManga(title: String, sourceId: Long): Manga?
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										296
									
								
								app/src/main/java/eu/kanade/presentation/components/Scaffold.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										296
									
								
								app/src/main/java/eu/kanade/presentation/components/Scaffold.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * <a href="https://material.io/design/layout/understanding-layout.html" class="external" target="_blank">Material Design layout</a>.
 | 
			
		||||
 *
 | 
			
		||||
 * 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<FabPlacement?> { 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 }
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
package eu.kanade.presentation.manga
 | 
			
		||||
 | 
			
		||||
enum class EditCoverAction {
 | 
			
		||||
    EDIT,
 | 
			
		||||
    DELETE,
 | 
			
		||||
}
 | 
			
		||||
@@ -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(),
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								app/src/main/java/eu/kanade/presentation/util/Modifier.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								app/src/main/java/eu/kanade/presentation/util/Modifier.kt
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -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<P : Presenter<*>>(bundle: Bundle? = null) :
 | 
			
		||||
    NucleusController<ComposeControllerBinding, P>(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<P : BasePresenter<*>>(bundle: Bundle?
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface FullComposeContentController {
 | 
			
		||||
    @Composable fun ComposeContent()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ComposeContentController {
 | 
			
		||||
    @Composable fun ComposeContent(nestedScrollInterop: NestedScrollConnection)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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())
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /**
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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<MangaFullCoverDialog.Presenter> {
 | 
			
		||||
 | 
			
		||||
    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<DatabaseHelper>()
 | 
			
		||||
        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<MangaFullCoverDialog>() {
 | 
			
		||||
 | 
			
		||||
        private var presenterScope: CoroutineScope = MainScope()
 | 
			
		||||
 | 
			
		||||
        private val _mangaFlow = MutableStateFlow<Manga?>(null)
 | 
			
		||||
        val manga = _mangaFlow.asStateFlow()
 | 
			
		||||
 | 
			
		||||
        private val imageSaver by injectLazy<ImageSaver>()
 | 
			
		||||
        private val coverCache by injectLazy<CoverCache>()
 | 
			
		||||
        private val updateManga by injectLazy<UpdateManga>()
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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.
 | 
			
		||||
         *
 | 
			
		||||
 
 | 
			
		||||
@@ -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()
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user