Show chapters list in downloads

pull/529/head
Koitharu 3 years ago
parent e172d619a1
commit c0e94f8415
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -128,6 +128,10 @@ class RemoteMangaRepository(
return details.await() return details.await()
} }
suspend fun peekDetails(manga: Manga): Manga? {
return cache.getDetails(source, manga.url)
}
suspend fun find(manga: Manga): Manga? { suspend fun find(manga: Manga): Manga? {
val list = getList(0, manga.title) val list = getList(0, manga.title)
return list.find { x -> x.id == manga.id } return list.find { x -> x.id == manga.id }

@ -19,6 +19,7 @@ data class DownloadState(
val eta: Long = -1L, val eta: Long = -1L,
val localManga: LocalManga? = null, val localManga: LocalManga? = null,
val downloadedChapters: LongArray = LongArray(0), val downloadedChapters: LongArray = LongArray(0),
val scheduledChapters: LongArray = LongArray(0),
val timestamp: Long = System.currentTimeMillis(), val timestamp: Long = System.currentTimeMillis(),
) { ) {
@ -42,6 +43,7 @@ data class DownloadState(
.putLong(DATA_TIMESTAMP, timestamp) .putLong(DATA_TIMESTAMP, timestamp)
.putString(DATA_ERROR, error) .putString(DATA_ERROR, error)
.putLongArray(DATA_CHAPTERS, downloadedChapters) .putLongArray(DATA_CHAPTERS, downloadedChapters)
.putLongArray(DATA_CHAPTERS_SRC, scheduledChapters)
.putBoolean(DATA_INDETERMINATE, isIndeterminate) .putBoolean(DATA_INDETERMINATE, isIndeterminate)
.putBoolean(DATA_PAUSED, isPaused) .putBoolean(DATA_PAUSED, isPaused)
.build() .build()
@ -64,10 +66,13 @@ data class DownloadState(
if (eta != other.eta) return false if (eta != other.eta) return false
if (localManga != other.localManga) return false if (localManga != other.localManga) return false
if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false if (!downloadedChapters.contentEquals(other.downloadedChapters)) return false
if (!scheduledChapters.contentEquals(other.scheduledChapters)) return false
if (timestamp != other.timestamp) return false if (timestamp != other.timestamp) return false
if (max != other.max) return false if (max != other.max) return false
if (progress != other.progress) return false if (progress != other.progress) return false
return percent == other.percent if (percent != other.percent) return false
return true
} }
override fun hashCode(): Int { override fun hashCode(): Int {
@ -83,6 +88,7 @@ data class DownloadState(
result = 31 * result + eta.hashCode() result = 31 * result + eta.hashCode()
result = 31 * result + (localManga?.hashCode() ?: 0) result = 31 * result + (localManga?.hashCode() ?: 0)
result = 31 * result + downloadedChapters.contentHashCode() result = 31 * result + downloadedChapters.contentHashCode()
result = 31 * result + scheduledChapters.contentHashCode()
result = 31 * result + timestamp.hashCode() result = 31 * result + timestamp.hashCode()
result = 31 * result + max result = 31 * result + max
result = 31 * result + progress result = 31 * result + progress
@ -90,12 +96,14 @@ data class DownloadState(
return result return result
} }
companion object { companion object {
private const val DATA_MANGA_ID = "manga_id" private const val DATA_MANGA_ID = "manga_id"
private const val DATA_MAX = "max" private const val DATA_MAX = "max"
private const val DATA_PROGRESS = "progress" private const val DATA_PROGRESS = "progress"
private const val DATA_CHAPTERS = "chapter" private const val DATA_CHAPTERS = "chapter"
private const val DATA_CHAPTERS_SRC = "chapters_src"
private const val DATA_ETA = "eta" private const val DATA_ETA = "eta"
private const val DATA_TIMESTAMP = "timestamp" private const val DATA_TIMESTAMP = "timestamp"
private const val DATA_ERROR = "error" private const val DATA_ERROR = "error"
@ -119,5 +127,7 @@ data class DownloadState(
fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L)) fun getTimestamp(data: Data): Date = Date(data.getLong(DATA_TIMESTAMP, 0L))
fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0) fun getDownloadedChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS) ?: LongArray(0)
fun getScheduledChapters(data: Data): LongArray = data.getLongArray(DATA_CHAPTERS_SRC) ?: LongArray(0)
} }
} }

@ -1,18 +1,26 @@
package org.koitharu.kotatsu.download.ui.list package org.koitharu.kotatsu.download.ui.list
import android.view.View import android.view.View
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
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.drawableEnd
import org.koitharu.kotatsu.core.util.ext.enqueueWith import org.koitharu.kotatsu.core.util.ext.enqueueWith
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.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
@ -25,6 +33,9 @@ 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)
val chaptersAdapter = BaseListAdapter<DownloadChapter>()
.addDelegate(ListItemType.CHAPTER, downloadChapterAD())
val clickListener = object : View.OnClickListener, View.OnLongClickListener { val clickListener = object : View.OnClickListener, View.OnLongClickListener {
override fun onClick(v: View) { override fun onClick(v: View) {
@ -45,6 +56,8 @@ fun downloadItemAD(
binding.buttonResume.setOnClickListener(clickListener) binding.buttonResume.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener) itemView.setOnClickListener(clickListener)
itemView.setOnLongClickListener(clickListener) itemView.setOnLongClickListener(clickListener)
binding.recyclerViewChapters.addItemDecoration(DividerItemDecoration(context, RecyclerView.VERTICAL))
binding.recyclerViewChapters.adapter = chaptersAdapter
bind { payloads -> bind { payloads ->
binding.textViewTitle.text = item.manga.title binding.textViewTitle.text = item.manga.title
@ -57,6 +70,10 @@ fun downloadItemAD(
source(item.manga.source) source(item.manga.source)
enqueueWith(coil) enqueueWith(coil)
} }
binding.textViewTitle.isChecked = item.isExpanded
binding.textViewTitle.drawableEnd = if (item.isExpandable) expandIcon else null
binding.cardDetails.isVisible = item.isExpanded
chaptersAdapter.items = item.chapters
when (item.workState) { when (item.workState) {
WorkInfo.State.ENQUEUED, WorkInfo.State.ENQUEUED,
WorkInfo.State.BLOCKED -> { WorkInfo.State.BLOCKED -> {

@ -2,6 +2,7 @@ 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 org.koitharu.kotatsu.download.ui.list.chapters.DownloadChapter
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
import java.util.Date import java.util.Date
@ -19,6 +20,8 @@ data class DownloadItemModel(
val progress: Int, val progress: Int,
val eta: Long, val eta: Long,
val timestamp: Date, val timestamp: Date,
val chapters: List<DownloadChapter>,
val isExpanded: Boolean,
) : ListModel, Comparable<DownloadItemModel> { ) : ListModel, Comparable<DownloadItemModel> {
val percent: Float val percent: Float
@ -33,6 +36,9 @@ 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() = chapters.isNotEmpty()
fun getEtaString(): CharSequence? = if (hasEta) { fun getEtaString(): CharSequence? = if (hasEta) {
DateUtils.getRelativeTimeSpanString( DateUtils.getRelativeTimeSpanString(
eta, eta,

@ -82,8 +82,12 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
if (selectionController.onItemClick(item.id.mostSignificantBits)) { if (selectionController.onItemClick(item.id.mostSignificantBits)) {
return return
} }
if (item.isExpandable) {
viewModel.expandCollapse(item)
} else {
startActivity(DetailsActivity.newIntent(view.context, item.manga)) 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)

@ -8,15 +8,19 @@ 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.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.plus import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.parser.MangaDataRepository import org.koitharu.kotatsu.core.parser.MangaDataRepository
import org.koitharu.kotatsu.core.parser.MangaRepository
import org.koitharu.kotatsu.core.parser.RemoteMangaRepository
import org.koitharu.kotatsu.core.ui.BaseViewModel import org.koitharu.kotatsu.core.ui.BaseViewModel
import org.koitharu.kotatsu.core.ui.model.DateTimeAgo import org.koitharu.kotatsu.core.ui.model.DateTimeAgo
import org.koitharu.kotatsu.core.ui.util.ReversibleAction import org.koitharu.kotatsu.core.ui.util.ReversibleAction
@ -24,6 +28,7 @@ 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.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
@ -31,6 +36,7 @@ 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.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 java.util.Date import java.util.Date
import java.util.LinkedList import java.util.LinkedList
import java.util.UUID import java.util.UUID
@ -41,13 +47,18 @@ import javax.inject.Inject
class DownloadsViewModel @Inject constructor( 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,
) : BaseViewModel() { ) : BaseViewModel() {
private val mangaCache = LongSparseArray<Manga>() private val mangaCache = LongSparseArray<Manga>()
private val cacheMutex = Mutex() private val cacheMutex = Mutex()
private val works = workScheduler.observeWorks() private val expanded = MutableStateFlow(emptySet<UUID>())
.mapLatest { it.toDownloadsList() } private val works = combine(
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null) workScheduler.observeWorks(),
expanded,
) { list, exp ->
list.toDownloadsList(exp)
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
val onActionDone = MutableEventFlow<ReversibleAction>() val onActionDone = MutableEventFlow<ReversibleAction>()
@ -169,11 +180,21 @@ class DownloadsViewModel @Inject constructor(
it.id.mostSignificantBits it.id.mostSignificantBits
} ?: emptySet() } ?: emptySet()
private suspend fun List<WorkInfo>.toDownloadsList(): List<DownloadItemModel> { fun expandCollapse(item: DownloadItemModel) {
expanded.update {
if (item.id in it) {
it - item.id
} else {
it + item.id
}
}
}
private suspend fun List<WorkInfo>.toDownloadsList(exp: Set<UUID>): List<DownloadItemModel> {
if (isEmpty()) { if (isEmpty()) {
return emptyList() return emptyList()
} }
val list = mapNotNullTo(ArrayList(size)) { it.toUiModel() } val list = mapNotNullTo(ArrayList(size)) { it.toUiModel(it.id in exp) }
list.sortByDescending { it.timestamp } list.sortByDescending { it.timestamp }
return list return list
} }
@ -213,11 +234,13 @@ class DownloadsViewModel @Inject constructor(
return destination return destination
} }
private suspend fun WorkInfo.toUiModel(): DownloadItemModel? { private suspend fun WorkInfo.toUiModel(isExpanded: Boolean): DownloadItemModel? {
val workData = if (outputData == Data.EMPTY) progress else outputData val workData = if (outputData == Data.EMPTY) progress else outputData
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 downloadedChapters = DownloadState.getDownloadedChapters(workData)
val scheduledChapters = DownloadState.getScheduledChapters(workData).toSet()
return DownloadItemModel( return DownloadItemModel(
id = id, id = id,
workState = state, workState = state,
@ -229,7 +252,19 @@ class DownloadsViewModel @Inject constructor(
progress = DownloadState.getProgress(workData), progress = DownloadState.getProgress(workData),
eta = DownloadState.getEta(workData), eta = DownloadState.getEta(workData),
timestamp = DownloadState.getTimestamp(workData), timestamp = DownloadState.getTimestamp(workData),
totalChapters = DownloadState.getDownloadedChapters(workData).size, totalChapters = downloadedChapters.size,
isExpanded = isExpanded,
chapters = manga.chapters?.mapNotNull {
if (it.id in scheduledChapters) {
DownloadChapter(
number = it.number,
name = it.name,
isDownloaded = it.id in downloadedChapters,
)
} else {
null
}
}.orEmpty(),
) )
} }
@ -261,8 +296,16 @@ class DownloadsViewModel @Inject constructor(
} }
return cacheMutex.withLock { return cacheMutex.withLock {
mangaCache.getOrElse(mangaId) { mangaCache.getOrElse(mangaId) {
mangaDataRepository.findMangaById(mangaId)?.also { mangaCache[mangaId] = it } ?: return null mangaDataRepository.findMangaById(mangaId)?.let {
tryLoad(it) ?: it
}?.also {
mangaCache[mangaId] = it
} ?: return null
} }
} }
} }
private suspend fun tryLoad(manga: Manga) = runCatchingCancellable {
(mangaRepositoryFactory.create(manga.source) as RemoteMangaRepository).peekDetails(manga)
}.getOrNull()
} }

@ -0,0 +1,14 @@
package org.koitharu.kotatsu.download.ui.list.chapters
import org.koitharu.kotatsu.list.ui.model.ListModel
data class DownloadChapter(
val number: Int,
val name: String,
val isDownloaded: Boolean,
) : ListModel {
override fun areItemsTheSame(other: ListModel): Boolean {
return other is DownloadChapter && other.name == name
}
}

@ -0,0 +1,20 @@
package org.koitharu.kotatsu.download.ui.list.chapters
import androidx.core.content.ContextCompat
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.drawableEnd
import org.koitharu.kotatsu.databinding.ItemChapterDownloadBinding
fun downloadChapterAD() = adapterDelegateViewBinding<DownloadChapter, DownloadChapter, ItemChapterDownloadBinding>(
{ layoutInflater, parent -> ItemChapterDownloadBinding.inflate(layoutInflater, parent, false) },
) {
val iconDone = ContextCompat.getDrawable(context, R.drawable.ic_check)
bind {
binding.textViewNumber.text = item.number.toString()
binding.textViewTitle.text = item.name
binding.textViewTitle.drawableEnd = if (item.isDownloaded) iconDone else null
}
}

@ -178,6 +178,9 @@ class DownloadWorker @AssistedInject constructor(
} }
} }
val chapters = getChapters(mangaDetails, includedIds) val chapters = getChapters(mangaDetails, includedIds)
publishState(
currentState.copy(scheduledChapters = LongArray(chapters.size) { i -> chapters[i].id }),
)
for ((chapterIndex, chapter) in chapters.withIndex()) { for ((chapterIndex, chapter) in chapters.withIndex()) {
if (chaptersToSkip.remove(chapter.id)) { if (chaptersToSkip.remove(chapter.id)) {
publishState( publishState(

@ -26,4 +26,5 @@ enum class ListItemType {
CATEGORY_LARGE, CATEGORY_LARGE,
MANGA_SCROBBLING, MANGA_SCROBBLING,
NAV_ITEM, NAV_ITEM,
CHAPTER,
} }

@ -59,6 +59,7 @@ class TypedListSpacingDecoration(
ListItemType.MANGA_NESTED_GROUP, ListItemType.MANGA_NESTED_GROUP,
ListItemType.CATEGORY_LARGE, ListItemType.CATEGORY_LARGE,
ListItemType.NAV_ITEM, ListItemType.NAV_ITEM,
ListItemType.CHAPTER,
null, null,
-> outRect.set(0) -> outRect.set(0)

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingVertical="4dp">
<TextView
android:id="@+id/textView_number"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:background="@drawable/bg_badge_default"
android:ellipsize="none"
android:gravity="center"
android:singleLine="true"
android:textAlignment="center"
android:textColor="?attr/colorOnPrimary"
android:textSize="12sp"
tools:text="13" />
<TextView
android:id="@+id/textView_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="?android:listPreferredItemPaddingStart"
android:layout_marginEnd="?android:listPreferredItemPaddingEnd"
android:drawablePadding="4dp"
android:ellipsize="end"
android:gravity="center_vertical"
android:singleLine="true"
android:textAppearance="?attr/textAppearanceBodySmall"
app:drawableTint="?colorControlNormal"
tools:drawableEnd="@drawable/ic_check"
tools:text="@tools:sample/lorem[15]" />
</LinearLayout>

@ -24,7 +24,7 @@
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" />
<TextView <CheckedTextView
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"
@ -32,11 +32,14 @@
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="12dp" android:layout_marginEnd="12dp"
android:ellipsize="end" android:ellipsize="end"
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_toEndOf="parent" 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"
tools:text="@tools:sample/lorem" /> tools:text="@tools:sample/lorem" />
<androidx.constraintlayout.widget.Barrier <androidx.constraintlayout.widget.Barrier
@ -100,6 +103,25 @@
app:layout_constraintTop_toBottomOf="@id/textView_status" app:layout_constraintTop_toBottomOf="@id/textView_status"
tools:text="@tools:sample/lorem[3]" /> tools:text="@tools:sample/lorem[3]" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/card_details"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginHorizontal="12dp"
android:layout_marginTop="12dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/progressBar"
app:shapeAppearance="?shapeAppearanceCornerMedium">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView_chapters"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
</com.google.android.material.card.MaterialCardView>
<Button <Button
android:id="@+id/button_pause" android:id="@+id/button_pause"
style="?materialButtonOutlinedStyle" style="?materialButtonOutlinedStyle"
@ -110,7 +132,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/card_details"
tools:visibility="visible" /> tools:visibility="visible" />
<Button <Button
@ -123,7 +145,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/card_details" />
<Button <Button
android:id="@+id/button_cancel" android:id="@+id/button_cancel"
@ -135,7 +157,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/card_details"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

Loading…
Cancel
Save