From a89b2c36a724ea5aa7d8ed9e3cda09d099fa2b0f Mon Sep 17 00:00:00 2001 From: Sweetbread Date: Sun, 19 Apr 2026 02:48:23 +0300 Subject: [PATCH] New TopBar --- .../pixeldragon/ui/activity/MainActivity.kt | 243 ++++++++++++++++-- 1 file changed, 217 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt index 37fc236..05acde1 100755 --- a/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt +++ b/app/src/main/java/ru/risdeveau/pixeldragon/ui/activity/MainActivity.kt @@ -5,41 +5,70 @@ package ru.risdeveau.pixeldragon.ui.activity +import android.annotation.SuppressLint import android.os.Bundle import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.CenterAlignedTopAppBar import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults.topAppBarColors +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 import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable +import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument import de.connect2x.trixnity.client.CryptoDriverModule -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import de.connect2x.trixnity.client.MatrixClient import de.connect2x.trixnity.client.create import de.connect2x.trixnity.client.cryptodriver.vodozemac.vodozemac +import de.connect2x.trixnity.client.flattenValues +import de.connect2x.trixnity.client.room +import de.connect2x.trixnity.client.store.type import de.connect2x.trixnity.clientserverapi.client.SyncState +import de.connect2x.trixnity.core.model.events.m.room.CreateEventContent +import io.github.rabehx.iconsax.Iconsax +import io.github.rabehx.iconsax.automirrored.outline.ArrowLeft2 +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import ru.risdeveau.pixeldragon.R import ru.risdeveau.pixeldragon.client +import ru.risdeveau.pixeldragon.ui.item.MXCImage import ru.risdeveau.pixeldragon.ui.layout.Room import ru.risdeveau.pixeldragon.ui.layout.RoomList import ru.risdeveau.pixeldragon.ui.theme.PixelDragonTheme @@ -47,11 +76,10 @@ import ru.risdeveau.pixeldragon.util.getMediaStore import ru.risdeveau.pixeldragon.util.getRoomStore import splitties.activities.start import splitties.init.appCtx -import splitties.resources.str +import de.connect2x.trixnity.client.store.Room as TrixnityRoom class MainActivity : ComponentActivity() { - @OptIn(ExperimentalMaterial3Api::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -59,34 +87,28 @@ class MainActivity : ComponentActivity() { setContent { PixelDragonTheme { var isClientReady by remember { mutableStateOf(false) } - val syncState by remember { mutableStateOf(SyncState.STOPPED) } - if (!isClientReady) { - CircularProgressIndicator() + if (!isClientReady || client == null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } } else { + val navController = rememberNavController() + val syncState by client!!.api.sync.currentSyncState + .collectAsState(initial = SyncState.STOPPED) + Scaffold( modifier = Modifier.fillMaxSize(), topBar = { - CenterAlignedTopAppBar( - colors = topAppBarColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - titleContentColor = MaterialTheme.colorScheme.primary, - ), - title = { - when (syncState) { - SyncState.STARTED -> Text("Syncing...") - SyncState.INITIAL_SYNC -> Text("Initial sync...") - SyncState.STOPPED, - SyncState.RUNNING -> Text(str(R.string.app_name)) - SyncState.TIMEOUT -> Text("No network connection") - SyncState.ERROR -> Text("Error syncing") - } - } + PixelDragonTopBar( + navController = navController, + syncState = syncState, ) }, ) { innerPadding -> - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "rooms") { composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) @@ -102,7 +124,7 @@ class MainActivity : ComponentActivity() { composable( "space/{rid}", arguments = listOf(navArgument("rid") { type = NavType.StringType }) - ) { navBackStackEntry -> + ) { Text( modifier = Modifier.padding(innerPadding), text = "Not implemented" @@ -142,3 +164,172 @@ class MainActivity : ComponentActivity() { } } } + +@SuppressLint("FlowOperatorInvokedInComposition") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PixelDragonTopBar( + navController: NavHostController, + syncState: SyncState, +) { + val backStackEntry by navController.currentBackStackEntryAsState() + val route = backStackEntry?.destination?.route + val rid = backStackEntry?.arguments?.getString("rid") + val isRoomLikeScreen = route == "room/{rid}" || route == "space/{rid}" + + val roomsFlow = remember(client) { + client!!.room.getAll().flattenValues().map { it.toList() } + } + val rooms by roomsFlow.collectAsState(initial = emptyList()) + + val currentRoom = remember(isRoomLikeScreen, rid, rooms) { + if (isRoomLikeScreen && rid != null) { + rooms.firstOrNull { it.roomId.toString() == rid } + } else { + null + } + } + + if (isRoomLikeScreen) { + RoomTopBar( + room = currentRoom, + fallbackTitle = rid ?: stringResource(R.string.app_name), + onBack = { navController.popBackStack() }, + ) + } else { + HomeTopBar(syncState = syncState) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun HomeTopBar(syncState: SyncState) { + CenterAlignedTopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + Text(syncState.toStatusTitle() ?: stringResource(R.string.app_name)) + }, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun RoomTopBar( + room: TrixnityRoom?, + fallbackTitle: String, + onBack: () -> Unit, +) { + CenterAlignedTopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(.5f), + titleContentColor = MaterialTheme.colorScheme.onSurface, + navigationIconContentColor = MaterialTheme.colorScheme.primary, + ), + navigationIcon = { + IconButton(onClick = onBack) { + Icon( + Iconsax.AutoMirrored.Outline.ArrowLeft2, + "To home", + ) + } + }, + title = { + if (room != null) { + RoomTopBarTitle(room) + } else { + Text( + text = fallbackTitle, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + ) + } + }, + ) +} + +@Composable +private fun RoomTopBarTitle(room: TrixnityRoom) { + val title = remember(room) { room.displayName() } + val isSpace = room.type == CreateEventContent.RoomType.Space + val avatarShape = if (isSpace) RoundedCornerShape(8.dp) else CircleShape + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (room.avatarUrl != null) { + MXCImage( + mxcUrl = room.avatarUrl!!, + modifier = Modifier + .size(36.dp) + .clip(avatarShape), + contentScale = ContentScale.Crop, + contentDescription = title, + showProgress = false, + ) + } else { + RoomAvatarPlaceholder( + title = title, + modifier = Modifier.size(36.dp), + isSpace = isSpace, + ) + } + + Spacer(Modifier.width(10.dp)) + + Text( + text = title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + ) + } +} + +@Composable +private fun RoomAvatarPlaceholder( + title: String, + modifier: Modifier = Modifier, + isSpace: Boolean = false, +) { + val shape = if (isSpace) RoundedCornerShape(8.dp) else CircleShape + val initial = title + .trim() + .firstOrNull() + ?.uppercaseChar() + ?.toString() + ?: "?" + + Box( + modifier = modifier + .clip(shape) + .background(MaterialTheme.colorScheme.secondaryContainer), + contentAlignment = Alignment.Center, + ) { + Text( + text = initial, + color = MaterialTheme.colorScheme.onSecondaryContainer, + style = MaterialTheme.typography.titleMedium, + ) + } +} + +private fun TrixnityRoom.displayName(): String { + return name?.explicitName + ?: name?.heroes?.firstNotNullOfOrNull { it.localpart } + ?: roomId.toString() +} + +private fun SyncState.toStatusTitle(): String? { + return when (this) { + SyncState.INITIAL_SYNC -> "Initial sync..." + SyncState.STARTED -> "Syncing..." + SyncState.TIMEOUT -> "No network connection" + SyncState.ERROR -> "Error syncing" + SyncState.RUNNING, + SyncState.STOPPED -> null + } +}