UI improvements

This commit is contained in:
Koitharu
2023-05-11 11:46:45 +03:00
parent 2b12dbd8d7
commit 248bf8ed03
11 changed files with 122 additions and 14 deletions

View File

@@ -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') {

View File

@@ -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?) {

View File

@@ -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)

View File

@@ -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()
}
}

View File

@@ -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,
),

View File

@@ -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

View File

@@ -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()
}
}

View 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>

View File

@@ -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"

View File

@@ -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" />

View File

@@ -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>