Manage download states
parent
f05bb20428
commit
41ac50c76a
@ -1,140 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.ui
|
|
||||||
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.launchIn
|
|
||||||
import kotlinx.coroutines.flow.onEach
|
|
||||||
import org.koitharu.kotatsu.R
|
|
||||||
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
|
||||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
|
||||||
import org.koitharu.kotatsu.parsers.util.format
|
|
||||||
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
|
||||||
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
|
||||||
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
|
||||||
import org.koitharu.kotatsu.utils.ext.onFirst
|
|
||||||
import org.koitharu.kotatsu.utils.ext.source
|
|
||||||
|
|
||||||
fun downloadItemAD(
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
coil: ImageLoader,
|
|
||||||
) = adapterDelegateViewBinding<DownloadItem, DownloadItem, ItemDownloadBinding>(
|
|
||||||
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
|
|
||||||
) {
|
|
||||||
var job: Job? = null
|
|
||||||
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
|
||||||
|
|
||||||
val clickListener = View.OnClickListener { v ->
|
|
||||||
when (v.id) {
|
|
||||||
R.id.button_cancel -> item.cancel()
|
|
||||||
R.id.button_resume -> item.resume()
|
|
||||||
else -> context.startActivity(
|
|
||||||
DetailsActivity.newIntent(context, item.progressValue.manga),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.buttonCancel.setOnClickListener(clickListener)
|
|
||||||
binding.buttonResume.setOnClickListener(clickListener)
|
|
||||||
itemView.setOnClickListener(clickListener)
|
|
||||||
|
|
||||||
bind {
|
|
||||||
job?.cancel()
|
|
||||||
job = item.progressAsFlow().onFirst { state ->
|
|
||||||
binding.imageViewCover.newImageRequest(lifecycleOwner, state.manga.coverUrl)?.run {
|
|
||||||
placeholder(state.cover)
|
|
||||||
fallback(R.drawable.ic_placeholder)
|
|
||||||
error(R.drawable.ic_error_placeholder)
|
|
||||||
source(state.manga.source)
|
|
||||||
allowRgb565(true)
|
|
||||||
enqueueWith(coil)
|
|
||||||
}
|
|
||||||
}.onEach { state ->
|
|
||||||
binding.textViewTitle.text = state.manga.title
|
|
||||||
when (state) {
|
|
||||||
is DownloadState.Cancelled -> {
|
|
||||||
binding.textViewStatus.setText(R.string.cancelling_)
|
|
||||||
binding.progressBar.isIndeterminate = true
|
|
||||||
binding.progressBar.isVisible = true
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = false
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.Done -> {
|
|
||||||
binding.textViewStatus.setText(R.string.download_complete)
|
|
||||||
binding.progressBar.isIndeterminate = false
|
|
||||||
binding.progressBar.isVisible = false
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = false
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.Error -> {
|
|
||||||
binding.textViewStatus.setText(R.string.error_occurred)
|
|
||||||
binding.progressBar.isIndeterminate = false
|
|
||||||
binding.progressBar.isVisible = false
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.text = state.error.getDisplayMessage(context.resources)
|
|
||||||
binding.textViewDetails.isVisible = true
|
|
||||||
binding.buttonCancel.isVisible = state.canRetry
|
|
||||||
binding.buttonResume.isVisible = state.canRetry
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.PostProcessing -> {
|
|
||||||
binding.textViewStatus.setText(R.string.processing_)
|
|
||||||
binding.progressBar.isIndeterminate = true
|
|
||||||
binding.progressBar.isVisible = true
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = false
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.Preparing -> {
|
|
||||||
binding.textViewStatus.setText(R.string.preparing_)
|
|
||||||
binding.progressBar.isIndeterminate = true
|
|
||||||
binding.progressBar.isVisible = true
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = true
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.Progress -> {
|
|
||||||
binding.textViewStatus.setText(R.string.manga_downloading_)
|
|
||||||
binding.progressBar.isIndeterminate = false
|
|
||||||
binding.progressBar.isVisible = true
|
|
||||||
binding.progressBar.max = state.max
|
|
||||||
binding.progressBar.setProgressCompat(state.progress, true)
|
|
||||||
binding.textViewPercent.text = percentPattern.format((state.percent * 100f).format(1))
|
|
||||||
binding.textViewPercent.isVisible = true
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = true
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
is DownloadState.Queued -> {
|
|
||||||
binding.textViewStatus.setText(R.string.queued)
|
|
||||||
binding.progressBar.isIndeterminate = false
|
|
||||||
binding.progressBar.isVisible = false
|
|
||||||
binding.textViewPercent.isVisible = false
|
|
||||||
binding.textViewDetails.isVisible = false
|
|
||||||
binding.buttonCancel.isVisible = true
|
|
||||||
binding.buttonResume.isVisible = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.launchIn(lifecycleOwner.lifecycleScope)
|
|
||||||
}
|
|
||||||
|
|
||||||
onViewRecycled {
|
|
||||||
job?.cancel()
|
|
||||||
job = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,46 +0,0 @@
|
|||||||
package org.koitharu.kotatsu.download.ui
|
|
||||||
|
|
||||||
import androidx.lifecycle.LifecycleOwner
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import coil.ImageLoader
|
|
||||||
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
|
||||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
|
||||||
import org.koitharu.kotatsu.utils.progress.PausingProgressJob
|
|
||||||
|
|
||||||
typealias DownloadItem = PausingProgressJob<DownloadState>
|
|
||||||
|
|
||||||
class DownloadsAdapter(
|
|
||||||
lifecycleOwner: LifecycleOwner,
|
|
||||||
coil: ImageLoader,
|
|
||||||
) : AsyncListDifferDelegationAdapter<DownloadItem>(DiffCallback()) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
delegatesManager.addDelegate(downloadItemAD(lifecycleOwner, coil))
|
|
||||||
setHasStableIds(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return items[position].progressValue.startId.toLong()
|
|
||||||
}
|
|
||||||
|
|
||||||
private class DiffCallback : DiffUtil.ItemCallback<DownloadItem>() {
|
|
||||||
|
|
||||||
override fun areItemsTheSame(
|
|
||||||
oldItem: DownloadItem,
|
|
||||||
newItem: DownloadItem,
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.progressValue.startId == newItem.progressValue.startId
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(
|
|
||||||
oldItem: DownloadItem,
|
|
||||||
newItem: DownloadItem,
|
|
||||||
): Boolean {
|
|
||||||
return oldItem.progressValue == newItem.progressValue && oldItem.isPaused == newItem.isPaused
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getChangePayload(oldItem: DownloadItem, newItem: DownloadItem): Any {
|
|
||||||
return Unit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import androidx.core.view.isVisible
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.databinding.ItemDownloadBinding
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.util.format
|
||||||
|
import org.koitharu.kotatsu.utils.ext.disposeImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.enqueueWith
|
||||||
|
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
|
||||||
|
import org.koitharu.kotatsu.utils.ext.newImageRequest
|
||||||
|
import org.koitharu.kotatsu.utils.ext.source
|
||||||
|
|
||||||
|
fun downloadItemAD(
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
listener: DownloadItemListener,
|
||||||
|
) = adapterDelegateViewBinding<DownloadItemModel, ListModel, ItemDownloadBinding>(
|
||||||
|
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
|
||||||
|
) {
|
||||||
|
|
||||||
|
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
|
||||||
|
|
||||||
|
val clickListener = View.OnClickListener { v ->
|
||||||
|
when (v.id) {
|
||||||
|
R.id.button_cancel -> listener.onCancelClick(item)
|
||||||
|
R.id.button_resume -> listener.onResumeClick(item)
|
||||||
|
R.id.button_pause -> listener.onPauseClick(item)
|
||||||
|
else -> listener.onItemClick(item, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.buttonCancel.setOnClickListener(clickListener)
|
||||||
|
binding.buttonPause.setOnClickListener(clickListener)
|
||||||
|
binding.buttonResume.setOnClickListener(clickListener)
|
||||||
|
itemView.setOnClickListener(clickListener)
|
||||||
|
|
||||||
|
bind { payloads ->
|
||||||
|
binding.textViewTitle.text = item.manga.title
|
||||||
|
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
||||||
|
placeholder(R.drawable.ic_placeholder)
|
||||||
|
fallback(R.drawable.ic_placeholder)
|
||||||
|
error(R.drawable.ic_error_placeholder)
|
||||||
|
allowRgb565(true)
|
||||||
|
source(item.manga.source)
|
||||||
|
enqueueWith(coil)
|
||||||
|
}
|
||||||
|
when (item.workState) {
|
||||||
|
WorkInfo.State.ENQUEUED,
|
||||||
|
WorkInfo.State.BLOCKED -> {
|
||||||
|
binding.textViewStatus.setText(R.string.queued)
|
||||||
|
binding.progressBar.isIndeterminate = false
|
||||||
|
binding.progressBar.isVisible = false
|
||||||
|
binding.textViewPercent.isVisible = false
|
||||||
|
binding.textViewDetails.isVisible = false
|
||||||
|
binding.buttonCancel.isVisible = true
|
||||||
|
binding.buttonResume.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkInfo.State.RUNNING -> {
|
||||||
|
binding.textViewStatus.setText(R.string.manga_downloading_)
|
||||||
|
binding.progressBar.isIndeterminate = false
|
||||||
|
binding.progressBar.isVisible = true
|
||||||
|
binding.progressBar.max = item.max
|
||||||
|
binding.progressBar.setProgressCompat(item.progress, payloads.isNotEmpty())
|
||||||
|
binding.textViewPercent.text = percentPattern.format((item.percent * 100f).format(1))
|
||||||
|
binding.textViewPercent.isVisible = true
|
||||||
|
binding.textViewDetails.isVisible = false
|
||||||
|
binding.buttonCancel.isVisible = true
|
||||||
|
binding.buttonResume.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkInfo.State.SUCCEEDED -> {
|
||||||
|
binding.textViewStatus.setText(R.string.download_complete)
|
||||||
|
binding.progressBar.isIndeterminate = false
|
||||||
|
binding.progressBar.isVisible = false
|
||||||
|
binding.textViewPercent.isVisible = false
|
||||||
|
binding.textViewDetails.isVisible = false
|
||||||
|
binding.buttonCancel.isVisible = false
|
||||||
|
binding.buttonResume.isVisible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkInfo.State.FAILED -> {
|
||||||
|
binding.textViewStatus.setText(R.string.error_occurred)
|
||||||
|
binding.progressBar.isIndeterminate = false
|
||||||
|
binding.progressBar.isVisible = false
|
||||||
|
binding.textViewPercent.isVisible = false
|
||||||
|
binding.textViewDetails.text = item.error?.getDisplayMessage(context.resources)
|
||||||
|
binding.textViewDetails.isVisible = true
|
||||||
|
binding.buttonCancel.isVisible = false
|
||||||
|
binding.buttonResume.isVisible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
WorkInfo.State.CANCELLED -> {
|
||||||
|
binding.textViewStatus.setText(R.string.canceled)
|
||||||
|
binding.progressBar.isIndeterminate = false
|
||||||
|
binding.progressBar.isVisible = false
|
||||||
|
binding.textViewPercent.isVisible = false
|
||||||
|
binding.textViewDetails.isVisible = false
|
||||||
|
binding.buttonCancel.isVisible = false
|
||||||
|
binding.buttonResume.isVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onViewRecycled {
|
||||||
|
binding.imageViewCover.disposeImageRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.OnListItemClickListener
|
||||||
|
|
||||||
|
interface DownloadItemListener : OnListItemClickListener<DownloadItemModel> {
|
||||||
|
|
||||||
|
fun onCancelClick(item: DownloadItemModel)
|
||||||
|
|
||||||
|
fun onPauseClick(item: DownloadItemModel)
|
||||||
|
|
||||||
|
fun onResumeClick(item: DownloadItemModel)
|
||||||
|
|
||||||
|
fun onRetryClick(item: DownloadItemModel)
|
||||||
|
}
|
||||||
@ -0,0 +1,25 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
data class DownloadItemModel(
|
||||||
|
val id: UUID,
|
||||||
|
val workState: WorkInfo.State,
|
||||||
|
val manga: Manga,
|
||||||
|
val error: Throwable?,
|
||||||
|
val max: Int,
|
||||||
|
val progress: Int,
|
||||||
|
val eta: Long,
|
||||||
|
val createdAt: Date,
|
||||||
|
) : ListModel {
|
||||||
|
|
||||||
|
val percent: Float
|
||||||
|
get() = if (max > 0) progress / max.toFloat() else 0f
|
||||||
|
|
||||||
|
val hasEta: Boolean
|
||||||
|
get() = eta > 0L
|
||||||
|
}
|
||||||
@ -0,0 +1,61 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import coil.ImageLoader
|
||||||
|
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
|
||||||
|
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.adapter.relatedDateItemAD
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import kotlin.jvm.internal.Intrinsics
|
||||||
|
|
||||||
|
class DownloadsAdapter(
|
||||||
|
lifecycleOwner: LifecycleOwner,
|
||||||
|
coil: ImageLoader,
|
||||||
|
listener: DownloadItemListener,
|
||||||
|
) : AsyncListDifferDelegationAdapter<ListModel>(DiffCallback()) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
delegatesManager.addDelegate(downloadItemAD(lifecycleOwner, coil, listener))
|
||||||
|
.addDelegate(loadingStateAD())
|
||||||
|
.addDelegate(emptyStateListAD(coil, lifecycleOwner, null))
|
||||||
|
.addDelegate(relatedDateItemAD())
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DiffCallback : DiffUtil.ItemCallback<ListModel>() {
|
||||||
|
|
||||||
|
override fun areItemsTheSame(oldItem: ListModel, newItem: ListModel) = when {
|
||||||
|
|
||||||
|
oldItem is DownloadItemModel && newItem is DownloadItemModel -> {
|
||||||
|
oldItem.id == newItem.id
|
||||||
|
}
|
||||||
|
|
||||||
|
oldItem is DateTimeAgo && newItem is DateTimeAgo -> {
|
||||||
|
oldItem == newItem
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> oldItem.javaClass == newItem.javaClass
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ListModel, newItem: ListModel): Boolean {
|
||||||
|
return Intrinsics.areEqual(oldItem, newItem)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItem: ListModel, newItem: ListModel): Any? {
|
||||||
|
return when (newItem) {
|
||||||
|
is DownloadItemModel -> {
|
||||||
|
oldItem as DownloadItemModel
|
||||||
|
if (oldItem.workState == newItem.workState) {
|
||||||
|
Unit
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> super.getChangePayload(oldItem, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,108 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.list
|
||||||
|
|
||||||
|
import androidx.collection.LongSparseArray
|
||||||
|
import androidx.collection.getOrElse
|
||||||
|
import androidx.collection.set
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.mapLatest
|
||||||
|
import org.koitharu.kotatsu.R
|
||||||
|
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||||
|
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||||
|
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||||
|
import org.koitharu.kotatsu.download.domain.DownloadState2
|
||||||
|
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||||
|
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||||
|
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class DownloadsViewModel @Inject constructor(
|
||||||
|
private val workScheduler: DownloadWorker.Scheduler,
|
||||||
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
|
) : BaseViewModel() {
|
||||||
|
|
||||||
|
private val mangaCache = LongSparseArray<Manga>()
|
||||||
|
|
||||||
|
val items = workScheduler.observeWorks()
|
||||||
|
.mapLatest { list ->
|
||||||
|
list.mapList()
|
||||||
|
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||||
|
|
||||||
|
private suspend fun List<WorkInfo>.mapList(): List<ListModel> {
|
||||||
|
val destination = ArrayList<ListModel>((size * 1.4).toInt())
|
||||||
|
var prevDate: DateTimeAgo? = null
|
||||||
|
for (item in this) {
|
||||||
|
val model = item.toUiModel() ?: continue
|
||||||
|
val date = timeAgo(model.createdAt)
|
||||||
|
if (prevDate != date) {
|
||||||
|
destination += date
|
||||||
|
}
|
||||||
|
prevDate = date
|
||||||
|
destination += model
|
||||||
|
}
|
||||||
|
if (destination.isEmpty()) {
|
||||||
|
destination.add(
|
||||||
|
EmptyState(
|
||||||
|
icon = R.drawable.ic_empty_common,
|
||||||
|
textPrimary = R.string.text_downloads_holder,
|
||||||
|
textSecondary = 0,
|
||||||
|
actionStringRes = 0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return destination
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun WorkInfo.toUiModel(): DownloadItemModel? {
|
||||||
|
val workData = if (progress != Data.EMPTY) progress else outputData
|
||||||
|
val mangaId = DownloadState2.getMangaId(workData)
|
||||||
|
if (mangaId == 0L) return null
|
||||||
|
val manga = mangaCache.getOrElse(mangaId) {
|
||||||
|
mangaDataRepository.findMangaById(mangaId)?.also { mangaCache[mangaId] = it } ?: return null
|
||||||
|
}
|
||||||
|
return DownloadItemModel(
|
||||||
|
id = id,
|
||||||
|
workState = state,
|
||||||
|
manga = manga,
|
||||||
|
error = null,
|
||||||
|
max = DownloadState2.getMax(workData),
|
||||||
|
progress = DownloadState2.getProgress(workData),
|
||||||
|
eta = DownloadState2.getEta(workData),
|
||||||
|
createdAt = DownloadState2.getTimestamp(workData),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(id: UUID) {
|
||||||
|
launchJob(Dispatchers.Default) {
|
||||||
|
workScheduler.cancel(id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restart(id: UUID) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun timeAgo(date: Date): DateTimeAgo {
|
||||||
|
val diff = (System.currentTimeMillis() - date.time).coerceAtLeast(0L)
|
||||||
|
val diffMinutes = TimeUnit.MILLISECONDS.toMinutes(diff).toInt()
|
||||||
|
val diffDays = -date.daysDiff(System.currentTimeMillis())
|
||||||
|
return when {
|
||||||
|
diffMinutes < 3 -> DateTimeAgo.JustNow
|
||||||
|
diffDays < 1 -> DateTimeAgo.Today
|
||||||
|
diffDays == 1 -> DateTimeAgo.Yesterday
|
||||||
|
diffDays < 6 -> DateTimeAgo.DaysAgo(diffDays)
|
||||||
|
else -> DateTimeAgo.Absolute(date)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,66 @@
|
|||||||
|
package org.koitharu.kotatsu.download.ui.worker
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.PatternMatcher
|
||||||
|
import androidx.core.app.PendingIntentCompat
|
||||||
|
import org.koitharu.kotatsu.download.ui.service.PausingHandle
|
||||||
|
import org.koitharu.kotatsu.utils.ext.toUUIDOrNull
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
class PausingReceiver(
|
||||||
|
private val id: UUID,
|
||||||
|
private val pausingHandle: PausingHandle,
|
||||||
|
) : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent?) {
|
||||||
|
val uuid = intent?.getStringExtra(EXTRA_UUID)?.toUUIDOrNull()
|
||||||
|
assert(uuid == id)
|
||||||
|
when (intent?.action) {
|
||||||
|
ACTION_RESUME -> pausingHandle.resume()
|
||||||
|
ACTION_PAUSE -> pausingHandle.pause()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
private const val ACTION_PAUSE = "org.koitharu.kotatsu.download.PAUSE"
|
||||||
|
private const val ACTION_RESUME = "org.koitharu.kotatsu.download.RESUME"
|
||||||
|
private const val EXTRA_UUID = "uuid"
|
||||||
|
private const val SCHEME = "workuid"
|
||||||
|
|
||||||
|
fun createIntentFilter(id: UUID) = IntentFilter().apply {
|
||||||
|
addAction(ACTION_PAUSE)
|
||||||
|
addAction(ACTION_RESUME)
|
||||||
|
addDataScheme(SCHEME)
|
||||||
|
addDataPath(id.toString(), PatternMatcher.PATTERN_SIMPLE_GLOB)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPauseIntent(id: UUID) = Intent(ACTION_PAUSE)
|
||||||
|
.setData(Uri.parse("$SCHEME://$id"))
|
||||||
|
.putExtra(EXTRA_UUID, id.toString())
|
||||||
|
|
||||||
|
fun getResumeIntent(id: UUID) = Intent(ACTION_RESUME)
|
||||||
|
.setData(Uri.parse("$SCHEME://$id"))
|
||||||
|
.putExtra(EXTRA_UUID, id.toString())
|
||||||
|
|
||||||
|
fun createPausePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
getPauseIntent(id),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun createResumePendingIntent(context: Context, id: UUID) = PendingIntentCompat.getBroadcast(
|
||||||
|
context,
|
||||||
|
0,
|
||||||
|
getResumeIntent(id),
|
||||||
|
0,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24"
|
||||||
|
android:tint="#FFFFFF">
|
||||||
|
<group android:scaleX="1.44427"
|
||||||
|
android:scaleY="1.44427"
|
||||||
|
android:translateX="-5.33124"
|
||||||
|
android:translateY="-5.33124">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/white"
|
||||||
|
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
|
||||||
|
</group>
|
||||||
|
</vector>
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 166 B |
Binary file not shown.
|
After Width: | Height: | Size: 131 B |
Binary file not shown.
|
After Width: | Height: | Size: 203 B |
Binary file not shown.
|
After Width: | Height: | Size: 223 B |
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M14,19H18V5H14M6,19H10V5H6V19Z" />
|
||||||
|
</vector>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M8,5.14V19.14L19,12.14L8,5.14Z" />
|
||||||
|
</vector>
|
||||||
Loading…
Reference in New Issue