Skip to content

Showing in-app progress bar for backup restoring & library updating #1835

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Add back support for drag-and-drop category reordering ([@cuong-tran](https://github.com/cuong-tran)) ([#1427](https://github.com/mihonapp/mihon/pull/1427))
- Add option to mark duplicate read chapters as read
- Display staff information on Anilist tracker search results ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1810](https://github.com/mihonapp/mihon/pull/1810))
- Showing in-app progress bar for backup restoring & library updating ([@cuong-tran](https://github.com/cuong-tran)) ([#1835](https://github.com/mihonapp/mihon/pull/1835))

### Changed
- Sliders UI
Expand Down
38 changes: 35 additions & 3 deletions app/src/main/java/eu/kanade/presentation/components/Banners.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -41,6 +43,11 @@ val IncognitoModeBannerBackgroundColor
val IndexingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.secondary

val RestoringBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.error
val UpdatingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary

@Composable
fun WarningBanner(
textRes: StringResource,
Expand All @@ -58,11 +65,19 @@ fun WarningBanner(
)
}

private val percentFormatter = NumberFormat.getPercentInstance().apply {
roundingMode = RoundingMode.DOWN
maximumFractionDigits = 0
}

@Composable
fun AppStateBanners(
downloadedOnlyMode: Boolean,
incognitoMode: Boolean,
indexing: Boolean,
restoring: Boolean,
updating: Boolean,
progress: Float? = null,
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
Expand All @@ -71,12 +86,26 @@ fun AppStateBanners(
SubcomposeLayout(modifier = modifier) { constraints ->
val indexingPlaceable = subcompose(0) {
AnimatedVisibility(
visible = indexing,
visible = indexing || restoring || updating,
enter = expandVertically(),
exit = shrinkVertically(),
) {
IndexingDownloadBanner(
modifier = Modifier.windowInsetsPadding(mainInsets),
text = when {
updating -> progress?.let {
stringResource(
MR.strings.notification_updating_progress,
percentFormatter.format(it),
)
} ?: stringResource(MR.strings.updating_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)
},
)
}
}.fastMap { it.measure(constraints) }
Expand Down Expand Up @@ -155,7 +184,10 @@ private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
}

@Composable
private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
private fun IndexingDownloadBanner(
text: String = stringResource(MR.strings.download_notifier_cache_renewal),
modifier: Modifier = Modifier,
) {
val density = LocalDensity.current
Row(
modifier = Modifier
Expand All @@ -173,7 +205,7 @@ private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(MR.strings.download_notifier_cache_renewal),
text = text,
color = MaterialTheme.colorScheme.onSecondary,
textAlign = TextAlign.Center,
style = MaterialTheme.typography.labelMedium,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ object SettingsDataScreen : SearchableSettings {
stringResource(MR.strings.backup_info) + "\n\n" +
stringResource(MR.strings.last_auto_backup_info, relativeTimeSpanString(lastAutoBackup)),
),
Preference.PreferenceItem.SwitchPreference(
preference = backupPreferences.showRestoringProgressBanner(),
title = stringResource(MR.strings.pref_show_restoring_progress_banner),
),
),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,10 @@ object SettingsLibraryScreen : SearchableSettings {
preference = libraryPreferences.newShowUpdatesCount(),
title = stringResource(MR.strings.pref_library_update_show_tab_badge),
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.showUpdatingProgressBanner(),
title = stringResource(MR.strings.pref_show_updating_progress_banner),
),
),
)
}
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/data/BannerProgressStatus.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.data

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

open class BannerProgressStatus {
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)
}

val progress = MutableStateFlow(0f)

suspend fun updateProgress(progress: Float) {
this.progress.emit(progress)
}
}

class LibraryUpdateStatus : BannerProgressStatus()
class BackupRestoreStatus : BannerProgressStatus()
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -16,13 +17,16 @@ 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

class BackupNotifier(private val context: Context) {

private val preferences: SecurityPreferences by injectLazy()
private val backupRestoreStatus: BackupRestoreStatus = Injekt.get()

private val progressNotificationBuilder = context.notificationBuilder(
Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS,
Expand Down Expand Up @@ -87,7 +91,7 @@ class BackupNotifier(private val context: Context) {
}
}

fun showRestoreProgress(
suspend fun showRestoreProgress(
content: String = "",
progress: Int = 0,
maxAmount: Int = 100,
Expand All @@ -107,6 +111,7 @@ class BackupNotifier(private val context: Context) {

setProgress(maxAmount, progress, false)
setOnlyAlertOnce(true)
backupRestoreStatus.updateProgress(progress.toFloat() / maxAmount)

clearActions()
addAction(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,12 +23,16 @@ import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.util.system.logcat
import tachiyomi.i18n.MR
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

class BackupRestoreJob(private val context: Context, workerParams: WorkerParameters) :
CoroutineWorker(context, workerParams) {

private val notifier = BackupNotifier(context)

private val backupRestoreStatus: BackupRestoreStatus = Injekt.get()

override suspend fun doWork(): Result {
val uri = inputData.getString(LOCATION_URI_KEY)?.toUri()
val options = inputData.getBooleanArray(OPTIONS_KEY)?.let { RestoreOptions.fromBooleanArray(it) }
Expand All @@ -36,6 +41,8 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
return Result.failure()
}

backupRestoreStatus.start()

val isSync = inputData.getBoolean(SYNC_KEY, false)

setForegroundSafely()
Expand All @@ -54,6 +61,7 @@ class BackupRestoreJob(private val context: Context, workerParams: WorkerParamet
}
} finally {
context.cancelNotification(Notifications.ID_RESTORE_PROGRESS)
backupRestoreStatus.stop()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import androidx.work.workDataOf
import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toSManga
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
Expand Down Expand Up @@ -87,6 +88,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
private val fetchInterval: FetchInterval = Injekt.get()
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
private val libraryUpdateStatus: LibraryUpdateStatus = Injekt.get()

private val notifier = LibraryUpdateNotifier(context)

Expand All @@ -108,6 +110,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
}

libraryUpdateStatus.start()

setForegroundSafely()

libraryPreferences.lastUpdatedTimestamp().set(Instant.now().toEpochMilli())
Expand All @@ -129,6 +133,7 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
} finally {
notifier.cancelProgressNotification()
libraryUpdateStatus.stop()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,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
Expand Down Expand Up @@ -47,6 +48,7 @@ class LibraryUpdateNotifier(

private val securityPreferences: SecurityPreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(),
private val libraryUpdateStatus: LibraryUpdateStatus = Injekt.get(),
) {

private val percentFormatter = NumberFormat.getPercentInstance().apply {
Expand Down Expand Up @@ -89,7 +91,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(
Expand All @@ -98,6 +100,8 @@ class LibraryUpdateNotifier(
),
)

libraryUpdateStatus.updateProgress(current.toFloat() / total)

if (!securityPreferences.hideNotificationContent().get()) {
val updatingText = manga.joinToString("\n") { it.title.chop(40) }
progressNotificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(updatingText))
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import app.cash.sqldelight.db.SqlDriver
import app.cash.sqldelight.driver.android.AndroidSqliteDriver
import eu.kanade.domain.track.store.DelayedTrackingStore
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.BackupRestoreStatus
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.download.DownloadCache
Expand Down Expand Up @@ -133,6 +135,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { LocalSourceFileSystem(get()) }
addSingletonFactory { LocalCoverManager(app, get()) }
addSingletonFactory { StorageManager(app, get()) }
addSingletonFactory { BackupRestoreStatus() }
addSingletonFactory { LibraryUpdateStatus() }

// Asynchronously init expensive components for a faster cold start
ContextCompat.getMainExecutor(app).execute {
Expand Down
24 changes: 24 additions & 0 deletions app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,15 @@ 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.UpdatingBannerBackgroundColor
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
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.BackupRestoreStatus
import eu.kanade.tachiyomi.data.LibraryUpdateStatus
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.download.DownloadCache
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
Expand Down Expand Up @@ -96,6 +100,7 @@ import mihon.core.migration.Migrator
import tachiyomi.core.common.Constants
import tachiyomi.core.common.util.lang.launchIO
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.release.interactor.GetApplicationRelease
import tachiyomi.i18n.MR
Expand All @@ -109,6 +114,10 @@ class MainActivity : BaseActivity() {
private val libraryPreferences: LibraryPreferences by injectLazy()
private val preferences: BasePreferences by injectLazy()

private val backupPreferences: BackupPreferences by injectLazy()
private val backupRestoreStatus: BackupRestoreStatus by injectLazy()
private val libraryUpdateStatus: LibraryUpdateStatus by injectLazy()

private val downloadCache: DownloadCache by injectLazy()
private val chapterCache: ChapterCache by injectLazy()

Expand Down Expand Up @@ -146,8 +155,19 @@ class MainActivity : BaseActivity() {
val downloadOnly by preferences.downloadedOnly().collectAsState()
val indexing by downloadCache.isInitializing.collectAsState()

val restoringState by backupRestoreStatus.isRunning.collectAsState()
val updatingState by libraryUpdateStatus.isRunning.collectAsState()
val restoringProgressBanner by backupPreferences.showRestoringProgressBanner().collectAsState()
val updatingProgressBanner by libraryPreferences.showUpdatingProgressBanner().collectAsState()
val restoring = restoringState && restoringProgressBanner
val updating = updatingState && updatingProgressBanner
val restoringProgress by backupRestoreStatus.progress.collectAsState()
val updatingProgress by libraryUpdateStatus.progress.collectAsState()

val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when {
updating -> UpdatingBannerBackgroundColor
restoring -> RestoringBannerBackgroundColor
indexing -> IndexingBannerBackgroundColor
downloadOnly -> DownloadedOnlyBannerBackgroundColor
incognito -> IncognitoModeBannerBackgroundColor
Expand Down Expand Up @@ -191,6 +211,10 @@ class MainActivity : BaseActivity() {
downloadedOnlyMode = downloadOnly,
incognitoMode = incognito,
indexing = indexing,
restoring = restoring,
updating = updating,
progress = updatingProgress.takeIf { updating }
?: restoringProgress.takeIf { restoring },
modifier = Modifier.windowInsetsPadding(scaffoldInsets),
)
},
Expand Down
Loading