From d05d93a0d7af8c9c288322720ff04b5b7980fb27 Mon Sep 17 00:00:00 2001 From: "Maurice L." Date: Fri, 27 Feb 2026 20:04:05 +0100 Subject: [PATCH] Refactored UI components and extracted screens into separate files. The UI logic from `MainActivity.kt` has been moved into a new `AppContent` composable, and individual screens (`LoginScreen`, `DashboardScreen`, `AccountSelectionScreen`) have been extracted to their own files within the `ui.screens` package. Additional changes: - Updated `MainActivity` to use the new `AppContent` structure and handled back navigation for new account logins. - Fixed a field name mismatch in `RefreshRequest` within `APIService.kt`, changing `refreshToken` to `refresh_token`. - Minor formatting update in `README.md`. --- README.md | 2 + .../shap_planner/activities/MainActivity.kt | 232 ++---------------- .../shap_planner/network/APIService.kt | 2 +- .../miaurizius/shap_planner/ui/AppContent.kt | 42 ++++ .../ui/screens/AccountSelectionScreen.kt | 69 ++++++ .../ui/screens/DashboardScreen.kt | 111 +++++++++ .../shap_planner/ui/screens/LoginScreen.kt | 70 ++++++ 7 files changed, 317 insertions(+), 211 deletions(-) create mode 100644 app/src/main/java/de/miaurizius/shap_planner/ui/AppContent.kt create mode 100644 app/src/main/java/de/miaurizius/shap_planner/ui/screens/AccountSelectionScreen.kt create mode 100644 app/src/main/java/de/miaurizius/shap_planner/ui/screens/DashboardScreen.kt create mode 100644 app/src/main/java/de/miaurizius/shap_planner/ui/screens/LoginScreen.kt diff --git a/README.md b/README.md index 914aa22..add664b 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ The app is fully open source, lightweight, and can run on small devices like Ras You can either build the app from source or download the apk _(will be available soon)_ +--- + ## License This work is marked CC0 1.0 \ No newline at end of file diff --git a/app/src/main/java/de/miaurizius/shap_planner/activities/MainActivity.kt b/app/src/main/java/de/miaurizius/shap_planner/activities/MainActivity.kt index 7b4141f..5f9758a 100644 --- a/app/src/main/java/de/miaurizius/shap_planner/activities/MainActivity.kt +++ b/app/src/main/java/de/miaurizius/shap_planner/activities/MainActivity.kt @@ -46,6 +46,7 @@ import de.miaurizius.shap_planner.UserPreferences import de.miaurizius.shap_planner.entities.Account import de.miaurizius.shap_planner.network.SessionState import de.miaurizius.shap_planner.room.AppDatabase +import de.miaurizius.shap_planner.ui.AppContent import de.miaurizius.shap_planner.ui.theme.ShapPlannerTheme import de.miaurizius.shap_planner.viewmodels.LoginViewModel import de.miaurizius.shap_planner.viewmodels.MainViewModel @@ -54,16 +55,12 @@ import java.util.UUID class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) -// enableEdgeToEdge() val prefs = UserPreferences(this) val loginViewModel = LoginViewModel(prefs, applicationContext) - val database = AppDatabase.getDatabase(applicationContext) val dao = database.accountDao() - val tokenStorage = TokenStorage(applicationContext) - val mainViewModel = MainViewModel(dao, tokenStorage) setContent { @@ -71,215 +68,30 @@ class MainActivity : ComponentActivity() { val isLoggedIn by loginViewModel.isLoggedIn.collectAsState() val accountList by mainViewModel.accounts.collectAsState() val selectedAccount = mainViewModel.selectedAccount - var showLoginForNewAccount by remember { mutableStateOf(false) } + val showLoginForNewAccount = remember { mutableStateOf(false) } - when { - - showLoginForNewAccount -> { - LoginScreen( - onLogin = { serverUrl, username, password -> - loginViewModel.login(serverUrl, username, password, mainViewModel) - showLoginForNewAccount = false - }, - onBack = { - showLoginForNewAccount = false - } - ) - } - - accountList.isEmpty() -> { - LoginScreen( - onLogin = { serverUrl, username, password -> - loginViewModel.login(serverUrl, username, password, mainViewModel) - } - ) - } - - selectedAccount != null -> { - DashboardScreen( - account = selectedAccount, - onBack = { mainViewModel.logoutFromAccount() }, - onDelete = { mainViewModel.deleteAccount(selectedAccount) }, - sessionState = mainViewModel.sessionState, - onValidate = { mainViewModel.validateSession(selectedAccount) }, - onSessionInvalid = { mainViewModel.logoutFromAccount() } - ) - } - - else -> { - AccountSelectionScreen( - accounts = accountList, - onAccountClick = { account -> - mainViewModel.selectAccount(account) - }, - onAddAccountClick = { - showLoginForNewAccount = true - } - ) - } + BackHandler(enabled = showLoginForNewAccount.value && accountList.isNotEmpty()) { + showLoginForNewAccount.value = false } + + AppContent( + isLoggedIn = isLoggedIn, + accountList = accountList, + selectedAccount = selectedAccount, + showLoginForNewAccount = showLoginForNewAccount.value, + onLogin = { server, user, pass -> + loginViewModel.login(server, user, pass, mainViewModel) + showLoginForNewAccount.value = false + }, + onSelectAccount = { mainViewModel.selectAccount(it) }, + onLogoutAccount = { mainViewModel.logoutFromAccount() }, + onAddAccountClick = { showLoginForNewAccount.value = true }, + onDeleteAccount = { mainViewModel.deleteAccount(selectedAccount!!) }, + sessionState = mainViewModel.sessionState, + onValidateSession = { mainViewModel.validateSession(selectedAccount!!) }, + onSessionInvalid = { mainViewModel.logoutFromAccount() } + ) } } } -} - -@Composable -fun AccountSelectionScreen(accounts: List, onAccountClick: (Account) -> Unit, onAddAccountClick: () -> Unit) { - LazyColumn( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .statusBarsPadding() - .navigationBarsPadding(), - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - item { - Text("Wähle einen Account", style = MaterialTheme.typography.headlineSmall) - } - - items(accounts) { account -> - Card(modifier = Modifier.fillMaxWidth().clickable{ onAccountClick(account) }) { - Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { - Box(modifier = Modifier.size(40.dp).background(Color.Gray, shape = CircleShape)) - Spacer(modifier = Modifier.width(16.dp)) - Column { - Text(text = account.name, fontWeight = FontWeight.Bold) - Text(text = account.wgName, style = MaterialTheme.typography.bodyMedium) - } - } - } - } - item { - Spacer(modifier = Modifier.height(8.dp)) - Button( - onClick = onAddAccountClick, - modifier = Modifier.fillMaxWidth() - ) { - Text("Anderen Account hinzufügen") - } - } - } -} - -@Composable -fun LoginScreen(onLogin: (String, String, String) -> Unit, onBack: (() -> Unit)? = null) { - - if (onBack != null) { - BackHandler { - onBack() - } - } - - var serverUrl by remember { mutableStateOf("") } - var username by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - - Column(modifier = Modifier.padding(16.dp).statusBarsPadding().navigationBarsPadding()) { - Text("Bitte anmelden") - Spacer(modifier = Modifier.height(8.dp)) - - //Home-Server - TextField( - value = serverUrl, - onValueChange = { serverUrl = it }, - label = { Text("Server-URL") } - ) - Spacer(modifier = Modifier.height(8.dp)) - - //Username - TextField( - value = username, - onValueChange = { username = it }, - label = { Text("Nutzername") } - ) - Spacer(modifier = Modifier.height(8.dp)) - - //Password - TextField( - value = password, - onValueChange = { password = it }, - label = { Text("Passwort") } - ) - Spacer(modifier = Modifier.height(8.dp)) - - Button(onClick = { if(serverUrl.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()) onLogin( - serverUrl, - username, - password - ) }) { - Text("Login") - } - } -} - -@Composable -fun DashboardScreen( - account: Account, - onBack: () -> Unit, - onDelete: () -> Unit, - sessionState: SessionState, - onValidate: () -> Unit, - onSessionInvalid: () -> Unit) { - - LaunchedEffect(Unit) { onValidate() } - - when (sessionState) { - SessionState.Loading -> { - Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - CircularProgressIndicator() - } - } - SessionState.Valid -> { - BackHandler { - onBack() - } - Column( - modifier = Modifier - .fillMaxSize() - .padding(16.dp) - .statusBarsPadding() - .navigationBarsPadding() - ) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text(text = "Hallo, ${account.name}!", style = MaterialTheme.typography.headlineMedium) - Text(text = "WG: ${account.wgName}", style = MaterialTheme.typography.bodyLarge, color = Color.Gray) - } - Button(onClick = onBack) { - Text("Wechseln") - } - } - - Spacer(modifier = Modifier.height(5.dp)) - - Button(onClick = onDelete) { - Text("Löschen") - } - - Spacer(modifier = Modifier.height(10.dp)) - - Box( - modifier = Modifier - .fillMaxSize() - .background(MaterialTheme.colorScheme.surfaceVariant, shape = MaterialTheme.shapes.medium), - contentAlignment = Alignment.Center - ) { - Text("Hier kommen bald deine WG-Kosten hin 🚀") - } - } - } - SessionState.Invalid -> { - LaunchedEffect(Unit) { - onSessionInvalid() - } - } - is SessionState.Error -> { - Text("Server error") - } - } - } \ No newline at end of file diff --git a/app/src/main/java/de/miaurizius/shap_planner/network/APIService.kt b/app/src/main/java/de/miaurizius/shap_planner/network/APIService.kt index f292955..fdf9ad6 100644 --- a/app/src/main/java/de/miaurizius/shap_planner/network/APIService.kt +++ b/app/src/main/java/de/miaurizius/shap_planner/network/APIService.kt @@ -11,7 +11,7 @@ data class LoginRequest(val username: String, val password: String) data class LoginUser(val id: String, val username: String, val role: String, val avatarUrl: String?) data class LoginResponse(val access_token: String, val refresh_token: String, val user: LoginUser, val wgName: String) -data class RefreshRequest(val refreshToken: String) +data class RefreshRequest(val refresh_token: String) data class RefreshResponse(val accessToken: String, val refreshToken: String) interface APIService { diff --git a/app/src/main/java/de/miaurizius/shap_planner/ui/AppContent.kt b/app/src/main/java/de/miaurizius/shap_planner/ui/AppContent.kt new file mode 100644 index 0000000..686ef5c --- /dev/null +++ b/app/src/main/java/de/miaurizius/shap_planner/ui/AppContent.kt @@ -0,0 +1,42 @@ +package de.miaurizius.shap_planner.ui + +import androidx.compose.runtime.Composable +import de.miaurizius.shap_planner.entities.Account +import de.miaurizius.shap_planner.network.SessionState +import de.miaurizius.shap_planner.ui.screens.AccountSelectionScreen +import de.miaurizius.shap_planner.ui.screens.DashboardScreen +import de.miaurizius.shap_planner.ui.screens.LoginScreen + +@Composable +fun AppContent( + isLoggedIn: Boolean, + accountList: List, + selectedAccount: Account?, + showLoginForNewAccount: Boolean, + onLogin: (String, String, String) -> Unit, + onSelectAccount: (Account) -> Unit, + onLogoutAccount: () -> Unit, + onAddAccountClick: () -> Unit, + onDeleteAccount: () -> Unit, + sessionState: SessionState, + onValidateSession: () -> Unit, + onSessionInvalid: () -> Unit +) { + when { + showLoginForNewAccount -> LoginScreen(onLogin) + accountList.isEmpty() -> LoginScreen(onLogin) + selectedAccount != null -> DashboardScreen( + account = selectedAccount, + onBack = onLogoutAccount, + onDelete = onDeleteAccount, + sessionState = sessionState, + onValidate = onValidateSession, + onSessionInvalid = onSessionInvalid + ) + else -> AccountSelectionScreen( + accounts = accountList, + onAccountClick = onSelectAccount, + onAddAccountClick = onAddAccountClick + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/de/miaurizius/shap_planner/ui/screens/AccountSelectionScreen.kt b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/AccountSelectionScreen.kt new file mode 100644 index 0000000..0d35dda --- /dev/null +++ b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/AccountSelectionScreen.kt @@ -0,0 +1,69 @@ +package de.miaurizius.shap_planner.ui.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import de.miaurizius.shap_planner.entities.Account + +@Composable +fun AccountSelectionScreen(accounts: List, onAccountClick: (Account) -> Unit, onAddAccountClick: () -> Unit) { + LazyColumn( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .statusBarsPadding() + .navigationBarsPadding(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + item { + Text("Wähle einen Account", style = MaterialTheme.typography.headlineSmall) + } + + items(accounts) { account -> + Card(modifier = Modifier.fillMaxWidth().clickable{ onAccountClick(account) }) { + Row(modifier = Modifier.padding(16.dp), verticalAlignment = Alignment.CenterVertically) { + Box(modifier = Modifier.size(40.dp).background(Color.Gray, shape = CircleShape)) + Spacer(modifier = Modifier.width(16.dp)) + Column { + Text(text = account.name, fontWeight = FontWeight.Bold) + Text(text = account.wgName, style = MaterialTheme.typography.bodyMedium) + } + } + } + } + item { + Spacer(modifier = Modifier.height(8.dp)) + Button( + onClick = onAddAccountClick, + modifier = Modifier.fillMaxWidth() + ) { + Text("Anderen Account hinzufügen") + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/miaurizius/shap_planner/ui/screens/DashboardScreen.kt b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/DashboardScreen.kt new file mode 100644 index 0000000..93f4058 --- /dev/null +++ b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/DashboardScreen.kt @@ -0,0 +1,111 @@ +package de.miaurizius.shap_planner.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import de.miaurizius.shap_planner.entities.Account +import de.miaurizius.shap_planner.network.SessionState + +@Composable +fun DashboardScreen( + account: Account, + onBack: () -> Unit, + onDelete: () -> Unit, + sessionState: SessionState, + onValidate: () -> Unit, + onSessionInvalid: () -> Unit) { + + LaunchedEffect(Unit) { onValidate() } + + when (sessionState) { + SessionState.Loading -> { + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + CircularProgressIndicator() + } + } + + SessionState.Valid -> { + BackHandler { + onBack() + } + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .statusBarsPadding() + .navigationBarsPadding() + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = "Hallo, ${account.name}!", + style = MaterialTheme.typography.headlineMedium + ) + Text( + text = "WG: ${account.wgName}", + style = MaterialTheme.typography.bodyLarge, + color = Color.Gray + ) + } + Button(onClick = onBack) { + Text("Wechseln") + } + } + + Spacer(modifier = Modifier.height(5.dp)) + + Button(onClick = onDelete) { + Text("Löschen") + } + + Spacer(modifier = Modifier.height(10.dp)) + + Box( + modifier = Modifier + .fillMaxSize() + .background( + MaterialTheme.colorScheme.surfaceVariant, + shape = MaterialTheme.shapes.medium + ), + contentAlignment = Alignment.Center + ) { + Text("Hier kommen bald deine WG-Kosten hin 🚀") + } + } + } + + SessionState.Invalid -> { + LaunchedEffect(Unit) { + onSessionInvalid() + } + } + + is SessionState.Error -> { + Text("Server error") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/de/miaurizius/shap_planner/ui/screens/LoginScreen.kt b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/LoginScreen.kt new file mode 100644 index 0000000..67b68f4 --- /dev/null +++ b/app/src/main/java/de/miaurizius/shap_planner/ui/screens/LoginScreen.kt @@ -0,0 +1,70 @@ +package de.miaurizius.shap_planner.ui.screens + +import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.statusBarsPadding +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun LoginScreen(onLogin: (String, String, String) -> Unit, onBack: (() -> Unit)? = null) { + + if (onBack != null) { + BackHandler { + onBack() + } + } + + var serverUrl by remember { mutableStateOf("") } + var username by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + + Column(modifier = Modifier.padding(16.dp).statusBarsPadding().navigationBarsPadding()) { + Text("Bitte anmelden") + Spacer(modifier = Modifier.height(8.dp)) + + //Home-Server + TextField( + value = serverUrl, + onValueChange = { serverUrl = it }, + label = { Text("Server-URL") } + ) + Spacer(modifier = Modifier.height(8.dp)) + + //Username + TextField( + value = username, + onValueChange = { username = it }, + label = { Text("Nutzername") } + ) + Spacer(modifier = Modifier.height(8.dp)) + + //Password + TextField( + value = password, + onValueChange = { password = it }, + label = { Text("Passwort") } + ) + Spacer(modifier = Modifier.height(8.dp)) + + Button(onClick = { if(serverUrl.isNotEmpty() && username.isNotEmpty() && password.isNotEmpty()) onLogin( + serverUrl, + username, + password + ) }) { + Text("Login") + } + } +} \ No newline at end of file