diff --git a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt index 5784679fe..1ebfc4ac6 100644 --- a/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt +++ b/server/src/main/kotlin/com/xebia/functional/xef/server/http/routes/AssistantRoutes.kt @@ -14,7 +14,7 @@ import io.ktor.server.request.* import io.ktor.server.response.* import io.ktor.server.routing.* -fun Routing.assistantRoutes(logger: KLogger) { +fun Routing.assistantRoutes(logger: KLogger) { authenticate("auth-bearer") { post("/v1/settings/assistants") { try { @@ -23,7 +23,7 @@ fun Routing.assistantRoutes(logger: KLogger) { val request = call.receive() val assistant = Assistant(request) val response = assistant.get() - logger.info{"Created assistant: ${response.name} with id: ${response.id}"} + logger.info { "Created assistant: ${response.name} with id: ${response.id}" } call.respond(status = HttpStatusCode.Created, response) } else { call.respond( @@ -42,13 +42,12 @@ fun Routing.assistantRoutes(logger: KLogger) { val token = call.getToken() val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants - val response = assistantsApi.listAssistants(configure = { - header("OpenAI-Beta", "assistants=v2") - }) + val response = + assistantsApi.listAssistants(configure = { header("OpenAI-Beta", "assistants=v2") }) call.respond(HttpStatusCode.OK, response) } catch (e: Exception) { val trace = e.stackTraceToString() - logger.error{"Error creating assistant: $trace"} + logger.error { "Error creating assistant: $trace" } call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } @@ -65,7 +64,7 @@ fun Routing.assistantRoutes(logger: KLogger) { } val assistant = Assistant(id) val response = assistant.modify(request).get() - logger.info{"Modified assistant: ${response.name} with id: ${response.id}"} + logger.info { "Modified assistant: ${response.name} with id: ${response.id}" } call.respond(HttpStatusCode.OK, response) } else { call.respond( @@ -75,7 +74,7 @@ fun Routing.assistantRoutes(logger: KLogger) { } } catch (e: Exception) { val trace = e.stackTraceToString() - logger.error{"Error modifying assistant: $trace"} + logger.error { "Error modifying assistant: $trace" } call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } @@ -91,16 +90,15 @@ fun Routing.assistantRoutes(logger: KLogger) { val openAI = OpenAI(Config(token = token.value), logRequests = true) val assistantsApi = openAI.assistants val assistant = assistantsApi.getAssistant(id) - val response = assistantsApi.deleteAssistant(id, configure = { - header("OpenAI-Beta", "assistants=v2") - }) - logger.info{"Deleted assistant: ${assistant.name} with id: ${response.id}"} + val response = + assistantsApi.deleteAssistant(id, configure = { header("OpenAI-Beta", "assistants=v2") }) + logger.info { "Deleted assistant: ${assistant.name} with id: ${response.id}" } call.respond(status = HttpStatusCode.NoContent, response) } catch (e: Exception) { val trace = e.stackTraceToString() - logger.error{"Error deleting assistant: $trace"} + logger.error { "Error deleting assistant: $trace" } call.respond(HttpStatusCode.BadRequest, "Invalid request: $trace") } } } -} \ No newline at end of file +} diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt index cb9e66be7..6278e6be5 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt @@ -7,14 +7,12 @@ import com.server.movile.xef.android.ui.viewmodels.AuthViewModel import com.xef.xefMobile.services.ApiService class MainActivity : ComponentActivity() { - private lateinit var authViewModel: AuthViewModel + private lateinit var authViewModel: AuthViewModel - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - authViewModel = AuthViewModel(this, ApiService()) + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + authViewModel = AuthViewModel(this, ApiService()) - setContent { - XefAndroidApp(authViewModel = authViewModel) - } - } + setContent { XefAndroidApp(authViewModel = authViewModel) } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt index c13cdb7a7..1029e61d5 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt @@ -22,24 +22,24 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel +import com.xef.xefMobile.ui.screens.Screens import kotlinx.coroutines.launch import org.xef.xefMobile.R -import com.xef.xefMobile.ui.screens.Screens @OptIn(ExperimentalMaterial3Api::class) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun MainLayout( - navController: NavController, - authViewModel: IAuthViewModel, - userName: String, - content: @Composable () -> Unit + navController: NavController, + authViewModel: IAuthViewModel, + userName: String, + content: @Composable () -> Unit ) { - val coroutineScope = rememberCoroutineScope() - val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) - val CustomLightBlue = Color(0xFFADD8E6) - val CustomTextBlue = Color(0xFF0199D7) - val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed) + val CustomLightBlue = Color(0xFFADD8E6) + val CustomTextBlue = Color(0xFF0199D7) + val context = LocalContext.current val authToken by authViewModel.authToken.observeAsState() @@ -276,5 +276,181 @@ fun MainLayout( content() } } + HorizontalDivider() + + NavigationDrawerItem( + label = { Text(text = "Home", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.home_24px), + contentDescription = "home", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Home.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Organizations", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.source_environment_24px), + contentDescription = "organizations", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Organizations.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Assistants", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.smart_toy_24px), + contentDescription = "assistants", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Assistants.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Projects", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.school_24px), + contentDescription = "projects", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Projects.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Chat", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.chat_24px), + contentDescription = "chat", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Chat.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Generic question", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.support_agent_24px), + contentDescription = "generic question", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.GenericQuestion.screen) { popUpTo(0) } + } + ) + NavigationDrawerItem( + label = { Text(text = "Settings", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.settings_24px), + contentDescription = "settings", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + navController.navigate(Screens.Settings.screen) { popUpTo(0) } + } + ) + + Spacer(modifier = Modifier.weight(1f)) + NavigationDrawerItem( + label = { Text(text = "Logout", color = CustomTextBlue) }, + selected = false, + icon = { + Image( + painter = painterResource(id = R.drawable.logout_24dp_fill0_wght400_grad0_opsz24), + contentDescription = "logout", + modifier = Modifier.size(24.dp), + colorFilter = ColorFilter.tint(CustomTextBlue) + ) + }, + onClick = { + coroutineScope.launch { drawerState.close() } + authViewModel.logout() + Toast.makeText(context, "Logged out", Toast.LENGTH_SHORT).show() + navController.navigate(Screens.Login.screen) { popUpTo(0) { inclusive = true } } + } + ) + } + }, + ) { + Scaffold( + topBar = { + TopAppBar( + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Image( + painter = painterResource(id = R.drawable.xef_brand_name_white), + contentDescription = "Logo", + modifier = Modifier.size(60.dp) + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = userName, + color = Color.White, + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(end = 16.dp) + ) + } + }, + colors = + TopAppBarDefaults.topAppBarColors( + containerColor = CustomLightBlue, + titleContentColor = Color.White, + navigationIconContentColor = Color.White + ), + navigationIcon = { + IconButton(onClick = { coroutineScope.launch { drawerState.open() } }) { + Icon(Icons.Rounded.Menu, contentDescription = "MenuButton") + } + }, + ) + } + ) { + Box(modifier = Modifier.padding(it)) { content() } } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt index a376757a5..4e1c68d38 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt @@ -23,10 +23,18 @@ import com.xef.xefMobile.ui.screens.SettingsScreen @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun XefAndroidApp(authViewModel: IAuthViewModel) { - val navigationController = rememberNavController() - val userName by authViewModel.userName.observeAsState("") + val navigationController = rememberNavController() + val userName by authViewModel.userName.observeAsState("") - NavHost( + NavHost( + navController = navigationController, + startDestination = Screens.Login.screen, + modifier = Modifier.padding(top = 16.dp) + ) { + composable(Screens.Login.screen) { LoginScreen(authViewModel, navigationController) } + composable(Screens.Register.screen) { RegisterScreen(authViewModel, navigationController) } + composable(Screens.Home.screen) { + MainLayout( navController = navigationController, startDestination = Screens.Login.screen, modifier = Modifier.padding(top = 16.dp) @@ -59,5 +67,24 @@ fun XefAndroidApp(authViewModel: IAuthViewModel) { } // ... other composable screens ... } + composable(Screens.Assistants.screen) { + MainLayout( + navController = navigationController, + authViewModel = authViewModel, + userName = userName.orEmpty() + ) { + AssistantScreen(navigationController, authViewModel) + } + } + composable(Screens.CreateAssistant.screen) { + MainLayout( + navController = navigationController, + authViewModel = authViewModel, + userName = userName.orEmpty() + ) { + CreateAssistantScreen(navigationController) + } + } + // ... other composable screens ... + } } - diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt index 668bb97d4..6a8e331b9 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt @@ -2,13 +2,6 @@ package com.xef.xefMobile.model import kotlinx.serialization.Serializable -@Serializable -data class Assistant( - val id: String, - val name: String -) +@Serializable data class Assistant(val id: String, val name: String) -@Serializable -data class AssistantsResponse( - val data: List -) +@Serializable data class AssistantsResponse(val data: List) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AuthenticationModels.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AuthenticationModels.kt index 3b4a8fcf7..2c5a8b234 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AuthenticationModels.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AuthenticationModels.kt @@ -2,33 +2,12 @@ package com.xef.xefMobile.model import kotlinx.serialization.Serializable -@Serializable -data class RegisterRequest( - val name: String, - val email: String, - val password: String -) +@Serializable data class RegisterRequest(val name: String, val email: String, val password: String) -@Serializable -data class RegisterResponse( +@Serializable data class RegisterResponse(val authToken: String) - val authToken: String -) +@Serializable data class LoginRequest(val email: String, val password: String) -@Serializable -data class LoginRequest( - val email: String, - val password: String -) +@Serializable data class LoginResponse(val authToken: String, val user: UserResponse) -@Serializable -data class LoginResponse( - val authToken: String, - val user: UserResponse -) - -@Serializable -data class UserResponse( - val id: Int, - val name: String -) \ No newline at end of file +@Serializable data class UserResponse(val id: Int, val name: String) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/network/client/HttpClient.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/network/client/HttpClient.kt index 4de2a18d5..abeecabf2 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/network/client/HttpClient.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/network/client/HttpClient.kt @@ -7,13 +7,16 @@ import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json object HttpClientProvider { - val client: HttpClient = HttpClient(Android) { - install(ContentNegotiation) { - json(Json { - ignoreUnknownKeys = true - isLenient = true - prettyPrint = true - }) - } + val client: HttpClient = + HttpClient(Android) { + install(ContentNegotiation) { + json( + Json { + ignoreUnknownKeys = true + isLenient = true + prettyPrint = true + } + ) + } } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt index fa1fb1f0b..ce92f4db2 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt @@ -10,52 +10,56 @@ import io.ktor.http.* class ApiService { - suspend fun registerUser(request: RegisterRequest): RegisterResponse { - return try { - HttpClientProvider.client.post { - url("http://10.0.2.2:8081/register") - contentType(ContentType.Application.Json) - setBody(request) - }.body() - } catch (e: Exception) { - Log.e("ApiService", "Register failed: ${e.message}", e) - throw e + suspend fun registerUser(request: RegisterRequest): RegisterResponse { + return try { + HttpClientProvider.client + .post { + url("http://10.0.2.2:8081/register") + contentType(ContentType.Application.Json) + setBody(request) } + .body() + } catch (e: Exception) { + Log.e("ApiService", "Register failed: ${e.message}", e) + throw e } + } - suspend fun loginUser(request: LoginRequest): LoginResponse { - return try { - val response: HttpResponse = HttpClientProvider.client.post { - url("http://10.0.2.2:8081/login") - contentType(ContentType.Application.Json) - setBody(request) - } - - val responseBody: String = response.bodyAsText() - Log.d("ApiService", "Login response body: $responseBody") - - response.body() - } catch (e: Exception) { - Log.e("ApiService", "Login failed: ${e.message}", e) - throw e + suspend fun loginUser(request: LoginRequest): LoginResponse { + return try { + val response: HttpResponse = + HttpClientProvider.client.post { + url("http://10.0.2.2:8081/login") + contentType(ContentType.Application.Json) + setBody(request) } + + val responseBody: String = response.bodyAsText() + Log.d("ApiService", "Login response body: $responseBody") + + response.body() + } catch (e: Exception) { + Log.e("ApiService", "Login failed: ${e.message}", e) + throw e } + } - suspend fun getAssistants(authToken: String): AssistantsResponse { - return try { - val response: HttpResponse = HttpClientProvider.client.get { - url("http://10.0.2.2:8081/v1/settings/assistants") - header(HttpHeaders.Authorization, "Bearer $authToken") - header("OpenAI-Beta", "assistants=v1") - } - - val responseBody: String = response.bodyAsText() - Log.d("ApiService", "Assistants response body: $responseBody") - - response.body() - } catch (e: Exception) { - Log.e("ApiService", "Fetching assistants failed: ${e.message}", e) - throw e + suspend fun getAssistants(authToken: String): AssistantsResponse { + return try { + val response: HttpResponse = + HttpClientProvider.client.get { + url("http://10.0.2.2:8081/v1/settings/assistants") + header(HttpHeaders.Authorization, "Bearer $authToken") + header("OpenAI-Beta", "assistants=v1") } + + val responseBody: String = response.bodyAsText() + Log.d("ApiService", "Assistants response body: $responseBody") + + response.body() + } catch (e: Exception) { + Log.e("ApiService", "Fetching assistants failed: ${e.message}", e) + throw e } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/UserRepositoryService.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/UserRepositoryService.kt index 9cda93716..53ec7d408 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/UserRepositoryService.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/UserRepositoryService.kt @@ -4,32 +4,41 @@ import com.xef.xefMobile.model.LoginRequest import com.xef.xefMobile.model.LoginResponse import com.xef.xefMobile.model.RegisterRequest import com.xef.xefMobile.model.RegisterResponse +import com.xef.xefMobile.network.client.HttpClientProvider import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json -import com.xef.xefMobile.network.client.HttpClientProvider class UserRepositoryService { - private val client = HttpClientProvider.client - private val baseUrl = "https://api.miservidor.com" - private val json = Json { isLenient = true; ignoreUnknownKeys = true } + private val client = HttpClientProvider.client + private val baseUrl = "https://api.miservidor.com" + private val json = Json { + isLenient = true + ignoreUnknownKeys = true + } - suspend fun register(request: RegisterRequest): RegisterResponse = withContext(Dispatchers.IO) { - client.post { - url("$baseUrl/register") - contentType(ContentType.Application.Json) - setBody(request) - }.body() + suspend fun register(request: RegisterRequest): RegisterResponse = + withContext(Dispatchers.IO) { + client + .post { + url("$baseUrl/register") + contentType(ContentType.Application.Json) + setBody(request) + } + .body() } - suspend fun login(request: LoginRequest): LoginResponse = withContext(Dispatchers.IO) { - client.post { - url("$baseUrl/login") - contentType(ContentType.Application.Json) - setBody(request) - }.body() + suspend fun login(request: LoginRequest): LoginResponse = + withContext(Dispatchers.IO) { + client + .post { + url("$baseUrl/login") + contentType(ContentType.Application.Json) + setBody(request) + } + .body() } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/theme/Theme.android.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/theme/Theme.android.kt index 92bf160bd..0f23712b5 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/theme/Theme.android.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/theme/Theme.android.kt @@ -8,12 +8,12 @@ import androidx.core.view.WindowInsetsControllerCompat @Composable internal fun SystemAppearance(isDark: Boolean) { - val view = LocalView.current - LaunchedEffect(isDark) { - val window = (view.context as Activity).window - WindowInsetsControllerCompat(window, window.decorView).apply { - isAppearanceLightStatusBars = !isDark - isAppearanceLightNavigationBars = !isDark - } + val view = LocalView.current + LaunchedEffect(isDark) { + val window = (view.context as Activity).window + WindowInsetsControllerCompat(window, window.decorView).apply { + isAppearanceLightStatusBars = !isDark + isAppearanceLightNavigationBars = !isDark } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt index da2166d55..24578f0d0 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt @@ -24,44 +24,71 @@ import com.xef.xefMobile.ui.viewmodels.PathViewModel @OptIn(ExperimentalPermissionsApi::class) @Composable fun FilePickerDialog( + onDismissRequest: () -> Unit, + customColors: CustomColors, + onFilesSelected: () -> Unit // Callback for when files are selected onDismissRequest: () -> Unit, customColors: CustomColors, onFilesSelected: () -> Unit ) { - val viewModel: PathViewModel = viewModel() - val state = viewModel.state - val context = LocalContext.current + val viewModel: PathViewModel = viewModel() + val state = viewModel.state + val context = LocalContext.current - val permissionState = rememberPermissionState( - permission = android.Manifest.permission.READ_EXTERNAL_STORAGE - ) + val permissionState = + rememberPermissionState(permission = android.Manifest.permission.READ_EXTERNAL_STORAGE) - var selectedFile by remember { mutableStateOf(null) } + var selectedFile by remember { mutableStateOf(null) } - SideEffect { - if (!permissionState.status.isGranted) { - permissionState.launchPermissionRequest() - } + SideEffect { + if (!permissionState.status.isGranted) { + permissionState.launchPermissionRequest() } + } - val filePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.GetMultipleContents(), - onResult = { uris -> - viewModel.onFilePathsListChange(uris, context) - if (uris.isNotEmpty()) { - onFilesSelected() // Call the callback when files are selected - selectedFile = state.filePaths.firstOrNull() - } + val filePickerLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.GetMultipleContents(), + onResult = { uris -> + viewModel.onFilePathsListChange(uris, context) + if (uris.isNotEmpty()) { + onFilesSelected() // Call the callback when files are selected + selectedFile = state.filePaths.firstOrNull() } + } ) - AlertDialog( - onDismissRequest = onDismissRequest, - title = { - Column(horizontalAlignment = Alignment.CenterHorizontally) { + AlertDialog( + onDismissRequest = onDismissRequest, + title = { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text(text = "Selected Files", fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(8.dp)) + HorizontalDivider() + } + }, + text = { + Column( + modifier = Modifier.fillMaxSize().padding(15.dp), + verticalArrangement = Arrangement.SpaceEvenly, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Box( + modifier = Modifier.fillMaxWidth().fillMaxHeight(0.76f), + contentAlignment = Alignment.Center + ) { + if (state.filePaths.isEmpty()) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + Text(text = "No files selected") + } + } else { + LazyColumn { + items(state.filePaths) { path -> Text( - text = "Selected Files", - fontWeight = FontWeight.Bold + text = path, + modifier = + Modifier.fillMaxWidth().clickable { selectedFile = path }.padding(8.dp), + color = if (selectedFile == path) Color.Blue else Color.Unspecified ) Spacer(modifier = Modifier.height(8.dp)) Divider() @@ -147,6 +174,52 @@ fun FilePickerDialog( ) { Text("Done") } + } } - ) + OutlinedButton( + onClick = { + if (permissionState.status.isGranted) { + filePickerLauncher.launch("*/*") + } else { + permissionState.launchPermissionRequest() + } + }, + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = Color.Transparent, + contentColor = customColors.buttonColor + ) + ) { + Text(text = "Browse files") + } + if (selectedFile != null) { + OutlinedButton( + onClick = { + viewModel.removeFilePath(selectedFile!!) + selectedFile = null + }, + colors = + ButtonDefaults.outlinedButtonColors( + containerColor = Color.Transparent, + contentColor = customColors.buttonColor + ) + ) { + Text(text = "Remove") + } + } + } + }, + confirmButton = { + Button( + onClick = { onDismissRequest() }, + colors = + ButtonDefaults.buttonColors( + containerColor = customColors.buttonColor, + contentColor = MaterialTheme.colorScheme.onPrimary + ) + ) { + Text("Done") + } + } + ) } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt index 2c59cf416..d6eba6bda 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt @@ -4,7 +4,6 @@ import android.content.ContentUris import android.content.Context import android.database.Cursor import android.net.Uri -import android.os.Build import android.os.Environment import android.provider.DocumentsContract import android.provider.MediaStore @@ -12,92 +11,96 @@ import java.lang.NumberFormatException class UriPathFinder { - fun getPath(context: Context, uri: Uri): String? { - return when { - DocumentsContract.isDocumentUri(context, uri) -> { - when { - isExternalStorageDocument(uri) -> handleExternalStorageDocument(uri) - isDownloadsDocument(uri) -> handleDownloadsDocument(context, uri) - isMediaDocument(uri) -> handleMediaDocument(context, uri) - else -> null - } - } - "content".equals(uri.scheme, ignoreCase = true) -> getDataColumn(context, uri, null, null) - "file".equals(uri.scheme, ignoreCase = true) -> uri.path - else -> null + fun getPath(context: Context, uri: Uri): String? { + return when { + DocumentsContract.isDocumentUri(context, uri) -> { + when { + isExternalStorageDocument(uri) -> handleExternalStorageDocument(uri) + isDownloadsDocument(uri) -> handleDownloadsDocument(context, uri) + isMediaDocument(uri) -> handleMediaDocument(context, uri) + else -> null } + } + "content".equals(uri.scheme, ignoreCase = true) -> getDataColumn(context, uri, null, null) + "file".equals(uri.scheme, ignoreCase = true) -> uri.path + else -> null } + } - private fun handleExternalStorageDocument(uri: Uri): String? { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":").toTypedArray() - val type = split[0] - return if ("primary".equals(type, ignoreCase = true)) { - Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } else { - // Handle non-primary volumes (e.g., "content://com.android.externalstorage.documents/document/primary:...") - val storageDefinition = System.getenv("SECONDARY_STORAGE")?.split(":") - storageDefinition?.find { it.contains(type) }?.let { "$it/${split[1]}" } - } + private fun handleExternalStorageDocument(uri: Uri): String? { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + return if ("primary".equals(type, ignoreCase = true)) { + Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } else { + // Handle non-primary volumes (e.g., + // "content://com.android.externalstorage.documents/document/primary:...") + val storageDefinition = System.getenv("SECONDARY_STORAGE")?.split(":") + storageDefinition?.find { it.contains(type) }?.let { "$it/${split[1]}" } } + } - private fun handleDownloadsDocument(context: Context, uri: Uri): String? { - val id = DocumentsContract.getDocumentId(uri) - return try { - val contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), - java.lang.Long.valueOf(id) - ) - getDataColumn(context, contentUri, null, null) - } catch (e: NumberFormatException) { - // Handle the case where the id is not a pure number - null - } + private fun handleDownloadsDocument(context: Context, uri: Uri): String? { + val id = DocumentsContract.getDocumentId(uri) + return try { + val contentUri = + ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + getDataColumn(context, contentUri, null, null) + } catch (e: NumberFormatException) { + // Handle the case where the id is not a pure number + null } + } - private fun handleMediaDocument(context: Context, uri: Uri): String? { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":").toTypedArray() - val type = split[0] - val contentUri: Uri? = when (type) { - "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> null - } - val selection = "_id=?" - val selectionArgs = arrayOf(split[1]) - return getDataColumn(context, contentUri, selection, selectionArgs) - } + private fun handleMediaDocument(context: Context, uri: Uri): String? { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + val contentUri: Uri? = + when (type) { + "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI + "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI + "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + else -> null + } + val selection = "_id=?" + val selectionArgs = arrayOf(split[1]) + return getDataColumn(context, contentUri, selection, selectionArgs) + } - private fun getDataColumn( - context: Context, - uri: Uri?, - selection: String?, - selectionArgs: Array? - ): String? { - val cursor: Cursor? = uri?.let { - context.contentResolver.query(it, arrayOf("_data"), selection, selectionArgs, null) - } - return cursor?.use { - if (it.moveToFirst()) { - val columnIndex: Int = it.getColumnIndexOrThrow("_data") - it.getString(columnIndex) - } else { - null - } - } + private fun getDataColumn( + context: Context, + uri: Uri?, + selection: String?, + selectionArgs: Array? + ): String? { + val cursor: Cursor? = + uri?.let { + context.contentResolver.query(it, arrayOf("_data"), selection, selectionArgs, null) + } + return cursor?.use { + if (it.moveToFirst()) { + val columnIndex: Int = it.getColumnIndexOrThrow("_data") + it.getString(columnIndex) + } else { + null + } } + } - private fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents" == uri.authority - } + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } - private fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents" == uri.authority - } + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } - private fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents" == uri.authority - } + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt index d54a113de..ef4b14032 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt @@ -13,19 +13,19 @@ import com.xef.xefMobile.ui.screens.Screens @Composable fun AppNavigator(authViewModel: IAuthViewModel) { - val navController = rememberNavController() - NavHost(navController = navController, startDestination = Screens.Login.screen) { - composable(Screens.Login.screen) { - LoginScreen(authViewModel = authViewModel, navController = navController) - } - composable(Screens.Register.screen) { - RegisterScreen(authViewModel = authViewModel, navController = navController) - } - composable(Screens.Assistants.screen) { - AssistantScreen(navController = navController, authViewModel = authViewModel) - } - composable(Screens.CreateAssistant.screen) { - CreateAssistantScreen(navController = navController) - } + val navController = rememberNavController() + NavHost(navController = navController, startDestination = Screens.Login.screen) { + composable(Screens.Login.screen) { + LoginScreen(authViewModel = authViewModel, navController = navController) } + composable(Screens.Register.screen) { + RegisterScreen(authViewModel = authViewModel, navController = navController) + } + composable(Screens.Assistants.screen) { + AssistantScreen(navController = navController, authViewModel = authViewModel) + } + composable(Screens.CreateAssistant.screen) { + CreateAssistantScreen(navController = navController) + } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/FilePicker/PathScreenState.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/FilePicker/PathScreenState.kt index 18d10e543..d5ecc72fa 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/FilePicker/PathScreenState.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/FilePicker/PathScreenState.kt @@ -1,6 +1,3 @@ package com.xef.xefMobile.ui.screens.FilePicker -data class PathScreenState( - val filePaths:List = emptyList() - -) +data class PathScreenState(val filePaths: List = emptyList()) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt index 11b567156..af9fd6fbb 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.* -import androidx.navigation.NavController import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState import androidx.compose.ui.Alignment @@ -16,95 +15,94 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel import com.xef.xefMobile.ui.screens.Screens @OptIn(ExperimentalMaterial3Api::class) @Composable fun LoginScreen(authViewModel: IAuthViewModel, navController: NavController) { - val authToken by authViewModel.authToken.observeAsState() - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var errorMessage by remember { mutableStateOf(null) } + val authToken by authViewModel.authToken.observeAsState() + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var errorMessage by remember { mutableStateOf(null) } - LaunchedEffect(authToken) { - if (authToken != null) { - Log.d("LoginScreen", "Navigation to Start Screen") - navController.navigate(Screens.Home.screen) { - popUpTo(Screens.Login.screen) { inclusive = true } - } - } + LaunchedEffect(authToken) { + if (authToken != null) { + Log.d("LoginScreen", "Navigation to Start Screen") + navController.navigate(Screens.Home.screen) { + popUpTo(Screens.Login.screen) { inclusive = true } + } } + } - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Xef Server", - fontSize = 24.sp, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) + Column( + modifier = Modifier.fillMaxSize().background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Xef Server", + fontSize = 24.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) - if (errorMessage != null) { - Text(text = errorMessage!!, color = Color.Red, textAlign = TextAlign.Center) - Spacer(modifier = Modifier.height(8.dp)) - } + if (errorMessage != null) { + Text(text = errorMessage!!, color = Color.Red, textAlign = TextAlign.Center) + Spacer(modifier = Modifier.height(8.dp)) + } - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) - Spacer(modifier = Modifier.height(16.dp)) - Button( - onClick = { - when { - email.isBlank() -> { - errorMessage = "Email field is empty" - Log.d("LoginScreen", "Email field is empty") - } - password.isBlank() -> { - errorMessage = "Password field is empty" - Log.d("LoginScreen", "Password field is empty") - } - else -> { - errorMessage = null - authViewModel.login(email = email, password = password) - Log.d("LoginScreen", "Login button pressed") - } - } - }, - modifier = Modifier.width(200.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF009688)) - ) { - Text("Login") - } - Spacer(modifier = Modifier.height(8.dp)) - TextButton( - onClick = { - navController.navigate(Screens.Register.screen) - Log.d("LoginScreen", "Navigate to Register Screen") - }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Create An Account") + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + Spacer(modifier = Modifier.height(16.dp)) + Button( + onClick = { + when { + email.isBlank() -> { + errorMessage = "Email field is empty" + Log.d("LoginScreen", "Email field is empty") + } + password.isBlank() -> { + errorMessage = "Password field is empty" + Log.d("LoginScreen", "Password field is empty") + } + else -> { + errorMessage = null + authViewModel.login(email = email, password = password) + Log.d("LoginScreen", "Login button pressed") + } } + }, + modifier = Modifier.width(200.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF009688)) + ) { + Text("Login") + } + Spacer(modifier = Modifier.height(8.dp)) + TextButton( + onClick = { + navController.navigate(Screens.Register.screen) + Log.d("LoginScreen", "Navigate to Register Screen") + }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Create An Account") } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt index a84df0346..d03b674c2 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt @@ -3,7 +3,6 @@ package com.server.movile.xef.android.ui.screens import androidx.compose.foundation.background import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.KeyboardOptions -import androidx.navigation.NavController import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.livedata.observeAsState @@ -15,111 +14,111 @@ import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.navigation.NavController import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel import com.xef.xefMobile.ui.screens.Screens @OptIn(ExperimentalMaterial3Api::class) @Composable fun RegisterScreen(authViewModel: IAuthViewModel, navController: NavController) { - var errorMessage by remember { mutableStateOf(null) } - val authToken by authViewModel.authToken.observeAsState() + var errorMessage by remember { mutableStateOf(null) } + val authToken by authViewModel.authToken.observeAsState() - LaunchedEffect(authToken) { - if (authToken != null) { - navController.navigate(Screens.Login.screen) { - popUpTo(Screens.Register.screen) { inclusive = true } - } - } + LaunchedEffect(authToken) { + if (authToken != null) { + navController.navigate(Screens.Login.screen) { + popUpTo(Screens.Register.screen) { inclusive = true } + } } + } - Column( - modifier = Modifier - .fillMaxSize() - .background(Color.White), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Text( - text = "Xef Server", - fontSize = 30.sp, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(8.dp)) - Text( - text = "Create an account", - fontSize = 24.sp, - textAlign = TextAlign.Center, - modifier = Modifier.fillMaxWidth() - ) - Spacer(modifier = Modifier.height(16.dp)) - var name by remember { mutableStateOf("") } - var email by remember { mutableStateOf("") } - var password by remember { mutableStateOf("") } - var rePassword by remember { mutableStateOf("") } + Column( + modifier = Modifier.fillMaxSize().background(Color.White), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text( + text = "Xef Server", + fontSize = 30.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Create an account", + fontSize = 24.sp, + textAlign = TextAlign.Center, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(16.dp)) + var name by remember { mutableStateOf("") } + var email by remember { mutableStateOf("") } + var password by remember { mutableStateOf("") } + var rePassword by remember { mutableStateOf("") } - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text("Name") }, - singleLine = true - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = email, - onValueChange = { email = it }, - label = { Text("Email") }, - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = password, - onValueChange = { password = it }, - label = { Text("Password") }, - visualTransformation = PasswordVisualTransformation(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) - Spacer(modifier = Modifier.height(8.dp)) - OutlinedTextField( - value = rePassword, - onValueChange = { rePassword = it }, - label = { Text("Re-Password") }, - visualTransformation = PasswordVisualTransformation(), - singleLine = true, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) - ) - Spacer(modifier = Modifier.height(16.dp)) - errorMessage?.let { - Text(text = it, color = Color.Red, style = MaterialTheme.typography.bodyLarge) - Spacer(modifier = Modifier.height(8.dp)) - } + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + singleLine = true + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = email, + onValueChange = { email = it }, + label = { Text("Email") }, + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email) + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = password, + onValueChange = { password = it }, + label = { Text("Password") }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + Spacer(modifier = Modifier.height(8.dp)) + OutlinedTextField( + value = rePassword, + onValueChange = { rePassword = it }, + label = { Text("Re-Password") }, + visualTransformation = PasswordVisualTransformation(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password) + ) + Spacer(modifier = Modifier.height(16.dp)) + errorMessage?.let { + Text(text = it, color = Color.Red, style = MaterialTheme.typography.bodyLarge) + Spacer(modifier = Modifier.height(8.dp)) + } - Button( - onClick = { - errorMessage = when { - name.isBlank() -> "Name is empty" - email.isBlank() -> "Email is empty" - password.isEmpty() -> "Password is empty" - password != rePassword -> "Passwords do not match" - else -> null - } - if (errorMessage == null) { - authViewModel.register(name, email, password) - } - }, - modifier = Modifier.width(200.dp), - colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF009688)) - ) { - Text("Create Account") - } - Spacer(modifier = Modifier.height(8.dp)) - TextButton( - onClick = { navController.navigate(Screens.Login.screen) }, - modifier = Modifier.fillMaxWidth() - ) { - Text("Back") + Button( + onClick = { + errorMessage = + when { + name.isBlank() -> "Name is empty" + email.isBlank() -> "Email is empty" + password.isEmpty() -> "Password is empty" + password != rePassword -> "Passwords do not match" + else -> null + } + if (errorMessage == null) { + authViewModel.register(name, email, password) } + }, + modifier = Modifier.width(200.dp), + colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF009688)) + ) { + Text("Create Account") + } + Spacer(modifier = Modifier.height(8.dp)) + TextButton( + onClick = { navController.navigate(Screens.Login.screen) }, + modifier = Modifier.fillMaxWidth() + ) { + Text("Back") } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt index d64384395..351e12c1a 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt @@ -1,15 +1,25 @@ package com.xef.xefMobile.ui.screens sealed class Screens(val screen: String) { - object Login : Screens("loginScreen") - object Register : Screens("registerScreen") - object Start : Screens("startScreen") - object Home : Screens("homeScreen") - object Organizations : Screens("organizationsScreen") - object Assistants : Screens("assistantsScreen") - object Projects : Screens("projectsScreen") - object Chat : Screens("chatScreen") - object GenericQuestion : Screens("genericQuestionScreen") - object Settings : Screens("settingsScreen") - object CreateAssistant : Screens("createAssistantScreen") + object Login : Screens("loginScreen") + + object Register : Screens("registerScreen") + + object Start : Screens("startScreen") + + object Home : Screens("homeScreen") + + object Organizations : Screens("organizationsScreen") + + object Assistants : Screens("assistantsScreen") + + object Projects : Screens("projectsScreen") + + object Chat : Screens("chatScreen") + + object GenericQuestion : Screens("genericQuestionScreen") + + object Settings : Screens("settingsScreen") + + object CreateAssistant : Screens("createAssistantScreen") } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt index 22ff013c9..778d9ac73 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt @@ -6,12 +6,9 @@ import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.navigation.NavController -import androidx.navigation.compose.rememberNavController -import com.server.movile.xef.android.ui.viewmodels.AuthViewModel import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel import com.xef.xefMobile.model.Assistant import com.xef.xefMobile.services.ApiService @@ -21,60 +18,52 @@ import kotlinx.coroutines.launch @Composable fun AssistantScreen(navController: NavController, authViewModel: IAuthViewModel) { - val customColors = LocalCustomColors.current - val coroutineScope = rememberCoroutineScope() - var assistants by remember { mutableStateOf>(emptyList()) } - var loading by remember { mutableStateOf(true) } - var errorMessage by remember { mutableStateOf(null) } + val customColors = LocalCustomColors.current + val coroutineScope = rememberCoroutineScope() + var assistants by remember { mutableStateOf>(emptyList()) } + var loading by remember { mutableStateOf(true) } + var errorMessage by remember { mutableStateOf(null) } - val authToken = authViewModel.authToken.value ?: error("Auth token not found") + val authToken = authViewModel.authToken.value ?: error("Auth token not found") - LaunchedEffect(Unit) { - coroutineScope.launch { - try { - val response = ApiService().getAssistants(authToken) - assistants = response.data - } catch (e: Exception) { - errorMessage = "Failed to load assistants" - } finally { - loading = false - } - } + LaunchedEffect(Unit) { + coroutineScope.launch { + try { + val response = ApiService().getAssistants(authToken) + assistants = response.data + } catch (e: Exception) { + errorMessage = "Failed to load assistants" + } finally { + loading = false + } } + } - Box(modifier = Modifier.fillMaxSize()) { - if (loading) { - CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) - } else if (errorMessage != null) { - Text( - text = errorMessage!!, - color = MaterialTheme.colorScheme.error, - modifier = Modifier.align(Alignment.Center) - ) - } else { - Column( - modifier = Modifier - .align(Alignment.TopCenter) - .padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally - ) { - Text( - text = "Assistants", - fontWeight = FontWeight.Bold, - fontSize = 24.sp, - ) - HorizontalDivider( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp) - ) + Box(modifier = Modifier.fillMaxSize()) { + if (loading) { + CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) + } else if (errorMessage != null) { + Text( + text = errorMessage!!, + color = MaterialTheme.colorScheme.error, + modifier = Modifier.align(Alignment.Center) + ) + } else { + Column( + modifier = Modifier.align(Alignment.TopCenter).padding(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text( + text = "Assistants", + fontWeight = FontWeight.Bold, + fontSize = 24.sp, + ) + HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 8.dp)) - Spacer(modifier = Modifier.height(16.dp)) + Spacer(modifier = Modifier.height(16.dp)) - assistants.forEach { assistant -> - Text( - text = assistant.name, - fontWeight = FontWeight.Bold + assistants.forEach { assistant -> + Text(text = assistant.name, fontWeight = FontWeight.Bold) ) Text(text = assistant.id) @@ -95,5 +84,19 @@ fun AssistantScreen(navController: NavController, authViewModel: IAuthViewModel) ) { Text(text = "Create New Assistant") } + } + + Button( + onClick = { navController.navigate(Screens.CreateAssistant.screen) }, + colors = + ButtonDefaults.buttonColors( + containerColor = customColors.buttonColor, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp) + ) { + Text(text = "Create New Assistant") + } } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt index c782e31d7..3fa941015 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt @@ -27,6 +27,7 @@ class CreateAssistantActivity : ComponentActivity() { CreateAssistantScreen(navController) } } + } } @OptIn(ExperimentalMaterial3Api::class) @@ -44,14 +45,37 @@ fun CreateAssistantScreen(navController: NavController) { var selectedText by remember { mutableStateOf(list[0]) } var showFilePicker by remember { mutableStateOf(false) } - val customColors = LocalCustomColors.current + val customColors = LocalCustomColors.current - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .padding(8.dp) - .fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.padding(8.dp).fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Text(text = "Create Assistant", fontSize = 24.sp, modifier = Modifier.padding(bottom = 16.dp)) + + TextField( + value = name, + onValueChange = { name = it }, + label = { Text("Name") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + TextField( + value = instructions, + onValueChange = { instructions = it }, + label = { Text("Instructions") }, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.height(8.dp)) + Column( + modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + ExposedDropdownMenuBox( + expanded = isExpanded, + onExpandedChange = { isExpanded = !isExpanded }, + modifier = Modifier.fillMaxWidth() ) { Text( text = "Create Assistant", @@ -213,6 +237,7 @@ fun CreateAssistantScreen(navController: NavController) { Text("Create") } } + } } if (showFilePicker) { FilePickerDialog( @@ -225,50 +250,46 @@ fun CreateAssistantScreen(navController: NavController) { ) } } + } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun AssistantFloatField(label: String, value: Float, onValueChange: (Float) -> Unit) { - val customColors = LocalCustomColors.current - Column( - modifier = Modifier - .fillMaxWidth() - ) { - Text( - text = label, - modifier = Modifier.padding(bottom = 2.dp) // Reduce padding for the label - ) - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Slider( - value = value, - onValueChange = onValueChange, - valueRange = 0f..2f, - steps = 100, // This ensures the slider moves in increments of 0.02 - modifier = Modifier.weight(3f), - colors = SliderDefaults.colors( - thumbColor = customColors.sliderThumbColor, - activeTrackColor = customColors.sliderTrackColor - ) - ) - Spacer(modifier = Modifier.width(2.dp)) // Add a small spacer between the slider and text field - TextField( - value = String.format("%.2f", value), - onValueChange = { - val newValue = it.toFloatOrNull() ?: 0f - onValueChange(newValue) - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - modifier = Modifier - .width(60.dp) - .height(50.dp), - textStyle = LocalTextStyle.current.copy(fontSize = 12.sp) // Optionally adjust text size - ) - } + val customColors = LocalCustomColors.current + Column(modifier = Modifier.fillMaxWidth()) { + Text( + text = label, + modifier = Modifier.padding(bottom = 2.dp) // Reduce padding for the label + ) + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) { + Slider( + value = value, + onValueChange = onValueChange, + valueRange = 0f..2f, + steps = 100, // This ensures the slider moves in increments of 0.02 + modifier = Modifier.weight(3f), + colors = + SliderDefaults.colors( + thumbColor = customColors.sliderThumbColor, + activeTrackColor = customColors.sliderTrackColor + ) + ) + Spacer( + modifier = Modifier.width(2.dp) + ) // Add a small spacer between the slider and text field + TextField( + value = String.format("%.2f", value), + onValueChange = { + val newValue = it.toFloatOrNull() ?: 0f + onValueChange(newValue) + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + modifier = Modifier.width(60.dp).height(50.dp), + textStyle = LocalTextStyle.current.copy(fontSize = 12.sp) // Optionally adjust text size + ) } + } } @Preview(showBackground = false) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/HomeScreen.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/HomeScreen.kt index b4be5b21f..41b03b1fd 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/HomeScreen.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/HomeScreen.kt @@ -2,6 +2,7 @@ package com.server.movile.xef.android.ui.screens.navigationdrawercompose import androidx.compose.foundation.Image import androidx.compose.foundation.layout.* +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -9,32 +10,29 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.compose.material3.MaterialTheme import androidx.navigation.NavController import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel import org.xef.xefMobile.R @Composable fun HomeScreen(authViewModel: IAuthViewModel, navController: NavController) { - Box(modifier = Modifier.fillMaxSize()) { - Column( - modifier = Modifier - .fillMaxSize() - .align(Alignment.Center), - verticalArrangement = Arrangement.Center, - horizontalAlignment = Alignment.CenterHorizontally - ) { - Image( - painter = painterResource(id = R.drawable.xef_brand_icon), - contentDescription = "Logo", - modifier = Modifier.size(50.dp) - ) - Spacer(modifier = Modifier.height(16.dp)) - Text( - text = "Welcome to Xef.ai", - fontSize = 30.sp, - color = MaterialTheme.colorScheme.onBackground - ) - } + Box(modifier = Modifier.fillMaxSize()) { + Column( + modifier = Modifier.fillMaxSize().align(Alignment.Center), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally + ) { + Image( + painter = painterResource(id = R.drawable.xef_brand_icon), + contentDescription = "Logo", + modifier = Modifier.size(50.dp) + ) + Spacer(modifier = Modifier.height(16.dp)) + Text( + text = "Welcome to Xef.ai", + fontSize = 30.sp, + color = MaterialTheme.colorScheme.onBackground + ) } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/MenuItem.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/MenuItem.kt index c04e940eb..61df520b8 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/MenuItem.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/MenuItem.kt @@ -3,8 +3,8 @@ package com.server.movile.xef.android.ui.screens.navigationdrawercompose import androidx.compose.ui.graphics.vector.ImageVector data class MenuItem( - val id: String, - val title: String, - val contentDescription: String, - val icon: ImageVector -) \ No newline at end of file + val id: String, + val title: String, + val contentDescription: String, + val icon: ImageVector +) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Theme.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Theme.kt index fc1f48a17..290670744 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Theme.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Theme.kt @@ -10,7 +10,8 @@ val CustomButtonColor = Color(0xFF01A2D1) val CustomSliderThumbColor = Color(0xFF03DAC5) val CustomSliderTrackColor = Color(0xFF018786) -private val LightColorScheme = lightColorScheme( +private val LightColorScheme = + lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, primaryContainer = md_theme_light_primaryContainer, @@ -40,9 +41,10 @@ private val LightColorScheme = lightColorScheme( surfaceTint = md_theme_light_surfaceTint, outlineVariant = md_theme_light_outlineVariant, scrim = md_theme_light_scrim, -) + ) -private val DarkColorScheme = darkColorScheme( +private val DarkColorScheme = + darkColorScheme( primary = md_theme_dark_primary, onPrimary = md_theme_dark_onPrimary, primaryContainer = md_theme_dark_primaryContainer, @@ -72,33 +74,31 @@ private val DarkColorScheme = darkColorScheme( surfaceTint = md_theme_dark_surfaceTint, outlineVariant = md_theme_dark_outlineVariant, scrim = md_theme_dark_scrim, -) + ) internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } val LocalCustomColors = staticCompositionLocalOf { CustomColors() } data class CustomColors( - val buttonColor: Color = CustomButtonColor, - val sliderThumbColor: Color = CustomSliderThumbColor, - val sliderTrackColor: Color = CustomSliderTrackColor + val buttonColor: Color = CustomButtonColor, + val sliderThumbColor: Color = CustomSliderThumbColor, + val sliderTrackColor: Color = CustomSliderTrackColor ) @Composable -internal fun AppTheme( - content: @Composable () -> Unit -) { - val systemIsDark = isSystemInDarkTheme() - val isDarkState = remember { mutableStateOf(systemIsDark) } - CompositionLocalProvider( - LocalThemeIsDark provides isDarkState, - LocalCustomColors provides CustomColors() - ) { - val isDark by isDarkState - //com.xef.xefMobile.theme.SystemAppearance(isDark) - MaterialTheme( - colorScheme = if (isDark) DarkColorScheme else LightColorScheme, - content = { Surface(content = content) } - ) - } -} \ No newline at end of file +internal fun AppTheme(content: @Composable () -> Unit) { + val systemIsDark = isSystemInDarkTheme() + val isDarkState = remember { mutableStateOf(systemIsDark) } + CompositionLocalProvider( + LocalThemeIsDark provides isDarkState, + LocalCustomColors provides CustomColors() + ) { + val isDark by isDarkState + // com.xef.xefMobile.theme.SystemAppearance(isDark) + MaterialTheme( + colorScheme = if (isDark) DarkColorScheme else LightColorScheme, + content = { Surface(content = content) } + ) + } +} diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Type.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Type.kt index 637bb1a72..03132d758 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Type.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Type.kt @@ -8,21 +8,12 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp import org.xef.xefMobile.R +val Montserrat = + FontFamily(Font(R.font.montserrat_regular), Font(R.font.montserrat_bold, FontWeight.Bold)) -val Montserrat = FontFamily( - Font(R.font.montserrat_regular), - Font(R.font.montserrat_bold, FontWeight.Bold) -) - -val Typography = Typography( - displayLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Bold, - fontSize = 24.sp - ), - bodyLarge = TextStyle( - fontFamily = Montserrat, - fontWeight = FontWeight.Bold, - fontSize = 15.sp - ) -) +val Typography = + Typography( + displayLarge = + TextStyle(fontFamily = Montserrat, fontWeight = FontWeight.Bold, fontSize = 24.sp), + bodyLarge = TextStyle(fontFamily = Montserrat, fontWeight = FontWeight.Bold, fontSize = 15.sp) + ) diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt index 67684fa1a..d3cd4c9e1 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt @@ -1,7 +1,6 @@ package com.server.movile.xef.android.ui.viewmodels import android.content.Context -import android.util.Log import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore @@ -11,40 +10,50 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.xef.xefMobile.model.LoginRequest import com.xef.xefMobile.model.RegisterRequest +import com.xef.xefMobile.services.ApiService +import java.io.IOException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.firstOrNull import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import com.xef.xefMobile.services.ApiService import retrofit2.HttpException -import java.io.IOException // Extension function to provide DataStore instance private val Context.dataStore by preferencesDataStore(name = "settings") -class AuthViewModel( - context: Context, - private val apiService: ApiService -) : ViewModel(), IAuthViewModel { +class AuthViewModel(context: Context, private val apiService: ApiService) : + ViewModel(), IAuthViewModel { - private val dataStore = context.dataStore + private val dataStore = context.dataStore - private val _authToken = MutableLiveData() - override val authToken: LiveData = _authToken + private val _authToken = MutableLiveData() + override val authToken: LiveData = _authToken - private val _isLoading = MutableLiveData() - override val isLoading: LiveData = _isLoading + private val _isLoading = MutableLiveData() + override val isLoading: LiveData = _isLoading - private val _errorMessage = MutableLiveData() - override val errorMessage: LiveData = _errorMessage + private val _errorMessage = MutableLiveData() + override val errorMessage: LiveData = _errorMessage - private val _userName = MutableLiveData() - override val userName: LiveData = _userName + private val _userName = MutableLiveData() + override val userName: LiveData = _userName - init { - loadAuthToken() + init { + loadAuthToken() + } + + private fun loadAuthToken() { + viewModelScope.launch { + val token = + dataStore.data + .map { preferences -> preferences[stringPreferencesKey("authToken")] } + .firstOrNull() + _authToken.value = token + + token?.let { loadUserName() } } + } private fun loadAuthToken() { viewModelScope.launch { @@ -58,8 +67,67 @@ class AuthViewModel( } } } + } + + override fun login(email: String, password: String) { + viewModelScope.launch { + _isLoading.value = true + val loginRequest = LoginRequest(email, password) + try { + val loginResponse = apiService.loginUser(loginRequest) + updateAuthToken(loginResponse.authToken) + updateUserName(loginResponse.user.name) // Extract user's name + _authToken.value = loginResponse.authToken + _userName.value = loginResponse.user.name + } catch (e: Exception) { + handleException(e) + } finally { + _isLoading.value = false + } + } + } + + private suspend fun updateAuthToken(token: String) { + withContext(Dispatchers.IO) { + dataStore.edit { preferences -> preferences[stringPreferencesKey("authToken")] = token } + } + } + + private suspend fun updateUserName(name: String) { + withContext(Dispatchers.IO) { + dataStore.edit { preferences -> preferences[stringPreferencesKey("userName")] = name } + } + } + + override fun register(name: String, email: String, password: String) { + viewModelScope.launch { + _isLoading.value = true + val request = RegisterRequest(name, email, password) + try { + val registerResponse = apiService.registerUser(request) + updateAuthToken(registerResponse.authToken) + updateUserName(name) // Directly use the name provided during registration + _authToken.value = registerResponse.authToken + _userName.value = name + } catch (e: Exception) { + handleException(e) + } finally { + _isLoading.value = false + } + } + } + + private fun handleException(e: Exception) { + when (e) { + is IOException -> _errorMessage.postValue("Network error") + is HttpException -> _errorMessage.postValue("Unexpected server error: ${e.code()}") + else -> _errorMessage.postValue("An unexpected error occurred: ${e.message}") + } + } - private suspend fun loadUserName() { + override fun logout() { + viewModelScope.launch { + try { withContext(Dispatchers.IO) { val name = dataStore.data.map { preferences -> preferences[stringPreferencesKey("userName")] @@ -144,5 +212,12 @@ class AuthViewModel( _errorMessage.postValue("Failed to sign out") } } + _authToken.postValue(null) + _userName.postValue(null) // Add this line + _errorMessage.postValue("Logged out successfully") + } catch (e: Exception) { + _errorMessage.postValue("Failed to sign out") + } } + } } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt index ba203733a..5cc84e2e5 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt @@ -3,12 +3,14 @@ package com.server.movile.xef.android.ui.viewmodels import androidx.lifecycle.LiveData interface IAuthViewModel { - val authToken: LiveData - val isLoading: LiveData - val errorMessage: LiveData - val userName: LiveData - - fun login(email: String, password: String) - fun register(name: String, email: String, password: String) - fun logout() + val authToken: LiveData + val isLoading: LiveData + val errorMessage: LiveData + val userName: LiveData + + fun login(email: String, password: String) + + fun register(name: String, email: String, password: String) + + fun logout() } diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt index 94328f41a..53fc6d469 100644 --- a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt +++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt @@ -13,31 +13,29 @@ import kotlinx.coroutines.launch class PathViewModel : ViewModel() { - var state by mutableStateOf(PathScreenState()) - private set + var state by mutableStateOf(PathScreenState()) + private set - private val uriPathFinder = UriPathFinder() + private val uriPathFinder = UriPathFinder() - fun onFilePathsListChange(list: List, context: Context) { - viewModelScope.launch { - val updatedList = state.filePaths.toMutableList() - val pathList = changeUriToPath(list, context) - updatedList += pathList - state = state.copy(filePaths = updatedList) - } + fun onFilePathsListChange(list: List, context: Context) { + viewModelScope.launch { + val updatedList = state.filePaths.toMutableList() + val pathList = changeUriToPath(list, context) + updatedList += pathList + state = state.copy(filePaths = updatedList) } + } - fun removeFilePath(path: String) { - viewModelScope.launch { - val updatedList = state.filePaths.toMutableList() - updatedList.remove(path) - state = state.copy(filePaths = updatedList) - } + fun removeFilePath(path: String) { + viewModelScope.launch { + val updatedList = state.filePaths.toMutableList() + updatedList.remove(path) + state = state.copy(filePaths = updatedList) } + } - private fun changeUriToPath(uris: List, context: Context): List { - return uris.mapNotNull { uri -> - uriPathFinder.getPath(context, uri) - } - } + private fun changeUriToPath(uris: List, context: Context): List { + return uris.mapNotNull { uri -> uriPathFinder.getPath(context, uri) } + } } diff --git a/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Theme.kt b/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Theme.kt index f16f5ba17..236e1776b 100644 --- a/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Theme.kt +++ b/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Theme.kt @@ -10,7 +10,8 @@ val CustomButtonColor = Color(0xFF01A2D1) val CustomSliderThumbColor = Color(0xFF03DAC5) val CustomSliderTrackColor = Color(0xFF018786) -private val LightColorScheme = lightColorScheme( +private val LightColorScheme = + lightColorScheme( primary = md_theme_light_primary, onPrimary = md_theme_light_onPrimary, primaryContainer = md_theme_light_primaryContainer, @@ -40,9 +41,10 @@ private val LightColorScheme = lightColorScheme( surfaceTint = md_theme_light_surfaceTint, outlineVariant = md_theme_light_outlineVariant, scrim = md_theme_light_scrim, -) + ) -private val DarkColorScheme = darkColorScheme( +private val DarkColorScheme = + darkColorScheme( primary = md_theme_dark_primary, onPrimary = md_theme_dark_onPrimary, primaryContainer = md_theme_dark_primaryContainer, @@ -72,36 +74,34 @@ private val DarkColorScheme = darkColorScheme( surfaceTint = md_theme_dark_surfaceTint, outlineVariant = md_theme_dark_outlineVariant, scrim = md_theme_dark_scrim, -) + ) internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } val LocalCustomColors = staticCompositionLocalOf { CustomColors() } data class CustomColors( - val buttonColor: Color = CustomButtonColor, - val sliderThumbColor: Color = CustomSliderThumbColor, - val sliderTrackColor: Color = CustomSliderTrackColor + val buttonColor: Color = CustomButtonColor, + val sliderThumbColor: Color = CustomSliderThumbColor, + val sliderTrackColor: Color = CustomSliderTrackColor ) @Composable -internal fun AppTheme( - content: @Composable () -> Unit -) { - val systemIsDark = isSystemInDarkTheme() - val isDarkState = remember { mutableStateOf(systemIsDark) } - CompositionLocalProvider( - LocalThemeIsDark provides isDarkState, - LocalCustomColors provides CustomColors() - ) { - val isDark by isDarkState - //com.xef.xefMobile.theme.SystemAppearance(isDark) - MaterialTheme( - colorScheme = if (isDark) DarkColorScheme else LightColorScheme, - content = { Surface(content = content) } - ) - } +internal fun AppTheme(content: @Composable () -> Unit) { + val systemIsDark = isSystemInDarkTheme() + val isDarkState = remember { mutableStateOf(systemIsDark) } + CompositionLocalProvider( + LocalThemeIsDark provides isDarkState, + LocalCustomColors provides CustomColors() + ) { + val isDark by isDarkState + // com.xef.xefMobile.theme.SystemAppearance(isDark) + MaterialTheme( + colorScheme = if (isDark) DarkColorScheme else LightColorScheme, + content = { Surface(content = content) } + ) + } } -//@Composable -//internal expect fun SystemAppearance(isDark: Boolean) +// @Composable +// internal expect fun SystemAppearance(isDark: Boolean) diff --git a/server/xefMobile/composeApp/src/commonTest/kotlin/org/xef/xefMobile/ComposeTest.kt b/server/xefMobile/composeApp/src/commonTest/kotlin/org/xef/xefMobile/ComposeTest.kt index 136caf75c..ac0fcf2bf 100644 --- a/server/xefMobile/composeApp/src/commonTest/kotlin/org/xef/xefMobile/ComposeTest.kt +++ b/server/xefMobile/composeApp/src/commonTest/kotlin/org/xef/xefMobile/ComposeTest.kt @@ -19,27 +19,19 @@ import kotlin.test.Test @OptIn(ExperimentalTestApi::class) class ComposeTest { - @Test - fun simpleCheck() = runComposeUiTest { - setContent { - var txt by remember { mutableStateOf("Go") } - Column { - Text( - text = txt, - modifier = Modifier.testTag("t_text") - ) - Button( - onClick = { txt += "." }, - modifier = Modifier.testTag("t_button") - ) { - Text("click me") - } - } + @Test + fun simpleCheck() = runComposeUiTest { + setContent { + var txt by remember { mutableStateOf("Go") } + Column { + Text(text = txt, modifier = Modifier.testTag("t_text")) + Button(onClick = { txt += "." }, modifier = Modifier.testTag("t_button")) { + Text("click me") } - - onNodeWithTag("t_button").apply { - repeat(3) { performClick() } - } - onNodeWithTag("t_text").assertTextEquals("Go...") + } } -} \ No newline at end of file + + onNodeWithTag("t_button").apply { repeat(3) { performClick() } } + onNodeWithTag("t_text").assertTextEquals("Go...") + } +}