App state banner tweaks (#8746)

* Move download indexing notification to this banner group
* Animate state changes
This commit is contained in:
Ivan Iskandar 2022-12-17 10:18:17 +07:00 committed by GitHub
parent 5f4825465e
commit e20c66b156
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 212 additions and 132 deletions

View File

@ -1,28 +1,45 @@
package eu.kanade.presentation.components package eu.kanade.presentation.components
import androidx.annotation.StringRes 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.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.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding 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.foundation.layout.windowInsetsPadding
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable 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.Modifier
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp 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 import eu.kanade.tachiyomi.R
val DownloadedOnlyBannerBackgroundColor val DownloadedOnlyBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.tertiary @Composable get() = MaterialTheme.colorScheme.tertiary
val IncognitoModeBannerBackgroundColor val IncognitoModeBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.primary @Composable get() = MaterialTheme.colorScheme.primary
val IndexingBannerBackgroundColor
@Composable get() = MaterialTheme.colorScheme.secondary
@Composable @Composable
fun WarningBanner( fun WarningBanner(
@ -45,24 +62,65 @@ fun WarningBanner(
fun AppStateBanners( fun AppStateBanners(
downloadedOnlyMode: Boolean, downloadedOnlyMode: Boolean,
incognitoMode: Boolean, incognitoMode: Boolean,
indexing: Boolean,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val insets = WindowInsets.systemBars.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) val density = LocalDensity.current
Column(modifier = modifier) { val mainInsets = WindowInsets.statusBars
if (downloadedOnlyMode) { val mainInsetsTop = mainInsets.getTop(density)
DownloadedOnlyModeBanner( SubcomposeLayout(modifier = modifier) { constraints ->
modifier = Modifier.windowInsetsPadding(insets), val indexingPlaceable = subcompose(0) {
AnimatedVisibility(
visible = indexing,
enter = expandVertically(),
exit = shrinkVertically(),
) {
IndexingDownloadBanner(
modifier = Modifier.windowInsetsPadding(mainInsets),
) )
} }
if (incognitoMode) { }.fastMap { it.measure(constraints) }
IncognitoModeBanner( val indexingHeight = indexingPlaceable.fastMaxBy { it.height }?.height ?: 0
modifier = if (!downloadedOnlyMode) {
Modifier.windowInsetsPadding(insets) val downloadedOnlyPlaceable = subcompose(1) {
} else { AnimatedVisibility(
Modifier 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, 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()
}
},
)
}
}

View File

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

View File

@ -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. * Status of download. Used for correct notification icon.
*/ */
@ -223,14 +212,4 @@ internal class DownloadNotifier(private val context: Context) {
errorThrown = true errorThrown = true
isDownloading = false isDownloading = false
} }
fun onCacheProgress() {
with(cacheNotificationBuilder) {
show(Notifications.ID_DOWNLOAD_CACHE)
}
}
fun dismissCacheProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CACHE)
}
} }

View File

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

View File

@ -12,10 +12,13 @@ import android.widget.Toast
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets 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.AlertDialog
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -23,6 +26,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember 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.AppStateBanners
import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor import eu.kanade.presentation.components.DownloadedOnlyBannerBackgroundColor
import eu.kanade.presentation.components.IncognitoModeBannerBackgroundColor 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.AssistContentScreen
import eu.kanade.presentation.util.DefaultNavigatorScreenTransition import eu.kanade.presentation.util.DefaultNavigatorScreenTransition
import eu.kanade.presentation.util.collectAsState import eu.kanade.presentation.util.collectAsState
@ -61,6 +67,7 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache 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.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.updater.AppUpdateChecker import eu.kanade.tachiyomi.data.updater.AppUpdateChecker
import eu.kanade.tachiyomi.data.updater.AppUpdateResult import eu.kanade.tachiyomi.data.updater.AppUpdateResult
@ -101,6 +108,7 @@ class MainActivity : BaseActivity() {
private val preferences: BasePreferences by injectLazy() private val preferences: BasePreferences by injectLazy()
private val chapterCache: ChapterCache 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. // To be checked by splash screen. If true then splash screen will be removed.
var ready = false var ready = false
@ -153,19 +161,16 @@ class MainActivity : BaseActivity() {
setComposeContent { setComposeContent {
val incognito by preferences.incognitoMode().collectAsState() val incognito by preferences.incognitoMode().collectAsState()
val downloadOnly by preferences.downloadedOnly().collectAsState() val downloadOnly by preferences.downloadedOnly().collectAsState()
Column { val indexing by downloadCache.isRenewing.collectAsState()
AppStateBanners(
downloadedOnlyMode = downloadOnly,
incognitoMode = incognito,
)
// Set statusbar color // Set statusbar color considering the top app state banner
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
val isSystemInDarkTheme = isSystemInDarkTheme() val isSystemInDarkTheme = isSystemInDarkTheme()
val statusBarBackgroundColor = when { val statusBarBackgroundColor = when {
indexing -> IndexingBannerBackgroundColor
downloadOnly -> DownloadedOnlyBannerBackgroundColor downloadOnly -> DownloadedOnlyBannerBackgroundColor
incognito -> IncognitoModeBannerBackgroundColor incognito -> IncognitoModeBannerBackgroundColor
else -> MaterialTheme.colorScheme.background else -> MaterialTheme.colorScheme.surface
} }
LaunchedEffect(systemUiController, statusBarBackgroundColor) { LaunchedEffect(systemUiController, statusBarBackgroundColor) {
systemUiController.setStatusBarColor( systemUiController.setStatusBarColor(
@ -211,16 +216,28 @@ class MainActivity : BaseActivity() {
} }
} }
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 // Consume insets already used by app state banners
val boxModifier = if (incognito || downloadOnly) { Box(
Modifier.consumeWindowInsets(WindowInsets.statusBars) modifier = Modifier
} else { .padding(contentPadding)
Modifier .consumeWindowInsets(contentPadding),
} ) {
Box(modifier = boxModifier) {
// Shows current screen // Shows current screen
DefaultNavigatorScreenTransition(navigator = navigator) DefaultNavigatorScreenTransition(navigator = navigator)
} }
}
// Pop source-related screens when incognito mode is turned off // Pop source-related screens when incognito mode is turned off
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -241,7 +258,6 @@ class MainActivity : BaseActivity() {
CheckForUpdate() CheckForUpdate()
} }
}
var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) } var showChangelog by remember { mutableStateOf(didMigration && !BuildConfig.DEBUG) }
if (showChangelog) { if (showChangelog) {