Show chapters in downloads list
This commit is contained in:
@@ -79,7 +79,11 @@ sealed class DateTimeAgo {
|
||||
private val day = date.daysDiff(0)
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.work.Data
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.WorkRequest
|
||||
import androidx.work.await
|
||||
import androidx.work.impl.WorkManagerImpl
|
||||
import androidx.work.impl.model.WorkSpec
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
@@ -69,5 +71,24 @@ suspend fun WorkManager.awaitUpdateWork(request: WorkRequest): WorkManager.Updat
|
||||
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
|
||||
@SuppressLint("RestrictedApi") inline get() = this as WorkManagerImpl
|
||||
|
||||
@@ -53,7 +53,7 @@ data class DownloadState(
|
||||
private const val DATA_PROGRESS = "progress"
|
||||
private const val DATA_CHAPTERS = "chapter_cnt"
|
||||
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_INDETERMINATE = "indeterminate"
|
||||
private const val DATA_PAUSED = "paused"
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
package org.koitharu.kotatsu.download.ui.list
|
||||
|
||||
import android.transition.TransitionManager
|
||||
import android.view.View
|
||||
import androidx.core.view.isGone
|
||||
import androidx.core.view.isVisible
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.work.WorkInfo
|
||||
import coil.ImageLoader
|
||||
import coil.request.SuccessResult
|
||||
import coil.util.CoilUtils
|
||||
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.core.ui.BaseListAdapter
|
||||
import org.koitharu.kotatsu.core.ui.image.TrimTransformation
|
||||
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.source
|
||||
import org.koitharu.kotatsu.core.util.ext.textAndVisible
|
||||
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.adapter.ListItemType
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.util.format
|
||||
|
||||
@@ -30,7 +38,7 @@ fun downloadItemAD(
|
||||
) {
|
||||
|
||||
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 {
|
||||
override fun onClick(v: View) {
|
||||
@@ -38,6 +46,7 @@ fun downloadItemAD(
|
||||
R.id.button_cancel -> listener.onCancelClick(item)
|
||||
R.id.button_resume -> listener.onResumeClick(item)
|
||||
R.id.button_pause -> listener.onPauseClick(item)
|
||||
R.id.imageView_expand -> listener.onExpandClick(item)
|
||||
else -> listener.onItemClick(item, v)
|
||||
}
|
||||
}
|
||||
@@ -46,31 +55,60 @@ fun downloadItemAD(
|
||||
return listener.onItemLongClick(item, v)
|
||||
}
|
||||
}
|
||||
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
|
||||
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
|
||||
|
||||
binding.recyclerViewChapters.adapter = chaptersAdapter
|
||||
binding.buttonCancel.setOnClickListener(clickListener)
|
||||
binding.buttonPause.setOnClickListener(clickListener)
|
||||
binding.buttonResume.setOnClickListener(clickListener)
|
||||
binding.imageViewExpand.setOnClickListener(clickListener)
|
||||
itemView.setOnClickListener(clickListener)
|
||||
itemView.setOnLongClickListener(clickListener)
|
||||
|
||||
bind { payloads ->
|
||||
if (ListModelDiffCallback.PAYLOAD_CHECKED_CHANGED in payloads && context.isAnimationsEnabled) {
|
||||
TransitionManager.beginDelayedTransition(binding.constraintLayout)
|
||||
fun scrollToCurrentChapter() {
|
||||
val rv = binding.recyclerViewChapters
|
||||
if (!rv.isVisible) {
|
||||
return
|
||||
}
|
||||
binding.textViewTitle.text = item.manga.title
|
||||
val chapters = chaptersAdapter.items
|
||||
if (chapters.isEmpty()) {
|
||||
return
|
||||
}
|
||||
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) {
|
||||
binding.imageViewCover.newImageRequest(lifecycleOwner, item.manga.coverUrl)?.apply {
|
||||
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)
|
||||
transformations(TrimTransformation())
|
||||
memoryCacheKey(item.coverCacheKey)
|
||||
source(item.manga.source)
|
||||
source(item.manga?.source)
|
||||
enqueueWith(coil)
|
||||
}
|
||||
}
|
||||
// binding.textViewTitle.isChecked = item.isExpanded
|
||||
// binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
|
||||
if (chaptersJob == null || payloads.isEmpty()) {
|
||||
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) {
|
||||
WorkInfo.State.ENQUEUED,
|
||||
WorkInfo.State.BLOCKED -> {
|
||||
|
||||
@@ -9,4 +9,6 @@ interface DownloadItemListener : OnListItemClickListener<DownloadItemModel> {
|
||||
fun onPauseClick(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 androidx.work.WorkInfo
|
||||
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.model.ListModel
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
@@ -14,7 +16,7 @@ data class DownloadItemModel(
|
||||
val workState: WorkInfo.State,
|
||||
val isIndeterminate: Boolean,
|
||||
val isPaused: Boolean,
|
||||
val manga: Manga,
|
||||
val manga: Manga?,
|
||||
val error: String?,
|
||||
val max: Int,
|
||||
val progress: Int,
|
||||
@@ -22,9 +24,10 @@ data class DownloadItemModel(
|
||||
val timestamp: Date,
|
||||
val chaptersDownloaded: Int,
|
||||
val isExpanded: Boolean,
|
||||
val chapters: StateFlow<List<DownloadChapter>?>,
|
||||
) : 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
|
||||
get() = if (max > 0) progress / max.toFloat() else 0f
|
||||
@@ -38,9 +41,6 @@ data class DownloadItemModel(
|
||||
val canResume: Boolean
|
||||
get() = workState == WorkInfo.State.RUNNING && isPaused
|
||||
|
||||
val isExpandable: Boolean
|
||||
get() = false // TODO
|
||||
|
||||
fun getEtaString(): CharSequence? = if (hasEta) {
|
||||
DateUtils.getRelativeTimeSpanString(
|
||||
eta,
|
||||
|
||||
@@ -84,17 +84,19 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
||||
if (selectionController.onItemClick(item.id.mostSignificantBits)) {
|
||||
return
|
||||
}
|
||||
if (item.isExpandable) {
|
||||
viewModel.expandCollapse(item)
|
||||
} else {
|
||||
startActivity(DetailsActivity.newIntent(view.context, item.manga))
|
||||
}
|
||||
startActivity(DetailsActivity.newIntent(view.context, item.manga ?: return))
|
||||
}
|
||||
|
||||
override fun onItemLongClick(item: DownloadItemModel, view: View): Boolean {
|
||||
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) {
|
||||
viewModel.cancel(item.id)
|
||||
}
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
package org.koitharu.kotatsu.download.ui.list
|
||||
|
||||
import androidx.collection.ArrayMap
|
||||
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.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
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.call
|
||||
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.ui.list.chapters.DownloadChapter
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
import org.koitharu.kotatsu.list.ui.model.EmptyState
|
||||
import org.koitharu.kotatsu.list.ui.model.ListHeader
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
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.util.mapToSet
|
||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||
@@ -47,11 +55,15 @@ class DownloadsViewModel @Inject constructor(
|
||||
private val workScheduler: DownloadWorker.Scheduler,
|
||||
private val mangaDataRepository: MangaDataRepository,
|
||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||
private val localMangaRepository: LocalMangaRepository,
|
||||
) : BaseViewModel() {
|
||||
|
||||
private val mangaCache = LongSparseArray<Manga>()
|
||||
private val cacheMutex = Mutex()
|
||||
private val expanded = MutableStateFlow(emptySet<UUID>())
|
||||
private val chaptersCache = ArrayMap<UUID, StateFlow<List<DownloadChapter>?>>()
|
||||
|
||||
private val works = combine(
|
||||
workScheduler.observeWorks(),
|
||||
expanded,
|
||||
@@ -234,10 +246,18 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
|
||||
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)
|
||||
if (mangaId == 0L) return null
|
||||
val manga = getManga(mangaId) ?: return null
|
||||
val chapters = synchronized(chaptersCache) {
|
||||
chaptersCache.getOrPut(id) {
|
||||
observeChapters(manga, id)
|
||||
}
|
||||
}
|
||||
return DownloadItemModel(
|
||||
id = id,
|
||||
workState = state,
|
||||
@@ -251,6 +271,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
timestamp = DownloadState.getTimestamp(workData),
|
||||
chaptersDownloaded = DownloadState.getDownloadedChapters(workData),
|
||||
isExpanded = isExpanded,
|
||||
chapters = chapters,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -282,16 +303,42 @@ class DownloadsViewModel @Inject constructor(
|
||||
}
|
||||
return cacheMutex.withLock {
|
||||
mangaCache.getOrElse(mangaId) {
|
||||
mangaDataRepository.findMangaById(mangaId)?.let {
|
||||
tryLoad(it) ?: it
|
||||
}?.also {
|
||||
mangaDataRepository.findMangaById(mangaId)?.also {
|
||||
mangaCache[mangaId] = it
|
||||
} ?: 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 {
|
||||
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).peekDetails(manga)
|
||||
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).getDetails(manga)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.download.ui.list.chapters
|
||||
|
||||
import org.koitharu.kotatsu.list.ui.ListModelDiffCallback
|
||||
import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
|
||||
data class DownloadChapter(
|
||||
@@ -11,4 +12,12 @@ data class DownloadChapter(
|
||||
override fun areItemsTheSame(other: ListModel): Boolean {
|
||||
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
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
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.deleteWorks
|
||||
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.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.util.ext.writeAllCancellable
|
||||
@@ -121,7 +124,9 @@ class DownloadWorker @AssistedInject constructor(
|
||||
val notification = notificationFactory.create(currentState.copy(isStopped = true))
|
||||
notificationManager.notify(id.hashCode(), notification)
|
||||
}
|
||||
throw e
|
||||
Result.failure(
|
||||
currentState.copy(eta = -1L).toWorkData(),
|
||||
)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTraceDebug()
|
||||
Result.retry()
|
||||
@@ -417,6 +422,19 @@ class DownloadWorker @AssistedInject constructor(
|
||||
fun observeWorks(): Flow<List<WorkInfo>> = workManager
|
||||
.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) {
|
||||
workManager.cancelWorkById(id).await()
|
||||
}
|
||||
|
||||
8
app/src/main/res/drawable/bg_card.xml
Normal file
8
app/src/main/res/drawable/bg_card.xml
Normal file
@@ -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"
|
||||
tools:src="@tools:sample/backgrounds/scenic" />
|
||||
|
||||
<CheckedTextView
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center_vertical"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
app:drawableTint="?android:colorControlNormal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/imageView_expand"
|
||||
app:layout_constraintStart_toEndOf="@id/imageView_cover"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:drawableEndCompat="@drawable/ic_expand_collapse"
|
||||
app:layout_goneMarginEnd="12dp"
|
||||
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
|
||||
android:id="@+id/barrier_top"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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
|
||||
android:id="@+id/progressBar"
|
||||
@@ -63,6 +76,32 @@
|
||||
app:trackColor="?android:colorBackground"
|
||||
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
|
||||
android:id="@+id/textView_status"
|
||||
android:layout_width="0dp"
|
||||
@@ -114,7 +153,7 @@
|
||||
android:text="@string/pause"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_resume"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<Button
|
||||
@@ -127,7 +166,7 @@
|
||||
android:text="@string/resume"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toStartOf="@id/button_cancel"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar" />
|
||||
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_cancel"
|
||||
@@ -139,7 +178,7 @@
|
||||
android:text="@android:string/cancel"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/progressBar"
|
||||
app:layout_constraintTop_toBottomOf="@id/recyclerView_chapters"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
@@ -157,4 +157,8 @@
|
||||
<attr name="pieChartTextAmount" format="string"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="NestedRecyclerView">
|
||||
<attr name="maxHeight" />
|
||||
</declare-styleable>
|
||||
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user