diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d7c9886dc..57e43ef21 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -73,6 +73,7 @@ android { signingConfig = debugType.signingConfig versionNameSuffix = debugType.versionNameSuffix applicationIdSuffix = debugType.applicationIdSuffix + matchingFallbacks.add("release") } } @@ -252,6 +253,7 @@ dependencies { implementation(libs.insetter) implementation(libs.markwon) implementation(libs.aboutLibraries.compose) + implementation(libs.cascade) // Conductor implementation(libs.bundles.conductor) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt index 11e1f7898..7c2a12630 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadService.kt @@ -25,6 +25,8 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach @@ -47,6 +49,9 @@ class DownloadService : Service() { */ val runningRelay: BehaviorRelay = BehaviorRelay.create(false) + private val _isRunning = MutableStateFlow(false) + val isRunning = _isRunning.asStateFlow() + /** * Starts this service. * @@ -98,6 +103,7 @@ class DownloadService : Service() { startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification()) wakeLock = acquireWakeLock(javaClass.name) runningRelay.call(true) + _isRunning.value = true subscriptions = CompositeSubscription() listenDownloaderState() listenNetworkChanges() @@ -109,6 +115,7 @@ class DownloadService : Service() { override fun onDestroy() { ioScope?.cancel() runningRelay.call(false) + _isRunning.value = false subscriptions.unsubscribe() downloadManager.stopDownloads() wakeLock.releaseIfNeeded() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt index 042c8ae52..b8209cdd0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/DownloadQueue.kt @@ -83,6 +83,8 @@ class DownloadQueue( .startWith(Unit) .map { this } + fun getUpdatedAsFlow(): Flow> = getUpdatedObservable().asFlow() + private fun setPagesFor(download: Download) { if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) { setPagesSubject(download.pages, null) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt index 0b6b1d663..709291102 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadController.kt @@ -1,132 +1,316 @@ package eu.kanade.tachiyomi.ui.download import android.view.LayoutInflater -import android.view.Menu -import android.view.MenuInflater import android.view.MenuItem import android.view.View -import androidx.core.view.isVisible +import android.view.ViewGroup.MarginLayoutParams +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Pause +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.MoreVert +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.stringResource +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.updateLayoutParams +import androidx.core.view.updatePadding import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton -import dev.chrisbanes.insetter.applyInsetter +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.EmptyScreen +import eu.kanade.presentation.components.ExtendedFloatingActionButton +import eu.kanade.presentation.components.Pill +import eu.kanade.presentation.components.Scaffold import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download -import eu.kanade.tachiyomi.databinding.DownloadControllerBinding +import eu.kanade.tachiyomi.databinding.DownloadListBinding import eu.kanade.tachiyomi.source.model.Page -import eu.kanade.tachiyomi.ui.base.controller.FabController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import eu.kanade.tachiyomi.util.view.shrinkOnScroll +import eu.kanade.tachiyomi.ui.base.controller.FullComposeController +import eu.kanade.tachiyomi.util.lang.launchUI +import me.saket.cascade.CascadeDropdownMenu import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import java.util.concurrent.TimeUnit +import kotlin.math.roundToInt /** * Controller that shows the currently active downloads. * Uses R.layout.fragment_download_queue. */ class DownloadController : - NucleusController(), - FabController, + FullComposeController(), DownloadAdapter.DownloadItemListener { + private lateinit var controllerBinding: DownloadListBinding + /** * Adapter containing the active downloads. */ private var adapter: DownloadAdapter? = null - private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null /** * Map of subscriptions for active downloads. */ private val progressSubscriptions by lazy { mutableMapOf() } - /** - * Whether the download queue is running or not. - */ - private var isRunning: Boolean = false - - init { - setHasOptionsMenu(true) - } - - override fun createBinding(inflater: LayoutInflater) = DownloadControllerBinding.inflate(inflater) - - override fun createPresenter(): DownloadPresenter { - return DownloadPresenter() - } - - override fun getTitle(): String? { - return resources?.getString(R.string.label_download_queue) - } + override fun createPresenter() = DownloadPresenter() override fun onViewCreated(view: View) { super.onViewCreated(view) - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } + viewScope.launchUI { + presenter.getDownloadStatusFlow() + .collect(this@DownloadController::onStatusChange) } - - // Check if download queue is empty and update information accordingly. - setInformationView() - - // Initialize adapter. - adapter = DownloadAdapter(this@DownloadController) - binding.recycler.adapter = adapter - adapter?.isHandleDragEnabled = true - adapter?.fastScroller = binding.fastScroller - - // Set the layout manager for the recycler and fixed size. - binding.recycler.layoutManager = LinearLayoutManager(view.context) - binding.recycler.setHasFixedSize(true) - - actionFabScrollListener = actionFab?.shrinkOnScroll(binding.recycler) - - // Subscribe to changes - DownloadService.runningRelay - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onQueueStatusChange(it) } - - presenter.getDownloadStatusObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onStatusChange(it) } - - presenter.getDownloadProgressObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { onUpdateDownloadedPages(it) } - - presenter.downloadQueue.getUpdatedObservable() - .observeOn(AndroidSchedulers.mainThread()) - .subscribeUntilDestroy { - updateTitle(it.size) - } - } - - override fun configureFab(fab: ExtendedFloatingActionButton) { - actionFab = fab - fab.setOnClickListener { - val context = applicationContext ?: return@setOnClickListener - - if (isRunning) { - DownloadService.stop(context) - presenter.pauseDownloads() - } else { - DownloadService.start(context) - } - - setInformationView() + viewScope.launchUI { + presenter.getDownloadProgressFlow() + .collect(this@DownloadController::onUpdateDownloadedPages) } } - override fun cleanupFab(fab: ExtendedFloatingActionButton) { - fab.setOnClickListener(null) - actionFabScrollListener?.let { binding.recycler.removeOnScrollListener(it) } - actionFab = null + @Composable + override fun ComposeContent() { + val context = LocalContext.current + val downloadList by presenter.state.collectAsState() + + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + var fabExpanded by remember { mutableStateOf(true) } + val nestedScrollConnection = remember { + // All this lines just for fab state :/ + object : NestedScrollConnection { + override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { + fabExpanded = available.y >= 0 + return scrollBehavior.nestedScrollConnection.onPreScroll(available, source) + } + + override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset { + return scrollBehavior.nestedScrollConnection.onPostScroll(consumed, available, source) + } + + override suspend fun onPreFling(available: Velocity): Velocity { + return scrollBehavior.nestedScrollConnection.onPreFling(available) + } + + override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity { + return scrollBehavior.nestedScrollConnection.onPostFling(consumed, available) + } + } + } + + Scaffold( + topBar = { + AppBar( + titleContent = { + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = stringResource(R.string.label_download_queue), + maxLines = 1, + modifier = Modifier.weight(1f, false), + overflow = TextOverflow.Ellipsis, + ) + if (downloadList.isNotEmpty()) { + val pillAlpha = if (isSystemInDarkTheme()) 0.12f else 0.08f + Pill( + text = "${downloadList.size}", + modifier = Modifier.padding(start = 4.dp), + color = MaterialTheme.colorScheme.onBackground + .copy(alpha = pillAlpha), + fontSize = 14.sp, + ) + } + } + }, + navigateUp = router::popCurrentController, + actions = { + if (downloadList.isNotEmpty()) { + val (expanded, onExpanded) = remember { mutableStateOf(false) } + Box { + IconButton(onClick = { onExpanded(!expanded) }) { + Icon( + imageVector = Icons.Outlined.MoreVert, + contentDescription = stringResource(R.string.label_more), + ) + } + CascadeDropdownMenu( + expanded = expanded, + onDismissRequest = { onExpanded(false) }, + ) { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_reorganize_by)) }, + children = { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_order_by_upload_date)) }, + children = { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_newest)) }, + onClick = { + reorderQueue({ it.download.chapter.date_upload }, true) + onExpanded(false) + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_oldest)) }, + onClick = { + reorderQueue({ it.download.chapter.date_upload }, false) + onExpanded(false) + }, + ) + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_order_by_chapter_number)) }, + children = { + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_asc)) }, + onClick = { + reorderQueue({ it.download.chapter.chapter_number }, false) + onExpanded(false) + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_desc)) }, + onClick = { + reorderQueue({ it.download.chapter.chapter_number }, true) + onExpanded(false) + }, + ) + }, + ) + }, + ) + DropdownMenuItem( + text = { Text(text = stringResource(id = R.string.action_cancel_all)) }, + onClick = { + presenter.clearQueue(context) + onExpanded(false) + }, + ) + } + } + } + }, + scrollBehavior = scrollBehavior, + ) + }, + floatingActionButton = { + AnimatedVisibility( + visible = downloadList.isNotEmpty(), + enter = fadeIn(), + exit = fadeOut(), + ) { + val isRunning by DownloadService.isRunning.collectAsState() + ExtendedFloatingActionButton( + text = { + val id = if (isRunning) { + R.string.action_pause + } else { + R.string.action_resume + } + Text(text = stringResource(id)) + }, + icon = { + val icon = if (isRunning) { + Icons.Default.Pause + } else { + Icons.Default.PlayArrow + } + Icon(imageVector = icon, contentDescription = null) + }, + onClick = { + if (isRunning) { + DownloadService.stop(context) + presenter.pauseDownloads() + } else { + DownloadService.start(context) + } + }, + expanded = fabExpanded, + modifier = Modifier.navigationBarsPadding(), + ) + } + }, + ) { contentPadding -> + if (downloadList.isEmpty()) { + EmptyScreen(textResource = R.string.information_no_downloads) + 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( + factory = { context -> + controllerBinding = DownloadListBinding.inflate(LayoutInflater.from(context)) + adapter = DownloadAdapter(this@DownloadController) + controllerBinding.recycler.adapter = adapter + adapter?.isHandleDragEnabled = true + adapter?.fastScroller = controllerBinding.fastScroller + controllerBinding.recycler.layoutManager = LinearLayoutManager(context) + + ViewCompat.setNestedScrollingEnabled(controllerBinding.root, true) + + controllerBinding.root + }, + update = { + controllerBinding.recycler + .updatePadding( + left = left, + top = top, + right = right, + bottom = bottom, + ) + + controllerBinding.fastScroller + .updateLayoutParams { + leftMargin = left + topMargin = top + rightMargin = right + bottomMargin = bottom + } + + adapter?.updateDataSet(downloadList) + }, + ) + } + } } override fun onDestroyView(view: View) { @@ -138,32 +322,6 @@ class DownloadController : super.onDestroyView(view) } - override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { - inflater.inflate(R.menu.download_queue, menu) - } - - override fun onPrepareOptionsMenu(menu: Menu) { - menu.findItem(R.id.clear_queue).isVisible = !presenter.downloadQueue.isEmpty() - menu.findItem(R.id.reorder).isVisible = !presenter.downloadQueue.isEmpty() - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - val context = applicationContext ?: return false - when (item.itemId) { - R.id.clear_queue -> { - DownloadService.stop(context) - presenter.clearQueue() - } - R.id.newest, R.id.oldest -> { - reorderQueue({ it.download.chapter.date_upload }, item.itemId == R.id.newest) - } - R.id.asc, R.id.desc -> { - reorderQueue({ it.download.chapter.chapter_number }, item.itemId == R.id.desc) - } - } - return super.onOptionsItemSelected(item) - } - private fun > reorderQueue(selector: (DownloadItem) -> R, reverse: Boolean = false) { val adapter = adapter ?: return val newDownloads = mutableListOf() @@ -242,30 +400,6 @@ class DownloadController : progressSubscriptions.remove(download)?.unsubscribe() } - /** - * Called when the queue's status has changed. Updates the visibility of the buttons. - * - * @param running whether the queue is now running or not. - */ - private fun onQueueStatusChange(running: Boolean) { - isRunning = running - activity?.invalidateOptionsMenu() - - // Check if download queue is empty and update information accordingly. - setInformationView() - } - - /** - * Called from the presenter to assign the downloads for the adapter. - * - * @param downloads the downloads from the queue. - */ - fun onNextDownloads(downloads: List) { - activity?.invalidateOptionsMenu() - setInformationView() - adapter?.updateDataSet(downloads) - } - /** * Called when the progress of a download changes. * @@ -291,39 +425,7 @@ class DownloadController : * @return the holder of the download or null if it's not bound. */ private fun getHolder(download: Download): DownloadHolder? { - return binding.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder - } - - /** - * Set information view when queue is empty - */ - private fun setInformationView() { - if (presenter.downloadQueue.isEmpty()) { - binding.emptyView.show(R.string.information_no_downloads) - actionFab?.isVisible = false - updateTitle() - } else { - binding.emptyView.hide() - actionFab?.apply { - isVisible = true - - setText( - if (isRunning) { - R.string.action_pause - } else { - R.string.action_resume - }, - ) - - setIconResource( - if (isRunning) { - R.drawable.ic_pause_24dp - } else { - R.drawable.ic_play_arrow_24dp - }, - ) - } - } + return controllerBinding.recycler.findViewHolderForItemId(download.chapter.id!!) as? DownloadHolder } /** @@ -373,7 +475,7 @@ class DownloadController : ?.filterIsInstance() ?.map(DownloadItem::download) ?.partition { item.download.manga.id == it.manga.id } - ?: Pair(listOf(), listOf()) + ?: Pair(listOf(), listOf()) presenter.reorder(selectedSeries + otherSeries) } R.id.cancel_download -> { @@ -391,14 +493,4 @@ class DownloadController : } } } - - private fun updateTitle(queueSize: Int = 0) { - val defaultTitle = getTitle() - - if (queueSize == 0) { - setTitle(defaultTitle) - } else { - setTitle("$defaultTitle ($queueSize)") - } - } } 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 index 6d4f72078..e8da53cb6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderItem.kt @@ -35,14 +35,25 @@ data class DownloadHeaderItem( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other is DownloadHeaderItem) { - return id == other.id && name == other.name - } - return false + 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 { - return id.hashCode() + var result = id.hashCode() + result = 31 * result + name.hashCode() + result = 31 * result + size + result = 31 * result + subItems.hashCode() + return result } init { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt index b5c917eea..078e756e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadPresenter.kt @@ -1,14 +1,21 @@ package eu.kanade.tachiyomi.ui.download +import android.content.Context import android.os.Bundle import eu.kanade.tachiyomi.data.download.DownloadManager +import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.DownloadQueue import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.catch +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import logcat.LogPriority -import rx.Observable -import rx.android.schedulers.AndroidSchedulers import uy.kohesive.injekt.injectLazy /** @@ -21,37 +28,34 @@ class DownloadPresenter : BasePresenter() { /** * Property to get the queue from the download manager. */ - val downloadQueue: DownloadQueue + private val downloadQueue: DownloadQueue get() = downloadManager.queue + private val _state = MutableStateFlow(emptyList()) + val state = _state.asStateFlow() + override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - downloadQueue.getUpdatedObservable() - .observeOn(AndroidSchedulers.mainThread()) - .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) }) + presenterScope.launch { + downloadQueue.getUpdatedAsFlow() + .catch { error -> logcat(LogPriority.ERROR, error) } + .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) }) + } } - } - } - .subscribeLatestCache(DownloadController::onNextDownloads) { _, error -> - logcat(LogPriority.ERROR, error) - } + } + .collect { newList -> _state.update { newList } } + } } - fun getDownloadStatusObservable(): Observable { - return downloadQueue.getStatusObservable() - .startWith(downloadQueue.getActiveDownloads()) - } + fun getDownloadStatusFlow() = downloadQueue.getStatusAsFlow() - fun getDownloadProgressObservable(): Observable { - return downloadQueue.getProgressObservable() - .onBackpressureBuffer() - } + fun getDownloadProgressFlow() = downloadQueue.getProgressAsFlow() /** * Pauses the download queue. @@ -63,7 +67,8 @@ class DownloadPresenter : BasePresenter() { /** * Clears the download queue. */ - fun clearQueue() { + fun clearQueue(context: Context) { + DownloadService.stop(context) downloadManager.clearQueue() } diff --git a/app/src/main/res/layout/download_header.xml b/app/src/main/res/layout/download_header.xml index 191e7e315..306f46174 100644 --- a/app/src/main/res/layout/download_header.xml +++ b/app/src/main/res/layout/download_header.xml @@ -30,6 +30,7 @@ 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" diff --git a/app/src/main/res/layout/download_item.xml b/app/src/main/res/layout/download_item.xml index cfd6dbfc3..92df1c370 100644 --- a/app/src/main/res/layout/download_item.xml +++ b/app/src/main/res/layout/download_item.xml @@ -87,6 +87,7 @@ 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" diff --git a/app/src/main/res/layout/download_controller.xml b/app/src/main/res/layout/download_list.xml similarity index 75% rename from app/src/main/res/layout/download_controller.xml rename to app/src/main/res/layout/download_list.xml index db9867b49..e2c96ff12 100644 --- a/app/src/main/res/layout/download_controller.xml +++ b/app/src/main/res/layout/download_list.xml @@ -11,7 +11,6 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:clipToPadding="false" - android:paddingBottom="@dimen/fab_list_padding" tools:listitem="@layout/download_item" /> - - diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 52321526e..73cff86f6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -62,6 +62,7 @@ flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c801 photoview = "com.github.chrisbanes:PhotoView:2.3.0" directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0" insetter = "dev.chrisbanes.insetter:insetter:0.6.1" +cascade = "me.saket.cascade:cascade-compose:2.0.0-beta1" conductor-core = { module = "com.bluelinelabs:conductor", version.ref = "conductor_version" } conductor-support-preference = { module = "com.github.tachiyomiorg:conductor-support-preference", version.ref = "conductor_version" }