From 248bf8ed03a26c660ce814ca3b2cd0b7eb77827f Mon Sep 17 00:00:00 2001 From: Koitharu Date: Thu, 11 May 2023 11:46:45 +0300 Subject: [PATCH] UI improvements --- app/build.gradle | 9 +--- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 2 + .../download/ui/list/DownloadsActivity.kt | 2 + .../download/ui/list/DownloadsMenuProvider.kt | 31 ++++++++++++- .../download/ui/list/DownloadsViewModel.kt | 22 +++++++++- .../kotatsu/list/ui/adapter/BadgeADUtil.kt | 3 +- .../kotatsu/utils/WorkServiceStopHelper.kt | 44 +++++++++++++++++++ .../main/res/drawable/ic_cancel_multiple.xml | 12 +++++ .../main/res/layout/dialog_two_buttons.xml | 2 - app/src/main/res/menu/mode_downloads.xml | 2 +- app/src/main/res/values/strings.xml | 7 +++ 11 files changed, 122 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt create mode 100644 app/src/main/res/drawable/ic_cancel_multiple.xml diff --git a/app/build.gradle b/app/build.gradle index bf3e4b9e9..08870f3c0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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') { diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 57ce0686c..95d48afa2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -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?) { diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt index 1fe368b37..ddc1e9154 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsActivity.kt @@ -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(), viewModel.items.observe(this) { downloadsAdapter.items = it } + viewModel.onActionDone.observe(this, ReversibleActionObserver(binding.recyclerView)) val menuObserver = Observer { _ -> invalidateOptionsMenu() } viewModel.hasActiveWorks.observe(this, menuObserver) viewModel.hasPausedWorks.observe(this, menuObserver) diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt index a864cf3ac..89428502e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsMenuProvider.kt @@ -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() + } } diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt index f34651e8d..cdcf5c4db 100644 --- a/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/list/DownloadsViewModel.kt @@ -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() + 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) { @@ -118,6 +135,7 @@ class DownloadsViewModel @Inject constructor( workScheduler.resume(work.id) } } + onActionDone.call(ReversibleAction(R.string.downloads_resumed, null)) } fun remove(ids: Set) { @@ -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, ), diff --git a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt index 4e598c86b..2cb70f71a 100644 --- a/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt +++ b/app/src/main/java/org/koitharu/kotatsu/list/ui/adapter/BadgeADUtil.kt @@ -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 diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt b/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt new file mode 100644 index 000000000..38a82cfe8 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/utils/WorkServiceStopHelper.kt @@ -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() + } +} + diff --git a/app/src/main/res/drawable/ic_cancel_multiple.xml b/app/src/main/res/drawable/ic_cancel_multiple.xml new file mode 100644 index 000000000..44aa35c5b --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_multiple.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/dialog_two_buttons.xml b/app/src/main/res/layout/dialog_two_buttons.xml index 85b031bb1..c6220526a 100644 --- a/app/src/main/res/layout/dialog_two_buttons.xml +++ b/app/src/main/res/layout/dialog_two_buttons.xml @@ -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" diff --git a/app/src/main/res/menu/mode_downloads.xml b/app/src/main/res/menu/mode_downloads.xml index 107ab434b..e0f185b65 100644 --- a/app/src/main/res/menu/mode_downloads.xml +++ b/app/src/main/res/menu/mode_downloads.xml @@ -17,7 +17,7 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fc464a07a..ca2718ff7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -450,4 +450,11 @@ Stop downloading when switching to a mobile network Enable No thanks + All active downloads will be cancelled, partially downloaded data will be lost + Your downloads history will be permanently deleted + You don\'t have any downloads + Downloads have been resumed + Downloads have been paused + Downloads have been removed + Downloads have been cancelled