Show chapters in downloads list

remotes/Isira-Seneviratne/java.time
Koitharu 2 years ago
parent a7a9ee9d59
commit 91179ef901
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -79,7 +79,11 @@ sealed class DateTimeAgo {
private val day = date.daysDiff(0) private val day = date.daysDiff(0)
override fun format(resources: Resources): String { override fun format(resources: Resources): String {
return date.format("d MMMM") return if (date.time == 0L) {
resources.getString(R.string.unknown)
} else {
date.format("d MMMM")
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -0,0 +1,44 @@
package org.koitharu.kotatsu.core.ui.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.MotionEvent
import androidx.core.content.withStyledAttributes
import androidx.recyclerview.widget.RecyclerView
import org.koitharu.kotatsu.R
class NestedRecyclerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null
) : RecyclerView(context, attrs) {
private var maxHeight: Int = 0
init {
context.withStyledAttributes(attrs, R.styleable.NestedRecyclerView) {
maxHeight = getDimensionPixelSize(R.styleable.NestedRecyclerView_maxHeight, maxHeight)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(e: MotionEvent?): Boolean {
if (e?.actionMasked == MotionEvent.ACTION_UP) {
requestDisallowInterceptTouchEvent(false)
} else {
requestDisallowInterceptTouchEvent(true)
}
return super.onTouchEvent(e)
}
override fun onMeasure(widthSpec: Int, heightSpec: Int) {
super.onMeasure(
widthSpec,
if (maxHeight == 0) {
heightSpec
} else {
MeasureSpec.makeMeasureSpec(maxHeight, MeasureSpec.AT_MOST)
},
)
}
}

@ -1,12 +1,14 @@
package org.koitharu.kotatsu.core.util.ext package org.koitharu.kotatsu.core.util.ext
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.work.Data
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery import androidx.work.WorkQuery
import androidx.work.WorkRequest import androidx.work.WorkRequest
import androidx.work.await import androidx.work.await
import androidx.work.impl.WorkManagerImpl import androidx.work.impl.WorkManagerImpl
import androidx.work.impl.model.WorkSpec
import java.util.UUID import java.util.UUID
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException import kotlin.coroutines.resumeWithException
@ -69,5 +71,24 @@ suspend fun WorkManager.awaitUpdateWork(request: WorkRequest): WorkManager.Updat
return updateWork(request).await() return updateWork(request).await()
} }
@SuppressLint("RestrictedApi")
suspend fun WorkManager.getWorkSpec(id: UUID): WorkSpec? = suspendCoroutine { cont ->
workManagerImpl.workTaskExecutor.executeOnTaskThread {
try {
val spec = workManagerImpl.workDatabase.workSpecDao().getWorkSpec(id.toString())
cont.resume(spec)
} catch (e: Exception) {
cont.resumeWithException(e)
}
}
}
@SuppressLint("RestrictedApi")
suspend fun WorkManager.getWorkInputData(id: UUID): Data? = getWorkSpec(id)?.input
val Data.isEmpty: Boolean
get() = this == Data.EMPTY
private val WorkManager.workManagerImpl private val WorkManager.workManagerImpl
@SuppressLint("RestrictedApi") inline get() = this as WorkManagerImpl @SuppressLint("RestrictedApi") inline get() = this as WorkManagerImpl

@ -53,7 +53,7 @@ data class DownloadState(
private const val DATA_PROGRESS = "progress" private const val DATA_PROGRESS = "progress"
private const val DATA_CHAPTERS = "chapter_cnt" private const val DATA_CHAPTERS = "chapter_cnt"
private const val DATA_ETA = "eta" private const val DATA_ETA = "eta"
private const val DATA_TIMESTAMP = "timestamp" const val DATA_TIMESTAMP = "timestamp"
private const val DATA_ERROR = "error" private const val DATA_ERROR = "error"
private const val DATA_INDETERMINATE = "indeterminate" private const val DATA_INDETERMINATE = "indeterminate"
private const val DATA_PAUSED = "paused" private const val DATA_PAUSED = "paused"

@ -1,23 +1,31 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import android.transition.TransitionManager
import android.view.View import android.view.View
import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.ImageLoader import coil.ImageLoader
import coil.request.SuccessResult import coil.request.SuccessResult
import coil.util.CoilUtils import coil.util.CoilUtils
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.ui.BaseListAdapter
import org.koitharu.kotatsu.core.ui.image.TrimTransformation import org.koitharu.kotatsu.core.ui.image.TrimTransformation
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
import org.koitharu.kotatsu.core.util.ext.isAnimationsEnabled
import org.koitharu.kotatsu.core.util.ext.newImageRequest import org.koitharu.kotatsu.core.util.ext.newImageRequest
import org.koitharu.kotatsu.core.util.ext.source import org.koitharu.kotatsu.core.util.ext.source
import org.koitharu.kotatsu.core.util.ext.textAndVisible import org.koitharu.kotatsu.core.util.ext.textAndVisible
import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.list.chapters.downloadChapterAD
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
@ -30,7 +38,7 @@ fun downloadItemAD(
) { ) {
val percentPattern = context.resources.getString(R.string.percent_string_pattern) val percentPattern = context.resources.getString(R.string.percent_string_pattern)
// val expandIcon = ContextCompat.getDrawable(context, R.drawable.ic_expand_collapse) var chaptersJob: Job? = null
val clickListener = object : View.OnClickListener, View.OnLongClickListener { val clickListener = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
@ -38,6 +46,7 @@ fun downloadItemAD(
R.id.button_cancel -> listener.onCancelClick(item) R.id.button_cancel -> listener.onCancelClick(item)
R.id.button_resume -> listener.onResumeClick(item) R.id.button_resume -> listener.onResumeClick(item)
R.id.button_pause -> listener.onPauseClick(item) R.id.button_pause -> listener.onPauseClick(item)
R.id.imageView_expand -> listener.onExpandClick(item)
else -> listener.onItemClick(item, v) else -> listener.onItemClick(item, v)
} }
} }
@ -46,31 +55,60 @@ fun downloadItemAD(
return listener.onItemLongClick(item, v) return listener.onItemLongClick(item, v)
} }
} }
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
binding.recyclerViewChapters.adapter = chaptersAdapter
binding.buttonCancel.setOnClickListener(clickListener) binding.buttonCancel.setOnClickListener(clickListener)
binding.buttonPause.setOnClickListener(clickListener) binding.buttonPause.setOnClickListener(clickListener)
binding.buttonResume.setOnClickListener(clickListener) binding.buttonResume.setOnClickListener(clickListener)
binding.imageViewExpand.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener) itemView.setOnClickListener(clickListener)
itemView.setOnLongClickListener(clickListener) itemView.setOnLongClickListener(clickListener)
bind { payloads -> fun scrollToCurrentChapter() {
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) { val rv = binding.recyclerViewChapters
TransitionManager.beginDelayedTransition(binding.constraintLayout) if (!rv.isVisible) {
return
}
val chapters = chaptersAdapter.items
if (chapters.isEmpty()) {
return
} }
binding.textViewTitle.text = item.manga.title val targetPos = item.chaptersDownloaded.coerceIn(chapters.indices)
(rv.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(targetPos, rv.height / 3)
}
bind { payloads ->
binding.textViewTitle.text = item.manga?.title ?: getString(R.string.unknown)
if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) { if ((CoilUtils.result(binding.imageViewCover) as? SuccessResult)?.memoryCacheKey != item.coverCacheKey) {
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply { binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga?.coverUrl)?.apply {
placeholder(R.drawable.ic_placeholder) placeholder(R.drawable.ic_placeholder)
fallback(R.drawable.ic_placeholder) fallback(R.drawable.ic_placeholder)
error(R.drawable.ic_error_placeholder) error(R.drawable.ic_error_placeholder)
allowRgb565(true) allowRgb565(true)
transformations(TrimTransformation()) transformations(TrimTransformation())
memoryCacheKey(item.coverCacheKey) memoryCacheKey(item.coverCacheKey)
source(item.manga.source) source(item.manga?.source)
enqueueWith(coil) enqueueWith(coil)
} }
} }
// binding.textViewTitle.isChecked = item.isExpanded if (chaptersJob == null || payloads.isEmpty()) {
// binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null chaptersJob?.cancel()
chaptersJob = lifecycleOwner.lifecycleScope.launch(start = CoroutineStart.UNDISPATCHED) {
item.chapters.collect { chapters ->
binding.imageViewExpand.isGone = chapters.isNullOrEmpty()
chaptersAdapter.emit(chapters)
scrollToCurrentChapter()
}
}
} else if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads) {
binding.recyclerViewChapters.post {
scrollToCurrentChapter()
}
}
binding.imageViewExpand.isChecked = item.isExpanded
binding.recyclerViewChapters.isVisible = item.isExpanded
when (item.workState) { when (item.workState) {
WorkInfo.State.ENQUEUED, WorkInfo.State.ENQUEUED,
WorkInfo.State.BLOCKED -> { WorkInfo.State.BLOCKED -> {

@ -9,4 +9,6 @@ interface DownloadItemListener : OnListItemClickListener<DownloadItemModel> {
fun onPauseClick(item: DownloadItemModel) fun onPauseClick(item: DownloadItemModel)
fun onResumeClick(item: DownloadItemModel) fun onResumeClick(item: DownloadItemModel)
fun onExpandClick(item: DownloadItemModel)
} }

@ -3,6 +3,8 @@ package org.koitharu.kotatsu.download.ui.list
import android.text.format.DateUtils import android.text.format.DateUtils
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.memory.MemoryCache import coil.memory.MemoryCache
import kotlinx.coroutines.flow.StateFlow
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
@ -14,7 +16,7 @@ data class DownloadItemModel(
val workState: WorkInfo.State, val workState: WorkInfo.State,
val isIndeterminate: Boolean, val isIndeterminate: Boolean,
val isPaused: Boolean, val isPaused: Boolean,
val manga: Manga, val manga: Manga?,
val error: String?, val error: String?,
val max: Int, val max: Int,
val progress: Int, val progress: Int,
@ -22,9 +24,10 @@ data class DownloadItemModel(
val timestamp: Date, val timestamp: Date,
val chaptersDownloaded: Int, val chaptersDownloaded: Int,
val isExpanded: Boolean, val isExpanded: Boolean,
val chapters: StateFlow<List<DownloadChapter>?>,
) : ListModel, Comparable<DownloadItemModel> { ) : ListModel, Comparable<DownloadItemModel> {
val coverCacheKey = MemoryCache.Key(manga.coverUrl, mapOf("dl" to "1")) val coverCacheKey = MemoryCache.Key(manga?.coverUrl.orEmpty(), mapOf("dl" to "1"))
val percent: Float val percent: Float
get() = if (max > 0) progress / max.toFloat() else 0f get() = if (max > 0) progress / max.toFloat() else 0f
@ -38,9 +41,6 @@ data class DownloadItemModel(
val canResume: Boolean val canResume: Boolean
get() = workState == WorkInfo.State.RUNNING && isPaused get() = workState == WorkInfo.State.RUNNING && isPaused
val isExpandable: Boolean
get() = false // TODO
fun getEtaString(): CharSequence? = if (hasEta) { fun getEtaString(): CharSequence? = if (hasEta) {
DateUtils.getRelativeTimeSpanString( DateUtils.getRelativeTimeSpanString(
eta, eta,

@ -84,17 +84,19 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
if (selectionController.onItemClick(item.id.mostSignificantBits)) { if (selectionController.onItemClick(item.id.mostSignificantBits)) {
return return
} }
if (item.isExpandable) { startActivity(DetailsActivity.newIntent(view.context, item.manga ?: return))
viewModel.expandCollapse(item)
} else {
startActivity(DetailsActivity.newIntent(view.context, item.manga))
}
} }
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean { override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {
return selectionController.onItemLongClick(item.id.mostSignificantBits) return selectionController.onItemLongClick(item.id.mostSignificantBits)
} }
override fun onExpandClick(item: DownloadItemModel) {
if (!selectionController.onItemClick(item.id.mostSignificantBits)) {
viewModel.expandCollapse(item)
}
}
override fun onCancelClick(item: DownloadItemModel) { override fun onCancelClick(item: DownloadItemModel) {
viewModel.cancel(item.id) viewModel.cancel(item.id)
} }

@ -1,16 +1,19 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import androidx.collection.ArrayMap
import androidx.collection.LongSparseArray import androidx.collection.LongSparseArray
import androidx.collection.getOrElse import androidx.collection.getOrElse
import androidx.collection.set import androidx.collection.set
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.work.Data
import androidx.work.WorkInfo import androidx.work.WorkInfo
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -27,12 +30,17 @@ import org.koitharu.kotatsu.core.ui.util.ReversibleAction
import org.koitharu.kotatsu.core.util.ext.MutableEventFlow import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.ext.daysDiff import org.koitharu.kotatsu.core.util.ext.daysDiff
import org.koitharu.kotatsu.core.util.ext.isEmpty
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
import org.koitharu.kotatsu.list.ui.model.EmptyState import org.koitharu.kotatsu.list.ui.model.EmptyState
import org.koitharu.kotatsu.list.ui.model.ListHeader import org.koitharu.kotatsu.list.ui.model.ListHeader
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
import org.koitharu.kotatsu.list.ui.model.LoadingState import org.koitharu.kotatsu.list.ui.model.LoadingState
import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.mapToSet import org.koitharu.kotatsu.parsers.util.mapToSet
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@ -47,11 +55,15 @@ class DownloadsViewModel @Inject constructor(
private val workScheduler: DownloadWorker.Scheduler, private val workScheduler: DownloadWorker.Scheduler,
private val mangaDataRepository: MangaDataRepository, private val mangaDataRepository: MangaDataRepository,
private val mangaRepositoryFactory: MangaRepository.Factory, private val mangaRepositoryFactory: MangaRepository.Factory,
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
private val localMangaRepository: LocalMangaRepository,
) : BaseViewModel() { ) : BaseViewModel() {
private val mangaCache = LongSparseArray<Manga>() private val mangaCache = LongSparseArray<Manga>()
private val cacheMutex = Mutex() private val cacheMutex = Mutex()
private val expanded = MutableStateFlow(emptySet<UUID>()) private val expanded = MutableStateFlow(emptySet<UUID>())
private val chaptersCache = ArrayMap<UUID, StateFlow<List<DownloadChapter>?>>()
private val works = combine( private val works = combine(
workScheduler.observeWorks(), workScheduler.observeWorks(),
expanded, expanded,
@ -234,10 +246,18 @@ class DownloadsViewModel @Inject constructor(
} }
private suspend fun WorkInfo.toUiModel(isExpanded: Boolean): DownloadItemModel? { private suspend fun WorkInfo.toUiModel(isExpanded: Boolean): DownloadItemModel? {
val workData = if (outputData == Data.EMPTY) progress else outputData val workData = outputData.takeUnless { it.isEmpty }
?: progress.takeUnless { it.isEmpty }
?: workScheduler.getInputData(id)
?: return null
val mangaId = DownloadState.getMangaId(workData) val mangaId = DownloadState.getMangaId(workData)
if (mangaId == 0L) return null if (mangaId == 0L) return null
val manga = getManga(mangaId) ?: return null val manga = getManga(mangaId) ?: return null
val chapters = synchronized(chaptersCache) {
chaptersCache.getOrPut(id) {
observeChapters(manga, id)
}
}
return DownloadItemModel( return DownloadItemModel(
id = id, id = id,
workState = state, workState = state,
@ -251,6 +271,7 @@ class DownloadsViewModel @Inject constructor(
timestamp = DownloadState.getTimestamp(workData), timestamp = DownloadState.getTimestamp(workData),
chaptersDownloaded = DownloadState.getDownloadedChapters(workData), chaptersDownloaded = DownloadState.getDownloadedChapters(workData),
isExpanded = isExpanded, isExpanded = isExpanded,
chapters = chapters,
) )
} }
@ -282,16 +303,42 @@ class DownloadsViewModel @Inject constructor(
} }
return cacheMutex.withLock { return cacheMutex.withLock {
mangaCache.getOrElse(mangaId) { mangaCache.getOrElse(mangaId) {
mangaDataRepository.findMangaById(mangaId)?.let { mangaDataRepository.findMangaById(mangaId)?.also {
tryLoad(it) ?: it
}?.also {
mangaCache[mangaId] = it mangaCache[mangaId] = it
} ?: return null } ?: return null
} }
} }
} }
private fun observeChapters(manga: Manga, workId: UUID): StateFlow<List<DownloadChapter>?> = flow {
val chapterIds = workScheduler.getInputChaptersIds(workId)?.toSet()
val chapters = (tryLoad(manga) ?: manga).chapters ?: return@flow
suspend fun mapChapters(): List<DownloadChapter> {
val size = chapterIds?.size ?: chapters.size
val localChapters =
localMangaRepository.findSavedManga(manga)?.manga?.chapters?.mapToSet { it.id }.orEmpty()
return chapters.mapNotNullTo(ArrayList(size)) {
if (chapterIds == null || it.id in chapterIds) {
DownloadChapter(
number = it.number,
name = it.name,
isDownloaded = it.id in localChapters,
)
} else {
null
}
}
}
emit(mapChapters())
localStorageChanges.collect {
if (it?.manga?.id == manga.id) {
emit(mapChapters())
}
}
}.stateIn(viewModelScope, SharingStarted.Eagerly, null)
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable { private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).peekDetails(manga) (mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga)
}.getOrNull() }.getOrNull()
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.download.ui.list.chapters package org.koitharu.kotatsu.download.ui.list.chapters
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
import org.koitharu.kotatsu.list.ui.model.ListModel import org.koitharu.kotatsu.list.ui.model.ListModel
data class DownloadChapter( data class DownloadChapter(
@ -11,4 +12,12 @@ data class DownloadChapter(
override fun areItemsTheSame(other: ListModel): Boolean { override fun areItemsTheSame(other: ListModel): Boolean {
return other is DownloadChapter && other.name == name return other is DownloadChapter && other.name == name
} }
override fun getChangePayload(previousState: ListModel): Any? {
return if (previousState is DownloadChapter && previousState.name == name && previousState.number == number) {
ListModelDiffCallback.PAYLOAD_PROGRESS_CHANGED
} else {
super.getChangePayload(previousState)
}
}
} }

@ -1,5 +1,6 @@
package org.koitharu.kotatsu.download.ui.worker package org.koitharu.kotatsu.download.ui.worker
import android.annotation.SuppressLint
import android.app.NotificationManager import android.app.NotificationManager
import android.content.Context import android.content.Context
import android.content.pm.ServiceInfo import android.content.pm.ServiceInfo
@ -55,6 +56,8 @@ import org.koitharu.kotatsu.core.util.ext.deleteAwait
import org.koitharu.kotatsu.core.util.ext.deleteWork import org.koitharu.kotatsu.core.util.ext.deleteWork
import org.koitharu.kotatsu.core.util.ext.deleteWorks import org.koitharu.kotatsu.core.util.ext.deleteWorks
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getWorkInputData
import org.koitharu.kotatsu.core.util.ext.getWorkSpec
import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty import org.koitharu.kotatsu.core.util.ext.ifNullOrEmpty
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
@ -121,7 +124,9 @@ class DownloadWorker @AssistedInject constructor(
val notification = notificationFactory.create(currentState.copy(isStopped = true)) val notification = notificationFactory.create(currentState.copy(isStopped = true))
notificationManager.notify(id.hashCode(), notification) notificationManager.notify(id.hashCode(), notification)
} }
throw e Result.failure(
currentState.copy(eta = -1L).toWorkData(),
)
} catch (e: IOException) { } catch (e: IOException) {
e.printStackTraceDebug() e.printStackTraceDebug()
Result.retry() Result.retry()
@ -417,6 +422,19 @@ class DownloadWorker @AssistedInject constructor(
fun observeWorks(): Flow<List<WorkInfo>> = workManager fun observeWorks(): Flow<List<WorkInfo>> = workManager
.getWorkInfosByTagFlow(TAG) .getWorkInfosByTagFlow(TAG)
@SuppressLint("RestrictedApi")
suspend fun getInputData(id: UUID): Data? {
val spec = workManager.getWorkSpec(id) ?: return null
return Data.Builder()
.putAll(spec.input)
.putLong(DownloadState.DATA_TIMESTAMP, spec.scheduleRequestedAt)
.build()
}
suspend fun getInputChaptersIds(workId: UUID): LongArray? {
return workManager.getWorkInputData(workId)?.getLongArray(CHAPTERS_IDS)?.takeUnless { it.isEmpty() }
}
suspend fun cancel(id: UUID) { suspend fun cancel(id: UUID) {
workManager.cancelWorkById(id).await() workManager.cancelWorkById(id).await()
} }

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<shape
xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="16dp" />
<solid android:color="?m3ColorCardBackground" />
</shape>

@ -25,30 +25,43 @@
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium" app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium"
tools:src="@tools:sample/backgrounds/scenic" /> tools:src="@tools:sample/backgrounds/scenic" />
<CheckedTextView <TextView
android:id="@+id/textView_title" android:id="@+id/textView_title"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:ellipsize="end" android:ellipsize="end"
android:gravity="center_vertical" android:gravity="center_vertical"
android:singleLine="true" android:singleLine="true"
android:textAppearance="?attr/textAppearanceTitleSmall" android:textAppearance="?attr/textAppearanceTitleSmall"
app:drawableTint="?android:colorControlNormal" app:layout_constraintEnd_toStartOf="@id/imageView_expand"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/imageView_cover" app:layout_constraintStart_toEndOf="@id/imageView_cover"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:drawableEndCompat="@drawable/ic_expand_collapse" app:layout_goneMarginEnd="12dp"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<org.koitharu.kotatsu.core.ui.widgets.CheckableImageView
android:id="@+id/imageView_expand"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackgroundBorderless"
android:minWidth="?minTouchTargetSize"
android:minHeight="?minTouchTargetSize"
android:scaleType="center"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_expand_collapse"
app:tint="?colorControlActivated"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier_top" android:id="@+id/barrier_top"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
app:barrierDirection="bottom" app:barrierDirection="bottom"
app:constraint_referenced_ids="imageView_cover, textView_status" /> app:constraint_referenced_ids="imageView_cover,textView_status,imageView_expand" />
<com.google.android.material.progressindicator.LinearProgressIndicator <com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/progressBar" android:id="@+id/progressBar"
@ -63,6 +76,32 @@
app:trackColor="?android:colorBackground" app:trackColor="?android:colorBackground"
tools:progress="25" /> tools:progress="25" />
<org.koitharu.kotatsu.core.ui.widgets.NestedRecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/bg_card"
android:clipToOutline="true"
android:clipToPadding="false"
android:fadeScrollbars="false"
android:nestedScrollingEnabled="false"
android:orientation="vertical"
android:outlineProvider="background"
android:paddingVertical="8dp"
android:scrollbarStyle="insideOverlay"
android:scrollbars="vertical"
android:visibility="gone"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
app:maxHeight="240dp"
tools:listitem="@layout/item_chapter_download"
tools:visibility="visible" />
<TextView <TextView
android:id="@+id/textView_status" android:id="@+id/textView_status"
android:layout_width="0dp" android:layout_width="0dp"
@ -114,7 +153,7 @@
android:text="@string/pause" android:text="@string/pause"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_resume" app:layout_constraintEnd_toStartOf="@id/button_resume"
app:layout_constraintTop_toBottomOf="@id/progressBar" app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
tools:visibility="visible" /> tools:visibility="visible" />
<Button <Button
@ -127,7 +166,7 @@
android:text="@string/resume" android:text="@string/resume"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_cancel" app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/progressBar" /> app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters" />
<Button <Button
android:id="@+id/button_cancel" android:id="@+id/button_cancel"
@ -139,7 +178,7 @@
android:text="@android:string/cancel" android:text="@android:string/cancel"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar" app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

@ -157,4 +157,8 @@
<attr name="pieChartTextAmount" format="string"/> <attr name="pieChartTextAmount" format="string"/>
</declare-styleable> </declare-styleable>
<declare-styleable name="NestedRecyclerView">
<attr name="maxHeight" />
</declare-styleable>
</resources> </resources>

Loading…
Cancel
Save