diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt index 6411bb3ea..87720d0c6 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/model/DateTimeAgo.kt @@ -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 { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/NestedRecyclerView.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/NestedRecyclerView.kt new file mode 100644 index 000000000..5056e9c08 --- /dev/null +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/ui/widgets/NestedRecyclerView.kt @@ -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) + }, + ) + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt index b46c13571..a879f2f39 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/WorkManager.kt @@ -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 diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt index d1f1539ec..d7ba74fc3 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/domain/DownloadState.kt @@ -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" diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt index b9f9a914f..0dc006845 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemAD.kt @@ -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() + .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 -> { diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt index d72a541c2..547c64381 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemListener.kt @@ -9,4 +9,6 @@ interface DownloadItemListener : OnListItemClickListener { fun onPauseClick(item: DownloadItemModel) fun onResumeClick(item: DownloadItemModel) + + fun onExpandClick(item: DownloadItemModel) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt index fe6bd5d94..5a19a7aa4 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadItemModel.kt @@ -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?>, ) : ListModel, Comparable { - 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, diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt index e7487bbc8..6b0051b2e 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt @@ -84,17 +84,19 @@ class DownloadsActivity : BaseActivity(), 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) } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt index 4eb4cdb95..987257c8d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -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, + private val localMangaRepository: LocalMangaRepository, ) : BaseViewModel() { private val mangaCache = LongSparseArray() private val cacheMutex = Mutex() private val expanded = MutableStateFlow(emptySet()) + private val chaptersCache = ArrayMap?>>() + 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?> = flow { + val chapterIds = workScheduler.getInputChaptersIds(workId)?.toSet() + val chapters = (tryLoad(manga) ?: manga).chapters ?: return@flow + + suspend fun mapChapters(): List { + 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() } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/chapters/DownloadChapter.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/chapters/DownloadChapter.kt index 378a28b42..a9e2b3577 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/chapters/DownloadChapter.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/list/chapters/DownloadChapter.kt @@ -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) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt index fbdc8cfa3..fe7e50dea 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/download/ui/worker/DownloadWorker.kt @@ -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> = 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() } diff --git a/app/src/main/res/drawable/bg_card.xml b/app/src/main/res/drawable/bg_card.xml new file mode 100644 index 000000000..6da4fae05 --- /dev/null +++ b/app/src/main/res/drawable/bg_card.xml @@ -0,0 +1,8 @@ + + + + + + diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml index 24278bb15..2f6c56686 100644 --- a/app/src/main/res/layout/item_download.xml +++ b/app/src/main/res/layout/item_download.xml @@ -25,30 +25,43 @@ app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Kotatsu.Cover.Medium" tools:src="@tools:sample/backgrounds/scenic" /> - + + + app:constraint_referenced_ids="imageView_cover,textView_status,imageView_expand" /> + +