Download control buttons in list

This commit is contained in:
Koitharu
2022-08-11 12:48:59 +03:00
parent 893d1a881d
commit c07a3b9d0d
6 changed files with 84 additions and 17 deletions

View File

@@ -1,5 +1,6 @@
package org.koitharu.kotatsu.download.ui package org.koitharu.kotatsu.download.ui
import android.view.View
import androidx.core.view.isVisible import androidx.core.view.isVisible
import coil.ImageLoader import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding
@@ -9,20 +10,33 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.databinding.ItemDownloadBinding import org.koitharu.kotatsu.databinding.ItemDownloadBinding
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.parsers.util.format import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.ext.* import org.koitharu.kotatsu.utils.ext.*
import org.koitharu.kotatsu.utils.progress.ProgressJob
fun downloadItemAD( fun downloadItemAD(
scope: CoroutineScope, scope: CoroutineScope,
coil: ImageLoader, coil: ImageLoader,
) = adapterDelegateViewBinding<ProgressJob<DownloadState>, ProgressJob<DownloadState>, ItemDownloadBinding>( ) = adapterDelegateViewBinding<DownloadItem, DownloadItem, ItemDownloadBinding>(
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }, { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
) { ) {
var job: Job? = null var job: Job? = null
val percentPattern = context.resources.getString(R.string.percent_string_pattern) val percentPattern = context.resources.getString(R.string.percent_string_pattern)
val clickListener = View.OnClickListener { v ->
when (v.id) {
R.id.button_cancel -> item.cancel()
R.id.button_resume -> item.resume()
else -> context.startActivity(
DetailsActivity.newIntent(context, item.progressValue.manga),
)
}
}
binding.buttonCancel.setOnClickListener(clickListener)
binding.buttonResume.setOnClickListener(clickListener)
itemView.setOnClickListener(clickListener)
bind { bind {
job?.cancel() job?.cancel()
job = item.progressAsFlow().onFirst { state -> job = item.progressAsFlow().onFirst { state ->
@@ -43,6 +57,8 @@ fun downloadItemAD(
binding.progressBar.isVisible = true binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
} }
is DownloadState.Done -> { is DownloadState.Done -> {
binding.textViewStatus.setText(R.string.download_complete) binding.textViewStatus.setText(R.string.download_complete)
@@ -50,6 +66,8 @@ fun downloadItemAD(
binding.progressBar.isVisible = false binding.progressBar.isVisible = false
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
} }
is DownloadState.Error -> { is DownloadState.Error -> {
binding.textViewStatus.setText(R.string.error_occurred) binding.textViewStatus.setText(R.string.error_occurred)
@@ -58,6 +76,8 @@ fun downloadItemAD(
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.text = state.error.getDisplayMessage(context.resources) binding.textViewDetails.text = state.error.getDisplayMessage(context.resources)
binding.textViewDetails.isVisible = true binding.textViewDetails.isVisible = true
binding.buttonCancel.isVisible = state.canRetry
binding.buttonResume.isVisible = state.canRetry
} }
is DownloadState.PostProcessing -> { is DownloadState.PostProcessing -> {
binding.textViewStatus.setText(R.string.processing_) binding.textViewStatus.setText(R.string.processing_)
@@ -65,6 +85,8 @@ fun downloadItemAD(
binding.progressBar.isVisible = true binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = false
binding.buttonResume.isVisible = false
} }
is DownloadState.Preparing -> { is DownloadState.Preparing -> {
binding.textViewStatus.setText(R.string.preparing_) binding.textViewStatus.setText(R.string.preparing_)
@@ -72,6 +94,8 @@ fun downloadItemAD(
binding.progressBar.isVisible = true binding.progressBar.isVisible = true
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
} }
is DownloadState.Progress -> { is DownloadState.Progress -> {
binding.textViewStatus.setText(R.string.manga_downloading_) binding.textViewStatus.setText(R.string.manga_downloading_)
@@ -82,6 +106,8 @@ fun downloadItemAD(
binding.textViewPercent.text = percentPattern.format((state.percent * 100f).format(1)) binding.textViewPercent.text = percentPattern.format((state.percent * 100f).format(1))
binding.textViewPercent.isVisible = true binding.textViewPercent.isVisible = true
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
} }
is DownloadState.Queued -> { is DownloadState.Queued -> {
binding.textViewStatus.setText(R.string.queued) binding.textViewStatus.setText(R.string.queued)
@@ -89,6 +115,8 @@ fun downloadItemAD(
binding.progressBar.isVisible = false binding.progressBar.isVisible = false
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false binding.textViewDetails.isVisible = false
binding.buttonCancel.isVisible = true
binding.buttonResume.isVisible = false
} }
} }
}.launchIn(scope) }.launchIn(scope)

View File

@@ -5,12 +5,14 @@ import coil.ImageLoader
import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter import com.hannesdorfmann.adapterdelegates4.AsyncListDifferDelegationAdapter
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.utils.progress.ProgressJob import org.koitharu.kotatsu.utils.progress.PausingProgressJob
typealias DownloadItem = PausingProgressJob<DownloadState>
class DownloadsAdapter( class DownloadsAdapter(
scope: CoroutineScope, scope: CoroutineScope,
coil: ImageLoader, coil: ImageLoader,
) : AsyncListDifferDelegationAdapter<ProgressJob<DownloadState>>(DiffCallback()) { ) : AsyncListDifferDelegationAdapter<DownloadItem>(DiffCallback()) {
init { init {
delegatesManager.addDelegate(downloadItemAD(scope, coil)) delegatesManager.addDelegate(downloadItemAD(scope, coil))
@@ -21,18 +23,18 @@ class DownloadsAdapter(
return items[position].progressValue.startId.toLong() return items[position].progressValue.startId.toLong()
} }
private class DiffCallback : DiffUtil.ItemCallback<ProgressJob<DownloadState>>() { private class DiffCallback : DiffUtil.ItemCallback<DownloadItem>() {
override fun areItemsTheSame( override fun areItemsTheSame(
oldItem: ProgressJob<DownloadState>, oldItem: DownloadItem,
newItem: ProgressJob<DownloadState>, newItem: DownloadItem,
): Boolean { ): Boolean {
return oldItem.progressValue.startId == newItem.progressValue.startId return oldItem.progressValue.startId == newItem.progressValue.startId
} }
override fun areContentsTheSame( override fun areContentsTheSame(
oldItem: ProgressJob<DownloadState>, oldItem: DownloadItem,
newItem: ProgressJob<DownloadState>, newItem: DownloadItem,
): Boolean { ): Boolean {
return oldItem.progressValue == newItem.progressValue return oldItem.progressValue == newItem.progressValue
} }

View File

@@ -16,6 +16,7 @@ import androidx.core.text.HtmlCompat
import androidx.core.text.htmlEncode import androidx.core.text.htmlEncode
import androidx.core.text.parseAsHtml import androidx.core.text.parseAsHtml
import androidx.core.util.forEach import androidx.core.util.forEach
import androidx.core.util.isNotEmpty
import androidx.core.util.size import androidx.core.util.size
import com.google.android.material.R as materialR import com.google.android.material.R as materialR
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
@@ -98,7 +99,7 @@ class DownloadNotification(private val context: Context) {
style.addLine( style.addLine(
context.getString( context.getString(
R.string.download_summary_pattern, R.string.download_summary_pattern,
state.manga.title.ellipsize(10).htmlEncode(), state.manga.title.ellipsize(16).htmlEncode(),
summary.htmlEncode(), summary.htmlEncode(),
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY), ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY),
) )
@@ -113,15 +114,17 @@ class DownloadNotification(private val context: Context) {
when (progress) { when (progress) {
1f -> groupBuilder.setProgress(0, 0, false) 1f -> groupBuilder.setProgress(0, 0, false)
0f -> groupBuilder.setProgress(1, 0, true) 0f -> groupBuilder.setProgress(1, 0, true)
else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), progress == 0f) else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), false)
} }
return groupBuilder.build() return groupBuilder.build()
} }
fun detach() { fun detach() {
manager.cancel(ID_GROUP) manager.cancel(ID_GROUP)
val notification = buildGroupNotification() if (states.isNotEmpty() && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
manager.notify(ID_GROUP_DETACHED, notification) val notification = buildGroupNotification()
manager.notify(ID_GROUP_DETACHED, notification)
}
} }
fun newItem(startId: Int) = Item(startId) fun newItem(startId: Int) = Item(startId)
@@ -171,6 +174,8 @@ class DownloadNotification(private val context: Context) {
builder.setStyle(null) builder.setStyle(null)
builder.setLargeIcon(state.cover?.toBitmap()) builder.setLargeIcon(state.cover?.toBitmap())
builder.clearActions() builder.clearActions()
builder.setSubText(null)
builder.setShowWhen(false)
builder.setVisibility( builder.setVisibility(
if (state.manga.isNsfw) { if (state.manga.isNsfw) {
NotificationCompat.VISIBILITY_PRIVATE NotificationCompat.VISIBILITY_PRIVATE
@@ -196,6 +201,8 @@ class DownloadNotification(private val context: Context) {
builder.setCategory(null) builder.setCategory(null)
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(false) builder.setOngoing(false)
builder.setShowWhen(true)
builder.setWhen(System.currentTimeMillis())
builder.priority = NotificationCompat.PRIORITY_DEFAULT builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.Error -> { is DownloadState.Error -> {
@@ -207,6 +214,8 @@ class DownloadNotification(private val context: Context) {
builder.setAutoCancel(!state.canRetry) builder.setAutoCancel(!state.canRetry)
builder.setOngoing(state.canRetry) builder.setOngoing(state.canRetry)
builder.setCategory(NotificationCompat.CATEGORY_ERROR) builder.setCategory(NotificationCompat.CATEGORY_ERROR)
builder.setShowWhen(true)
builder.setWhen(System.currentTimeMillis())
builder.setStyle(NotificationCompat.BigTextStyle().bigText(message)) builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
if (state.canRetry) { if (state.canRetry) {
builder.addAction(cancelAction) builder.addAction(cancelAction)
@@ -239,12 +248,13 @@ class DownloadNotification(private val context: Context) {
} }
is DownloadState.Progress -> { is DownloadState.Progress -> {
builder.setProgress(state.max, state.progress, false) builder.setProgress(state.max, state.progress, false)
val percent = context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
if (timeLeft > 0L) { if (timeLeft > 0L) {
val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS) val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS)
builder.setContentText(eta) builder.setContentText(eta)
builder.setSubText(percent)
} else { } else {
val percent = (state.percent * 100).format() builder.setContentText(percent)
builder.setContentText(context.getString(R.string.percent_string_pattern, percent))
} }
builder.setCategory(NotificationCompat.CATEGORY_PROGRESS) builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
builder.setStyle(null) builder.setStyle(null)

View File

@@ -129,6 +129,8 @@ class DownloadService : BaseService() {
} }
if (job.isCancelled) { if (job.isCancelled) {
notificationItem.dismiss() notificationItem.dismiss()
jobs.remove(startId)
jobCount.value = jobs.size
} else { } else {
notificationItem.notify(job.progressValue, -1L) notificationItem.notify(job.progressValue, -1L)
} }

View File

@@ -77,10 +77,10 @@
android:id="@+id/textView_percent" android:id="@+id/textView_percent"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textAppearance="?attr/textAppearanceBodyMedium" android:textAppearance="?attr/textAppearanceBodyMedium"
app:layout_constraintBaseline_toBaselineOf="@id/textView_status" app:layout_constraintBaseline_toBaselineOf="@id/textView_status"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
android:layout_marginEnd="8dp"
tools:text="25%" /> tools:text="25%" />
<TextView <TextView
@@ -98,4 +98,30 @@
app:layout_constraintTop_toBottomOf="@id/textView_status" app:layout_constraintTop_toBottomOf="@id/textView_status"
tools:text="@tools:sample/lorem[3]" /> tools:text="@tools:sample/lorem[3]" />
<Button
android:id="@+id/button_cancel"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@android:string/cancel"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/textView_details"
tools:visibility="visible" />
<Button
android:id="@+id/button_resume"
style="?buttonBarButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="@string/try_again"
android:visibility="gone"
app:layout_constraintEnd_toStartOf="@id/button_cancel"
app:layout_constraintTop_toBottomOf="@id/textView_details"
tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View File

@@ -317,5 +317,4 @@
<string name="data_deletion">Удаление данных</string> <string name="data_deletion">Удаление данных</string>
<string name="clear_cookies_summary">Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы</string> <string name="clear_cookies_summary">Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы</string>
<string name="show_all">Показать все</string> <string name="show_all">Показать все</string>
<string name="downloading_manga">Downloading manga</string>
</resources> </resources>