UI improvements
This commit is contained in:
@@ -15,8 +15,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 540
|
||||
versionName '5.0.2'
|
||||
versionCode 541
|
||||
versionName '5.1-a1'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
@@ -103,11 +103,6 @@ dependencies {
|
||||
//noinspection LifecycleAnnotationProcessorWithJava8
|
||||
kapt 'androidx.lifecycle:lifecycle-compiler:2.6.1'
|
||||
|
||||
/**
|
||||
* TODO: check
|
||||
* https://issuetracker.google.com/issues/270245927
|
||||
* https://issuetracker.google.com/issues/280504155
|
||||
*/
|
||||
implementation 'androidx.work:work-runtime-ktx:2.8.1'
|
||||
//noinspection GradleDependency
|
||||
implementation('com.google.guava:guava:31.1-android') {
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.koitharu.kotatsu.local.data.PagesCache
|
||||
import org.koitharu.kotatsu.local.domain.LocalMangaRepository
|
||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||
import org.koitharu.kotatsu.reader.domain.PageLoader
|
||||
import org.koitharu.kotatsu.utils.WorkServiceStopHelper
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
import javax.inject.Inject
|
||||
|
||||
@@ -56,6 +57,7 @@ class KotatsuApp : Application(), Configuration.Provider {
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
setupDatabaseObservers()
|
||||
}
|
||||
WorkServiceStopHelper(applicationContext).setup()
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context?) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.ui.BaseActivity
|
||||
import org.koitharu.kotatsu.base.ui.list.ListSelectionController
|
||||
import org.koitharu.kotatsu.base.ui.list.decor.SpacingItemDecoration
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleActionObserver
|
||||
import org.koitharu.kotatsu.databinding.ActivityDownloadsBinding
|
||||
import org.koitharu.kotatsu.details.ui.DetailsActivity
|
||||
import org.koitharu.kotatsu.download.ui.worker.PausingReceiver
|
||||
@@ -60,6 +61,7 @@ class DownloadsActivity : BaseActivity<ActivityDownloadsBinding>(),
|
||||
viewModel.items.observe(this) {
|
||||
downloadsAdapter.items = it
|
||||
}
|
||||
viewModel.onActionDone.observe(this, ReversibleActionObserver(binding.recyclerView))
|
||||
val menuObserver = Observer<Any> { _ -> invalidateOptionsMenu() }
|
||||
viewModel.hasActiveWorks.observe(this, menuObserver)
|
||||
viewModel.hasPausedWorks.observe(this, menuObserver)
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import androidx.core.view.MenuProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import org.koitharu.kotatsu.R
|
||||
|
||||
class DownloadsMenuProvider(
|
||||
@@ -20,8 +21,8 @@ class DownloadsMenuProvider(
|
||||
when (menuItem.itemId) {
|
||||
R.id.action_pause -> viewModel.pauseAll()
|
||||
R.id.action_resume -> viewModel.resumeAll()
|
||||
R.id.action_cancel_all -> viewModel.cancelAll()
|
||||
R.id.action_remove_completed -> viewModel.removeCompleted()
|
||||
R.id.action_cancel_all -> confirmCancelAll()
|
||||
R.id.action_remove_completed -> confirmRemoveCompleted()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
@@ -33,4 +34,30 @@ class DownloadsMenuProvider(
|
||||
menu.findItem(R.id.action_resume)?.isVisible = viewModel.hasPausedWorks.value == true
|
||||
menu.findItem(R.id.action_cancel_all)?.isVisible = viewModel.hasCancellableWorks.value == true
|
||||
}
|
||||
|
||||
private fun confirmCancelAll() {
|
||||
MaterialAlertDialogBuilder(
|
||||
context,
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
|
||||
).setTitle(R.string.cancel_all)
|
||||
.setMessage(R.string.cancel_all_downloads_confirm)
|
||||
.setIcon(R.drawable.ic_cancel_multiple)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.confirm) { _, _ ->
|
||||
viewModel.cancelAll()
|
||||
}.show()
|
||||
}
|
||||
|
||||
private fun confirmRemoveCompleted() {
|
||||
MaterialAlertDialogBuilder(
|
||||
context,
|
||||
com.google.android.material.R.style.ThemeOverlay_Material3_MaterialAlertDialog_Centered,
|
||||
).setTitle(R.string.remove_completed)
|
||||
.setMessage(R.string.remove_completed_downloads_confirm)
|
||||
.setIcon(R.drawable.ic_clear_all)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(R.string.clear) { _, _ ->
|
||||
viewModel.removeCompleted()
|
||||
}.show()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.sync.withLock
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.base.domain.MangaDataRepository
|
||||
import org.koitharu.kotatsu.base.ui.BaseViewModel
|
||||
import org.koitharu.kotatsu.base.ui.util.ReversibleAction
|
||||
import org.koitharu.kotatsu.core.ui.DateTimeAgo
|
||||
import org.koitharu.kotatsu.download.domain.DownloadState
|
||||
import org.koitharu.kotatsu.download.ui.worker.DownloadWorker
|
||||
@@ -26,6 +27,7 @@ import org.koitharu.kotatsu.list.ui.model.ListModel
|
||||
import org.koitharu.kotatsu.list.ui.model.LoadingState
|
||||
import org.koitharu.kotatsu.parsers.model.Manga
|
||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||
import org.koitharu.kotatsu.utils.SingleLiveEvent
|
||||
import org.koitharu.kotatsu.utils.asFlowLiveData
|
||||
import org.koitharu.kotatsu.utils.ext.daysDiff
|
||||
import java.util.Date
|
||||
@@ -45,6 +47,8 @@ class DownloadsViewModel @Inject constructor(
|
||||
.mapLatest { it.toDownloadsList() }
|
||||
.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||
|
||||
val onActionDone = SingleLiveEvent<ReversibleAction>()
|
||||
|
||||
val items = works.map {
|
||||
it?.toUiList() ?: listOf(LoadingState)
|
||||
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
|
||||
@@ -75,12 +79,14 @@ class DownloadsViewModel @Inject constructor(
|
||||
workScheduler.cancel(work.id)
|
||||
}
|
||||
}
|
||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelAll() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
workScheduler.cancelAll()
|
||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_cancelled, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,24 +97,35 @@ class DownloadsViewModel @Inject constructor(
|
||||
workScheduler.pause(work.id)
|
||||
}
|
||||
}
|
||||
onActionDone.call(ReversibleAction(R.string.downloads_paused, null))
|
||||
}
|
||||
|
||||
fun pauseAll() {
|
||||
val snapshot = works.value ?: return
|
||||
var isPaused = false
|
||||
for (work in snapshot) {
|
||||
if (work.canPause) {
|
||||
workScheduler.pause(work.id)
|
||||
isPaused = true
|
||||
}
|
||||
}
|
||||
if (isPaused) {
|
||||
onActionDone.call(ReversibleAction(R.string.downloads_paused, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun resumeAll() {
|
||||
val snapshot = works.value ?: return
|
||||
var isResumed = false
|
||||
for (work in snapshot) {
|
||||
if (work.workState == WorkInfo.State.RUNNING && work.isPaused) {
|
||||
workScheduler.resume(work.id)
|
||||
isResumed = true
|
||||
}
|
||||
}
|
||||
if (isResumed) {
|
||||
onActionDone.call(ReversibleAction(R.string.downloads_resumed, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun resume(ids: Set<Long>) {
|
||||
@@ -118,6 +135,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
workScheduler.resume(work.id)
|
||||
}
|
||||
}
|
||||
onActionDone.call(ReversibleAction(R.string.downloads_resumed, null))
|
||||
}
|
||||
|
||||
fun remove(ids: Set<Long>) {
|
||||
@@ -128,12 +146,14 @@ class DownloadsViewModel @Inject constructor(
|
||||
workScheduler.delete(work.id)
|
||||
}
|
||||
}
|
||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null))
|
||||
}
|
||||
}
|
||||
|
||||
fun removeCompleted() {
|
||||
launchJob(Dispatchers.Default) {
|
||||
workScheduler.removeCompleted()
|
||||
onActionDone.emitCall(ReversibleAction(R.string.downloads_removed, null))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,7 +227,7 @@ class DownloadsViewModel @Inject constructor(
|
||||
private fun emptyStateList() = listOf(
|
||||
EmptyState(
|
||||
icon = R.drawable.ic_empty_common,
|
||||
textPrimary = R.string.text_downloads_holder,
|
||||
textPrimary = R.string.text_downloads_list_holder,
|
||||
textSecondary = 0,
|
||||
actionStringRes = 0,
|
||||
),
|
||||
|
||||
@@ -10,6 +10,7 @@ import com.google.android.material.badge.BadgeDrawable
|
||||
import com.google.android.material.badge.BadgeUtils
|
||||
import com.google.android.material.badge.ExperimentalBadgeUtils
|
||||
import org.koitharu.kotatsu.R
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@CheckResult
|
||||
fun View.bindBadge(badge: BadgeDrawable?, counter: Int): BadgeDrawable? {
|
||||
@@ -44,7 +45,7 @@ private fun BadgeDrawable.align(anchor: View) {
|
||||
val extraOffset = if (anchor is CardView) {
|
||||
(anchor.radius / 2f).toInt()
|
||||
} else {
|
||||
0
|
||||
anchor.resources.getDimensionPixelOffset(materialR.dimen.m3_badge_offset)
|
||||
}
|
||||
horizontalOffset = intrinsicWidth + extraOffset
|
||||
verticalOffset = intrinsicHeight + extraOffset
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.koitharu.kotatsu.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.asFlow
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.impl.foreground.SystemForegroundService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
|
||||
|
||||
/**
|
||||
* Workaround for issue
|
||||
* https://issuetracker.google.com/issues/270245927
|
||||
* https://issuetracker.google.com/issues/280504155
|
||||
*/
|
||||
class WorkServiceStopHelper(
|
||||
private val context: Context,
|
||||
) {
|
||||
|
||||
fun setup() {
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
WorkManager.getInstance(context)
|
||||
.getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
|
||||
.asFlow()
|
||||
.collectLatest {
|
||||
if (it.isEmpty()) {
|
||||
delay(1_000)
|
||||
stopWorkerService()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private fun stopWorkerService() {
|
||||
SystemForegroundService.getInstance()?.stop()
|
||||
}
|
||||
}
|
||||
|
||||
12
app/src/main/res/drawable/ic_cancel_multiple.xml
Normal file
12
app/src/main/res/drawable/ic_cancel_multiple.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:tint="?colorControlNormal"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#000000"
|
||||
android:pathData="M18.54 9.88L17.12 8.47L15 10.59L12.88 8.47L11.47 9.88L13.59 12L11.47 14.12L12.88 15.54L15 13.41L17.12 15.54L18.54 14.12L16.41 12M2 12C2 9.21 3.64 6.8 6 5.68V3.5C2.5 4.76 0 8.09 0 12S2.5 19.24 6 20.5V18.32C3.64 17.2 2 14.79 2 12M15 3C10.04 3 6 7.04 6 12S10.04 21 15 21 24 16.96 24 12 19.96 3 15 3M15 19C11.14 19 8 15.86 8 12S11.14 5 15 5 22 8.14 22 12 18.86 19 15 19Z" />
|
||||
</vector>
|
||||
@@ -33,7 +33,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="62dp"
|
||||
android:textAllCaps="true"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Top"
|
||||
@@ -46,7 +45,6 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="62dp"
|
||||
android:textAllCaps="true"
|
||||
android:visibility="gone"
|
||||
app:shapeAppearance="?shapeAppearanceCornerMedium"
|
||||
app:shapeAppearanceOverlay="@style/ShapeAppearanceOverlay.Material3.Corner.Bottom"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<item
|
||||
android:id="@+id/action_cancel"
|
||||
android:icon="@drawable/abc_ic_clear_material"
|
||||
android:icon="@drawable/ic_cancel_multiple"
|
||||
android:title="@android:string/cancel"
|
||||
app:showAsAction="ifRoom|withText" />
|
||||
|
||||
|
||||
@@ -450,4 +450,11 @@
|
||||
<string name="downloads_wifi_only_summary">Stop downloading when switching to a mobile network</string>
|
||||
<string name="enable">Enable</string>
|
||||
<string name="no_thanks">No thanks</string>
|
||||
<string name="cancel_all_downloads_confirm">All active downloads will be cancelled, partially downloaded data will be lost</string>
|
||||
<string name="remove_completed_downloads_confirm">Your downloads history will be permanently deleted</string>
|
||||
<string name="text_downloads_list_holder">You don\'t have any downloads</string>
|
||||
<string name="downloads_resumed">Downloads have been resumed</string>
|
||||
<string name="downloads_paused">Downloads have been paused</string>
|
||||
<string name="downloads_removed">Downloads have been removed</string>
|
||||
<string name="downloads_cancelled">Downloads have been cancelled</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user