Move the download queue list to Compose

This commit is contained in:
AntsyLich
2026-10-21 02:17:15 +06:00
parent 2ca6961e1c
commit d001e6f210
19 changed files with 559 additions and 791 deletions
-1
View File
@@ -296,7 +296,6 @@ dependencies {
// UI libraries
implementation(libs.material)
implementation(libs.flexibleAdapter)
implementation(libs.photoView)
implementation(libs.directionalViewPager) {
exclude(group = "androidx.viewpager", module = "viewpager")
@@ -0,0 +1,494 @@
package eu.kanade.presentation.download
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.MutatePriority
import androidx.compose.foundation.gestures.ScrollScope
import androidx.compose.foundation.gestures.ScrollableState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.download.DownloadQueueSection
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.rounded.DragHandle
import mihon.icons.materialsymbols.rounded.MoreVert
import sh.calvin.reorderable.ReorderableCollectionItemScope
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyListState
import sh.calvin.reorderable.rememberScroller
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.ListGroupHeader
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.i18n.stringResource
@Composable
fun DownloadQueueList(
sections: List<DownloadQueueSection>,
lazyListState: LazyListState,
contentPadding: PaddingValues,
onReorder: (List<Download>) -> Unit,
onMove: (Download, toTop: Boolean) -> Unit,
onMoveSeries: (Download, toTop: Boolean) -> Unit,
onCancel: (Download) -> Unit,
onCancelSeries: (Download) -> Unit,
modifier: Modifier = Modifier,
) {
val sectionsState = remember { sections.toMutableStateList() }
// While a section is dragged its downloads are hidden, so it moves past whole sections
var isSectionDragging by remember { mutableStateOf(false) }
// While a download is dragged only its own section's rows can be swapped with it
var draggingSectionId by remember { mutableStateOf<Long?>(null) }
// The release animation starts from the finger, so the last dragged row stays held while it settles too
var lastDraggedKey by remember { mutableStateOf<String?>(null) }
val listCoordinates = remember { CoordinatesHolder() }
val draggingSection by rememberUpdatedState(sectionsState.find { it.id == draggingSectionId })
// Auto-scroll would otherwise carry a download held at its section's edge off screen
val sectionScrollState = remember(lazyListState) {
LimitedScrollableState(lazyListState) { pixels ->
draggingSection?.let { lazyListState.limitScrollToSection(it, pixels) } ?: pixels
}
}
val reorderableState = rememberReorderableLazyListState(
lazyListState = lazyListState,
scrollThresholdPadding = contentPadding,
scroller = rememberScroller(
scrollableState = sectionScrollState,
pixelAmountProvider = { lazyListState.layoutInfo.viewportSize.height * 0.05f },
),
) { from, to ->
val fromKey = from.key as String
val toKey = to.key as String
when {
fromKey.startsWith(SECTION_KEY) && toKey.startsWith(SECTION_KEY) -> {
val fromIndex = sectionsState.indexOfFirst { it.key == fromKey }
val toIndex = sectionsState.indexOfFirst { it.key == toKey }
sectionsState.add(toIndex, sectionsState.removeAt(fromIndex))
}
// Downloads don't change section
fromKey.startsWith(DOWNLOAD_KEY) && toKey.startsWith(DOWNLOAD_KEY) -> {
val sectionIndex = sectionsState.indexOfFirst { section ->
section.downloads.any { it.key == fromKey }
}
val section = sectionsState[sectionIndex]
val toIndex = section.downloads.indexOfFirst { it.key == toKey }
if (toIndex != -1) {
val downloads = section.downloads.toMutableList()
val fromIndex = downloads.indexOfFirst { it.key == fromKey }
downloads.add(toIndex, downloads.removeAt(fromIndex))
sectionsState[sectionIndex] = section.copy(downloads = downloads)
}
}
}
}
LaunchedEffect(sections) {
if (!reorderableState.isAnyItemDragging) {
sectionsState.clear()
sectionsState.addAll(sections)
}
}
val onDragStopped = {
isSectionDragging = false
draggingSectionId = null
onReorder(sectionsState.flatMap { it.downloads })
}
val firstDownload = sectionsState.firstOrNull()?.downloads?.firstOrNull()
val lastDownload = sectionsState.lastOrNull()?.downloads?.lastOrNull()
LazyColumn(
modifier = modifier
.fillMaxSize()
.onGloballyPositioned { listCoordinates.value = it },
state = lazyListState,
contentPadding = contentPadding,
) {
sectionsState.forEach { section ->
item(key = section.key) {
ReorderableItem(reorderableState, section.key, enabled = draggingSectionId == null) { isDragging ->
DownloadQueueHeader(
section = section,
isDragging = isDragging,
onDragStarted = { isSectionDragging = true },
onDragStopped = onDragStopped,
)
}
}
if (!isSectionDragging) {
items(
items = section.downloads,
key = { it.key },
) { download ->
ReorderableItem(
state = reorderableState,
key = download.key,
enabled = draggingSectionId == null || draggingSectionId == section.id,
) { isDragging ->
DownloadQueueItem(
download = download,
isDragging = isDragging,
modifier = if (isDragging || download.key == lastDraggedKey) {
Modifier.keepInSection(
lazyListState = lazyListState,
listCoordinates = listCoordinates,
sectionKey = section.key,
lastDownloadKey = section.downloads.last().key,
)
} else {
Modifier
},
canMoveToTop = download != firstDownload,
canMoveToBottom = download != lastDownload,
onDragStarted = {
draggingSectionId = section.id
lastDraggedKey = download.key
},
onDragStopped = onDragStopped,
onMove = { toTop -> onMove(download, toTop) },
onMoveSeries = { toTop -> onMoveSeries(download, toTop) },
onCancel = { onCancel(download) },
onCancelSeries = { onCancelSeries(download) },
)
}
}
}
}
}
}
// Lazy list keys must fit in a Bundle
private const val SECTION_KEY = "section-"
private const val DOWNLOAD_KEY = "download-"
private val DownloadQueueSection.key get() = "$SECTION_KEY$id"
private val Download.key get() = "$DOWNLOAD_KEY${chapter.id}"
private class CoordinatesHolder {
var value: LayoutCoordinates? = null
}
/**
* [lazyListState], with every scroll step passed through [limit] first.
*/
private class LimitedScrollableState(
private val lazyListState: LazyListState,
private val limit: (Float) -> Float,
) : ScrollableState by lazyListState {
override suspend fun scroll(scrollPriority: MutatePriority, block: suspend ScrollScope.() -> Unit) {
lazyListState.scroll(scrollPriority) {
val scope = this
object : ScrollScope {
override fun scrollBy(pixels: Float) = scope.scrollBy(limit(pixels))
}.block()
}
}
}
/**
* Limits [pixels] so the list scrolls no further than the end of [section] reaching the bottom, or its header
* reaching the top, while that edge is on screen.
*/
private fun LazyListState.limitScrollToSection(section: DownloadQueueSection, pixels: Float): Float {
val items = layoutInfo.visibleItemsInfo
return if (pixels > 0) {
val last = items.firstOrNull { it.key == section.downloads.last().key } ?: return pixels
val contentEnd = layoutInfo.viewportEndOffset - layoutInfo.afterContentPadding
pixels.coerceAtMost((last.offset + last.size - contentEnd).coerceAtLeast(0).toFloat())
} else {
val header = items.firstOrNull { it.key == section.key } ?: return pixels
pixels.coerceAtLeast((header.offset).coerceAtMost(0).toFloat())
}
}
/**
* Stops a dragged download at the edges of its section, between the header and the section's last slot, instead of
* letting it float over another source it can't be dropped in.
*/
@Composable
private fun Modifier.keepInSection(
lazyListState: LazyListState,
listCoordinates: CoordinatesHolder,
sectionKey: String,
lastDownloadKey: String,
): Modifier {
var correction by remember { mutableFloatStateOf(0f) }
return this
// Before the correction layer, so it sees where the drag put the row
.onGloballyPositioned { coordinates ->
val list = listCoordinates.value?.takeIf { it.isAttached } ?: return@onGloballyPositioned
val top = list.localPositionOf(coordinates, Offset.Zero).y
val layoutInfo = lazyListState.layoutInfo
val itemStart = layoutInfo.beforeContentPadding
val header = layoutInfo.visibleItemsInfo.firstOrNull { it.key == sectionKey }
val last = layoutInfo.visibleItemsInfo.firstOrNull { it.key == lastDownloadKey }
val minTop = header?.let { (itemStart + it.offset + it.size).toFloat() } ?: Float.NEGATIVE_INFINITY
val maxTop = last?.let { (itemStart + it.offset + it.size - coordinates.size.height).toFloat() }
?: Float.POSITIVE_INFINITY
correction = top.coerceIn(minTop, maxOf(minTop, maxTop)) - top
}
.graphicsLayer { translationY = correction }
}
@Composable
private fun ReorderableCollectionItemScope.DownloadQueueHeader(
section: DownloadQueueSection,
isDragging: Boolean,
onDragStarted: () -> Unit,
onDragStopped: () -> Unit,
) {
Surface(
modifier = Modifier.padding(top = MaterialTheme.padding.small),
color = listItemContainerColor(isDragging),
shadowElevation = listItemElevation(isDragging),
) {
Row(
modifier = Modifier.padding(end = MaterialTheme.padding.small),
verticalAlignment = Alignment.CenterVertically,
) {
ListGroupHeader(
text = "${section.name} (${section.downloads.size})",
modifier = Modifier.weight(1f),
)
DragHandle(onDragStarted = onDragStarted, onDragStopped = onDragStopped)
}
}
}
@Composable
private fun ReorderableCollectionItemScope.DownloadQueueItem(
download: Download,
isDragging: Boolean,
canMoveToTop: Boolean,
canMoveToBottom: Boolean,
onDragStarted: () -> Unit,
onDragStopped: () -> Unit,
onMove: (toTop: Boolean) -> Unit,
onMoveSeries: (toTop: Boolean) -> Unit,
onCancel: () -> Unit,
onCancelSeries: () -> Unit,
modifier: Modifier = Modifier,
) {
val progress by remember(download) { download.pagesProgressFlow() }.collectAsState(null)
val animatedProgress by animateFloatAsState(progress?.fraction ?: 0f, label = "progress")
Surface(
modifier = modifier,
color = listItemContainerColor(isDragging),
shadowElevation = listItemElevation(isDragging),
) {
Row(
modifier = Modifier.padding(
start = MaterialTheme.padding.small,
end = MaterialTheme.padding.extraSmall,
),
verticalAlignment = Alignment.CenterVertically,
) {
DragHandle(onDragStarted = onDragStarted, onDragStopped = onDragStopped)
Column(
modifier = Modifier
.weight(1f)
.padding(vertical = MaterialTheme.padding.small),
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.extraSmall),
) {
Row(horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small)) {
Text(
text = download.manga.title,
modifier = Modifier.weight(1f),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
progress?.let {
Text(
text = "${it.downloadedPages}/${it.pageCount}",
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall,
)
}
}
Text(
text = download.chapter.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodySmall,
)
LinearProgressIndicator(
progress = { animatedProgress },
modifier = Modifier
.fillMaxWidth()
.padding(top = MaterialTheme.padding.extraSmall),
)
}
DownloadQueueItemMenu(
canMoveToTop = canMoveToTop,
canMoveToBottom = canMoveToBottom,
onMove = onMove,
onMoveSeries = onMoveSeries,
onCancel = onCancel,
onCancelSeries = onCancelSeries,
)
}
}
}
@Composable
private fun ReorderableCollectionItemScope.DragHandle(onDragStarted: () -> Unit, onDragStopped: () -> Unit) {
Icon(
imageVector = MaterialSymbols.Rounded.DragHandle,
contentDescription = null,
modifier = Modifier
.draggableHandle(
onDragStarted = { onDragStarted() },
onDragStopped = onDragStopped,
)
.padding(MaterialTheme.padding.small),
)
}
@Composable
private fun DownloadQueueItemMenu(
canMoveToTop: Boolean,
canMoveToBottom: Boolean,
onMove: (toTop: Boolean) -> Unit,
onMoveSeries: (toTop: Boolean) -> Unit,
onCancel: () -> Unit,
onCancelSeries: () -> Unit,
) {
var expanded by remember { mutableStateOf(false) }
val closeMenu = { expanded = false }
Box {
IconButton(onClick = { expanded = true }) {
Icon(
imageVector = MaterialSymbols.Rounded.MoreVert,
contentDescription = stringResource(MR.strings.action_menu),
)
}
DropdownMenu(expanded = expanded, onDismissRequest = closeMenu) {
if (canMoveToTop) {
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_move_to_top)) },
onClick = {
closeMenu()
onMove(true)
},
)
}
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_move_to_top_all_for_series)) },
onClick = {
closeMenu()
onMoveSeries(true)
},
)
if (canMoveToBottom) {
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_move_to_bottom)) },
onClick = {
closeMenu()
onMove(false)
},
)
}
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_move_to_bottom_all_for_series)) },
onClick = {
closeMenu()
onMoveSeries(false)
},
)
DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_cancel)) },
onClick = {
closeMenu()
onCancel()
},
)
DropdownMenuItem(
text = { Text(stringResource(MR.strings.cancel_all_for_series)) },
onClick = {
closeMenu()
onCancelSeries()
},
)
}
}
}
// A neutral lifted surface while dragged, rather than Material's tertiaryContainer list tint
@Composable
private fun listItemContainerColor(isDragging: Boolean): Color {
return if (isDragging) MaterialTheme.colorScheme.surfaceContainerHigh else MaterialTheme.colorScheme.background
}
@Composable
private fun listItemElevation(isDragging: Boolean): Dp {
val elevation = ListItemDefaults.elevation()
return if (isDragging) elevation.draggedElevation else elevation.elevation
}
private class PagesProgress(
val pageCount: Int,
val downloadedPages: Int,
totalProgress: Int,
) {
val fraction = if (pageCount == 0) 0f else totalProgress / (pageCount * 100f)
}
/**
* Progress of the pages of this download, or null until its pages are known.
*/
private fun Download.pagesProgressFlow() = pagesFlow.flatMapLatest { pages ->
when {
pages == null -> flowOf(null)
pages.isEmpty() -> flowOf(PagesProgress(pageCount = 0, downloadedPages = 0, totalProgress = 0))
else ->
combine(pages.map { page -> combine(page.statusFlow, page.progressFlow, ::Pair) }) { states ->
PagesProgress(
pageCount = pages.size,
downloadedPages = states.count { (status, _) -> status == Page.State.Ready },
totalProgress = states.sumOf { (_, progress) -> progress },
)
}
}
}
@@ -29,9 +29,6 @@ data class Download(
_pagesFlow.value = pages
}
val totalProgress: Int
get() = pages?.sumOf(Page::progress) ?: 0
val downloadedImages: Int
get() = pages?.count { it.status == Page.State.Ready } ?: 0
@@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
/**
* Adapter storing a list of downloads.
*
* @param downloadItemListener Listener called when an item of the list is released.
*/
class DownloadAdapter(val downloadItemListener: DownloadItemListener) : FlexibleAdapter<AbstractFlexibleItem<*>>(
null,
downloadItemListener,
true,
) {
override fun shouldMove(fromPosition: Int, toPosition: Int): Boolean {
// Don't let sub-items changing group
return getHeaderOf(getItem(fromPosition)) == getHeaderOf(getItem(toPosition))
}
interface DownloadItemListener {
fun onItemReleased(position: Int)
fun onMenuItemClick(position: Int, menuItem: MenuItem)
}
}
@@ -1,34 +0,0 @@
package eu.kanade.tachiyomi.ui.download
import android.annotation.SuppressLint
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.ExpandableViewHolder
import eu.kanade.tachiyomi.databinding.DownloadHeaderBinding
class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : ExpandableViewHolder(view, adapter) {
private val binding = DownloadHeaderBinding.bind(view)
@SuppressLint("SetTextI18n")
fun bind(item: DownloadHeaderItem) {
setDragHandleView(binding.reorder)
binding.title.text = "${item.name} (${item.size})"
}
override fun onActionStateChanged(position: Int, actionState: Int) {
super.onActionStateChanged(position, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
binding.container.isDragged = true
mAdapter.collapseAll()
}
}
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
binding.container.isDragged = false
mAdapter.expandAll()
(mAdapter as DownloadAdapter).downloadItemListener.onItemReleased(position)
}
}
@@ -1,64 +0,0 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractExpandableHeaderItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
data class DownloadHeaderItem(
val id: Long,
val name: String,
val size: Int,
) : AbstractExpandableHeaderItem<DownloadHeaderHolder, DownloadItem>() {
override fun getLayoutRes(): Int {
return R.layout.download_header
}
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
): DownloadHeaderHolder {
return DownloadHeaderHolder(view, adapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: DownloadHeaderHolder,
position: Int,
payloads: List<Any?>?,
) {
holder.bind(this)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as DownloadHeaderItem
if (id != other.id) return false
if (name != other.name) return false
if (size != other.size) return false
if (subItemsCount != other.subItemsCount) return false
if (subItems !== other.subItems) return false
return true
}
override fun hashCode(): Int {
var result = id.hashCode()
result = 31 * result + name.hashCode()
result = 31 * result + size
result = 31 * result + subItems.hashCode()
return result
}
init {
isHidden = false
isExpanded = true
isSelectable = false
}
}
@@ -1,101 +0,0 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import androidx.recyclerview.widget.ItemTouchHelper
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.DownloadItemBinding
import eu.kanade.tachiyomi.util.view.popupMenu
/**
* Class used to hold the data of a download.
* All the elements from the layout file "download_item" are available in this class.
*
* @param view the inflated view for this holder.
* @constructor creates a new download holder.
*/
class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
FlexibleViewHolder(view, adapter) {
private val binding = DownloadItemBinding.bind(view)
init {
setDragHandleView(binding.reorder)
binding.menu.setOnClickListener { it.post { showPopupMenu(it) } }
}
private lateinit var download: Download
/**
* Binds this holder with the given category.
*
* @param category The category to bind.
*/
fun bind(download: Download) {
this.download = download
// Update the chapter name.
binding.chapterTitle.text = download.chapter.name
// Update the manga title
binding.mangaFullTitle.text = download.manga.title
// Update the progress bar and the number of downloaded pages
val pages = download.pages
if (pages == null) {
binding.downloadProgress.progress = 0
binding.downloadProgress.max = 1
binding.downloadProgressText.text = ""
} else {
binding.downloadProgress.max = pages.size * 100
notifyProgress()
notifyDownloadedPages()
}
}
/**
* Updates the progress bar of the download.
*/
fun notifyProgress() {
val pages = download.pages ?: return
if (binding.downloadProgress.max == 1) {
binding.downloadProgress.max = pages.size * 100
}
binding.downloadProgress.setProgressCompat(download.totalProgress, true)
}
/**
* Updates the text field of the number of downloaded pages.
*/
fun notifyDownloadedPages() {
val pages = download.pages ?: return
binding.downloadProgressText.text = "${download.downloadedImages}/${pages.size}"
}
override fun onItemReleased(position: Int) {
super.onItemReleased(position)
adapter.downloadItemListener.onItemReleased(position)
binding.container.isDragged = false
}
override fun onActionStateChanged(position: Int, actionState: Int) {
super.onActionStateChanged(position, actionState)
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
binding.container.isDragged = true
}
}
private fun showPopupMenu(view: View) {
view.popupMenu(
menuRes = R.menu.download_single,
initMenu = {
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition > 1
findItem(R.id.move_to_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1
},
onMenuItemClick = {
adapter.downloadItemListener.onMenuItemClick(bindingAdapterPosition, this)
},
)
}
}
@@ -1,68 +0,0 @@
package eu.kanade.tachiyomi.ui.download
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
class DownloadItem(
val download: Download,
header: DownloadHeaderItem,
) : AbstractSectionableItem<DownloadHolder, DownloadHeaderItem>(header) {
override fun getLayoutRes(): Int {
return R.layout.download_item
}
/**
* Returns a new view holder for this item.
*
* @param view The view of this item.
* @param adapter The adapter of this item.
*/
override fun createViewHolder(
view: View,
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
): DownloadHolder {
return DownloadHolder(view, adapter as DownloadAdapter)
}
/**
* Binds the given view holder with this item.
*
* @param adapter The adapter of this item.
* @param holder The holder to bind.
* @param position The position of this item in the adapter.
* @param payloads List of partial changes.
*/
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: DownloadHolder,
position: Int,
payloads: MutableList<Any>,
) {
holder.bind(download)
}
/**
* Returns true if this item is draggable.
*/
override fun isDraggable(): Boolean {
return true
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other is DownloadItem) {
return download.chapter.id == other.download.chapter.id
}
return false
}
override fun hashCode(): Int {
return download.chapter.id.toInt()
}
}
@@ -1,11 +1,9 @@
package eu.kanade.tachiyomi.ui.download
import android.view.LayoutInflater
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -19,7 +17,6 @@ import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -27,17 +24,11 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Velocity
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.core.view.ViewCompat
import androidx.core.view.updatePadding
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.recyclerview.widget.LinearLayoutManager
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import dev.zacsweers.metrox.viewmodel.metroViewModel
@@ -45,10 +36,8 @@ import eu.kanade.presentation.components.AppBar
import eu.kanade.presentation.components.AppBarActions
import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.NestedMenuItem
import eu.kanade.presentation.download.DownloadQueueList
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.databinding.DownloadListBinding
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import mihon.icons.materialsymbols.MaterialSymbols
import mihon.icons.materialsymbols.automirroredrounded.Sort
import mihon.icons.materialsymbols.roundedfilled.Pause
@@ -58,18 +47,16 @@ import tachiyomi.presentation.core.components.Pill
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.screens.EmptyScreen
import kotlin.math.roundToInt
object DownloadQueueScreen : Screen() {
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val scope = rememberCoroutineScope()
val viewModel = metroViewModel<DownloadQueueViewModel>()
val downloadList by viewModel.state.collectAsStateWithLifecycle()
val downloadCount by remember {
derivedStateOf { downloadList.sumOf { it.subItems.size } }
derivedStateOf { downloadList.sumOf { it.downloads.size } }
}
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -135,7 +122,7 @@ object DownloadQueueScreen : Screen() {
text = { Text(text = stringResource(MR.strings.action_newest)) },
onClick = {
viewModel.reorderQueue(
{ it.download.chapter.dateUpload },
{ it.chapter.dateUpload },
true,
)
closeMenu()
@@ -145,7 +132,7 @@ object DownloadQueueScreen : Screen() {
text = { Text(text = stringResource(MR.strings.action_oldest)) },
onClick = {
viewModel.reorderQueue(
{ it.download.chapter.dateUpload },
{ it.chapter.dateUpload },
false,
)
closeMenu()
@@ -160,7 +147,7 @@ object DownloadQueueScreen : Screen() {
text = { Text(text = stringResource(MR.strings.action_asc)) },
onClick = {
viewModel.reorderQueue(
{ it.download.chapter.chapterNumber },
{ it.chapter.chapterNumber },
false,
)
closeMenu()
@@ -170,7 +157,7 @@ object DownloadQueueScreen : Screen() {
text = { Text(text = stringResource(MR.strings.action_desc)) },
onClick = {
viewModel.reorderQueue(
{ it.download.chapter.chapterNumber },
{ it.chapter.chapterNumber },
true,
)
closeMenu()
@@ -240,49 +227,17 @@ object DownloadQueueScreen : Screen() {
return@Scaffold
}
val density = LocalDensity.current
val layoutDirection = LocalLayoutDirection.current
val left = with(density) { contentPadding.calculateLeftPadding(layoutDirection).toPx().roundToInt() }
val top = with(density) { contentPadding.calculateTopPadding().toPx().roundToInt() }
val right = with(density) { contentPadding.calculateRightPadding(layoutDirection).toPx().roundToInt() }
val bottom = with(density) { contentPadding.calculateBottomPadding().toPx().roundToInt() }
Box(modifier = Modifier.nestedScroll(nestedScrollConnection)) {
AndroidView(
modifier = Modifier.fillMaxWidth(),
factory = { context ->
viewModel.controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context))
viewModel.adapter = DownloadAdapter(viewModel.listener)
viewModel.controllerBinding.root.adapter = viewModel.adapter
viewModel.adapter?.isHandleDragEnabled = true
viewModel.controllerBinding.root.layoutManager = LinearLayoutManager(context)
ViewCompat.setNestedScrollingEnabled(viewModel.controllerBinding.root, true)
scope.launch(Dispatchers.Main) {
viewModel.getDownloadStatusFlow()
.collect(viewModel::onStatusChange)
}
scope.launch(Dispatchers.Main) {
viewModel.getDownloadProgressFlow()
.collect(viewModel::onUpdateDownloadedPages)
}
viewModel.controllerBinding.root
},
update = {
viewModel.controllerBinding.root
.updatePadding(
left = left,
top = top,
right = right,
bottom = bottom,
)
viewModel.adapter?.updateDataSet(downloadList)
},
)
}
DownloadQueueList(
sections = downloadList,
lazyListState = rememberLazyListState(),
contentPadding = contentPadding,
onReorder = viewModel::reorder,
onMove = viewModel::move,
onMoveSeries = viewModel::moveSeries,
onCancel = { viewModel.cancel(listOf(it)) },
onCancelSeries = viewModel::cancelSeries,
modifier = Modifier.nestedScroll(nestedScrollConnection),
)
}
}
}
@@ -1,33 +1,22 @@
package eu.kanade.tachiyomi.ui.download
import android.view.MenuItem
import androidx.compose.runtime.Immutable
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesIntoMap
import dev.zacsweers.metro.Inject
import dev.zacsweers.metrox.viewmodel.ViewModelKey
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.databinding.DownloadListBinding
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
@Inject
@@ -37,117 +26,18 @@ class DownloadQueueViewModel(
private val downloadManager: DownloadManager,
) : ViewModel() {
val state: StateFlow<List<DownloadHeaderItem>> = downloadManager.queueState
val state: StateFlow<List<DownloadQueueSection>> = downloadManager.queueState
.map { downloads ->
downloads
.groupBy { it.source }
.map { entry ->
DownloadHeaderItem(entry.key.id, entry.key.name, entry.value.size).apply {
addSubItems(0, entry.value.map { DownloadItem(it, this) })
}
}
.map { (source, downloads) -> DownloadQueueSection(source.id, source.name, downloads) }
}
.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), emptyList())
lateinit var controllerBinding: DownloadListBinding
/**
* Adapter containing the active downloads.
*/
var adapter: DownloadAdapter? = null
/**
* Map of jobs for active downloads.
*/
private val progressJobs = mutableMapOf<Download, Job>()
val listener = object : DownloadAdapter.DownloadItemListener {
/**
* Called when an item is released from a drag.
*
* @param position The position of the released item.
*/
override fun onItemReleased(position: Int) {
val adapter = adapter ?: return
val downloads = adapter.headerItems.flatMap { header ->
adapter.getSectionItems(header).map { item ->
(item as DownloadItem).download
}
}
reorder(downloads)
}
/**
* Called when the menu item of a download is pressed
*
* @param position The position of the item
* @param menuItem The menu Item pressed
*/
override fun onMenuItemClick(position: Int, menuItem: MenuItem) {
val item = adapter?.getItem(position) ?: return
if (item is DownloadItem) {
when (menuItem.itemId) {
R.id.move_to_top, R.id.move_to_bottom -> {
val headerItems = adapter?.headerItems ?: return
val newDownloads = mutableListOf<Download>()
headerItems.forEach { headerItem ->
headerItem as DownloadHeaderItem
if (headerItem == item.header) {
headerItem.removeSubItem(item)
if (menuItem.itemId == R.id.move_to_top) {
headerItem.addSubItem(0, item)
} else {
headerItem.addSubItem(item)
}
}
newDownloads.addAll(headerItem.subItems.map { it.download })
}
reorder(newDownloads)
}
R.id.move_to_top_series, R.id.move_to_bottom_series -> {
val (selectedSeries, otherSeries) = adapter?.currentItems
?.filterIsInstance<DownloadItem>()
?.map(DownloadItem::download)
?.partition { item.download.manga.id == it.manga.id }
?: Pair(emptyList(), emptyList())
if (menuItem.itemId == R.id.move_to_top_series) {
reorder(selectedSeries + otherSeries)
} else {
reorder(otherSeries + selectedSeries)
}
}
R.id.cancel_download -> {
cancel(listOf(item.download))
}
R.id.cancel_series -> {
val allDownloadsForSeries = adapter?.currentItems
?.filterIsInstance<DownloadItem>()
?.filter { item.download.manga.id == it.download.manga.id }
?.map(DownloadItem::download)
if (!allDownloadsForSeries.isNullOrEmpty()) {
cancel(allDownloadsForSeries)
}
}
}
}
}
}
override fun onCleared() {
for (job in progressJobs.values) {
job.cancel()
}
progressJobs.clear()
adapter = null
}
val isDownloaderRunning = downloadManager.isDownloaderRunning
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5.seconds), false)
fun getDownloadStatusFlow() = downloadManager.statusFlow()
fun getDownloadProgressFlow() = downloadManager.progressFlow()
fun startDownloads() {
downloadManager.startDownloads()
}
@@ -168,102 +58,59 @@ class DownloadQueueViewModel(
viewModelScope.launch { downloadManager.cancelQueuedDownloads(downloads) }
}
fun <R : Comparable<R>> reorderQueue(selector: (DownloadItem) -> R?, reverse: Boolean = false) {
val adapter = adapter ?: return
val newDownloads = mutableListOf<Download>()
adapter.headerItems.forEach { headerItem ->
headerItem as DownloadHeaderItem
headerItem.subItems = headerItem.subItems.sortedBy(selector).toMutableList().apply {
if (reverse) {
reverse()
/**
* Moves [download] to the top or bottom of its section.
*/
fun move(download: Download, toTop: Boolean) {
reorder(
state.value.flatMap { section ->
if (download in section.downloads) {
val others = section.downloads - download
if (toTop) listOf(download) + others else others + download
} else {
section.downloads
}
}
newDownloads.addAll(headerItem.subItems.map { it.download })
}
reorder(newDownloads)
},
)
}
/**
* Called when the status of a download changes.
*
* @param download the download whose status has changed.
* Moves all the downloads of the manga of [download] to the top or bottom of the queue.
*/
fun onStatusChange(download: Download) {
when (download.status) {
Download.State.DOWNLOADING -> {
launchProgressJob(download)
// Initial update of the downloaded pages
onUpdateDownloadedPages(download)
}
Download.State.DOWNLOADED -> {
cancelProgressJob(download)
onUpdateProgress(download)
onUpdateDownloadedPages(download)
}
Download.State.ERROR -> cancelProgressJob(download)
else -> {
/* unused */
}
fun moveSeries(download: Download, toTop: Boolean) {
val (selectedSeries, otherSeries) = state.value
.flatMap { it.downloads }
.partition { it.manga.id == download.manga.id }
reorder(if (toTop) selectedSeries + otherSeries else otherSeries + selectedSeries)
}
fun cancelSeries(download: Download) {
val allDownloadsForSeries = state.value
.flatMap { it.downloads }
.filter { it.manga.id == download.manga.id }
if (allDownloadsForSeries.isNotEmpty()) {
cancel(allDownloadsForSeries)
}
}
/**
* Observe the progress of a download and notify the view.
*
* @param download the download to observe its progress.
* Sorts the downloads of every section by [selector].
*/
private fun launchProgressJob(download: Download) {
val job = viewModelScope.launch {
val pages = download.pagesFlow.filterNotNull().first()
val progressFlows = pages.map(Page::progressFlow)
combine(progressFlows, Array<Int>::sum)
.distinctUntilChanged()
.debounce(50.milliseconds)
.collectLatest {
onUpdateProgress(download)
}
}
// Avoid leaking jobs
progressJobs.remove(download)?.cancel()
progressJobs[download] = job
}
/**
* Unsubscribes the given download from the progress subscriptions.
*
* @param download the download to unsubscribe.
*/
private fun cancelProgressJob(download: Download) {
progressJobs.remove(download)?.cancel()
}
/**
* Called when the progress of a download changes.
*
* @param download the download whose progress has changed.
*/
private fun onUpdateProgress(download: Download) {
getHolder(download)?.notifyProgress()
}
/**
* Called when a page of a download is downloaded.
*
* @param download the download whose page has been downloaded.
*/
fun onUpdateDownloadedPages(download: Download) {
getHolder(download)?.notifyDownloadedPages()
}
/**
* Returns the holder for the given download.
*
* @param download the download to find.
* @return the holder of the download or null if it's not bound.
*/
private fun getHolder(download: Download): DownloadHolder? {
return controllerBinding.root.findViewHolderForItemId(download.chapter.id) as? DownloadHolder
fun <R : Comparable<R>> reorderQueue(selector: (Download) -> R?, reverse: Boolean = false) {
reorder(
state.value.flatMap { section ->
section.downloads.sortedBy(selector).let { if (reverse) it.reversed() else it }
},
)
}
}
/**
* Downloads of the queue from one source.
*/
@Immutable
data class DownloadQueueSection(
val id: Long,
val name: String,
val downloads: List<Download>,
)
@@ -4,15 +4,9 @@ package eu.kanade.tachiyomi.util.view
import android.content.res.Resources
import android.graphics.Rect
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.annotation.MenuRes
import androidx.appcompat.R
import androidx.appcompat.widget.PopupMenu
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
@@ -58,33 +52,6 @@ fun ComposeView.setComposeContent(
}
}
/**
* Shows a popup menu on top of this view.
*
* @param menuRes menu items to inflate the menu with.
* @param initMenu function to execute when the menu after is inflated.
* @param onMenuItemClick function to execute when a menu item is clicked.
*/
inline fun View.popupMenu(
@MenuRes menuRes: Int,
noinline initMenu: (Menu.() -> Unit)? = null,
noinline onMenuItemClick: MenuItem.() -> Unit,
): PopupMenu {
val popup = PopupMenu(context, this, Gravity.NO_GRAVITY, R.attr.actionOverflowMenuStyle, 0)
popup.menuInflater.inflate(menuRes, popup.menu)
if (initMenu != null) {
popup.menu.initMenu()
}
popup.setOnMenuItemClickListener {
it.onMenuItemClick()
true
}
popup.show()
return popup
}
fun View?.isVisibleOnScreen(): Boolean {
if (this == null) {
return false
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:alpha="0.08" android:color="?attr/colorPrimary" app:state_dragged="true" />
<item android:alpha="0.08" android:color="?attr/colorPrimary" android:state_activated="true" />
<item android:color="@android:color/transparent" />
</selector>
@@ -1,43 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:cardBackgroundColor="?android:attr/colorBackground"
app:cardElevation="0dp"
app:cardForegroundColor="@color/draggable_card_foreground">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/title"
style="@style/TextAppearance.Tachiyomi.SectionHeader"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp"
android:layout_weight="1"
android:layout_gravity="center_vertical"
tools:text="Title" />
<ImageView
android:id="@+id/reorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:layout_marginEnd="4dp"
android:paddingHorizontal="10dp"
android:paddingVertical="8dp"
android:scaleType="center"
app:srcCompat="@drawable/rounded_drag_handle"
app:tint="?android:attr/textColorHint"
tools:ignore="ContentDescription" />
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
-104
View File
@@ -1,104 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView 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:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cardBackgroundColor="?android:attr/colorBackground"
app:cardElevation="0dp"
app:cardForegroundColor="@color/draggable_card_foreground">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingVertical="4dp">
<ImageView
android:id="@+id/reorder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_gravity="start"
android:padding="16dp"
android:scaleType="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/rounded_drag_handle"
app:tint="?android:attr/textColorHint"
tools:ignore="ContentDescription" />
<TextView
android:id="@+id/manga_full_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_toEndOf="@id/reorder"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintEnd_toStartOf="@+id/download_progress_text"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toTopOf="parent"
tools:text="Manga title" />
<TextView
android:id="@+id/chapter_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:layout_toEndOf="@id/reorder"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textSize="12sp"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintStart_toStartOf="@+id/manga_full_title"
app:layout_constraintTop_toBottomOf="@+id/manga_full_title"
tools:text="Chapter Title" />
<com.google.android.material.progressindicator.LinearProgressIndicator
android:id="@+id/download_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="4dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintStart_toEndOf="@+id/reorder"
app:layout_constraintTop_toBottomOf="@+id/chapter_title" />
<TextView
android:id="@+id/download_progress_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@id/manga_full_title"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:textColorSecondary"
android:textSize="12sp"
app:layout_constraintBottom_toBottomOf="@+id/manga_full_title"
app:layout_constraintEnd_toStartOf="@+id/menu"
app:layout_constraintTop_toTopOf="@+id/manga_full_title"
tools:text="0/10" />
<ImageButton
android:id="@+id/menu"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="4dp"
android:layout_toEndOf="@id/download_progress_text"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_menu"
android:paddingHorizontal="10dp"
android:paddingVertical="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/rounded_more_vert"
app:tint="?attr/colorOnBackground" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
tools:listitem="@layout/download_item" />
-28
View File
@@ -1,28 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/move_to_top"
android:title="@string/action_move_to_top" />
<item
android:id="@+id/move_to_top_series"
android:title="@string/action_move_to_top_all_for_series" />
<item
android:id="@+id/move_to_bottom"
android:title="@string/action_move_to_bottom" />
<item
android:id="@+id/move_to_bottom_series"
android:title="@string/action_move_to_bottom_all_for_series" />
<item
android:id="@+id/cancel_download"
android:title="@string/action_cancel" />
<item
android:id="@+id/cancel_series"
android:title="@string/cancel_all_for_series" />
</menu>
-4
View File
@@ -31,10 +31,6 @@
<style name="TextAppearance.Tachiyomi.Preference.Secondary" parent="TextAppearance.Material3.BodySmall" />
<style name="TextAppearance.Tachiyomi.SectionHeader" parent="TextAppearance.Material3.LabelLarge">
<item name="android:textColor">?android:attr/textColorSecondary</item>
</style>
<!--=======================-->
<!--Widgets.MaterialDivider-->
-2
View File
@@ -40,7 +40,6 @@ directionalViewPager = "1.0.0"
diskLruCache = "2.0.2"
firebase-bom = "35.0.2"
firebase-crashlytics = "3.0.8"
flexibleAdapter = "5.1.0"
google-services = "4.5.0"
image-decoder = "17"
injekt = "1.16.1"
@@ -141,7 +140,6 @@ diskLruCache = { module = "com.jakewharton:disklrucache", version.ref = "diskLru
firebase-analytics = { module = "com.google.firebase:firebase-analytics" }
firebase-bom = { module = "com.google.firebase:firebase-bom", version.ref = "firebase-bom" }
firebase-crashlytics = { module = "com.google.firebase:firebase-crashlytics" }
flexibleAdapter = { module = "eu.davidea:flexible-adapter", version.ref = "flexibleAdapter" }
image-decoder = { module = "ca.mpreg:imagedecoder", version.ref = "image-decoder" }
injekt = { module = "uy.kohesive.injekt:injekt-core", version.ref = "injekt" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
-2
View File
@@ -71,9 +71,7 @@ androidComponents {
"rounded/book",
"rounded/check",
"rounded/close",
"rounded/drag_handle",
"rounded/eyeglasses_2",
"rounded/more_vert",
"rounded/refresh",
"rounded/share",
"roundedFilled/extension",