From d001e6f210e6a077a30dd9a1392573a3225bba2f Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Tue, 20 Oct 2026 20:54:25 +0600 Subject: [PATCH] Move the download queue list to Compose --- app/build.gradle.kts | 1 - .../download/DownloadQueueList.kt | 494 ++++++++++++++++++ .../tachiyomi/data/download/model/Download.kt | 3 - .../tachiyomi/ui/download/DownloadAdapter.kt | 27 - .../ui/download/DownloadHeaderHolder.kt | 34 -- .../ui/download/DownloadHeaderItem.kt | 64 --- .../tachiyomi/ui/download/DownloadHolder.kt | 101 ---- .../tachiyomi/ui/download/DownloadItem.kt | 68 --- .../ui/download/DownloadQueueScreen.kt | 81 +-- .../ui/download/DownloadQueueViewModel.kt | 247 ++------- .../tachiyomi/util/view/ViewExtensions.kt | 33 -- .../res/color/draggable_card_foreground.xml | 7 - app/src/main/res/layout/download_header.xml | 43 -- app/src/main/res/layout/download_item.xml | 104 ---- app/src/main/res/layout/download_list.xml | 7 - app/src/main/res/menu/download_single.xml | 28 - app/src/main/res/values/styles.xml | 4 - gradle/libs.versions.toml | 2 - icons/material-symbols/build.gradle.kts | 2 - 19 files changed, 559 insertions(+), 791 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/download/DownloadQueueList.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt delete mode 100644 app/src/main/res/color/draggable_card_foreground.xml delete mode 100644 app/src/main/res/layout/download_header.xml delete mode 100644 app/src/main/res/layout/download_item.xml delete mode 100644 app/src/main/res/layout/download_list.xml delete mode 100644 app/src/main/res/menu/download_single.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index a5ce2ebc1..69016a57a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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") diff --git a/app/src/main/java/eu/kanade/presentation/download/DownloadQueueList.kt b/app/src/main/java/eu/kanade/presentation/download/DownloadQueueList.kt new file mode 100644 index 000000000..7e1c1ab33 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/download/DownloadQueueList.kt @@ -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, + lazyListState: LazyListState, + contentPadding: PaddingValues, + onReorder: (List) -> 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(null) } + // The release animation starts from the finger, so the last dragged row stays held while it settles too + var lastDraggedKey by remember { mutableStateOf(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 }, + ) + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index 30201c1bf..df2920b3e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt deleted file mode 100644 index 44ff8b110..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadAdapter.kt +++ /dev/null @@ -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>( - 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt deleted file mode 100644 index 3362c99ed..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt deleted file mode 100644 index e8da53cb6..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt +++ /dev/null @@ -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() { - - override fun getLayoutRes(): Int { - return R.layout.download_header - } - - override fun createViewHolder( - view: View, - adapter: FlexibleAdapter>, - ): DownloadHeaderHolder { - return DownloadHeaderHolder(view, adapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: DownloadHeaderHolder, - position: Int, - payloads: List?, - ) { - 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 - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt deleted file mode 100644 index 514357d65..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHolder.kt +++ /dev/null @@ -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) - }, - ) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt deleted file mode 100644 index 6dd4f9b25..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadItem.kt +++ /dev/null @@ -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(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>, - ): 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>, - holder: DownloadHolder, - position: Int, - payloads: MutableList, - ) { - 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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt index 235dc63b4..02b75815b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueScreen.kt @@ -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() 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), + ) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt index 4dd939406..ccf100b38 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadQueueViewModel.kt @@ -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> = downloadManager.queueState + val state: StateFlow> = 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() - - 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() - 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() - ?.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() - ?.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 > reorderQueue(selector: (DownloadItem) -> R?, reverse: Boolean = false) { - val adapter = adapter ?: return - val newDownloads = mutableListOf() - 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::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 > 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, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt index 58895acab..e599dfae7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/view/ViewExtensions.kt @@ -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 diff --git a/app/src/main/res/color/draggable_card_foreground.xml b/app/src/main/res/color/draggable_card_foreground.xml deleted file mode 100644 index fcf172740..000000000 --- a/app/src/main/res/color/draggable_card_foreground.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/download_header.xml b/app/src/main/res/layout/download_header.xml deleted file mode 100644 index 87cf8f03d..000000000 --- a/app/src/main/res/layout/download_header.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml deleted file mode 100644 index ca9851e53..000000000 --- a/app/src/main/res/layout/download_item.xml +++ /dev/null @@ -1,104 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/download_list.xml b/app/src/main/res/layout/download_list.xml deleted file mode 100644 index ec3e3bab9..000000000 --- a/app/src/main/res/layout/download_list.xml +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/app/src/main/res/menu/download_single.xml b/app/src/main/res/menu/download_single.xml deleted file mode 100644 index 1cf14ff0c..000000000 --- a/app/src/main/res/menu/download_single.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 22313ff42..2f7dacafd 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -31,10 +31,6 @@ - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a024f12e7..d00408e74 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" } diff --git a/icons/material-symbols/build.gradle.kts b/icons/material-symbols/build.gradle.kts index 078b5d63d..842d1bb56 100644 --- a/icons/material-symbols/build.gradle.kts +++ b/icons/material-symbols/build.gradle.kts @@ -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",