From dd30db2130d55dcb9254fd5098123f992893ae0c 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 | 219 +++++++++++++++--- 1 file changed, 186 insertions(+), 33 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..5c7438e 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,13 +5,22 @@ 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 @@ -19,27 +28,42 @@ 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.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 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 @@ -48,45 +72,41 @@ 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) enableEdgeToEdge() setContent { PixelDragonTheme { - var isClientReady by remember { mutableStateOf(false) } - val syncState by remember { mutableStateOf(SyncState.STOPPED) } + var matrixClient by remember { mutableStateOf(client) } + val currentClient = matrixClient - if (!isClientReady) { - CircularProgressIndicator() + if (currentClient == null) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() + } } else { + val navController = rememberNavController() + 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") - } - } + val syncState by currentClient.api.sync.currentSyncState + .collectAsState(initial = SyncState.STOPPED) + + PixelDragonTopBar( + navController = navController, + matrixClient = currentClient, + syncState = syncState, ) }, ) { innerPadding -> - val navController = rememberNavController() - NavHost(navController = navController, startDestination = "rooms") { composable("rooms") { RoomList(Modifier.padding(innerPadding), navController) @@ -95,14 +115,17 @@ class MainActivity : ComponentActivity() { "room/{rid}", arguments = listOf(navArgument("rid") { type = NavType.StringType }) ) { navBackStackEntry -> - Room(Modifier - .padding(innerPadding) - .fillMaxSize(), navBackStackEntry.arguments!!.getString("rid")!!) + Room( + Modifier + .padding(innerPadding) + .fillMaxSize(), + navBackStackEntry.arguments!!.getString("rid")!!, + ) } composable( "space/{rid}", arguments = listOf(navArgument("rid") { type = NavType.StringType }) - ) { navBackStackEntry -> + ) { Text( modifier = Modifier.padding(innerPadding), text = "Not implemented" @@ -127,9 +150,15 @@ class MainActivity : ComponentActivity() { } } - Log.i("MainActivity", "Log in as ${client!!.userId}") - client!!.startSync() - isClientReady = true + val readyClient = client ?: run { + start() + finish() + return@LaunchedEffect + } + + matrixClient = readyClient + Log.i("MainActivity", "Log in as ${readyClient.userId}") + readyClient.startSync() } } } @@ -142,3 +171,127 @@ class MainActivity : ComponentActivity() { } } } + +@SuppressLint("FlowOperatorInvokedInComposition") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PixelDragonTopBar( + navController: NavHostController, + matrixClient: MatrixClient, + syncState: SyncState, +) { + val backStackEntry by navController.currentBackStackEntryAsState() + val route = backStackEntry?.destination?.route + val rid = backStackEntry?.arguments?.getString("rid") + + val roomsFlow = remember(matrixClient) { + matrixClient.room.getAll().flattenValues().map { it.toList() } + } + val rooms by roomsFlow.collectAsState(initial = emptyList()) + + val currentRoom = remember(route, rid, rooms) { + if ((route == "room/{rid}" || route == "space/{rid}") && rid != null) { + rooms.firstOrNull { it.roomId.toString() == rid } + } else { + null + } + } + + CenterAlignedTopAppBar( + colors = topAppBarColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + titleContentColor = MaterialTheme.colorScheme.primary, + ), + title = { + val statusTitle = syncState.toStatusTitle() + when { + statusTitle != null -> Text(statusTitle) + currentRoom != null -> RoomTopBarTitle(currentRoom) + else -> Text(appCtx.str(R.string.app_name)) + } + }, + ) +} + +@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 + } +}