cosmo.17: tap-the-rocket hint, green/red/amber status dot (stronger green pill), card header = small buttons 'Обновить подписку'/'Проверить серверы', traffic 'used / ∞' + expiry line (no info/telegram icons), bottom bar Home + My subscription back

This commit is contained in:
Cosmo VPN 2026-10-01 03:48:28 +03:00
parent 4e57cc1145
commit 03c7f7119d
4 changed files with 119 additions and 56 deletions

View file

@ -16,7 +16,7 @@ android {
minSdk = 24
targetSdk = 37
versionCode = 749
versionName = "2.3.9-cosmo.16"
versionName = "2.3.9-cosmo.17"
val abiFilterList = (properties["ABI_FILTERS"] as? String)?.split(';')
splits {

View file

@ -144,8 +144,21 @@ fun CosmoMainScreen(
else onAction(MainAction.UpdateSubscriptions)
}
// No bottom bar: settings (gear, top-left) and "My subscription" (ⓘ in the card) open as separate screens.
Scaffold { padding ->
// Bottom bar: Home + My subscription; Settings opens from the gear (top-left) as a separate screen.
Scaffold(
bottomBar = {
NavigationBar {
CosmoTab.entries.forEachIndexed { index, item ->
NavigationBarItem(
selected = tab == index,
onClick = { tab = index },
icon = { Icon(painterResource(item.icon), contentDescription = null) },
label = { Text(stringResource(item.label)) },
)
}
}
}
) { padding ->
Box(Modifier.padding(padding).fillMaxSize()) {
when (tab) {
0 -> CosmoHomeTab(
@ -156,14 +169,12 @@ fun CosmoMainScreen(
onOpenSubscription = { tab = 1 },
onOpenSettings = { tab = 2 },
)
1 -> CosmoSubPage(onBack = { tab = 0 }) {
CosmoSubscriptionTab(
ui = ui,
serverCount = group.rows.size,
onAction = onAction,
onRenew = { showRenew = true },
)
}
1 -> CosmoSubscriptionTab(
ui = ui,
serverCount = group.rows.size,
onAction = onAction,
onRenew = { showRenew = true },
)
else -> CosmoSubPage(onBack = { tab = 0 }) {
CosmoSettingsTab(
onAction = onAction,
@ -176,7 +187,12 @@ fun CosmoMainScreen(
}
}
/** Secondary screen with a back arrow (Home has no bottom bar). */
private enum class CosmoTab(@DrawableRes val icon: Int, @StringRes val label: Int) {
Home(R.drawable.ic_cosmo_home_24dp, R.string.cosmo_tab_home),
Subscription(R.drawable.ic_subscriptions_24dp, R.string.cosmo_tab_subscription),
}
/** Secondary screen with a back arrow (Settings, opened from the gear). */
@Composable
private fun CosmoSubPage(onBack: () -> Unit, content: @Composable () -> Unit) {
Column(Modifier.fillMaxSize()) {
@ -335,26 +351,44 @@ private fun CosmoHomeTab(
color = if (state == CosmoConnState.Connected) MaterialTheme.colorScheme.onSurface
else MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
)
// Make it obvious that the rocket circle is the on/off button.
Text(
stringResource(
when (state) {
CosmoConnState.Connected -> R.string.cosmo_tap_to_disconnect
CosmoConnState.Connecting -> R.string.cosmo_hint_connecting
CosmoConnState.Disconnected -> R.string.cosmo_tap_to_connect
}
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = 4.dp),
)
}
}
// ---- One card: header (name + update/test icons), info line, fastest, servers ----
item {
Column(Modifier.fillMaxWidth().clip(RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)).background(cardBg)) {
Row(Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp, top = 4.dp), verticalAlignment = Alignment.CenterVertically) {
Text(
stringResource(R.string.app_name),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
Row(
Modifier.fillMaxWidth().padding(start = 12.dp, end = 4.dp, top = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
CosmoSmallButton(
icon = R.drawable.ic_cosmo_refresh_24dp,
text = stringResource(R.string.cosmo_update_sub),
onClick = { onAction(MainAction.UpdateSubscriptions) },
modifier = Modifier.weight(1f),
)
CosmoSmallButton(
icon = R.drawable.ic_cosmo_speed_24dp,
text = stringResource(R.string.cosmo_check_servers),
busy = ui.isTesting && !findingFastest,
enabled = !ui.isTesting,
onClick = { onAction(MainAction.TestRealAllServers) },
modifier = Modifier.weight(1f),
)
IconButton(onClick = { onAction(MainAction.UpdateSubscriptions) }) {
Icon(painterResource(R.drawable.ic_cosmo_refresh_24dp), contentDescription = stringResource(R.string.cosmo_update_sub), tint = MaterialTheme.colorScheme.primary)
}
IconButton(onClick = { onAction(MainAction.TestRealAllServers) }, enabled = !ui.isTesting) {
if (ui.isTesting && !findingFastest) CircularProgressIndicator(Modifier.size(20.dp), strokeWidth = 2.dp)
else Icon(painterResource(R.drawable.ic_cosmo_speed_24dp), contentDescription = stringResource(R.string.cosmo_check_servers), tint = MaterialTheme.colorScheme.primary)
}
IconButton(onClick = { listExpanded = !listExpanded }) {
Icon(
painterResource(R.drawable.ic_expand_more_24dp),
@ -363,7 +397,7 @@ private fun CosmoHomeTab(
)
}
}
CosmoSubscriptionInfoRow(subInfo, onOpenSubscription)
CosmoSubscriptionInfoRow(subInfo)
}
}
if (listExpanded) {
@ -422,18 +456,46 @@ private fun CosmoHomeTab(
}
}
/** Compact line under the card header: traffic | expiry | details (ⓘ) | support. */
/** Small outlined button with an icon and a short label (card header). */
@Composable
private fun CosmoSubscriptionInfoRow(info: CosmoSubscriptionInfo?, onOpenSubscription: () -> Unit) {
val context = LocalContext.current
Row(Modifier.fillMaxWidth().padding(start = 16.dp, end = 12.dp, bottom = 10.dp), verticalAlignment = Alignment.CenterVertically) {
private fun CosmoSmallButton(
@DrawableRes icon: Int,
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
busy: Boolean = false,
) {
OutlinedButton(
onClick = onClick,
enabled = enabled,
shape = RoundedCornerShape(12.dp),
contentPadding = androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp),
modifier = modifier.height(40.dp),
) {
if (busy) CircularProgressIndicator(Modifier.size(16.dp), strokeWidth = 2.dp)
else Icon(painterResource(icon), contentDescription = null, modifier = Modifier.size(18.dp))
Spacer(Modifier.width(6.dp))
Text(text, style = MaterialTheme.typography.labelLarge, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
}
/** Compact line under the card buttons: used traffic / limit (∞) on the left, expiry date on the right. */
@Composable
private fun CosmoSubscriptionInfoRow(info: CosmoSubscriptionInfo?) {
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically) {
val traffic = when {
info == null -> "—"
info.isUnlimitedTraffic -> stringResource(R.string.cosmo_sub_unlimited)
else -> stringResource(R.string.cosmo_sub_traffic_left, info.trafficLeftBytes.toTrafficString())
info.isUnlimitedTraffic -> "${(info.upload + info.download).toTrafficString()} / ∞"
else -> "${(info.upload + info.download).toTrafficString()} / ${info.total.toTrafficString()}"
}
Text(traffic, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1)
Spacer(Modifier.width(10.dp))
Box(
Modifier.border(BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant), RoundedCornerShape(8.dp))
.padding(horizontal = 10.dp, vertical = 3.dp),
) {
Text(traffic, style = MaterialTheme.typography.bodyMedium, maxLines = 1)
}
Spacer(Modifier.weight(1f))
val expire = info?.expire?.takeIf { it > 0 }?.times(1000)
val expired = info?.isExpired() == true
if (expire != null && expire - System.currentTimeMillis() < NO_EXPIRY_THRESHOLD_DAYS * 86_400_000L) {
@ -442,23 +504,8 @@ private fun CosmoSubscriptionInfoRow(info: CosmoSubscriptionInfo?, onOpenSubscri
style = MaterialTheme.typography.bodyMedium,
color = if (expired) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.onSurfaceVariant,
maxLines = 1,
modifier = Modifier.weight(1f),
)
} else {
Spacer(Modifier.weight(1f))
}
Icon(
painterResource(R.drawable.ic_cosmo_info_24dp),
contentDescription = stringResource(R.string.cosmo_tab_subscription),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp).clip(CircleShape).clickable(onClick = onOpenSubscription).padding(6.dp),
)
Icon(
painterResource(R.drawable.ic_telegram_24dp),
contentDescription = stringResource(R.string.cosmo_mysub_support),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(36.dp).clip(CircleShape).clickable { Utils.openUri(context, "https://t.me/cosmonetvpn_bot") }.padding(6.dp),
)
}
}
@ -544,15 +591,27 @@ private fun CosmoStatusPill(state: CosmoConnState, large: Boolean = false) {
CosmoConnState.Connecting -> Triple(
MaterialTheme.colorScheme.secondaryContainer, MaterialTheme.colorScheme.onSecondaryContainer, R.string.cosmo_state_connecting
)
CosmoConnState.Connected -> Triple(Color(0xFFC8F5DC), Color(0xFF0F4A2C), R.string.cosmo_state_connected)
CosmoConnState.Connected -> Triple(Color(0xFF1FA35C), Color.White, R.string.cosmo_state_connected)
}
// Status dot: green = connected, red = not connected, amber = connecting.
val dot = when (state) {
CosmoConnState.Connected -> Color(0xFF7CFFB0)
CosmoConnState.Connecting -> Color(0xFFFFC14D)
CosmoConnState.Disconnected -> Color(0xFFFF5252)
}
Surface(color = bg, contentColor = fg, shape = RoundedCornerShape(50)) {
Text(
stringResource(label),
style = if (large) MaterialTheme.typography.titleSmall else MaterialTheme.typography.labelMedium,
modifier = if (large) Modifier.padding(horizontal = 16.dp, vertical = 7.dp)
else Modifier.padding(horizontal = 10.dp, vertical = 4.dp),
)
Row(
Modifier.padding(horizontal = if (large) 16.dp else 10.dp, vertical = if (large) 7.dp else 4.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Box(Modifier.size(if (large) 10.dp else 8.dp).clip(CircleShape).background(dot))
Spacer(Modifier.width(8.dp))
Text(
stringResource(label),
style = if (large) MaterialTheme.typography.titleSmall else MaterialTheme.typography.labelMedium,
fontWeight = if (state == CosmoConnState.Connected) FontWeight.SemiBold else FontWeight.Normal,
)
}
}
}

View file

@ -3,11 +3,13 @@
<string name="cosmo_tab_home">Главная</string>
<string name="cosmo_tab_countries">Страны</string>
<string name="cosmo_tab_subscription">Моя подписка</string>
<string name="cosmo_tap_to_connect">Нажмите на ракету, чтобы подключиться</string>
<string name="cosmo_tap_to_disconnect">Нажмите на ракету, чтобы отключиться</string>
<string name="cosmo_hide_all">Свернуть список</string>
<string name="cosmo_show_all">Показать серверы</string>
<string name="cosmo_expires">Истекает: %1$s</string>
<string name="cosmo_update_sub">Обновить подписку</string>
<string name="cosmo_check_servers">Проверить доступность</string>
<string name="cosmo_check_servers">Проверить серверы</string>
<string name="cosmo_pick_server">Выберите сервер из списка ниже</string>
<string name="cosmo_mysub_active">Подписка активна</string>
<string name="cosmo_mysub_expired">Подписка закончилась</string>

View file

@ -4,6 +4,8 @@
<string name="cosmo_tab_home">Home</string>
<string name="cosmo_tab_countries">Countries</string>
<string name="cosmo_tab_subscription">My subscription</string>
<string name="cosmo_tap_to_connect">Tap the rocket to connect</string>
<string name="cosmo_tap_to_disconnect">Tap the rocket to disconnect</string>
<string name="cosmo_hide_all">Collapse list</string>
<string name="cosmo_show_all">Show servers</string>
<string name="cosmo_expires">Expires: %1$s</string>