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.util.Log
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.hippo.ehviewer.EhDB
import com.hippo.ehviewer.Settings
@@ -47,6 +53,7 @@ import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import splitties.init.appCtx
@@ -246,15 +253,12 @@ object DownloadManager : OnSpiderListener {
private val callbackFlowScope = CoroutineScope(Dispatchers.IO)
private val _stateFlow = callbackFlow {
val _stateFlow = callbackFlow {
val listener = object : DownloadInfoListener {
override fun onUpdate(info: DownloadInfo) {
trySend(info.gid)
}
override fun onUpdateAll() {
trySend(0)
trySend(info)
}
override fun onUpdateAll() {}
}
mDownloadInfoListener = listener
awaitClose {
@@ -262,7 +266,21 @@ object DownloadManager : OnSpiderListener {
}
}.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
@@ -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
import android.annotation.SuppressLint
import android.app.Activity
import android.content.DialogInterface
import android.content.Intent
import android.graphics.Rect
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Button
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.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.DriveFileMove
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.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material3.Card
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ShapeDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringArrayResource
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.os.bundleOf
import androidx.lifecycle.lifecycleScope
@@ -75,27 +61,17 @@ import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
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.R
import com.hippo.ehviewer.Settings
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.databinding.DrawerListRvBinding
import com.hippo.ehviewer.databinding.ItemDownloadBinding
import com.hippo.ehviewer.databinding.ItemDrawerListBinding
import com.hippo.ehviewer.databinding.SceneDownloadBinding
import com.hippo.ehviewer.download.DownloadManager
import com.hippo.ehviewer.download.DownloadService
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.confirmRemoveDownloadRange
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.HandlerDrawable
import com.hippo.ehviewer.ui.legacy.ViewTransition
import com.hippo.ehviewer.ui.main.DEFAULT_ASPECT
import com.hippo.ehviewer.ui.main.requestOf
import com.hippo.ehviewer.ui.main.DownloadCard
import com.hippo.ehviewer.ui.navToReader
import com.hippo.ehviewer.ui.setMD3Content
import com.hippo.ehviewer.ui.tools.CropDefaults
import com.hippo.ehviewer.ui.tools.DialogState
import com.hippo.ehviewer.util.FileUtils
import com.hippo.ehviewer.util.containsIgnoreCase
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.launchUI
import eu.kanade.tachiyomi.util.lang.withNonCancellableContext
import eu.kanade.tachiyomi.util.lang.withUIContext
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import rikka.core.res.resolveColor
@@ -555,16 +525,6 @@ class DownloadsScene : SearchBarScene() {
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) :
RecyclerView.ViewHolder(binding.root)
@@ -666,218 +626,58 @@ class DownloadsScene : SearchBarScene() {
}
}
@SuppressLint("ClickableViewAccessibility")
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"
}
}
}
class DownloadHolder(val view: ComposeView) : RecyclerView.ViewHolder(view)
private val diffCallback = object : DiffUtil.ItemCallback<DownloadInfo>() {
override fun areItemsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem == newItem
override fun getChangePayload(oldItem: DownloadInfo, newItem: DownloadInfo): Any? {
return if (oldItem.downloadInfo != newItem.downloadInfo) {
PAYLOAD_DOWNLOAD_INFO
} else {
super.getChangePayload(oldItem, newItem)
}
}
override fun areContentsTheSame(oldItem: DownloadInfo, newItem: DownloadInfo) = oldItem.gid == newItem.gid
}
private inner class DownloadAdapter : ListAdapter<DownloadInfo, DownloadHolder>(diffCallback) {
private val mInflater: LayoutInflater = layoutInflater
var tracker: GallerySelectionTracker<DownloadInfo>? = null
override fun getItemId(position: Int) = getItem(position).gid
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): DownloadHolder {
val holder = DownloadHolder(ItemDownloadBinding.inflate(mInflater, parent, false))
holder.itemView.setOnClickListener { onItemClick(holder.bindingAdapterPosition) }
return holder
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = DownloadHolder(ComposeView(parent.context))
override fun onBindViewHolder(holder: DownloadHolder, position: Int) {
val info = getItem(position)
holder.bind(info, tracker?.isSelected(info.gid) ?: false)
}
override fun onBindViewHolder(
holder: DownloadHolder,
position: Int,
payloads: MutableList<Any>,
) {
payloads.forEach { payload ->
when (payload) {
PAYLOAD_DOWNLOAD_INFO -> {
val info = getItem(position)
holder.bindForState(info)
return
val checked = tracker?.isSelected(info.gid) ?: false
holder.view.setMD3Content {
val context = LocalContext.current
val height by collectListThumbSizeAsState()
key(info.gid) {
CheckableItem(checked = checked) {
DownloadCard(
onClick = {
context.navToReader(info.galleryInfo)
},
onThumbClick = {
navAnimated(
R.id.galleryDetailScene,
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
}
}
@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="search_layout_margin_h">20dp</dimen>
</resources>
@@ -18,6 +18,4 @@
<dimen name="gallery_list_margin_h">76dp</dimen>
<dimen name="search_layout_margin_h">76dp</dimen>
</resources>
-4
View File
@@ -27,7 +27,6 @@
<!-- Text size -->
<dimen name="text_little_small">16sp</dimen>
<dimen name="text_super_small">12sp</dimen>
<!-- RatingView -->
<dimen name="rating_size">16dp</dimen>
@@ -39,12 +38,9 @@
<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_short">320dp</dimen>
<dimen name="search_layout_margin_h">4dp</dimen>
<dimen name="search_layout_margin_v">4dp</dimen>
<!-- Search item -->
-2
View File
@@ -17,9 +17,7 @@
<resources>
<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_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_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>
-13
View File
@@ -29,19 +29,6 @@
<item name="android:minHeight">48dp</item>
</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">
<item name="materialThemeOverlay">@style/SnackbarTheme</item>
<item name="actionTextColorAlpha">1</item>