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`.
This commit is contained in:
@@ -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 <a href="https://creativecommons.org/publicdomain/zero/1.0/">CC0 1.0</a>
|
||||
@@ -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<Account>, 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Account>,
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<Account>, 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user