feat: floating update badge across all pages
Small pulsing circle icon (top-right) that persists across all tabs, blinks slowly (alpha 0.35↔1.0, 1.2s cycle). Tapping opens UpdateDialog. version 1.8.5 -> 1.8.6 (code 22 -> 23) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,8 +13,8 @@ android {
|
|||||||
applicationId = "com.a2i.forwarder"
|
applicationId = "com.a2i.forwarder"
|
||||||
minSdk = 34
|
minSdk = 34
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 22
|
versionCode = 23
|
||||||
versionName = "1.8.5"
|
versionName = "1.8.6"
|
||||||
}
|
}
|
||||||
|
|
||||||
signingConfigs {
|
signingConfigs {
|
||||||
|
|||||||
@@ -1,7 +1,20 @@
|
|||||||
package com.a2i.forwarder.ui.nav
|
package com.a2i.forwarder.ui.nav
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.SystemUpdate
|
||||||
import androidx.compose.material.icons.outlined.Apps
|
import androidx.compose.material.icons.outlined.Apps
|
||||||
import androidx.compose.material.icons.outlined.FilterAlt
|
import androidx.compose.material.icons.outlined.FilterAlt
|
||||||
import androidx.compose.material.icons.outlined.Notifications
|
import androidx.compose.material.icons.outlined.Notifications
|
||||||
@@ -14,8 +27,15 @@ import androidx.compose.material3.NavigationBarItemDefaults
|
|||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
@@ -23,11 +43,13 @@ import androidx.navigation.compose.NavHost
|
|||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
import com.a2i.forwarder.A2iApp
|
||||||
import com.a2i.forwarder.ui.screens.AppManageScreen
|
import com.a2i.forwarder.ui.screens.AppManageScreen
|
||||||
import com.a2i.forwarder.ui.screens.FilterScreen
|
import com.a2i.forwarder.ui.screens.FilterScreen
|
||||||
import com.a2i.forwarder.ui.screens.HomeScreen
|
import com.a2i.forwarder.ui.screens.HomeScreen
|
||||||
import com.a2i.forwarder.ui.screens.LogScreen
|
import com.a2i.forwarder.ui.screens.LogScreen
|
||||||
import com.a2i.forwarder.ui.screens.SettingsScreen
|
import com.a2i.forwarder.ui.screens.SettingsScreen
|
||||||
|
import com.a2i.forwarder.ui.screens.UpdateDialog
|
||||||
|
|
||||||
private sealed class Tab(val route: String, val label: String, val icon: ImageVector) {
|
private sealed class Tab(val route: String, val label: String, val icon: ImageVector) {
|
||||||
object Home : Tab("home", "首页", Icons.Outlined.Notifications)
|
object Home : Tab("home", "首页", Icons.Outlined.Notifications)
|
||||||
@@ -46,6 +68,11 @@ fun AppRoot() {
|
|||||||
val current = backStack?.destination?.route
|
val current = backStack?.destination?.route
|
||||||
val showBar = current in tabRoutes
|
val showBar = current in tabRoutes
|
||||||
|
|
||||||
|
val app = A2iApp.instance
|
||||||
|
val updateInfo by app.updateChecker.updateInfo.collectAsState()
|
||||||
|
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Box {
|
||||||
Scaffold(
|
Scaffold(
|
||||||
containerColor = MaterialTheme.colorScheme.background,
|
containerColor = MaterialTheme.colorScheme.background,
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
@@ -96,4 +123,49 @@ fun AppRoot() {
|
|||||||
composable("log") { LogScreen(onBack = { nav.popBackStack() }) }
|
composable("log") { LogScreen(onBack = { nav.popBackStack() }) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 浮动更新提醒小图标(跨所有页面,缓慢闪烁)
|
||||||
|
if (updateInfo != null) {
|
||||||
|
UpdateBadge(
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd),
|
||||||
|
onClick = { showUpdateDialog = true },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showUpdateDialog && updateInfo != null) {
|
||||||
|
UpdateDialog(updateInfo!!, onDismiss = { showUpdateDialog = false })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 跨页面浮动更新提醒:右上角小圆点 + 缓慢闪烁动画。 */
|
||||||
|
@Composable
|
||||||
|
private fun UpdateBadge(modifier: Modifier = Modifier, onClick: () -> Unit) {
|
||||||
|
val transition = rememberInfiniteTransition(label = "updateBlink")
|
||||||
|
val pulseAlpha by transition.animateFloat(
|
||||||
|
initialValue = 0.35f,
|
||||||
|
targetValue = 1f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(1200),
|
||||||
|
repeatMode = RepeatMode.Reverse,
|
||||||
|
),
|
||||||
|
label = "pulseAlpha",
|
||||||
|
)
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(top = 12.dp, end = 12.dp)
|
||||||
|
.size(40.dp)
|
||||||
|
.alpha(pulseAlpha)
|
||||||
|
.background(MaterialTheme.colorScheme.primary, CircleShape)
|
||||||
|
.clickable(onClick = onClick),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Filled.SystemUpdate,
|
||||||
|
contentDescription = "发现新版本,点击更新",
|
||||||
|
tint = Color.White,
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user