Compose DownloadCard (#339)

* Cleanup res

* Compose DownloadCard

* Refine

* Fix CI

* Fixes

* Fix recompose

* Fix textStyle

* Fixes

* Fixes

* Restore design specs and clean up

---------

Co-authored-by: GG Bond <whatever@tagkong.com>
This commit is contained in:
Vladimir DostoyevskiyandGG Bond authored and GitHub committed 2023-11-25 16:46:07 +00:00
1 parent 618f14a732
commit 198960f93c
13 files changed
+342 -604

No files matched your search

@@ -18,6 +18,12 @@ package com.hippo.ehviewer.download
import android.net.Uri import android.net.Uri
import android.util.Log import android.util.Log
import android.util.SparseLongArray import android.util.SparseLongArray
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisallowComposableCalls
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.remember
import com.google.android.material.math.MathUtils import com.google.android.material.math.MathUtils
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
@@ -47,6 +53,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import splitties.init.appCtx import splitties.init.appCtx
@@ -246,15 +253,12 @@ object DownloadManager : OnSpiderListener {
private val callbackFlowScope = CoroutineScope(Dispatchers.IO) private val callbackFlowScope = CoroutineScope(Dispatchers.IO)
private val _stateFlow = callbackFlow { val _stateFlow = callbackFlow {
val listener = object : DownloadInfoListener { val listener = object : DownloadInfoListener {
override fun onUpdate(info: DownloadInfo) { override fun onUpdate(info: DownloadInfo) {
trySend(info.gid) trySend(info)
}
override fun onUpdateAll() {
trySend(0)
} }
override fun onUpdateAll() {}
} }
mDownloadInfoListener = listener mDownloadInfoListener = listener
awaitClose { awaitClose {
@@ -262,7 +266,21 @@ object DownloadManager : OnSpiderListener {
} }
}.shareIn(callbackFlowScope, SharingStarted.Eagerly) }.shareIn(callbackFlowScope, SharingStarted.Eagerly)
fun stateFlow(gid: Long) = _stateFlow.filter { it == gid } @Stable
@Composable
fun collectDownloadState(gid: Long): State<Int> {
val flow = remember { stateFlow(gid).map { getDownloadState(it) } }
return flow.collectAsState(getDownloadState(gid))
}
@Stable
@Composable
inline fun <T> updatedDownloadInfo(info: DownloadInfo, crossinline transform: @DisallowComposableCalls DownloadInfo.() -> T): T {
val flow = remember { _stateFlow.filter { it.gid == info.gid }.map { transform(it) } }
return flow.collectAsState(transform(info)).value
}
fun stateFlow(gid: Long) = _stateFlow.map { it.gid }.filter { it == gid }
fun stateFlow() = _stateFlow fun stateFlow() = _stateFlow
@@ -0,0 +1,278 @@
package com.hippo.ehviewer.ui.main
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.DragHandle
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ProvideTextStyle
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import com.hippo.ehviewer.EhApplication
import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.downloadDir
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.CrystalCard
import com.hippo.ehviewer.ui.tools.GalleryListCardRating
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@Composable
private fun AsyncThumb(
info: DownloadInfo,
modifier: Modifier = Modifier,
) {
val path = remember(info.gid) { info.downloadDir?.subFile(".thumb") }
var contentScale by remember(info.gid) { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current
var localReq by remember(info.gid) {
path?.takeIf { it.isFile }?.uri?.let {
context.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
}
}.let { mutableStateOf(it) }
}
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = modifier,
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
path?.let {
coroutineScope.launch {
runCatching {
if (!path.exists() && path.ensureFile()) {
val key = info.thumbKey!!
EhApplication.imageCache.read(key) {
UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use { src ->
path.openFileDescriptor("w").use { dst ->
src sendTo dst
}
}
}
}
}.onFailure {
it.printStackTrace()
}
}
}
}
if (state is AsyncImagePainter.State.Error) {
path?.let {
coroutineScope.launch {
if (path.exists()) {
path.delete()
localReq = null
}
}
}
}
},
contentScale = contentScale,
)
}
@Composable
fun DownloadCard(
onClick: () -> Unit,
onThumbClick: () -> Unit,
onLongClick: () -> Unit,
onStart: () -> Unit,
onStop: () -> Unit,
onDrag: () -> Unit,
info: DownloadInfo,
modifier: Modifier = Modifier,
) {
CrystalCard(
modifier = modifier,
onClick = onClick,
onLongClick = onLongClick,
) {
Row {
Card(onClick = onThumbClick) {
AsyncThumb(
info = info,
modifier = Modifier.aspectRatio(DEFAULT_ASPECT).fillMaxSize(),
)
}
val stateFailed = stringResource(R.string.download_state_failed)
val stateFailed2 = stringResource(R.string.download_state_failed_2, info.legacy)
val downloadState by DownloadManager.collectDownloadState(info.gid)
val stateText = when (downloadState) {
DownloadInfo.STATE_NONE -> stringResource(R.string.download_state_none)
DownloadInfo.STATE_WAIT -> stringResource(R.string.download_state_wait)
DownloadInfo.STATE_DOWNLOAD -> null
DownloadInfo.STATE_FAILED -> if (info.legacy <= 0) stateFailed else stateFailed2
DownloadInfo.STATE_FINISH -> stringResource(R.string.download_state_finish)
else -> error("Illegal State $downloadState!!!")
}
ConstraintLayout(modifier = Modifier.padding(8.dp, 4.dp).fillMaxSize()) {
val (
titleRef, uploaderRef, ratingRef, categoryRef, actionsRef, stateTextRef,
progressBarRef, progressTextRef, speedRef,
) = createRefs()
Text(
text = EhUtils.getSuitableTitle(info),
maxLines = 2,
modifier = Modifier.constrainAs(titleRef) {
top.linkTo(parent.top)
start.linkTo(parent.start)
}.fillMaxWidth(),
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.titleSmall,
)
if (downloadState != DownloadInfo.STATE_DOWNLOAD) {
ProvideTextStyle(MaterialTheme.typography.labelLarge) {
info.uploader?.let {
Text(
text = it,
modifier = Modifier.constrainAs(uploaderRef) {
start.linkTo(parent.start)
bottom.linkTo(ratingRef.top)
}.alpha(if (info.disowned) 0.5f else 1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
GalleryListCardRating(
rating = info.rating,
modifier = Modifier.constrainAs(ratingRef) {
start.linkTo(parent.start)
bottom.linkTo(categoryRef.top)
},
)
val categoryColor = EhUtils.getCategoryColor(info.category)
val categoryText = EhUtils.getCategory(info.category).uppercase()
Text(
text = categoryText,
modifier = Modifier.constrainAs(categoryRef) {
start.linkTo(parent.start)
bottom.linkTo(parent.bottom)
}.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
textAlign = TextAlign.Center,
)
}
} else {
val (total, finished, speed) = DownloadManager.updatedDownloadInfo(info) {
Triple(total, finished, speed)
}
ProvideTextStyle(MaterialTheme.typography.labelMedium) {
if (total <= 0 || finished < 0) {
LinearProgressIndicator(
modifier = Modifier.constrainAs(progressBarRef) {
bottom.linkTo(actionsRef.top)
}.fillMaxWidth(),
)
} else {
LinearProgressIndicator(
progress = { finished.toFloat() / total.toFloat() },
modifier = Modifier.constrainAs(progressBarRef) {
bottom.linkTo(actionsRef.top)
}.fillMaxWidth(),
)
Text(
text = "$finished/$total",
modifier = Modifier.constrainAs(progressTextRef) {
bottom.linkTo(progressBarRef.top)
start.linkTo(progressBarRef.start)
},
)
}
Text(
text = FileUtils.humanReadableByteCount(speed.coerceAtLeast(0), false) + "/S",
modifier = Modifier.constrainAs(speedRef) {
bottom.linkTo(progressBarRef.top)
end.linkTo(progressBarRef.end)
},
)
}
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.constrainAs(actionsRef) {
end.linkTo(parent.end)
bottom.linkTo(parent.bottom)
},
) {
IconButton(onClick = { }) {
Icon(
imageVector = Icons.Default.DragHandle,
contentDescription = null,
modifier = Modifier.pointerInput(info.gid) {
awaitPointerEventScope { onDrag() }
},
)
}
if (downloadState == DownloadInfo.STATE_WAIT || downloadState == DownloadInfo.STATE_DOWNLOAD) {
IconButton(onClick = onStop) {
Icon(imageVector = Icons.Default.Pause, contentDescription = null)
}
} else {
IconButton(onClick = onStart) {
Icon(imageVector = Icons.Default.PlayArrow, contentDescription = null)
}
}
}
if (stateText != null) {
Text(
text = stateText,
modifier = Modifier.constrainAs(stateTextRef) {
end.linkTo(parent.end, 16.dp)
bottom.linkTo(actionsRef.top)
},
style = MaterialTheme.typography.titleSmall,
)
}
}
}
}
}
@@ -16,24 +16,17 @@
package com.hippo.ehviewer.ui.scene package com.hippo.ehviewer.ui.scene
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.graphics.Rect import android.graphics.Rect
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.MotionEvent
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.DriveFileMove import androidx.compose.material.icons.automirrored.filled.DriveFileMove
import androidx.compose.material.icons.automirrored.outlined.Label import androidx.compose.material.icons.automirrored.outlined.Label
@@ -42,29 +35,22 @@ import androidx.compose.material.icons.filled.DoneAll
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource import androidx.compose.ui.res.stringArrayResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@@ -75,27 +61,17 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.viewbinding.ViewBinding import androidx.viewbinding.ViewBinding
import arrow.core.partially1
import coil.compose.AsyncImage
import coil.compose.AsyncImagePainter
import com.hippo.ehviewer.EhApplication.Companion.imageCache
import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.EhDB
import com.hippo.ehviewer.R import com.hippo.ehviewer.R
import com.hippo.ehviewer.Settings import com.hippo.ehviewer.Settings
import com.hippo.ehviewer.Settings.detailSize import com.hippo.ehviewer.Settings.detailSize
import com.hippo.ehviewer.client.EhUtils
import com.hippo.ehviewer.client.data.GalleryInfo
import com.hippo.ehviewer.coil.read
import com.hippo.ehviewer.dao.DownloadInfo import com.hippo.ehviewer.dao.DownloadInfo
import com.hippo.ehviewer.databinding.DrawerListRvBinding import com.hippo.ehviewer.databinding.DrawerListRvBinding
import com.hippo.ehviewer.databinding.ItemDownloadBinding
import com.hippo.ehviewer.databinding.ItemDrawerListBinding import com.hippo.ehviewer.databinding.ItemDrawerListBinding
import com.hippo.ehviewer.databinding.SceneDownloadBinding import com.hippo.ehviewer.databinding.SceneDownloadBinding
import com.hippo.ehviewer.download.DownloadManager import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService import com.hippo.ehviewer.download.DownloadService
import com.hippo.ehviewer.download.DownloadService.Companion.clear import com.hippo.ehviewer.download.DownloadService.Companion.clear
import com.hippo.ehviewer.download.downloadDir
import com.hippo.ehviewer.ktbuilder.imageRequest
import com.hippo.ehviewer.ui.confirmRemoveDownload import com.hippo.ehviewer.ui.confirmRemoveDownload
import com.hippo.ehviewer.ui.confirmRemoveDownloadRange import com.hippo.ehviewer.ui.confirmRemoveDownloadRange
import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager import com.hippo.ehviewer.ui.legacy.AutoStaggeredGridLayoutManager
@@ -103,23 +79,17 @@ import com.hippo.ehviewer.ui.legacy.BaseDialogBuilder
import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder import com.hippo.ehviewer.ui.legacy.EditTextDialogBuilder
import com.hippo.ehviewer.ui.legacy.HandlerDrawable import com.hippo.ehviewer.ui.legacy.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.main.DEFAULT_ASPECT import com.hippo.ehviewer.ui.main.DownloadCard
import com.hippo.ehviewer.ui.main.requestOf
import com.hippo.ehviewer.ui.navToReader import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.DialogState import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.containsIgnoreCase import com.hippo.ehviewer.util.containsIgnoreCase
import com.hippo.ehviewer.util.mapToLongArray import com.hippo.ehviewer.util.mapToLongArray
import com.hippo.ehviewer.util.sendTo
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import rikka.core.res.resolveColor import rikka.core.res.resolveColor
@@ -555,16 +525,6 @@ class DownloadsScene : SearchBarScene() {
return drawerBinding.root return drawerBinding.root
} }
fun onItemClick(position: Int): Boolean {
val context = context ?: return false
val list = mList ?: return false
if (position < 0 || position >= list.size) {
return false
}
context.navToReader(list[position].galleryInfo)
return true
}
private class DownloadLabelHolder(val binding: ItemDrawerListBinding) : private class DownloadLabelHolder(val binding: ItemDrawerListBinding) :
RecyclerView.ViewHolder(binding.root) RecyclerView.ViewHolder(binding.root)
@@ -666,218 +626,58 @@ class DownloadsScene : SearchBarScene() {
} }
} }
@SuppressLint("ClickableViewAccessibility") class DownloadHolder(val view: ComposeView) : RecyclerView.ViewHolder(view)
private inner class DownloadHolder(
private val binding: ItemDownloadBinding,
) : RecyclerView.ViewHolder(binding.root), View.OnClickListener {
init {
// TODO cancel on click listener when select items
binding.start.setOnClickListener(this)
binding.stop.setOnClickListener(this)
binding.thumb.setMD3Content {
val height by collectListThumbSizeAsState()
Spacer(modifier = Modifier.height(height).fillMaxWidth())
}
binding.handle.setOnTouchListener { _, event ->
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
mItemTouchHelper!!.startDrag(this)
}
false
}
}
override fun onClick(v: View) {
val context = context
val activity: Activity? = mainActivity
val recyclerView = this@DownloadsScene.binding.recyclerView
if (null == context || null == activity || tracker.isInCustomChoice) {
return
}
val list = mList ?: return
val size = list.size
val index = recyclerView.getChildAdapterPosition(itemView)
if (index < 0 || index >= size) {
return
}
when (v) {
binding.thumb -> {
navAnimated(
R.id.galleryDetailScene,
bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(list[index].galleryInfo)),
)
}
binding.start -> {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, list[index].galleryInfo)
ContextCompat.startForegroundService(activity, intent)
}
binding.stop -> {
lifecycleScope.launchIO {
DownloadManager.stopDownload(list[index].gid)
}
}
}
}
fun bind(info: DownloadInfo, isChecked: Boolean) {
binding.root.isChecked = isChecked
binding.thumb.setMD3Content {
val height by collectListThumbSizeAsState()
Card(onClick = ::onClick.partially1(binding.thumb)) {
CompanionAsyncThumb(
info = info,
path = info.downloadDir?.subFile(".thumb"),
modifier = Modifier.height(height).aspectRatio(DEFAULT_ASPECT),
)
}
}
binding.title.text = EhUtils.getSuitableTitle(info)
binding.uploader.text = info.uploader
binding.rating.rating = info.rating
binding.category.setMD3Content {
val categoryColor = EhUtils.getCategoryColor(info.category)
val categoryText = EhUtils.getCategory(info.category).uppercase()
Text(
text = categoryText,
modifier = Modifier.clip(ShapeDefaults.Small).background(categoryColor).padding(vertical = 2.dp, horizontal = 8.dp),
color = if (Settings.harmonizeCategoryColor) Color.Unspecified else EhUtils.categoryTextColor,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelLarge,
)
}
bindForState(info)
}
fun bindForState(info: DownloadInfo) {
val context = context ?: return
when (info.state) {
DownloadInfo.STATE_NONE -> bindState(
info,
context.getString(R.string.download_state_none),
)
DownloadInfo.STATE_WAIT -> bindState(
info,
context.getString(R.string.download_state_wait),
)
DownloadInfo.STATE_DOWNLOAD -> bindProgress(info)
DownloadInfo.STATE_FAILED -> {
val text: String = if (info.legacy <= 0) {
context.getString(R.string.download_state_failed)
} else {
context.getString(R.string.download_state_failed_2, info.legacy)
}
bindState(info, text)
}
DownloadInfo.STATE_FINISH -> bindState(
info,
context.getString(R.string.download_state_finish),
)
}
}
private fun bindState(info: DownloadInfo, newState: String) {
binding.run {
uploader.visibility = View.VISIBLE
rating.visibility = View.VISIBLE
category.visibility = View.VISIBLE
state.visibility = View.VISIBLE
progressBar.visibility = View.GONE
percent.visibility = View.GONE
speed.visibility = View.GONE
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
start.visibility = View.GONE
stop.visibility = View.VISIBLE
} else {
start.visibility = View.VISIBLE
stop.visibility = View.GONE
}
state.text = newState
}
}
@SuppressLint("SetTextI18n")
private fun bindProgress(info: DownloadInfo) {
binding.run {
uploader.visibility = View.GONE
rating.visibility = View.GONE
category.visibility = View.GONE
state.visibility = View.GONE
progressBar.visibility = View.VISIBLE
percent.visibility = View.VISIBLE
speed.visibility = View.VISIBLE
if (info.state == DownloadInfo.STATE_WAIT || info.state == DownloadInfo.STATE_DOWNLOAD) {
start.visibility = View.GONE
stop.visibility = View.VISIBLE
} else {
start.visibility = View.VISIBLE
stop.visibility = View.GONE
}
if (info.total <= 0 || info.finished < 0) {
percent.text = null
progressBar.isIndeterminate = true
} else {
percent.text = info.finished.toString() + "/" + info.total
progressBar.isIndeterminate = false
progressBar.max = info.total
progressBar.progress = info.finished
}
speed.text =
FileUtils.humanReadableByteCount(info.speed.coerceAtLeast(0), false) + "/S"
}
}
}
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() { private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
return if (oldItem.downloadInfo != newItem.downloadInfo) {
PAYLOAD_DOWNLOAD_INFO
} else {
super.getChangePayload(oldItem, newItem)
}
}
} }
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) { private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
private val mInflater: LayoutInflater = layoutInflater
var tracker: GallerySelectionTracker<DownloadInfo>? = null var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int) = getItem(position).gid override fun getItemId(position: Int) = getItem(position).gid
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DownloadHolder(ComposeView(parent.context))
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener { onItemClick(holder.bindingAdapterPosition) }
return holder
}
override fun onBindViewHolder(holder: DownloadHolder, position: Int) { override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val info = getItem(position) val info = getItem(position)
holder.bind(info, tracker?.isSelected(info.gid) ?: false) val checked = tracker?.isSelected(info.gid) ?: false
} holder.view.setMD3Content {
val context = LocalContext.current
override fun onBindViewHolder( val height by collectListThumbSizeAsState()
holder: DownloadHolder, key(info.gid) {
position: Int, CheckableItem(checked = checked) {
payloads: MutableList<Any>, DownloadCard(
) { onClick = {
payloads.forEach { payload -> context.navToReader(info.galleryInfo)
when (payload) { },
PAYLOAD_DOWNLOAD_INFO -> { onThumbClick = {
val info = getItem(position) navAnimated(
holder.bindForState(info) R.id.galleryDetailScene,
return bundleOf(GalleryDetailScene.KEY_ARGS to GalleryInfoArgs(info.galleryInfo)),
)
},
onLongClick = {
},
onStart = {
val intent = Intent(activity, DownloadService::class.java)
intent.action = DownloadService.ACTION_START
intent.putExtra(DownloadService.KEY_GALLERY_INFO, info.galleryInfo)
ContextCompat.startForegroundService(requireActivity(), intent)
},
onStop = {
lifecycleScope.launchIO {
DownloadManager.stopDownload(info.gid)
}
},
onDrag = { mItemTouchHelper?.startDrag(holder) },
info = info,
modifier = Modifier.height(height),
)
} }
} }
} }
super.onBindViewHolder(holder, position, payloads)
} }
} }
@@ -1021,65 +821,3 @@ class DownloadsScene : SearchBarScene() {
private const val PAYLOAD_DOWNLOAD_INFO = 0 private const val PAYLOAD_DOWNLOAD_INFO = 0
} }
} }
@Composable
private fun CompanionAsyncThumb(
info: GalleryInfo,
path: UniFile?,
modifier: Modifier = Modifier,
) {
var contentScale by remember(info.gid) { mutableStateOf(ContentScale.Fit) }
val coroutineScope = rememberCoroutineScope { Dispatchers.IO }
val context = LocalContext.current
var localReq by remember(info.gid) {
path?.takeIf { it.isFile }?.uri?.let {
context.imageRequest {
data(it.toString())
memoryCacheKey(info.thumbKey)
}
}.let { mutableStateOf(it) }
}
AsyncImage(
model = localReq ?: requestOf(info),
contentDescription = null,
modifier = modifier,
onState = { state ->
if (state is AsyncImagePainter.State.Success) {
state.result.drawable.run {
if (CropDefaults.shouldCrop(intrinsicWidth, intrinsicHeight)) {
contentScale = ContentScale.Crop
}
}
path?.let {
coroutineScope.launch {
runCatching {
if (!path.exists() && path.ensureFile()) {
val key = info.thumbKey!!
imageCache.read(key) {
UniFile.fromFile(data.toFile())!!.openFileDescriptor("r").use { src ->
path.openFileDescriptor("w").use { dst ->
src sendTo dst
}
}
}
}
}.onFailure {
it.printStackTrace()
}
}
}
}
if (state is AsyncImagePainter.State.Error) {
path?.let {
coroutineScope.launch {
if (path.exists()) {
path.delete()
localReq = null
}
}
}
}
},
contentScale = contentScale,
)
}
@@ -1,36 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2018 Hippo Seven
~
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="120dp"
android:height="120dp"
android:tint="?colorTertiary"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M16.5,10.8c-0.61,0-1.1,0.49-1.1,1.1c0,0.61,0.49,1.1,1.1,1.1s1.1-0.49,1.1-1.1C17.6,11.29,17.11,10.8,16.5,10.8z" />
<path
android:fillColor="@android:color/white"
android:pathData="M8.2,10.1c-0.61,0-1.1,0.49-1.1,1.1s0.49,1.1,1.1,1.1s1.1-0.49,1.1-1.1S8.81,10.1,8.2,10.1z" />
<path
android:fillColor="@android:color/white"
android:pathData="M22.1,16.4c-0.46-3.33-2.24-5.93-4.81-7.45L19.5,6.3c0.1-0.1,0.1-0.3,0-0.4c-0.1-0.1-0.3-0.1-0.4,0l-2.27,2.79c-1.26-0.66-2.69-1.07-4.23-1.19c-1.4-0.15-2.77-0.01-4.05,0.37L6.7,4.7C6.6,4.6,6.4,4.5,6.3,4.6C6.2,4.7,6.1,4.9,6.2,5l1.79,3.05c-2.83,1.02-5.12,3.3-6.19,6.55l15.44,1.37c0,0.02-0.01,0.04-0.01,0.05c0,0.59,0.48,1.07,1.07,1.07c0.54,0,0.97-0.41,1.04-0.93L22.1,16.4z M2.63,14.07c1.51-3.74,4.83-6.03,8.84-6.03c0.35,0,0.71,0.02,1.07,0.06c4.55,0.36,7.94,3.33,8.84,7.64l-2.12-0.19c-0.27-0.68-0.95-1.46-0.95-1.46s-0.58,0.66-0.89,1.29L2.63,14.07z" />
</vector>
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_help_circle" />
</vector>
-27
View File
@@ -1,27 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ 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.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="@string/pd_play" />
</vector>
-151
View File
@@ -1,151 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ 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.
-->
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.CardView.Outlined"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:checkable="true"
android:clickable="true"
android:focusable="true"
app:strokeColor="@android:color/transparent">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.compose.ui.platform.ComposeView
android:id="@+id/thumb"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<TextView
android:id="@+id/title"
style="@style/CardTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="8dp"
android:layout_marginTop="4dp"
android:layout_marginRight="8dp"
android:layout_toEndOf="@id/thumb" />
<TextView
android:id="@+id/uploader"
style="@style/CardMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/rating"
android:layout_alignStart="@id/title"
android:layout_marginTop="2dp" />
<com.hippo.ehviewer.ui.legacy.SimpleRatingView
android:id="@+id/rating"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/category"
android:layout_alignStart="@id/title"
android:layout_marginBottom="2dp" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/category"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignStart="@id/title"
android:layout_alignBottom="@id/thumb"
android:layout_marginBottom="8dp" />
<ImageView
android:id="@+id/handle"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_alignBottom="@+id/thumb"
android:layout_toStartOf="@+id/actions"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/ic_baseline_reorder_24" />
<FrameLayout
android:id="@+id/actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/thumb"
android:layout_alignParentEnd="true"
android:paddingEnd="8dp">
<ImageView
android:id="@+id/start"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/v_play_x24" />
<ImageView
android:id="@+id/stop"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?selectableItemBackgroundBorderless"
android:clickable="true"
android:focusable="true"
android:padding="8dp"
app:srcCompat="@drawable/v_pause_x24" />
</FrameLayout>
<TextView
android:id="@+id/state"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/actions"
android:layout_alignParentEnd="true"
android:layout_marginBottom="4dp"
android:paddingEnd="16dp" />
<ProgressBar
android:id="@+id/progress_bar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/actions"
android:layout_alignStart="@id/title"
android:layout_marginEnd="8dp"
android:indeterminate="false" />
<TextView
android:id="@+id/percent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/progress_bar"
android:layout_alignStart="@id/title"
android:textSize="@dimen/text_super_small" />
<TextView
android:id="@+id/speed"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/progress_bar"
android:layout_alignParentEnd="true"
android:layout_marginEnd="8dp"
android:textSize="@dimen/text_super_small" />
</RelativeLayout>
</com.google.android.material.card.MaterialCardView>
@@ -1,32 +0,0 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ Copyright 2016 Hippo Seven
~
~ 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.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/action_help"
android:icon="@drawable/v_help_circle_x24"
android:title="@string/add"
app:showAsAction="always" />
<item
android:id="@+id/action_add"
android:icon="@drawable/v_plus_dark_x24"
android:title="@string/add"
app:showAsAction="always" />
</menu>
@@ -23,6 +23,4 @@
<dimen name="gallery_list_margin_h">20dp</dimen> <dimen name="gallery_list_margin_h">20dp</dimen>
<dimen name="search_layout_margin_h">20dp</dimen>
</resources> </resources>
@@ -18,6 +18,4 @@
<dimen name="gallery_list_margin_h">76dp</dimen> <dimen name="gallery_list_margin_h">76dp</dimen>
<dimen name="search_layout_margin_h">76dp</dimen>
</resources> </resources>
-4
View File
@@ -27,7 +27,6 @@
<!-- Text size --> <!-- Text size -->
<dimen name="text_little_small">16sp</dimen> <dimen name="text_little_small">16sp</dimen>
<dimen name="text_super_small">12sp</dimen>
<!-- RatingView --> <!-- RatingView -->
<dimen name="rating_size">16dp</dimen> <dimen name="rating_size">16dp</dimen>
@@ -39,12 +38,9 @@
<dimen name="gallery_grid_interval">4dp</dimen> <dimen name="gallery_grid_interval">4dp</dimen>
<dimen name="gallery_search_bar_margin_v">10dp</dimen>
<dimen name="gallery_list_column_width_long">480dp</dimen> <dimen name="gallery_list_column_width_long">480dp</dimen>
<dimen name="gallery_list_column_width_short">320dp</dimen> <dimen name="gallery_list_column_width_short">320dp</dimen>
<dimen name="search_layout_margin_h">4dp</dimen>
<dimen name="search_layout_margin_v">4dp</dimen> <dimen name="search_layout_margin_v">4dp</dimen>
<!-- Search item --> <!-- Search item -->
-2
View File
@@ -17,9 +17,7 @@
<resources> <resources>
<string name="pd_pause" translatable="false">M14,19.14H18V5.14H14M6,19.14H10V5.14H6V19.14Z</string> <string name="pd_pause" translatable="false">M14,19.14H18V5.14H14M6,19.14H10V5.14H6V19.14Z</string>
<string name="pd_play" translatable="false">M8,5.14V19.14L19,12.14L8,5.14Z</string>
<string name="pd_plus" translatable="false">M21,13H13V21H11V13H3V11H11V3H13V11H21V13Z</string> <string name="pd_plus" translatable="false">M21,13H13V21H11V13H3V11H11V3H13V11H21V13Z</string>
<string name="pd_help_circle" translatable="false">M15.07,11.25L14.17,12.17C13.45,12.89 13,13.5 13,15H11V14.5C11,13.39 11.45,12.39 12.17,11.67L13.41,10.41C13.78,10.05 14,9.55 14,9C14,7.89 13.1,7 12,7A2,2 0 0,0 10,9H8A4,4 0 0,1 12,5A4,4 0 0,1 16,9C16,9.88 15.64,10.67 15.07,11.25M13,19H11V17H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z</string>
<string name="pd_download_box" translatable="false">M19,3H5C3.9,3,3,3.9,3,5V19C3,20.1,3.9,21,5,21H19C20.1,21,21,20.1,21,19V5C21,3.9,20.1,3,19,3ZM12,17L7,12H10V8H14V12H17L12,17Z</string> <string name="pd_download_box" translatable="false">M19,3H5C3.9,3,3,3.9,3,5V19C3,20.1,3.9,21,5,21H19C20.1,21,21,20.1,21,19V5C21,3.9,20.1,3,19,3ZM12,17L7,12H10V8H14V12H17L12,17Z</string>
<string name="pd_refresh" translatable="false">M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z</string> <string name="pd_refresh" translatable="false">M17.65,6.35C16.2,4.9 14.21,4 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20C15.73,20 18.84,17.45 19.73,14H17.65C16.83,16.33 14.61,18 12,18A6,6 0 0,1 6,12A6,6 0 0,1 12,6C13.66,6 15.14,6.69 16.22,7.78L13,11H20V4L17.65,6.35Z</string>
<string name="pd_file_image" translatable="false">M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z</string> <string name="pd_file_image" translatable="false">M13,9H18.5L13,3.5V9M6,2H14L20,8V20A2,2 0 0,1 18,22H6C4.89,22 4,21.1 4,20V4C4,2.89 4.89,2 6,2M6,20H15L18,20V12L14,16L12,14L6,20M8,9A2,2 0 0,0 6,11A2,2 0 0,0 8,13A2,2 0 0,0 10,11A2,2 0 0,0 8,9Z</string>
-13
View File
@@ -29,19 +29,6 @@
<item name="android:minHeight">48dp</item> <item name="android:minHeight">48dp</item>
</style> </style>
<style name="CardTitle">
<item name="android:textAppearance">@style/TextAppearance.Material3.TitleSmall</item>
<item name="android:ellipsize">end</item>
<item name="android:maxLines">2</item>
</style>
<style name="CardMessage">
<item name="android:textAppearance">@style/TextAppearance.Material3.LabelLarge</item>
<item name="android:ellipsize">end</item>
<item name="android:singleLine">true</item>
<item name="android:maxLines">1</item>
</style>
<style name="Snackbar" parent="Widget.Material3.Snackbar"> <style name="Snackbar" parent="Widget.Material3.Snackbar">
<item name="materialThemeOverlay">@style/SnackbarTheme</item> <item name="materialThemeOverlay">@style/SnackbarTheme</item>
<item name="actionTextColorAlpha">1</item> <item name="actionTextColorAlpha">1</item>