mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-03 23:58:55 +01:00 
			
		
		
		
	ChapterDownloadView: Convert to compose (#7354)
This commit is contained in:
		@@ -0,0 +1,146 @@
 | 
			
		||||
package eu.kanade.presentation.components
 | 
			
		||||
 | 
			
		||||
import androidx.compose.animation.core.animateFloatAsState
 | 
			
		||||
import androidx.compose.foundation.layout.Box
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.filled.ArrowDownward
 | 
			
		||||
import androidx.compose.material.icons.filled.CheckCircle
 | 
			
		||||
import androidx.compose.material3.CircularProgressIndicator
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
import androidx.compose.material3.IconButton
 | 
			
		||||
import androidx.compose.material3.LocalMinimumTouchTargetEnforcement
 | 
			
		||||
import androidx.compose.material3.MaterialTheme
 | 
			
		||||
import androidx.compose.material3.ProgressIndicatorDefaults
 | 
			
		||||
import androidx.compose.material3.Text
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.CompositionLocalProvider
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.remember
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.Alignment
 | 
			
		||||
import androidx.compose.ui.Modifier
 | 
			
		||||
import androidx.compose.ui.graphics.Color
 | 
			
		||||
import androidx.compose.ui.res.stringResource
 | 
			
		||||
import androidx.compose.ui.unit.dp
 | 
			
		||||
import eu.kanade.presentation.manga.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.util.secondaryItemAlpha
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
 | 
			
		||||
@Composable
 | 
			
		||||
fun ChapterDownloadIndicator(
 | 
			
		||||
    modifier: Modifier = Modifier,
 | 
			
		||||
    downloadState: Download.State,
 | 
			
		||||
    downloadProgress: Int,
 | 
			
		||||
    onClick: (ChapterDownloadAction) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Box(modifier = modifier, contentAlignment = Alignment.Center) {
 | 
			
		||||
        CompositionLocalProvider(LocalMinimumTouchTargetEnforcement provides false) {
 | 
			
		||||
            val isDownloaded = downloadState == Download.State.DOWNLOADED
 | 
			
		||||
            val isDownloading = downloadState != Download.State.NOT_DOWNLOADED
 | 
			
		||||
            var isMenuExpanded by remember(downloadState) { mutableStateOf(false) }
 | 
			
		||||
            IconButton(
 | 
			
		||||
                onClick = {
 | 
			
		||||
                    if (isDownloaded || isDownloading) {
 | 
			
		||||
                        isMenuExpanded = true
 | 
			
		||||
                    } else {
 | 
			
		||||
                        onClick(ChapterDownloadAction.START)
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
            ) {
 | 
			
		||||
                if (isDownloaded) {
 | 
			
		||||
                    Icon(
 | 
			
		||||
                        imageVector = Icons.Default.CheckCircle,
 | 
			
		||||
                        contentDescription = null,
 | 
			
		||||
                        modifier = Modifier.size(IndicatorSize),
 | 
			
		||||
                        tint = MaterialTheme.colorScheme.onSurfaceVariant,
 | 
			
		||||
                    )
 | 
			
		||||
                    DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
 | 
			
		||||
                        DropdownMenuItem(
 | 
			
		||||
                            text = { Text(text = stringResource(id = R.string.action_delete)) },
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                onClick(ChapterDownloadAction.DELETE)
 | 
			
		||||
                                isMenuExpanded = false
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                } else {
 | 
			
		||||
                    val progressIndicatorModifier = Modifier
 | 
			
		||||
                        .size(IndicatorSize)
 | 
			
		||||
                        .padding(IndicatorStrokeWidth)
 | 
			
		||||
                    val inactiveAlphaModifier = if (!isDownloading) Modifier.secondaryItemAlpha() else Modifier
 | 
			
		||||
                    val arrowModifier = Modifier
 | 
			
		||||
                        .size(IndicatorSize - 7.dp)
 | 
			
		||||
                        .then(inactiveAlphaModifier)
 | 
			
		||||
                    val arrowColor: Color
 | 
			
		||||
                    val strokeColor = MaterialTheme.colorScheme.onSurfaceVariant
 | 
			
		||||
                    if (isDownloading) {
 | 
			
		||||
                        val indeterminate = downloadState == Download.State.QUEUE ||
 | 
			
		||||
                            (downloadState == Download.State.DOWNLOADING && downloadProgress == 0)
 | 
			
		||||
                        if (indeterminate) {
 | 
			
		||||
                            arrowColor = strokeColor
 | 
			
		||||
                            CircularProgressIndicator(
 | 
			
		||||
                                modifier = progressIndicatorModifier,
 | 
			
		||||
                                color = strokeColor,
 | 
			
		||||
                                strokeWidth = IndicatorStrokeWidth,
 | 
			
		||||
                            )
 | 
			
		||||
                        } else {
 | 
			
		||||
                            val animatedProgress by animateFloatAsState(
 | 
			
		||||
                                targetValue = downloadProgress / 100f,
 | 
			
		||||
                                animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
 | 
			
		||||
                            )
 | 
			
		||||
                            arrowColor = if (animatedProgress < 0.5f) {
 | 
			
		||||
                                strokeColor
 | 
			
		||||
                            } else {
 | 
			
		||||
                                MaterialTheme.colorScheme.background
 | 
			
		||||
                            }
 | 
			
		||||
                            CircularProgressIndicator(
 | 
			
		||||
                                progress = animatedProgress,
 | 
			
		||||
                                modifier = progressIndicatorModifier,
 | 
			
		||||
                                color = strokeColor,
 | 
			
		||||
                                strokeWidth = IndicatorSize / 2,
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    } else {
 | 
			
		||||
                        arrowColor = strokeColor
 | 
			
		||||
                        CircularProgressIndicator(
 | 
			
		||||
                            progress = 1f,
 | 
			
		||||
                            modifier = progressIndicatorModifier.then(inactiveAlphaModifier),
 | 
			
		||||
                            color = strokeColor,
 | 
			
		||||
                            strokeWidth = IndicatorStrokeWidth,
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                    Icon(
 | 
			
		||||
                        imageVector = Icons.Default.ArrowDownward,
 | 
			
		||||
                        contentDescription = null,
 | 
			
		||||
                        modifier = arrowModifier,
 | 
			
		||||
                        tint = arrowColor,
 | 
			
		||||
                    )
 | 
			
		||||
                    DropdownMenu(expanded = isMenuExpanded, onDismissRequest = { isMenuExpanded = false }) {
 | 
			
		||||
                        DropdownMenuItem(
 | 
			
		||||
                            text = { Text(text = stringResource(id = R.string.action_start_downloading_now)) },
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                onClick(ChapterDownloadAction.START_NOW)
 | 
			
		||||
                                isMenuExpanded = false
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                        DropdownMenuItem(
 | 
			
		||||
                            text = { Text(text = stringResource(id = R.string.action_cancel)) },
 | 
			
		||||
                            onClick = {
 | 
			
		||||
                                onClick(ChapterDownloadAction.CANCEL)
 | 
			
		||||
                                isMenuExpanded = false
 | 
			
		||||
                            },
 | 
			
		||||
                        )
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
private val IndicatorSize = 26.dp
 | 
			
		||||
private val IndicatorStrokeWidth = 2.dp
 | 
			
		||||
@@ -4,3 +4,10 @@ enum class EditCoverAction {
 | 
			
		||||
    EDIT,
 | 
			
		||||
    DELETE,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum class ChapterDownloadAction {
 | 
			
		||||
    START,
 | 
			
		||||
    START_NOW,
 | 
			
		||||
    CANCEL,
 | 
			
		||||
    DELETE,
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,95 +1,40 @@
 | 
			
		||||
package eu.kanade.tachiyomi.ui.manga.chapter
 | 
			
		||||
 | 
			
		||||
import android.content.Context
 | 
			
		||||
import android.content.res.ColorStateList
 | 
			
		||||
import android.util.AttributeSet
 | 
			
		||||
import android.view.LayoutInflater
 | 
			
		||||
import android.widget.FrameLayout
 | 
			
		||||
import androidx.core.view.isVisible
 | 
			
		||||
import com.google.android.material.progressindicator.BaseProgressIndicator
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import androidx.compose.runtime.Composable
 | 
			
		||||
import androidx.compose.runtime.getValue
 | 
			
		||||
import androidx.compose.runtime.mutableStateOf
 | 
			
		||||
import androidx.compose.runtime.setValue
 | 
			
		||||
import androidx.compose.ui.platform.AbstractComposeView
 | 
			
		||||
import eu.kanade.presentation.components.ChapterDownloadIndicator
 | 
			
		||||
import eu.kanade.presentation.manga.ChapterDownloadAction
 | 
			
		||||
import eu.kanade.presentation.theme.TachiyomiTheme
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.databinding.ChapterDownloadViewBinding
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.dpToPx
 | 
			
		||||
import eu.kanade.tachiyomi.util.system.getThemeColor
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.setVectorCompat
 | 
			
		||||
 | 
			
		||||
class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
 | 
			
		||||
    FrameLayout(context, attrs) {
 | 
			
		||||
class ChapterDownloadView @JvmOverloads constructor(
 | 
			
		||||
    context: Context,
 | 
			
		||||
    attrs: AttributeSet? = null,
 | 
			
		||||
    defStyle: Int = 0,
 | 
			
		||||
) : AbstractComposeView(context, attrs, defStyle) {
 | 
			
		||||
 | 
			
		||||
    private val binding: ChapterDownloadViewBinding =
 | 
			
		||||
        ChapterDownloadViewBinding.inflate(LayoutInflater.from(context), this, false)
 | 
			
		||||
    private var state by mutableStateOf(Download.State.NOT_DOWNLOADED)
 | 
			
		||||
    private var progress by mutableStateOf(0)
 | 
			
		||||
 | 
			
		||||
    private var state: Download.State? = null
 | 
			
		||||
    private var progress = -1
 | 
			
		||||
    var listener: (ChapterDownloadAction) -> Unit = {}
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        addView(binding.root)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun setState(state: Download.State, progress: Int = -1) {
 | 
			
		||||
        val isDirty = this.state?.value != state.value || this.progress != progress
 | 
			
		||||
        if (isDirty) {
 | 
			
		||||
            updateLayout(state, progress)
 | 
			
		||||
    @Composable
 | 
			
		||||
    override fun Content() {
 | 
			
		||||
        TachiyomiTheme {
 | 
			
		||||
            ChapterDownloadIndicator(
 | 
			
		||||
                downloadState = state,
 | 
			
		||||
                downloadProgress = progress,
 | 
			
		||||
                onClick = listener,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun updateLayout(state: Download.State, progress: Int) {
 | 
			
		||||
        binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED ||
 | 
			
		||||
            state == Download.State.DOWNLOADING || state == Download.State.QUEUE
 | 
			
		||||
        binding.downloadIcon.imageTintList = if (state == Download.State.DOWNLOADING && progress > 0) {
 | 
			
		||||
            ColorStateList.valueOf(context.getThemeColor(android.R.attr.colorBackground))
 | 
			
		||||
        } else {
 | 
			
		||||
            ColorStateList.valueOf(context.getThemeColor(android.R.attr.textColorHint))
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.downloadProgress.apply {
 | 
			
		||||
            val shouldBeVisible = state == Download.State.DOWNLOADING ||
 | 
			
		||||
                state == Download.State.NOT_DOWNLOADED || state == Download.State.QUEUE
 | 
			
		||||
            if (shouldBeVisible) {
 | 
			
		||||
                hideAnimationBehavior = BaseProgressIndicator.HIDE_NONE
 | 
			
		||||
                if (state == Download.State.NOT_DOWNLOADED || state == Download.State.QUEUE) {
 | 
			
		||||
                    trackThickness = 2.dpToPx
 | 
			
		||||
                    setIndicatorColor(context.getThemeColor(android.R.attr.textColorHint))
 | 
			
		||||
                    if (state == Download.State.NOT_DOWNLOADED) {
 | 
			
		||||
                        if (isIndeterminate) {
 | 
			
		||||
                            hide()
 | 
			
		||||
                            isIndeterminate = false
 | 
			
		||||
                        }
 | 
			
		||||
                        setProgressCompat(100, false)
 | 
			
		||||
                    } else if (!isIndeterminate) {
 | 
			
		||||
                        hide()
 | 
			
		||||
                        isIndeterminate = true
 | 
			
		||||
                        show()
 | 
			
		||||
                    }
 | 
			
		||||
                } else if (state == Download.State.DOWNLOADING) {
 | 
			
		||||
                    if (isIndeterminate) {
 | 
			
		||||
                        hide()
 | 
			
		||||
                    }
 | 
			
		||||
                    trackThickness = 12.dpToPx
 | 
			
		||||
                    setIndicatorColor(context.getThemeColor(android.R.attr.textColorPrimary))
 | 
			
		||||
                    setProgressCompat(progress, true)
 | 
			
		||||
                }
 | 
			
		||||
                show()
 | 
			
		||||
            } else {
 | 
			
		||||
                hideAnimationBehavior = BaseProgressIndicator.HIDE_OUTWARD
 | 
			
		||||
                hide()
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.downloadStatusIcon.apply {
 | 
			
		||||
            if (state == Download.State.DOWNLOADED || state == Download.State.ERROR) {
 | 
			
		||||
                isVisible = true
 | 
			
		||||
                if (state == Download.State.DOWNLOADED) {
 | 
			
		||||
                    setVectorCompat(R.drawable.ic_check_circle_24dp, android.R.attr.textColorPrimary)
 | 
			
		||||
                } else {
 | 
			
		||||
                    setVectorCompat(R.drawable.ic_error_outline_24dp, R.attr.colorError)
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                isVisible = false
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
    fun setState(state: Download.State, progress: Int = 0) {
 | 
			
		||||
        this.state = state
 | 
			
		||||
        this.progress = progress
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,7 @@ class ChapterHolder(
 | 
			
		||||
    private val binding = ChaptersItemBinding.bind(view)
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
        binding.download.setOnClickListener {
 | 
			
		||||
            onDownloadClick(it, bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
        binding.download.setOnLongClickListener {
 | 
			
		||||
            onDownloadLongClick(bindingAdapterPosition)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
        binding.download.listener = downloadActionListener
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun bind(item: ChapterItem, manga: Manga) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,58 +2,19 @@ package eu.kanade.tachiyomi.ui.manga.chapter.base
 | 
			
		||||
 | 
			
		||||
import android.view.View
 | 
			
		||||
import eu.davidea.viewholders.FlexibleViewHolder
 | 
			
		||||
import eu.kanade.tachiyomi.R
 | 
			
		||||
import eu.kanade.tachiyomi.data.download.model.Download
 | 
			
		||||
import eu.kanade.tachiyomi.util.view.popupMenu
 | 
			
		||||
import eu.kanade.presentation.manga.ChapterDownloadAction
 | 
			
		||||
 | 
			
		||||
open class BaseChapterHolder(
 | 
			
		||||
    view: View,
 | 
			
		||||
    private val adapter: BaseChaptersAdapter<*>,
 | 
			
		||||
) : FlexibleViewHolder(view, adapter) {
 | 
			
		||||
 | 
			
		||||
    fun onDownloadClick(view: View, position: Int) {
 | 
			
		||||
        val item = adapter.getItem(position) as? BaseChapterItem<*, *> ?: return
 | 
			
		||||
        when (item.status) {
 | 
			
		||||
            Download.State.NOT_DOWNLOADED, Download.State.ERROR -> {
 | 
			
		||||
                adapter.clickListener.downloadChapter(position)
 | 
			
		||||
            }
 | 
			
		||||
            else -> {
 | 
			
		||||
                view.popupMenu(
 | 
			
		||||
                    R.menu.chapter_download,
 | 
			
		||||
                    initMenu = {
 | 
			
		||||
                        // Download.State.DOWNLOADED
 | 
			
		||||
                        findItem(R.id.delete_download).isVisible = item.status == Download.State.DOWNLOADED
 | 
			
		||||
 | 
			
		||||
                        // Download.State.DOWNLOADING, Download.State.QUEUE
 | 
			
		||||
                        findItem(R.id.cancel_download).isVisible = item.status != Download.State.DOWNLOADED
 | 
			
		||||
 | 
			
		||||
                        // Download.State.QUEUE
 | 
			
		||||
                        findItem(R.id.start_download).isVisible = item.status == Download.State.QUEUE
 | 
			
		||||
                    },
 | 
			
		||||
                    onMenuItemClick = {
 | 
			
		||||
                        if (itemId == R.id.start_download) {
 | 
			
		||||
                            adapter.clickListener.startDownloadNow(position)
 | 
			
		||||
                        } else {
 | 
			
		||||
                            adapter.clickListener.deleteChapter(position)
 | 
			
		||||
                        }
 | 
			
		||||
                    },
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun onDownloadLongClick(position: Int) {
 | 
			
		||||
        val item = adapter.getItem(position) as? BaseChapterItem<*, *> ?: return
 | 
			
		||||
        when (item.status) {
 | 
			
		||||
            Download.State.NOT_DOWNLOADED, Download.State.ERROR -> {
 | 
			
		||||
                adapter.clickListener.downloadChapter(position)
 | 
			
		||||
            }
 | 
			
		||||
            Download.State.DOWNLOADED, Download.State.DOWNLOADING -> {
 | 
			
		||||
                adapter.clickListener.deleteChapter(position)
 | 
			
		||||
            }
 | 
			
		||||
            // Download.State.QUEUE
 | 
			
		||||
            else -> {
 | 
			
		||||
                adapter.clickListener.startDownloadNow(position)
 | 
			
		||||
    val downloadActionListener: (ChapterDownloadAction) -> Unit = { action ->
 | 
			
		||||
        when (action) {
 | 
			
		||||
            ChapterDownloadAction.START -> adapter.clickListener.downloadChapter(bindingAdapterPosition)
 | 
			
		||||
            ChapterDownloadAction.START_NOW -> adapter.clickListener.startDownloadNow(bindingAdapterPosition)
 | 
			
		||||
            ChapterDownloadAction.CANCEL, ChapterDownloadAction.DELETE -> {
 | 
			
		||||
                adapter.clickListener.deleteChapter(bindingAdapterPosition)
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -27,13 +27,7 @@ class UpdatesHolder(private val view: View, private val adapter: UpdatesAdapter)
 | 
			
		||||
            adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        binding.download.setOnClickListener {
 | 
			
		||||
            onDownloadClick(it, bindingAdapterPosition)
 | 
			
		||||
        }
 | 
			
		||||
        binding.download.setOnLongClickListener {
 | 
			
		||||
            onDownloadLongClick(bindingAdapterPosition)
 | 
			
		||||
            true
 | 
			
		||||
        }
 | 
			
		||||
        binding.download.listener = downloadActionListener
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun bind(item: UpdatesItem) {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user