Parcourir la source

App state banner tweaks (#8746)

* Move download indexing notification to this banner group
* Animate state changes
Ivan Iskandar il y a 2 ans
Parent
commit
e20c66b156

+ 109 - 19
app/src/main/java/eu/kanade/presentation/components/Banners.kt

@@ -1,28 +1,45 @@
 package eu.kanade.presentation.components
 
 import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.expandVertically
+import androidx.compose.animation.shrinkVertically
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.only
 import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.systemBars
+import androidx.compose.foundation.layout.requiredSize
+import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.width
 import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
 import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.layout.SubcomposeLayout
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.text.style.TextAlign
 import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastMap
+import androidx.compose.ui.util.fastMaxBy
 import eu.kanade.tachiyomi.R
 
 val DownloadedOnlyBannerBackgroundColor
     @Composable get() = MaterialTheme.colorScheme.tertiary
 val IncognitoModeBannerBackgroundColor
     @Composable get() = MaterialTheme.colorScheme.primary
+val IndexingBannerBackgroundColor
+    @Composable get() = MaterialTheme.colorScheme.secondary
 
 @Composable
 fun WarningBanner(
@@ -45,23 +62,64 @@ fun WarningBanner(
 fun AppStateBanners(
     downloadedOnlyMode: Boolean,
     incognitoMode: Boolean,
+    indexing: Boolean,
     modifier: Modifier = Modifier,
 ) {
-    val insets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal)
-    Column(modifier = modifier) {
-        if (downloadedOnlyMode) {
-            DownloadedOnlyModeBanner(
-                modifier = Modifier.windowInsetsPadding(insets),
-            )
-        }
-        if (incognitoMode) {
-            IncognitoModeBanner(
-                modifier = if (!downloadedOnlyMode) {
-                    Modifier.windowInsetsPadding(insets)
-                } else {
-                    Modifier
-                },
-            )
+    val density = LocalDensity.current
+    val mainInsets = WindowInsets.statusBars
+    val mainInsetsTop = mainInsets.getTop(density)
+    SubcomposeLayout(modifier = modifier) { constraints ->
+        val indexingPlaceable = subcompose(0) {
+            AnimatedVisibility(
+                visible = indexing,
+                enter = expandVertically(),
+                exit = shrinkVertically(),
+            ) {
+                IndexingDownloadBanner(
+                    modifier = Modifier.windowInsetsPadding(mainInsets),
+                )
+            }
+        }.fastMap { it.measure(constraints) }
+        val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0
+
+        val downloadedOnlyPlaceable = subcompose(1) {
+            AnimatedVisibility(
+                visible = downloadedOnlyMode,
+                enter = expandVertically(),
+                exit = shrinkVertically(),
+            ) {
+                val top = (mainInsetsTop - indexingHeight).coerceAtLeast(0)
+                DownloadedOnlyModeBanner(
+                    modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
+                )
+            }
+        }.fastMap { it.measure(constraints) }
+        val downloadedOnlyHeight = downloadedOnlyPlaceable.fastMaxBy { it.height }?.height ?: 0
+
+        val incognitoPlaceable = subcompose(2) {
+            AnimatedVisibility(
+                visible = incognitoMode,
+                enter = expandVertically(),
+                exit = shrinkVertically(),
+            ) {
+                val top = (mainInsetsTop - indexingHeight - downloadedOnlyHeight).coerceAtLeast(0)
+                IncognitoModeBanner(
+                    modifier = Modifier.windowInsetsPadding(WindowInsets(top = top)),
+                )
+            }
+        }.fastMap { it.measure(constraints) }
+        val incognitoHeight = incognitoPlaceable.fastMaxBy { it.height }?.height ?: 0
+
+        layout(constraints.maxWidth, indexingHeight + downloadedOnlyHeight + incognitoHeight) {
+            indexingPlaceable.fastForEach {
+                it.place(0, 0)
+            }
+            downloadedOnlyPlaceable.fastForEach {
+                it.place(0, indexingHeight)
+            }
+            incognitoPlaceable.fastForEach {
+                it.place(0, indexingHeight + downloadedOnlyHeight)
+            }
         }
     }
 }
@@ -95,3 +153,35 @@ private fun IncognitoModeBanner(modifier: Modifier = Modifier) {
         style = MaterialTheme.typography.labelMedium,
     )
 }
+
+@Composable
+private fun IndexingDownloadBanner(modifier: Modifier = Modifier) {
+    val density = LocalDensity.current
+    Row(
+        modifier = Modifier
+            .background(color = IndexingBannerBackgroundColor)
+            .fillMaxWidth()
+            .padding(8.dp)
+            .then(modifier),
+        horizontalArrangement = Arrangement.Center,
+    ) {
+        var textHeight by remember { mutableStateOf(0.dp) }
+        CircularProgressIndicator(
+            modifier = Modifier.requiredSize(textHeight),
+            color = MaterialTheme.colorScheme.onSecondary,
+            strokeWidth = textHeight / 8,
+        )
+        Spacer(modifier = Modifier.width(8.dp))
+        Text(
+            text = stringResource(R.string.download_notifier_cache_renewal),
+            color = MaterialTheme.colorScheme.onSecondary,
+            textAlign = TextAlign.Center,
+            style = MaterialTheme.typography.labelMedium,
+            onTextLayout = {
+                with(density) {
+                    textHeight = it.size.height.toDp()
+                }
+            },
+        )
+    }
+}

+ 7 - 5
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadCache.kt

@@ -20,11 +20,14 @@ import kotlinx.coroutines.awaitAll
 import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
 import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.map
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.flow.onStart
 import kotlinx.coroutines.flow.receiveAsFlow
 import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.stateIn
 import kotlinx.coroutines.withTimeout
 import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
@@ -53,8 +56,6 @@ class DownloadCache(
         .onStart { emit(Unit) }
         .shareIn(scope, SharingStarted.Eagerly, 1)
 
-    private val notifier by lazy { DownloadNotifier(context) }
-
     /**
      * The interval after which this cache should be invalidated. 1 hour shouldn't cause major
      * issues, as the cache is only used for UI feedback.
@@ -66,6 +67,10 @@ class DownloadCache(
      */
     private var lastRenew = 0L
     private var renewalJob: Job? = null
+    val isRenewing = changes
+        .map { renewalJob?.isActive ?: false }
+        .distinctUntilChanged()
+        .stateIn(scope, SharingStarted.WhileSubscribed(), false)
 
     private var rootDownloadsDir = RootDirectory(getDirectoryFromPreference())
 
@@ -260,8 +265,6 @@ class DownloadCache(
         }
 
         renewalJob = scope.launchIO {
-            notifier.onCacheProgress()
-
             var sources = getSources()
 
             // Try to wait until extensions and sources have loaded
@@ -320,7 +323,6 @@ class DownloadCache(
             lastRenew = System.currentTimeMillis()
             notifyChanges()
         }
-        renewalJob?.invokeOnCompletion { notifier.dismissCacheProgress() }
     }
 
     private fun getSources(): List<Source> {

+ 0 - 21
app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadNotifier.kt

@@ -39,17 +39,6 @@ internal class DownloadNotifier(private val context: Context) {
         }
     }
 
-    private val cacheNotificationBuilder by lazy {
-        context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_CACHE) {
-            setSmallIcon(R.drawable.ic_tachi)
-            setContentTitle(context.getString(R.string.download_notifier_cache_renewal))
-            setProgress(100, 100, true)
-            setOngoing(true)
-            setAutoCancel(false)
-            setOnlyAlertOnce(true)
-        }
-    }
-
     /**
      * Status of download. Used for correct notification icon.
      */
@@ -223,14 +212,4 @@ internal class DownloadNotifier(private val context: Context) {
         errorThrown = true
         isDownloading = false
     }
-
-    fun onCacheProgress() {
-        with(cacheNotificationBuilder) {
-            show(Notifications.ID_DOWNLOAD_CACHE)
-        }
-    }
-
-    fun dismissCacheProgress() {
-        context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CACHE)
-    }
 }

+ 1 - 8
app/src/main/java/eu/kanade/tachiyomi/data/notification/Notifications.kt

@@ -40,8 +40,6 @@ object Notifications {
     const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
     const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
     const val ID_DOWNLOAD_CHAPTER_ERROR = -202
-    const val CHANNEL_DOWNLOADER_CACHE = "downloader_cache_renewal"
-    const val ID_DOWNLOAD_CACHE = -204
 
     /**
      * Notification channel and ids used by the library updater.
@@ -91,6 +89,7 @@ object Notifications {
         "library_channel",
         "library_progress_channel",
         "updates_ext_channel",
+        "downloader_cache_renewal",
     )
 
     /**
@@ -155,12 +154,6 @@ object Notifications {
                     setGroup(GROUP_DOWNLOADER)
                     setShowBadge(false)
                 },
-                buildNotificationChannel(CHANNEL_DOWNLOADER_CACHE, IMPORTANCE_LOW) {
-                    setName(context.getString(R.string.channel_downloader_cache))
-                    setGroup(GROUP_DOWNLOADER)
-                    setShowBadge(false)
-                    setSound(null, null)
-                },
                 buildNotificationChannel(CHANNEL_BACKUP_RESTORE_PROGRESS, IMPORTANCE_LOW) {
                     setName(context.getString(R.string.channel_progress))
                     setGroup(GROUP_BACKUP_RESTORE)

+ 89 - 73
app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt

@@ -12,10 +12,13 @@ import android.widget.Toast
 import androidx.activity.compose.BackHandler
 import androidx.compose.foundation.isSystemInDarkTheme
 import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.WindowInsetsSides
 import androidx.compose.foundation.layout.consumeWindowInsets
-import androidx.compose.foundation.layout.statusBars
+import androidx.compose.foundation.layout.navigationBars
+import androidx.compose.foundation.layout.only
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.windowInsetsPadding
 import androidx.compose.material3.AlertDialog
 import androidx.compose.material3.MaterialTheme
 import androidx.compose.material3.Text
@@ -23,6 +26,7 @@ import androidx.compose.material3.TextButton
 import androidx.compose.material3.surfaceColorAtElevation
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.collectAsState
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.remember
@@ -54,6 +58,8 @@ import eu.kanade.domain.ui.UiPreferences
 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.Scaffold
 import eu.kanade.presentation.util.AssistContentScreen
 import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
 import eu.kanade.presentation.util.collectAsState
@@ -61,6 +67,7 @@ import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.Migrations
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.cache.ChapterCache
+import eu.kanade.tachiyomi.data.download.DownloadCache
 import eu.kanade.tachiyomi.data.notification.NotificationReceiver
 import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
 import eu.kanade.tachiyomi.data.updater.AppUpdateResult
@@ -101,6 +108,7 @@ class MainActivity : BaseActivity() {
     private val preferences: BasePreferences by injectLazy()
 
     private val chapterCache: ChapterCache by injectLazy()
+    private val downloadCache: DownloadCache by injectLazy()
 
     // To be checked by splash screen. If true then splash screen will be removed.
     var ready = false
@@ -153,94 +161,102 @@ class MainActivity : BaseActivity() {
         setComposeContent {
             val incognito by preferences.incognitoMode().collectAsState()
             val downloadOnly by preferences.downloadedOnly().collectAsState()
-            Column {
-                AppStateBanners(
-                    downloadedOnlyMode = downloadOnly,
-                    incognitoMode = incognito,
+            val indexing by downloadCache.isRenewing.collectAsState()
+
+            // Set statusbar color considering the top app state banner
+            val systemUiController = rememberSystemUiController()
+            val isSystemInDarkTheme = isSystemInDarkTheme()
+            val statusBarBackgroundColor = when {
+                indexing -> IndexingBannerBackgroundColor
+                downloadOnly -> DownloadedOnlyBannerBackgroundColor
+                incognito -> IncognitoModeBannerBackgroundColor
+                else -> MaterialTheme.colorScheme.surface
+            }
+            LaunchedEffect(systemUiController, statusBarBackgroundColor) {
+                systemUiController.setStatusBarColor(
+                    color = ComposeColor.Transparent,
+                    darkIcons = statusBarBackgroundColor.luminance() > 0.5,
+                    transformColorForLightContent = { ComposeColor.Black },
                 )
+            }
 
-                // Set statusbar color
-                val systemUiController = rememberSystemUiController()
-                val isSystemInDarkTheme = isSystemInDarkTheme()
-                val statusBarBackgroundColor = when {
-                    downloadOnly -> DownloadedOnlyBannerBackgroundColor
-                    incognito -> IncognitoModeBannerBackgroundColor
-                    else -> MaterialTheme.colorScheme.background
-                }
-                LaunchedEffect(systemUiController, statusBarBackgroundColor) {
-                    systemUiController.setStatusBarColor(
-                        color = ComposeColor.Transparent,
-                        darkIcons = statusBarBackgroundColor.luminance() > 0.5,
-                        transformColorForLightContent = { ComposeColor.Black },
-                    )
-                }
+            // Set navigation bar color
+            val context = LocalContext.current
+            val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
+            LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) {
+                systemUiController.setNavigationBarColor(
+                    color = if (context.isNavigationBarNeedsScrim()) {
+                        navbarScrimColor.copy(alpha = 0.7f)
+                    } else {
+                        ComposeColor.Transparent
+                    },
+                    darkIcons = !isSystemInDarkTheme,
+                    navigationBarContrastEnforced = false,
+                    transformColorForLightContent = { ComposeColor.Black },
+                )
+            }
 
-                // Set navigation bar color
-                val context = LocalContext.current
-                val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
-                LaunchedEffect(systemUiController, isSystemInDarkTheme, navbarScrimColor) {
-                    systemUiController.setNavigationBarColor(
-                        color = if (context.isNavigationBarNeedsScrim()) {
-                            navbarScrimColor.copy(alpha = 0.7f)
-                        } else {
-                            ComposeColor.Transparent
-                        },
-                        darkIcons = !isSystemInDarkTheme,
-                        navigationBarContrastEnforced = false,
-                        transformColorForLightContent = { ComposeColor.Black },
-                    )
+            Navigator(
+                screen = HomeScreen,
+                disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
+            ) { navigator ->
+                if (navigator.size == 1) {
+                    ConfirmExit()
                 }
 
-                Navigator(
-                    screen = HomeScreen,
-                    disposeBehavior = NavigatorDisposeBehavior(disposeNestedNavigators = false, disposeSteps = true),
-                ) { navigator ->
-                    if (navigator.size == 1) {
-                        ConfirmExit()
-                    }
+                LaunchedEffect(navigator) {
+                    [email protected] = navigator
 
-                    LaunchedEffect(navigator) {
-                        [email protected] = navigator
+                    if (savedInstanceState == null) {
+                        // Set start screen
+                        handleIntentAction(intent)
 
-                        if (savedInstanceState == null) {
-                            // Set start screen
-                            handleIntentAction(intent)
-
-                            // Reset Incognito Mode on relaunch
-                            preferences.incognitoMode().set(false)
-                        }
+                        // Reset Incognito Mode on relaunch
+                        preferences.incognitoMode().set(false)
                     }
+                }
 
+                val scaffoldInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
+                Scaffold(
+                    topBar = {
+                        AppStateBanners(
+                            downloadedOnlyMode = downloadOnly,
+                            incognitoMode = incognito,
+                            indexing = indexing,
+                            modifier = Modifier.windowInsetsPadding(scaffoldInsets),
+                        )
+                    },
+                    contentWindowInsets = scaffoldInsets,
+                ) { contentPadding ->
                     // Consume insets already used by app state banners
-                    val boxModifier = if (incognito || downloadOnly) {
-                        Modifier.consumeWindowInsets(WindowInsets.statusBars)
-                    } else {
-                        Modifier
-                    }
-                    Box(modifier = boxModifier) {
+                    Box(
+                        modifier = Modifier
+                            .padding(contentPadding)
+                            .consumeWindowInsets(contentPadding),
+                    ) {
                         // Shows current screen
                         DefaultNavigatorScreenTransition(navigator = navigator)
                     }
+                }
 
-                    // Pop source-related screens when incognito mode is turned off
-                    LaunchedEffect(Unit) {
-                        preferences.incognitoMode().changes()
-                            .drop(1)
-                            .onEach {
-                                if (!it) {
-                                    val currentScreen = navigator.lastItem
-                                    if (currentScreen is BrowseSourceScreen ||
-                                        (currentScreen is MangaScreen && currentScreen.fromSource)
-                                    ) {
-                                        navigator.popUntilRoot()
-                                    }
+                // Pop source-related screens when incognito mode is turned off
+                LaunchedEffect(Unit) {
+                    preferences.incognitoMode().changes()
+                        .drop(1)
+                        .onEach {
+                            if (!it) {
+                                val currentScreen = navigator.lastItem
+                                if (currentScreen is BrowseSourceScreen ||
+                                    (currentScreen is MangaScreen && currentScreen.fromSource)
+                                ) {
+                                    navigator.popUntilRoot()
                                 }
                             }
-                            .launchIn(this)
-                    }
-
-                    CheckForUpdate()
+                        }
+                        .launchIn(this)
                 }
+
+                CheckForUpdate()
             }
 
             var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }