progress-banner (#163)

* feat: banner showing updating status

* showing progress for updating

* showing progress for syncing & backup restoring too

* preferences for progress banner
This commit is contained in:
Tran M. Cuong 2024-07-09 15:00:37 +07:00 committed by GitHub
parent 0496c608c0
commit 050c661c9e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 170 additions and 40 deletions

View file

@ -98,4 +98,11 @@ class SyncPreferences(
preferenceStore.getBoolean("sync_on_app_resume", false)
.set(syncTriggerOptions.syncOnAppResume)
}
// KMK -->
fun showSyncingProgressBanner() = preferenceStore.getBoolean(
Preference.appStateKey("pref_show_syncing_progress_banner_key"),
true,
)
// KMK <--
}

View file

@ -33,6 +33,8 @@ import androidx.compose.ui.util.fastMaxBy
import dev.icerock.moko.resources.StringResource
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.i18n.stringResource
import java.math.RoundingMode
import java.text.NumberFormat
val DownloadedOnlyBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary
@ -41,6 +43,15 @@ val IncognitoModeBannerBackgroundColor
val IndexingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.secondary
// KMK -->
val RestoringBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.error
val SyncingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.secondary
val UpdatingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary
// KMK <--
@Composable
fun WarningBanner(
textRes: StringResource,
@ -58,6 +69,13 @@ fun WarningBanner(
)
}
// KMK -->
private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
}
// KMK <--
@Composable
fun AppStateBanners(
downloadedOnlyMode: Boolean,
@ -66,8 +84,12 @@ fun AppStateBanners(
// KMK -->
restoring: Boolean,
syncing: Boolean,
updating: Boolean,
// KMK <--
modifier: Modifier = Modifier,
// KMK -->
progress: Float? = null,
// KMK <--
) {
val density = LocalDensity.current
val mainInsets = WindowInsets.statusBars
@ -76,7 +98,7 @@ fun AppStateBanners(
val indexingPlaceable = subcompose(0) {
AnimatedVisibility(
// KMK -->
visible = indexing || restoring || syncing,
visible = indexing || restoring || syncing || updating,
// KMK <--
enter = expandVertically(),
exit = shrinkVertically(),
@ -85,8 +107,18 @@ fun AppStateBanners(
modifier = Modifier.windowInsetsPadding(mainInsets),
// KMK -->
text = when {
syncing -> stringResource(MR.strings.syncing_library)
restoring -> stringResource(MR.strings.restoring_backup)
updating -> progress?.let {
stringResource(
MR.strings.notification_updating_progress,
percentFormatter.format(it),
)
} ?: stringResource(MR.strings.updating_library)
syncing -> progress?.let {
stringResource(MR.strings.syncing_library) + " (${percentFormatter.format(it)})"
} ?: stringResource(MR.strings.syncing_library)
restoring -> progress?.let {
stringResource(MR.strings.restoring_backup) + " (${percentFormatter.format(it)})"
} ?: stringResource(MR.strings.restoring_backup)
else -> stringResource(MR.strings.download_notifier_cache_renewal)
},
// KMK <--

View file

@ -71,6 +71,7 @@ import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
@ -274,6 +275,12 @@ object SettingsDataScreen : SearchableSettings {
stringResource(MR.strings.backup_info) + "\n\n" +
stringResource(MR.strings.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)),
),
// KMK -->
Preference.PreferenceItem.SwitchPreference(
pref = backupPreferences.showRestoringProgressBanner(),
title = stringResource(KMR.strings.pref_show_restoring_progress_banner),
),
// KMK <--
),
)
}
@ -403,7 +410,16 @@ object SettingsDataScreen : SearchableSettings {
@Composable
private fun getAdditionalPreferences(syncPreferences: SyncPreferences): List<Preference> {
return listOf(getSyncNowPref(), getAutomaticSyncGroup(syncPreferences))
return listOf(
getSyncNowPref(),
getAutomaticSyncGroup(syncPreferences),
// KMK -->
Preference.PreferenceItem.SwitchPreference(
pref = syncPreferences.showSyncingProgressBanner(),
title = stringResource(KMR.strings.pref_show_syncing_progress_banner),
),
// KMK <--
)
}
@Composable

View file

@ -40,6 +40,7 @@ import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_C
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_NON_READ
import tachiyomi.domain.library.service.LibraryPreferences.Companion.MANGA_OUTSIDE_RELEASE_PERIOD
import tachiyomi.i18n.MR
import tachiyomi.i18n.kmk.KMR
import tachiyomi.i18n.sy.SYMR
import tachiyomi.presentation.core.i18n.pluralStringResource
import tachiyomi.presentation.core.i18n.stringResource
@ -242,6 +243,12 @@ object SettingsLibraryScreen : SearchableSettings {
subtitle = stringResource(SYMR.strings.pref_library_mark_duplicate_chapters_summary),
),
// SY <--
// KMK -->
Preference.PreferenceItem.SwitchPreference(
pref = libraryPreferences.showUpdatingProgressBanner(),
title = stringResource(KMR.strings.pref_show_updating_progress_banner),
),
// KMK <--
),
)
}

View file

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.restore
package eu.kanade.tachiyomi.data
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -7,7 +7,7 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.stateIn
class BackupRestoreStatus {
open class BannerProgressStatus {
private val scope = CoroutineScope(Dispatchers.IO)
private val _isRunning = MutableStateFlow(false)
@ -23,4 +23,14 @@ class BackupRestoreStatus {
suspend fun stop() {
_isRunning.emit(false)
}
val progress = MutableStateFlow(0f)
suspend fun updateProgress(progress: Float) {
this.progress.emit(progress)
}
}
class LibraryUpdateStatus : BannerProgressStatus()
class SyncStatus : BannerProgressStatus()
class BackupRestoreStatus : BannerProgressStatus()

View file

@ -6,6 +6,7 @@ import androidx.core.app.NotificationCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.BackupRestoreStatus
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -16,6 +17,8 @@ import tachiyomi.core.common.i18n.pluralStringResource
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
@ -24,6 +27,10 @@ class BackupNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
// KMK -->
private val backupRestoreStatus: BackupRestoreStatus = Injekt.get()
// KMK <--
private val progressNotificationBuilder = context.notificationBuilder(
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS,
) {
@ -87,7 +94,7 @@ class BackupNotifier(private val context: Context) {
}
}
fun showRestoreProgress(
suspend fun showRestoreProgress(
content: String = "",
progress: Int = 0,
maxAmount: Int = 100,
@ -107,6 +114,9 @@ class BackupNotifier(private val context: Context) {
setProgress(maxAmount, progress, false)
setOnlyAlertOnce(true)
// KMK -->
backupRestoreStatus.updateProgress(progress.toFloat() / maxAmount)
// KMK <--
clearActions()
addAction(

View file

@ -11,6 +11,7 @@ import androidx.work.ForegroundInfo
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import eu.kanade.tachiyomi.data.BackupRestoreStatus
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification

View file

@ -24,6 +24,7 @@ import eu.kanade.domain.manga.model.toSManga
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.domain.track.model.toDbTrack
import eu.kanade.domain.track.model.toDomainTrack
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.notification.Notifications
@ -134,6 +135,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val notifier = LibraryUpdateNotifier(context)
// KMK -->
private val libraryUpdateStatus: LibraryUpdateStatus = Injekt.get()
// KMK <--
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
override suspend fun doWork(): Result {
@ -150,6 +155,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
}
// KMK -->
libraryUpdateStatus.start()
// KMK <--
setForegroundSafely()
val target = inputData.getString(KEY_TARGET)?.let { Target.valueOf(it) } ?: Target.CHAPTERS
@ -187,6 +196,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
} finally {
notifier.cancelProgressNotification()
// KMK -->
libraryUpdateStatus.stop()
// KMK <--
}
}
}

View file

@ -16,6 +16,7 @@ import coil3.transform.CircleCropTransformation
import eu.kanade.presentation.util.formatChapterNumber
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.download.Downloader
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
@ -49,6 +50,9 @@ class LibraryUpdateNotifier(
private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
) {
// KMK -->
private val libraryUpdateStatus: LibraryUpdateStatus = Injekt.get()
// KMK <--
private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
@ -90,7 +94,7 @@ class LibraryUpdateNotifier(
* @param current the current progress.
* @param total the total progress.
*/
fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
suspend fun showProgressNotification(manga: List<Manga>, current: Int, total: Int) {
progressNotificationBuilder
.setContentTitle(
context.stringResource(
@ -99,6 +103,10 @@ class LibraryUpdateNotifier(
),
)
// KMK -->
libraryUpdateStatus.updateProgress(current.toFloat() / total)
// KMK <--
if (!securityPreferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))

View file

@ -13,6 +13,7 @@ import androidx.work.WorkInfo
import androidx.work.WorkQuery
import androidx.work.WorkerParameters
import eu.kanade.domain.sync.SyncPreferences
import eu.kanade.tachiyomi.data.SyncStatus
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.isRunning

View file

@ -5,17 +5,24 @@ import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.SyncStatus
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.cancelNotification
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notify
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
class SyncNotifier(private val context: Context) {
private val preferences: SecurityPreferences by injectLazy()
// KMK -->
private val syncStatus: SyncStatus = Injekt.get()
// KMK <--
private val progressNotificationBuilder = context.notificationBuilder(
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS,
) {
@ -38,7 +45,11 @@ class SyncNotifier(private val context: Context) {
context.notify(id, build())
}
fun showSyncProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
suspend fun showSyncProgress(
content: String = "",
progress: Int = 0,
maxAmount: Int = 100,
): NotificationCompat.Builder {
val builder = with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.syncing_library))
@ -48,6 +59,9 @@ class SyncNotifier(private val context: Context) {
setProgress(maxAmount, progress, true)
setOnlyAlertOnce(true)
// KMK -->
syncStatus.updateProgress(progress.toFloat() / maxAmount)
// KMK <--
clearActions()
addAction(

View file

@ -1,26 +0,0 @@
package eu.kanade.tachiyomi.data.sync
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.stateIn
class SyncStatus {
private val scope = CoroutineScope(Dispatchers.IO)
private val _isRunning = MutableStateFlow(false)
val isRunning = _isRunning
.debounce(1000L) // Don't notify if it finishes quickly enough
.stateIn(scope, SharingStarted.WhileSubscribed(), false)
suspend fun start() {
_isRunning.emit(true)
}
suspend fun stop() {
_isRunning.emit(false)
}
}

View file

@ -10,7 +10,9 @@ import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.core.security.SecurityPreferences
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreStatus
import eu.kanade.tachiyomi.data.BackupRestoreStatus
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.SyncStatus
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
@ -18,7 +20,6 @@ import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadProvider
import eu.kanade.tachiyomi.data.saver.ImageSaver
import eu.kanade.tachiyomi.data.sync.SyncStatus
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
import eu.kanade.tachiyomi.data.track.TrackerManager
import eu.kanade.tachiyomi.extension.ExtensionManager
@ -177,6 +178,7 @@ class AppModule(val app: Application) : InjektModule {
// KMK -->
addSingletonFactory { BackupRestoreStatus() }
addSingletonFactory { SyncStatus() }
addSingletonFactory { LibraryUpdateStatus() }
// KMK <--
// Asynchronously init expensive components for a faster cold start

View file

@ -54,6 +54,9 @@ import eu.kanade.presentation.components.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor
import eu.kanade.presentation.components.IndexingBannerBackgroundColor
import eu.kanade.presentation.components.RestoringBannerBackgroundColor
import eu.kanade.presentation.components.SyncingBannerBackgroundColor
import eu.kanade.presentation.components.UpdatingBannerBackgroundColor
import eu.kanade.presentation.more.settings.screen.ConfigureExhDialog
import eu.kanade.presentation.more.settings.screen.about.WhatsNewDialog
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
@ -61,12 +64,13 @@ import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
import eu.kanade.presentation.util.AssistContentScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreStatus
import eu.kanade.tachiyomi.data.BackupRestoreStatus
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.SyncStatus
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.coil.MangaCoverMetadata
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.sync.SyncStatus
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.extension.api.ExtensionApi
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
@ -127,6 +131,7 @@ class MainActivity : BaseActivity() {
// KMK -->
private val backupRestoreStatus: BackupRestoreStatus by injectLazy()
private val syncStatus: SyncStatus by injectLazy()
private val libraryUpdateStatus: LibraryUpdateStatus by injectLazy()
// KMK <--
private val downloadCache: DownloadCache by injectLazy()
@ -197,14 +202,23 @@ class MainActivity : BaseActivity() {
val downloadOnly by preferences.downloadedOnly().collectAsState()
val indexing by downloadCache.isInitializing.collectAsState()
// KMK -->
val syncing by syncStatus.isRunning.collectAsState()
val restoring by backupRestoreStatus.isRunning.collectAsState()
val syncing by syncStatus.isRunning.collectAsState()
val updating by libraryUpdateStatus.isRunning.collectAsState()
val restoringProgress by backupRestoreStatus.progress.collectAsState()
val syncingProgress by syncStatus.progress.collectAsState()
val updatingProgress by libraryUpdateStatus.progress.collectAsState()
// KMK <--
// Set status bar color considering the top app state banner
val systemUiController = rememberSystemUiController()
val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when {
// KMK -->
updating -> UpdatingBannerBackgroundColor
syncing -> SyncingBannerBackgroundColor
restoring -> RestoringBannerBackgroundColor
// KMK <--
indexing -> IndexingBannerBackgroundColor
downloadOnly -> DownloadedOnlyBannerBackgroundColor
incognito -> IncognitoModeBannerBackgroundColor
@ -274,6 +288,10 @@ class MainActivity : BaseActivity() {
// KMK -->
restoring = restoring,
syncing = syncing,
updating = updating,
progress = updatingProgress.takeIf { updating }
?: syncingProgress.takeIf { syncing }
?: restoringProgress.takeIf { restoring },
// KMK <--
modifier = Modifier.windowInsetsPadding(scaffoldInsets),
)

View file

@ -10,4 +10,11 @@ class BackupPreferences(
fun backupInterval() = preferenceStore.getInt("backup_interval", 12)
fun lastAutoBackupTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_auto_backup_timestamp"), 0L)
// KMK -->
fun showRestoringProgressBanner() = preferenceStore.getBoolean(
Preference.appStateKey("pref_show_restoring_progress_banner_key"),
true,
)
// KMK <--
}

View file

@ -36,6 +36,12 @@ class LibraryPreferences(
fun autoUpdateInterval() = preferenceStore.getInt("pref_library_update_interval_key", 0)
// KMK -->
fun showUpdatingProgressBanner() = preferenceStore.getBoolean(
Preference.appStateKey("pref_show_updating_progress_banner_key"),
true,
)
// KMK <--
fun coverRatios() = preferenceStore.getStringSet(
Preference.appStateKey("pref_library_cover_ratios_key"),
emptySet(),

View file

@ -33,6 +33,9 @@
<string name="theme_mocha">Mocha</string>
<string name="theme_sapphire">Sapphire</string>
<!-- Library section -->
<string name="pref_show_updating_progress_banner">Show updating progress banner</string>
<!-- Extension section -->
<string name="ext_unofficial">Unofficial</string>
<string name="unofficial_extension_message">This extension is not from the official repo.</string>
@ -53,6 +56,8 @@
<string name="pref_auto_sync_read_chapters">Auto sync read chapters from Trackers</string>
<!-- Sync section -->
<string name="pref_show_restoring_progress_banner">Show restoring progress banner</string>
<string name="pref_show_syncing_progress_banner">Show syncing progress banner</string>
<!-- Advanced section -->
<string name="pref_clean_invalid_downloads">Clean invalid downloads</string>
<string name="pref_clean_invalid_downloads_summary">Find and remove all downloads, files, folders which are not saved in your library</string>