diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/KordophoneApp.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/KordophoneApp.kt index 48510e1..5972ddc 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/KordophoneApp.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/KordophoneApp.kt @@ -20,9 +20,13 @@ import net.buzzert.kordophonedroid.data.AppContainer import net.buzzert.kordophonedroid.ui.theme.KordophoneTheme import net.buzzert.kordophonedroid.ui.conversationlist.ConversationListScreen import net.buzzert.kordophonedroid.ui.messagelist.MessageListScreen +import net.buzzert.kordophonedroid.ui.settings.SettingsScreen sealed class Destination(val route: String) { object ConversationList : Destination("conversations") + + object Settings : Destination("settings") + object MessageList : Destination("messages/{id}") { fun createRoute(data: String) = "messages/$data" } @@ -64,6 +68,8 @@ fun KordophoneApp( composable(route = Destination.ConversationList.route) { ConversationListScreen(onConversationSelected = { navController.navigate(Destination.MessageList.createRoute(it)) + }, onSettingsInvoked = { + navController.navigate(Destination.Settings.route) }) } @@ -73,6 +79,12 @@ fun KordophoneApp( navController.popBackStack() }) } + + composable(Destination.Settings.route) { + SettingsScreen(backAction = { + navController.popBackStack() + }) + } } errorVisible.value?.let { diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListScreen.kt index 53d0ed4..cc32fab 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/conversationlist/ConversationListScreen.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Info +import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -45,25 +46,30 @@ fun formatDateTime(dateTime: LocalDateTime): String { @Composable fun ConversationListScreen( viewModel: ConversationListViewModel = hiltViewModel(), - onConversationSelected: (conversationID: String) -> Unit + onConversationSelected: (conversationID: String) -> Unit, + onSettingsInvoked: () -> Unit, ) { val conversations by viewModel.conversations.collectAsStateWithLifecycle(initialValue = emptyList()) - ConversationListView(conversations = conversations, onConversationSelected = onConversationSelected) + ConversationListView( + conversations = conversations, + onConversationSelected = onConversationSelected, + onSettingsInvoked = onSettingsInvoked, + ) } @Composable fun ConversationListView( conversations: List, - onConversationSelected: (conversationID: String) -> Unit + onConversationSelected: (conversationID: String) -> Unit, + onSettingsInvoked: () -> Unit, ) { - val listState = rememberLazyListState() Scaffold( topBar = { TopAppBar(title = { Text("Conversations") }, actions = { - IconButton(onClick = { /*TODO*/ }) { - Icon(Icons.Rounded.Info, contentDescription = "Info") + IconButton(onClick = onSettingsInvoked) { + Icon(Icons.Rounded.Settings, contentDescription = "Settings") } }) } @@ -173,5 +179,5 @@ fun ConversationListItemPreview() { @Preview @Composable fun ConversationListScreenPreview() { - ConversationListScreen(onConversationSelected = {}) + ConversationListScreen(onConversationSelected = {}, onSettingsInvoked = {}) } \ No newline at end of file diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt index c9f2dbb..1865a6e 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/messagelist/MessageListScreen.kt @@ -48,6 +48,7 @@ import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import net.buzzert.kordophone.backend.model.GUID import net.buzzert.kordophone.backend.model.Message +import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar private val IncomingChatBubbleShape = RoundedCornerShape(4.dp, 20.dp, 20.dp, 20.dp) private val OutgoingChatBubbleShape = RoundedCornerShape(20.dp, 4.dp, 20.dp, 20.dp) @@ -82,7 +83,7 @@ fun MessageListScreen( ) } - Scaffold(topBar = { MessagesListTopAppBar(title = viewModel.title, backAction = backAction) }) { padding -> + Scaffold(topBar = { KordophoneTopAppBar(title = viewModel.title, backAction = backAction) }) { padding -> MessageTranscript( messages = messageItems, paddingValues = padding, @@ -93,19 +94,6 @@ fun MessageListScreen( } } -@Composable -fun MessagesListTopAppBar(title: String, backAction: () -> Unit) { - TopAppBar( - title = { Text(title) }, - navigationIcon = { - IconButton(onClick = backAction) { - Icon(Icons.Filled.ArrowBack, null) - } - }, - actions = {} - ) -} - @Composable fun MessageTranscript( messages: List, diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsScreen.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsScreen.kt new file mode 100644 index 0000000..064cce3 --- /dev/null +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsScreen.kt @@ -0,0 +1,222 @@ +package net.buzzert.kordophonedroid.ui.settings + +import android.provider.Settings +import androidx.annotation.DrawableRes +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.Button +import androidx.compose.material.Divider +import androidx.compose.material.ExperimentalMaterialApi +import androidx.compose.material.Icon +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Scaffold +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.material.TextField +import androidx.compose.material.TopAppBar +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.AccountBox +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.hilt.navigation.compose.hiltViewModel +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import net.buzzert.kordophonedroid.R +import net.buzzert.kordophonedroid.ui.theme.KordophoneTopAppBar + +@Composable +fun SettingsScreen( + viewModel: SettingsViewModel = hiltViewModel(), + backAction: () -> Unit, +) { + Scaffold( + topBar = { + KordophoneTopAppBar( + title = "Settings", + backAction = backAction, + ) + }, + + ) { + SettingsFormView( + viewModel = viewModel, + modifier = Modifier + .verticalScroll(rememberScrollState()) + .padding(it) + .padding(6.dp) + ) + } +} + +@Composable +fun SettingsFormView( + viewModel: SettingsViewModel, + modifier: Modifier = Modifier +) { + val serverName = viewModel.serverPreference.collectAsState() + val userName = viewModel.usernamePreference.collectAsState() + + Column(modifier) { + var serverNameInput by remember { mutableStateOf(TextFieldValue(serverName.value)) } + SettingsTextField( + name = "Server", + icon = R.drawable.storage, + state = serverName, + onSave = { viewModel.saveServerPreference(serverNameInput.text) } + ) { state -> + TextField(serverNameInput, onValueChange = { + serverNameInput = it + }) + } + + SettingsTextField( + name = "Authentication", + icon = R.drawable.account_circle, + state = userName, + onSave = { /* TODO */ } + ) { + // TODO + Text("Hoohah!") + } + } +} + +@OptIn(ExperimentalMaterialApi::class) +@Composable +fun SettingsTextField( + name: String, + @DrawableRes icon: Int, + state: State, + onSave: () -> Unit, + dialogContent: @Composable (State) -> Unit, +) { + var showingDialog by remember { mutableStateOf(false) } + + if (showingDialog) { + Dialog( + onDismissRequest = { showingDialog = false } + ) { + EditDialog( + name = name, + onDismiss = { + onSave() + showingDialog = false + }, + content = { dialogContent(state) } + ) + } + } + + Surface( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + , + onClick = { + showingDialog = true + }, + ) { + val valueString = state.value.toString().ifEmpty { "(Not set)" } + + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.Start, + modifier = Modifier.padding(vertical = 8.dp) + ) { + Icon( + painterResource(id = icon), + contentDescription = "", + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(16.dp)) + Column(modifier = Modifier.padding(8.dp)) { + // Title + Text( + text = name, + style = MaterialTheme.typography.body1, + textAlign = TextAlign.Start, + ) + + Spacer(modifier = Modifier.height(4.dp)) + + // Value + Text( + text = valueString, + style = MaterialTheme.typography.body2, + textAlign = TextAlign.Start, + color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f), + ) + } + } + + Divider() + } + } +} + +@Composable +private fun EditDialog( + name: String, + onDismiss: () -> Unit, + content: @Composable () -> Unit, +) { + Surface() { + Column( + modifier = Modifier + .wrapContentHeight() + .fillMaxWidth() + .padding(16.dp) + ) { + Text(name) + + Spacer(modifier = Modifier.height(8.dp)) + + content() + + Spacer(modifier = Modifier.height(8.dp)) + + Row { + Spacer(modifier = Modifier.weight(1f)) + Button(onClick = { + onDismiss() + }) { + Text("Save") + } + } + } + } +} + + +@Preview +@Composable +fun SettingsPreview() { + SettingsScreen(backAction = {}) +} \ No newline at end of file diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsViewModel.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsViewModel.kt new file mode 100644 index 0000000..3813a0b --- /dev/null +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/settings/SettingsViewModel.kt @@ -0,0 +1,30 @@ +package net.buzzert.kordophonedroid.ui.settings + +import androidx.compose.runtime.MutableState +import androidx.lifecycle.ViewModel +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor() : ViewModel() { + private val _serverPreference: MutableStateFlow = MutableStateFlow("") + var serverPreference = _serverPreference.asStateFlow() + + private val _usernamePreference: MutableStateFlow = MutableStateFlow("") + var usernamePreference = _usernamePreference.asStateFlow() + + private val _passwordPreference: MutableStateFlow = MutableStateFlow("") + var passwordPreference = _passwordPreference.asStateFlow() + + fun saveServerPreference(serverName: String) { + _serverPreference.value = serverName + } + + fun saveAuthenticationPreferences(username: String, password: String) { + _usernamePreference.value = username + _passwordPreference.value = password + } +} \ No newline at end of file diff --git a/app/src/main/java/net/buzzert/kordophonedroid/ui/theme/Theme.kt b/app/src/main/java/net/buzzert/kordophonedroid/ui/theme/Theme.kt index eb70ca8..cfa39bd 100644 --- a/app/src/main/java/net/buzzert/kordophonedroid/ui/theme/Theme.kt +++ b/app/src/main/java/net/buzzert/kordophonedroid/ui/theme/Theme.kt @@ -1,8 +1,14 @@ package net.buzzert.kordophonedroid.ui.theme import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material.Icon +import androidx.compose.material.IconButton import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.material.TopAppBar import androidx.compose.material.darkColors +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.lightColors import androidx.compose.runtime.Composable @@ -44,4 +50,18 @@ fun KordophoneTheme( shapes = Shapes, content = content ) +} + + +@Composable +fun KordophoneTopAppBar(title: String, backAction: () -> Unit) { + TopAppBar( + title = { Text(title) }, + navigationIcon = { + IconButton(onClick = backAction) { + Icon(Icons.Filled.ArrowBack, null) + } + }, + actions = {} + ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/account_circle.xml b/app/src/main/res/drawable/account_circle.xml new file mode 100644 index 0000000..fec33e7 --- /dev/null +++ b/app/src/main/res/drawable/account_circle.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/storage.xml b/app/src/main/res/drawable/storage.xml new file mode 100644 index 0000000..4ec6318 --- /dev/null +++ b/app/src/main/res/drawable/storage.xml @@ -0,0 +1,10 @@ + + +