This commit is contained in:
2026-02-24 18:03:22 +00:00
parent 1c8c306652
commit 39010f882d
6 changed files with 252 additions and 12 deletions

View File

@@ -29,6 +29,7 @@ class PreferencesManager(private val context: Context) {
private val KEY_IS_LOGGED_IN = booleanPreferencesKey("is_logged_in") private val KEY_IS_LOGGED_IN = booleanPreferencesKey("is_logged_in")
private val KEY_CHANNEL_ORDER = stringPreferencesKey("channel_order") private val KEY_CHANNEL_ORDER = stringPreferencesKey("channel_order")
private val KEY_CHILD_SPACE_ORDER = stringPreferencesKey("child_space_order") private val KEY_CHILD_SPACE_ORDER = stringPreferencesKey("child_space_order")
private val KEY_HIDE_SPACES_WHEN_CLOSED = booleanPreferencesKey("hide_spaces_when_closed")
} }
val isLoggedIn: Flow<Boolean> = context.dataStore.data.map { prefs -> val isLoggedIn: Flow<Boolean> = context.dataStore.data.map { prefs ->
@@ -117,6 +118,16 @@ class PreferencesManager(private val context: Context) {
} }
} }
val hideSpacesWhenClosed: Flow<Boolean> = context.dataStore.data.map { prefs ->
prefs[KEY_HIDE_SPACES_WHEN_CLOSED] == true
}
suspend fun setHideSpacesWhenClosed(enabled: Boolean) {
context.dataStore.edit { prefs ->
prefs[KEY_HIDE_SPACES_WHEN_CLOSED] = enabled
}
}
suspend fun clearSession() { suspend fun clearSession() {
context.dataStore.edit { it.clear() } context.dataStore.edit { it.clear() }
} }

View File

@@ -29,6 +29,7 @@ import com.example.fluffytrix.data.local.PreferencesManager
import com.example.fluffytrix.data.repository.AuthRepository import com.example.fluffytrix.data.repository.AuthRepository
import com.example.fluffytrix.ui.screens.login.LoginScreen import com.example.fluffytrix.ui.screens.login.LoginScreen
import com.example.fluffytrix.ui.screens.main.MainScreen import com.example.fluffytrix.ui.screens.main.MainScreen
import com.example.fluffytrix.ui.screens.settings.SettingsScreen
import com.example.fluffytrix.ui.screens.verification.VerificationScreen import com.example.fluffytrix.ui.screens.verification.VerificationScreen
import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.firstOrNull
import org.koin.compose.koinInject import org.koin.compose.koinInject
@@ -143,7 +144,20 @@ fun FluffytrixNavigation() {
navController.navigate(Screen.Login.route) { navController.navigate(Screen.Login.route) {
popUpTo(Screen.Main.route) { inclusive = true } popUpTo(Screen.Main.route) { inclusive = true }
} }
} },
onSettingsClick = {
navController.navigate(Screen.Settings.route)
},
)
}
composable(Screen.Settings.route) {
SettingsScreen(
onBack = { navController.popBackStack() },
onLogout = {
navController.navigate(Screen.Login.route) {
popUpTo(0) { inclusive = true }
}
},
) )
} }
} }

View File

@@ -4,4 +4,5 @@ sealed class Screen(val route: String) {
data object Login : Screen("login") data object Login : Screen("login")
data object Verification : Screen("verification") data object Verification : Screen("verification")
data object Main : Screen("main") data object Main : Screen("main")
data object Settings : Screen("settings")
} }

View File

@@ -18,15 +18,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex import androidx.compose.ui.zIndex
import com.example.fluffytrix.data.local.PreferencesManager
import com.example.fluffytrix.ui.screens.main.components.ChannelList import com.example.fluffytrix.ui.screens.main.components.ChannelList
import com.example.fluffytrix.ui.screens.main.components.MemberList import com.example.fluffytrix.ui.screens.main.components.MemberList
import com.example.fluffytrix.ui.screens.main.components.MessageTimeline import com.example.fluffytrix.ui.screens.main.components.MessageTimeline
import com.example.fluffytrix.ui.screens.main.components.SpaceList import com.example.fluffytrix.ui.screens.main.components.SpaceList
import org.koin.androidx.compose.koinViewModel import org.koin.androidx.compose.koinViewModel
import org.koin.compose.koinInject
@Composable @Composable
fun MainScreen( fun MainScreen(
onLogout: () -> Unit, onLogout: () -> Unit,
onSettingsClick: () -> Unit = {},
viewModel: MainViewModel = koinViewModel(), viewModel: MainViewModel = koinViewModel(),
) { ) {
val spaces by viewModel.spaces.collectAsState() val spaces by viewModel.spaces.collectAsState()
@@ -43,6 +46,8 @@ fun MainScreen(
val channelSections by viewModel.channelSections.collectAsState() val channelSections by viewModel.channelSections.collectAsState()
val unreadMarkerIndex by viewModel.unreadMarkerIndex.collectAsState() val unreadMarkerIndex by viewModel.unreadMarkerIndex.collectAsState()
val listState = viewModel.channelListState val listState = viewModel.channelListState
val preferencesManager: PreferencesManager = koinInject()
val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsState(initial = false)
// Back button opens channel list when in a chat, or does nothing if already open // Back button opens channel list when in a chat, or does nothing if already open
BackHandler(enabled = selectedChannel != null && !showChannelList) { BackHandler(enabled = selectedChannel != null && !showChannelList) {
@@ -71,15 +76,18 @@ fun MainScreen(
) { ) {
// Main content: SpaceList + chat + members // Main content: SpaceList + chat + members
Row(modifier = Modifier.fillMaxSize()) { Row(modifier = Modifier.fillMaxSize()) {
SpaceList( if (!hideSpacesWhenClosed) {
spaces = spaces, SpaceList(
selectedSpace = selectedSpace, spaces = spaces,
homeUnreadStatus = homeUnreadStatus, selectedSpace = selectedSpace,
onSpaceClick = { viewModel.selectSpace(it) }, homeUnreadStatus = homeUnreadStatus,
onHomeClick = { viewModel.selectHome() }, onSpaceClick = { viewModel.selectSpace(it) },
onToggleChannelList = { viewModel.toggleChannelList() }, onHomeClick = { viewModel.selectHome() },
contentPadding = padding, onToggleChannelList = { viewModel.toggleChannelList() },
) onSettingsClick = onSettingsClick,
contentPadding = padding,
)
}
MessageTimeline( MessageTimeline(
selectedChannel = selectedChannel, selectedChannel = selectedChannel,
@@ -110,7 +118,20 @@ fun MainScreen(
modifier = Modifier.zIndex(1f), modifier = Modifier.zIndex(1f),
) { ) {
Row { Row {
Spacer(modifier = Modifier.width(64.dp)) if (hideSpacesWhenClosed) {
SpaceList(
spaces = spaces,
selectedSpace = selectedSpace,
homeUnreadStatus = homeUnreadStatus,
onSpaceClick = { viewModel.selectSpace(it) },
onHomeClick = { viewModel.selectHome() },
onToggleChannelList = { viewModel.toggleChannelList() },
onSettingsClick = onSettingsClick,
contentPadding = padding,
)
} else {
Spacer(modifier = Modifier.width(64.dp))
}
ChannelList( ChannelList(
channels = channels, channels = channels,
sections = channelSections, sections = channelSections,

View File

@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@@ -16,6 +17,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Home
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -44,15 +46,20 @@ fun SpaceList(
onSpaceClick: (String) -> Unit, onSpaceClick: (String) -> Unit,
onHomeClick: () -> Unit, onHomeClick: () -> Unit,
onToggleChannelList: () -> Unit, onToggleChannelList: () -> Unit,
onSettingsClick: () -> Unit = {},
contentPadding: PaddingValues, contentPadding: PaddingValues,
) { ) {
LazyColumn( Column(
modifier = Modifier modifier = Modifier
.width(64.dp) .width(64.dp)
.fillMaxHeight() .fillMaxHeight()
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
.padding(top = contentPadding.calculateTopPadding()), .padding(top = contentPadding.calculateTopPadding()),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
) {
LazyColumn(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(vertical = 8.dp), contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
@@ -149,6 +156,17 @@ fun SpaceList(
} }
} }
} }
IconButton(
onClick = onSettingsClick,
modifier = Modifier.padding(bottom = contentPadding.calculateBottomPadding() + 8.dp),
) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings",
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
} }
@Composable @Composable

View File

@@ -0,0 +1,175 @@
package com.example.fluffytrix.ui.screens.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import com.example.fluffytrix.data.local.PreferencesManager
import kotlinx.coroutines.launch
import org.koin.compose.koinInject
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onBack: () -> Unit,
onLogout: () -> Unit,
) {
val preferencesManager: PreferencesManager = koinInject()
val userId by preferencesManager.userId.collectAsState(initial = null)
val homeserver by preferencesManager.homeserverUrl.collectAsState(initial = null)
val deviceId by preferencesManager.deviceId.collectAsState(initial = null)
val hideSpacesWhenClosed by preferencesManager.hideSpacesWhenClosed.collectAsState(initial = false)
val scope = rememberCoroutineScope()
val context = LocalContext.current
val appVersion = try {
context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "Unknown"
} catch (_: Exception) {
"Unknown"
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Settings") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
)
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface,
),
)
},
) { padding ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(horizontal = 16.dp),
) {
SectionHeader("Account")
SettingRow("User ID", userId ?: "Not logged in")
SettingRow("Homeserver", homeserver ?: "Unknown")
SettingRow("Device ID", deviceId ?: "Unknown")
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = onLogout,
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.error,
),
modifier = Modifier.fillMaxWidth(),
) {
Text("Log out")
}
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
SectionHeader("Appearance")
SettingToggleRow(
label = "Hide spaces bar when rooms list is closed",
description = "Only show the spaces sidebar when the rooms list is open",
checked = hideSpacesWhenClosed,
onCheckedChange = { scope.launch { preferencesManager.setHideSpacesWhenClosed(it) } },
)
SettingRow("Theme", "System default")
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
SectionHeader("Notifications")
SettingRow("Push notifications", "Enabled")
HorizontalDivider(modifier = Modifier.padding(vertical = 16.dp))
SectionHeader("About")
SettingRow("App version", appVersion)
SettingRow("App name", "Fluffytrix")
Spacer(modifier = Modifier.height(32.dp))
}
}
}
@Composable
private fun SectionHeader(title: String) {
Text(
text = title,
style = MaterialTheme.typography.titleSmall,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(vertical = 8.dp),
)
}
@Composable
private fun SettingToggleRow(
label: String,
description: String,
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 6.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
Column(modifier = Modifier.weight(1f)) {
Text(text = label, style = MaterialTheme.typography.bodyMedium)
Text(
text = description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Switch(checked = checked, onCheckedChange = onCheckedChange)
}
}
@Composable
private fun SettingRow(label: String, value: String) {
Column(modifier = Modifier.padding(vertical = 6.dp)) {
Text(
text = label,
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = value,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}