diff --git a/server/xefMobile/.gitignore b/server/xefMobile/.gitignore
new file mode 100644
index 000000000..efd959ba4
--- /dev/null
+++ b/server/xefMobile/.gitignore
@@ -0,0 +1,153 @@
+# Created by https://www.gitignore.io/api/android,intellij
+
+### Android ###
+# Built application files
+*.apk
+*.ap_
+
+# Files for the ART/Dalvik VM
+*.dex
+
+# Java class files
+*.class
+
+# Generated files
+bin/
+gen/
+out/
+tokenizer/karma.config.d/
+
+# Gradle files
+.gradle/
+build/
+
+# dokka + ank apidocs merge to sources
+apidocs/
+
+# Local configuration file (sdk path, etc)
+local.properties
+
+# Proguard folder generated by Eclipse
+proguard/
+
+# Log Files
+*.log
+
+# Android Studio Navigation editor temp files
+.navigation/
+
+# Android Studio captures folder
+captures/
+
+# Intellij
+*.iml
+.idea/*
+!.idea/vcs.xml
+
+# Keystore files
+*.jks
+
+# External native build folder generated in Android Studio 2.2 and later
+.externalNativeBuild
+
+# Google Services (e.g. APIs or Firebase)
+google-services.json
+
+# Freeline
+freeline.py
+freeline/
+freeline_project_description.json
+
+### Android Patch ###
+gen-external-apklibs
+
+### Intellij ###
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+# User-specific stuff:
+.idea/**/workspace.xml
+.idea/**/tasks.xml
+
+# Sensitive or high-churn files:
+.idea/**/dataSources/
+.idea/**/dataSources.ids
+.idea/**/dataSources.xml
+.idea/**/dataSources.local.xml
+.idea/**/sqlDataSources.xml
+.idea/**/dynamic.xml
+.idea/**/uiDesigner.xml
+
+# Gradle:
+.idea/**/gradle.xml
+.idea/**/libraries
+
+#Kotlintest
+**/.kotlintest
+
+# Mongo Explorer plugin:
+.idea/**/mongoSettings.xml
+
+## File-based project format:
+*.iws
+
+## Plugin-specific files:
+
+# IntelliJ
+/out/
+
+# mpeltonen/sbt-idea plugin
+.idea_modules/
+
+# JIRA plugin
+atlassian-ide-plugin.xml
+
+# Crashlytics plugin (for Android Studio and IntelliJ)
+com_crashlytics_export_strings.xml
+crashlytics.properties
+crashlytics-build.properties
+fabric.properties
+
+# Jekyll
+_site
+.sass-cache
+vendor
+.bundle
+
+# Ruby
+Gemfile.lock
+
+### Intellij Patch ###
+# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
+
+# *.iml
+# modules.xml
+# .idea/misc.xml
+# *.ipr
+
+######################
+
+reports/
+
+# End of https://www.gitignore.io/api/android,intellij
+/.idea/misc.xml
+
+.DS_Store
+
+target/
+
+.bash_profile
+**/.kotlintest/
+
+
+_site/
+kotlin-js-store/
+.sass-cache/
+.jekyll-cache/
+.jekyll-metadata
+
+.env
+
+*.bin
+model.log
+operations.log
\ No newline at end of file
diff --git a/server/xefMobile/.idea/vcs.xml b/server/xefMobile/.idea/vcs.xml
new file mode 100644
index 000000000..b2bdec2d7
--- /dev/null
+++ b/server/xefMobile/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/.kotlin/sessions/kotlin-compiler-859541645610285268.salive b/server/xefMobile/.kotlin/sessions/kotlin-compiler-859541645610285268.salive
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/xefMobile/README.MD b/server/xefMobile/README.MD
new file mode 100644
index 000000000..583cb36ec
--- /dev/null
+++ b/server/xefMobile/README.MD
@@ -0,0 +1,15 @@
+# Compose Multiplatform Application
+
+## Before running!
+ - install JDK 17 or higher on your machine
+ - add `local.properties` file to the project root and set a path to Android SDK there
+
+### Android
+To run the application on android device/emulator:
+ - open project in Android Studio and run imported android run configuration
+
+To build the application bundle:
+ - run `./gradlew :composeApp:assembleDebug`
+ - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk`
+Run android simulator UI tests: `./gradlew :composeApp:pixel5Check`
+
diff --git a/server/xefMobile/build.gradle.kts b/server/xefMobile/build.gradle.kts
new file mode 100644
index 000000000..4b93c8e5a
--- /dev/null
+++ b/server/xefMobile/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ alias(libs.plugins.multiplatform).apply(false)
+ alias(libs.plugins.compose.compiler).apply(false)
+ alias(libs.plugins.compose).apply(false)
+ alias(libs.plugins.android.application).apply(false)
+ alias(libs.plugins.buildConfig).apply(false)
+ alias(libs.plugins.kotlinx.serialization).apply(false)
+}
diff --git a/server/xefMobile/composeApp/build.gradle.kts b/server/xefMobile/composeApp/build.gradle.kts
new file mode 100644
index 000000000..3b024bef1
--- /dev/null
+++ b/server/xefMobile/composeApp/build.gradle.kts
@@ -0,0 +1,143 @@
+import org.jetbrains.compose.ExperimentalComposeLibrary
+import com.android.build.api.dsl.ManagedVirtualDevice
+import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree
+
+plugins {
+ alias(libs.plugins.multiplatform)
+ alias(libs.plugins.compose.compiler)
+ alias(libs.plugins.compose)
+ alias(libs.plugins.android.application)
+ alias(libs.plugins.buildConfig)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+kotlin {
+ androidTarget {
+ compilations.all {
+ compileTaskProvider {
+ compilerOptions {
+ jvmTarget.set(JvmTarget.JVM_1_8)
+ freeCompilerArgs.add("-Xjdk-release=${JavaVersion.VERSION_1_8}")
+ }
+ }
+ }
+ @OptIn(ExperimentalKotlinGradlePluginApi::class)
+ instrumentedTestVariant {
+ sourceSetTree.set(KotlinSourceSetTree.test)
+ dependencies {
+ debugImplementation(libs.androidx.testManifest)
+ implementation(libs.androidx.junit4)
+ }
+ }
+ }
+
+ sourceSets {
+ all {
+ languageSettings {
+ optIn("org.jetbrains.compose.resources.ExperimentalResourceApi")
+ }
+ }
+ val commonMain by getting {
+ dependencies {
+ implementation(compose.runtime)
+ implementation(compose.foundation)
+ implementation(compose.material3)
+ implementation(compose.components.resources)
+ implementation(compose.components.uiToolingPreview)
+ implementation(libs.voyager.navigator)
+ implementation(libs.composeImageLoader)
+ implementation(libs.napier)
+ implementation(libs.kotlinx.coroutines.core)
+ implementation(libs.ktor.core)
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.multiplatformSettings)
+ implementation(libs.ktor.client.json)
+ implementation("io.ktor:ktor-client-content-negotiation:2.3.11")
+ implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ implementation("com.squareup.retrofit2:retrofit:2.11.0")
+ implementation("com.squareup.retrofit2:converter-gson:2.11.0")
+ implementation("io.ktor:ktor-client-serialization-jvm:2.3.11")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3")
+ implementation("com.google.accompanist:accompanist-permissions:0.34.0")
+ }
+ }
+
+ val commonTest by getting {
+ dependencies {
+ implementation(kotlin("test"))
+ @OptIn(ExperimentalComposeLibrary::class)
+ implementation(compose.uiTest)
+ implementation(libs.kotlinx.coroutines.test)
+ }
+ }
+
+ val androidMain by getting {
+ dependencies {
+ implementation(compose.uiTooling)
+ implementation(libs.androidx.activityCompose)
+ implementation(libs.kotlinx.coroutines.android)
+ implementation(libs.ktor.client.okhttp)
+ implementation("io.ktor:ktor-client-android:2.3.11")
+ implementation(libs.ktor.client.json)
+ implementation("io.ktor:ktor-client-content-negotiation:2.3.11")
+ implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.11")
+ implementation("androidx.navigation:navigation-compose:2.7.7")
+ implementation("androidx.datastore:datastore-preferences:1.1.1")
+ implementation("androidx.compose.runtime:runtime-livedata:1.6.7")
+ implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.6.3")
+ implementation("io.ktor:ktor-client-serialization-jvm:2.3.11")
+ implementation("com.google.accompanist:accompanist-permissions:0.34.0")
+ }
+ }
+ }
+}
+
+android {
+ namespace = "org.xef.xefMobile"
+ compileSdk = 34
+
+ defaultConfig {
+ minSdk = 24
+ targetSdk = 34
+ applicationId = "org.xef.xefMobile.androidApp"
+ versionCode = 1
+ versionName = "1.0.0"
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ sourceSets["main"].apply {
+ manifest.srcFile("src/androidMain/AndroidManifest.xml")
+ res.srcDirs("src/androidMain/res")
+ }
+
+ @Suppress("UnstableApiUsage")
+ testOptions {
+ managedDevices.devices {
+ maybeCreate("pixel5").apply {
+ device = "Pixel 5"
+ apiLevel = 34
+ systemImageSource = "aosp"
+ }
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+
+ buildFeatures {
+ compose = true
+ }
+
+ composeOptions {
+ kotlinCompilerExtensionVersion = "1.5.11"
+ }
+}
+
+buildConfig {
+ // BuildConfig configuration here.
+}
diff --git a/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml b/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml
new file mode 100644
index 000000000..c6670f1a1
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/AndroidManifest.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 000000000..cb9e66be7
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainActivity.kt
@@ -0,0 +1,20 @@
+package com.xef.xefMobile
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import com.server.movile.xef.android.ui.viewmodels.AuthViewModel
+import com.xef.xefMobile.services.ApiService
+
+class MainActivity : ComponentActivity() {
+ private lateinit var authViewModel: AuthViewModel
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ authViewModel = AuthViewModel(this, ApiService())
+
+ 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
new file mode 100644
index 000000000..595921724
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/MainLayout.kt
@@ -0,0 +1,271 @@
+package com.xef.xefMobile
+
+import android.annotation.SuppressLint
+import android.widget.Toast
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.rounded.Menu
+import androidx.compose.material3.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.platform.LocalContext
+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 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
+) {
+ val coroutineScope = rememberCoroutineScope()
+ val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
+ val CustomLightBlue = Color(0xFFADD8E6)
+ val CustomTextBlue = Color(0xFF0199D7)
+ val context = LocalContext.current
+
+ ModalNavigationDrawer(
+ drawerState = drawerState,
+ gesturesEnabled = true,
+ drawerContent = {
+ ModalDrawerSheet {
+ Box(
+ modifier = Modifier
+ .background(CustomLightBlue)
+ .fillMaxWidth()
+ .height(100.dp)
+ ) {
+ Image(
+ painter = painterResource(id = R.drawable.xef_brand_name),
+ contentDescription = "Logo",
+ modifier = Modifier
+ .size(150.dp)
+ .align(Alignment.Center)
+ )
+ }
+ 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
new file mode 100644
index 000000000..23cc9b8c9
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/XefAndroidApp.kt
@@ -0,0 +1,57 @@
+package com.xef.xefMobile
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.server.movile.xef.android.ui.screens.*
+import com.server.movile.xef.android.ui.screens.menu.AssistantScreen
+import com.server.movile.xef.android.ui.screens.menu.CreateAssistantScreen
+import com.server.movile.xef.android.ui.screens.navigationdrawercompose.HomeScreen
+import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
+import com.xef.xefMobile.ui.screens.Screens
+
+@OptIn(ExperimentalMaterial3Api::class)
+@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
+@Composable
+fun XefAndroidApp(authViewModel: IAuthViewModel) {
+ val navigationController = rememberNavController()
+ val userName by authViewModel.userName.observeAsState("")
+
+ 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, authViewModel = authViewModel, userName = userName.orEmpty()) {
+ HomeScreen(authViewModel, navigationController)
+ }
+ }
+ 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
new file mode 100644
index 000000000..668bb97d4
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AssistantModel.kt
@@ -0,0 +1,14 @@
+package com.xef.xefMobile.model
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class Assistant(
+ val id: String,
+ val name: String
+)
+
+@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
new file mode 100644
index 000000000..3b4a8fcf7
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/model/AuthenticationModels.kt
@@ -0,0 +1,34 @@
+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 RegisterResponse(
+
+ val authToken: String
+)
+
+@Serializable
+data class LoginRequest(
+ val email: String,
+ val password: String
+)
+
+@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
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
new file mode 100644
index 000000000..4de2a18d5
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/network/client/HttpClient.kt
@@ -0,0 +1,19 @@
+package com.xef.xefMobile.network.client
+
+import io.ktor.client.*
+import io.ktor.client.engine.android.*
+import io.ktor.client.plugins.contentnegotiation.*
+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
+ })
+ }
+ }
+}
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
new file mode 100644
index 000000000..fa1fb1f0b
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/ApiService.kt
@@ -0,0 +1,61 @@
+package com.xef.xefMobile.services
+
+import android.util.Log
+import com.xef.xefMobile.model.*
+import com.xef.xefMobile.network.client.HttpClientProvider
+import io.ktor.client.call.*
+import io.ktor.client.request.*
+import io.ktor.client.statement.*
+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 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
+ }
+ }
+}
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
new file mode 100644
index 000000000..9cda93716
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/services/UserRepositoryService.kt
@@ -0,0 +1,35 @@
+package com.xef.xefMobile.services
+
+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 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 }
+
+ 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()
+ }
+}
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
new file mode 100644
index 000000000..92bf160bd
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/theme/Theme.android.kt
@@ -0,0 +1,19 @@
+package com.xef.xefMobile.theme
+
+import android.app.Activity
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.LocalView
+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
+ }
+ }
+}
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
new file mode 100644
index 000000000..7c9e11755
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/FilePickerDialog.kt
@@ -0,0 +1,153 @@
+package com.xef.xefMobile.ui.composable
+
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+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 androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.accompanist.permissions.ExperimentalPermissionsApi
+import com.google.accompanist.permissions.isGranted
+import com.google.accompanist.permissions.rememberPermissionState
+
+import com.server.movile.xef.android.ui.themes.CustomColors
+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
+) {
+ val viewModel: PathViewModel = viewModel()
+ val state = viewModel.state
+ val context = LocalContext.current
+
+ val permissionState = rememberPermissionState(
+ permission = android.Manifest.permission.READ_EXTERNAL_STORAGE
+ )
+
+ var selectedFile by remember { mutableStateOf(null) }
+
+ 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()
+ }
+ }
+ )
+
+ 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 = path,
+ modifier = Modifier
+ .fillMaxWidth()
+ .clickable {
+ selectedFile = path
+ }
+ .padding(8.dp),
+ color = if (selectedFile == path) Color.Blue else Color.Unspecified
+ )
+ }
+ }
+ }
+ }
+ 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
new file mode 100644
index 000000000..2c59cf416
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/composable/UriPathFinder.kt
@@ -0,0 +1,103 @@
+package com.xef.xefMobile.ui.composable
+
+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
+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
+ }
+ }
+
+ 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 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 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 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
new file mode 100644
index 000000000..d54a113de
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/navigation/Navigation.kt
@@ -0,0 +1,31 @@
+package com.xef.xefMobile.ui.navigation
+
+import androidx.compose.runtime.Composable
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import com.server.movile.xef.android.ui.screens.LoginScreen
+import com.server.movile.xef.android.ui.screens.RegisterScreen
+import com.server.movile.xef.android.ui.screens.menu.AssistantScreen
+import com.server.movile.xef.android.ui.screens.menu.CreateAssistantScreen
+import com.server.movile.xef.android.ui.viewmodels.IAuthViewModel
+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)
+ }
+ }
+}
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
new file mode 100644
index 000000000..18d10e543
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/FilePicker/PathScreenState.kt
@@ -0,0 +1,6 @@
+package com.xef.xefMobile.ui.screens.FilePicker
+
+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
new file mode 100644
index 000000000..11b567156
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/LoginScreen.kt
@@ -0,0 +1,110 @@
+package com.server.movile.xef.android.ui.screens
+
+import android.util.Log
+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
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+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 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) }
+
+ 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))
+
+ 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")
+ }
+ }
+}
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
new file mode 100644
index 000000000..a84df0346
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/RegisterScreen.kt
@@ -0,0 +1,125 @@
+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
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+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 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()
+
+ 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("") }
+
+ 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")
+ }
+ }
+}
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
new file mode 100644
index 000000000..d64384395
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/Screens.kt
@@ -0,0 +1,15 @@
+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")
+}
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
new file mode 100644
index 000000000..defee5364
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/AssistantScreen.kt
@@ -0,0 +1,101 @@
+package com.server.movile.xef.android.ui.screens.menu
+
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.*
+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
+import com.xef.xefMobile.theme.theme.LocalCustomColors
+import com.xef.xefMobile.ui.screens.Screens
+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 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
+ }
+ }
+ }
+
+ 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))
+
+ assistants.forEach { assistant ->
+ Text(
+ text = assistant.name,
+ fontWeight = FontWeight.Bold
+
+ )
+ Text(text = assistant.id)
+ Spacer(modifier = Modifier.height(8.dp))
+ }
+ }
+
+ 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
new file mode 100644
index 000000000..0527f6fbd
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/menu/CreateAssistantScreen.kt
@@ -0,0 +1,270 @@
+package com.server.movile.xef.android.ui.screens.menu
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.text.input.KeyboardType
+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.themes.LocalCustomColors
+
+
+class CreateAssistantActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContent {
+ // Pass the NavController to CreateAssistantScreen
+ val navController = rememberNavController()
+ CreateAssistantScreen(navController)
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun CreateAssistantScreen(navController: NavController) {
+ var name by remember { mutableStateOf("") }
+ var instructions by remember { mutableStateOf("") }
+ var temperature by remember { mutableStateOf(1f) }
+ var topP by remember { mutableStateOf(1f) }
+ var fileSearchEnabled by remember { mutableStateOf(false) }
+ var codeInterpreterEnabled by remember { mutableStateOf(false) }
+ var model by remember { mutableStateOf("gpt-4-turbo") }
+ val list = listOf("gpt-4o", "gpt-4", "gpt-3.5-turbo-16K", "gpt-3.5-turbo-0125", "gpt-3.5-turbo")
+ var isExpanded by remember { mutableStateOf(false) }
+ var selectedText by remember { mutableStateOf(list[0]) }
+
+ val customColors = LocalCustomColors.current
+
+ 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()
+ ) {
+ TextField(
+ modifier = Modifier
+ .fillMaxWidth()
+ .menuAnchor(),
+ value = selectedText,
+ onValueChange = {},
+ readOnly = true,
+ trailingIcon = {
+ ExposedDropdownMenuDefaults.TrailingIcon(expanded = isExpanded)
+ }
+ )
+ ExposedDropdownMenu(expanded = isExpanded, onDismissRequest = { isExpanded = false }) {
+ list.forEachIndexed { index, text ->
+ DropdownMenuItem(
+ text = { Text(text = text) },
+ onClick = {
+ selectedText = list[index]
+ isExpanded = false
+ },
+ contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding
+ )
+ }
+ }
+ }
+ }
+ Spacer(modifier = Modifier.height(12.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "TOOLS")
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 10.dp)
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ OutlinedButton(
+ onClick = { /* handle cancel */ },
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = customColors.buttonColor
+ )
+ ) {
+ Text("File Search")
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ Switch(
+ checked = fileSearchEnabled,
+ onCheckedChange = { fileSearchEnabled = it },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = customColors.sliderThumbColor,
+ checkedTrackColor = customColors.sliderTrackColor
+ )
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ OutlinedButton(
+ onClick = { /* handle cancel */ },
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = customColors.buttonColor
+ )
+ ) {
+ Text("Code Interpreter")
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ Switch(
+ checked = codeInterpreterEnabled,
+ onCheckedChange = { codeInterpreterEnabled = it },
+ colors = SwitchDefaults.colors(
+ checkedThumbColor = customColors.sliderThumbColor,
+ checkedTrackColor = customColors.sliderTrackColor
+ )
+ )
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ OutlinedButton(
+ onClick = { /* handle cancel */ },
+ colors = ButtonDefaults.outlinedButtonColors(
+ containerColor = Color.Transparent,
+ contentColor = customColors.buttonColor
+ )
+ ) {
+ Text("Functions")
+ }
+ Spacer(modifier = Modifier.weight(1f))
+ }
+ Spacer(modifier = Modifier.height(8.dp))
+ Row(verticalAlignment = Alignment.CenterVertically) {
+ Text(text = "MODEL CONFIGURATION")
+
+ HorizontalDivider(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp)
+ )
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ AssistantFloatField(label = "Temperature", value = temperature, onValueChange = { temperature = it })
+
+ AssistantFloatField(label = "Top P", value = topP, onValueChange = { topP = it })
+
+ Row(
+ horizontalArrangement = Arrangement.Center,
+ modifier = Modifier.fillMaxWidth()
+ ) {
+ Button(
+ onClick = { navController.navigateUp() },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = customColors.buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text("Cancel")
+ }
+ Spacer(modifier = Modifier.width(8.dp))
+ Button(
+ onClick = { /* handle create */ },
+ colors = ButtonDefaults.buttonColors(
+ containerColor = customColors.buttonColor,
+ contentColor = MaterialTheme.colorScheme.onPrimary
+ )
+ ) {
+ Text("Create")
+ }
+ }
+ }
+ }
+}
+
+@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
+ )
+ }
+ }
+}
+
+@Preview(showBackground = false)
+@Composable
+fun CreateAssistantScreenPreview() {
+ // Create a mock NavController for the preview
+ val navController = rememberNavController()
+ CreateAssistantScreen(navController)
+}
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
new file mode 100644
index 000000000..b4be5b21f
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/HomeScreen.kt
@@ -0,0 +1,40 @@
+package com.server.movile.xef.android.ui.screens.navigationdrawercompose
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+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
+ )
+ }
+ }
+}
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
new file mode 100644
index 000000000..c04e940eb
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/screens/navigationdrawercompose/MenuItem.kt
@@ -0,0 +1,10 @@
+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
diff --git a/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Color.kt b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Color.kt
new file mode 100644
index 000000000..131c831bb
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Color.kt
@@ -0,0 +1,70 @@
+package com.server.movile.xef.android.ui.themes
+
+import androidx.compose.ui.graphics.Color
+
+// Light Theme Colors
+internal val md_theme_light_primary = Color(0xFF00687A)
+internal val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+internal val md_theme_light_primaryContainer = Color(0xFFABEDFF)
+internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26)
+internal val md_theme_light_secondary = Color(0xFF00696E)
+internal val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_light_onSecondaryContainer = Color(0xFF002022)
+internal val md_theme_light_tertiary = Color(0xFF904D00)
+internal val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500)
+internal val md_theme_light_error = Color(0xFFBA1A1A)
+internal val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+internal val md_theme_light_onError = Color(0xFFFFFFFF)
+internal val md_theme_light_onErrorContainer = Color(0xFF410002)
+internal val md_theme_light_background = Color(0xFFFFFBFF)
+internal val md_theme_light_onBackground = Color(0xFF221B00)
+internal val md_theme_light_surface = Color(0xFFFFFBFF)
+internal val md_theme_light_onSurface = Color(0xFF221B00)
+internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
+internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B)
+internal val md_theme_light_outline = Color(0xFF70797B)
+internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0)
+internal val md_theme_light_inverseSurface = Color(0xFF3A3000)
+internal val md_theme_light_inversePrimary = Color(0xFF55D6F4)
+internal val md_theme_light_shadow = Color(0xFF000000)
+internal val md_theme_light_surfaceTint = Color(0xFF00687A)
+internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
+internal val md_theme_light_scrim = Color(0xFF000000)
+
+// Dark Theme Colors
+internal val md_theme_dark_primary = Color(0xFF55D6F4)
+internal val md_theme_dark_onPrimary = Color(0xFF003640)
+internal val md_theme_dark_primaryContainer = Color(0xFF004E5C)
+internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF)
+internal val md_theme_dark_secondary = Color(0xFF4CD9E2)
+internal val md_theme_dark_onSecondary = Color(0xFF00373A)
+internal val md_theme_dark_secondaryContainer = Color(0xFF004F53)
+internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_dark_tertiary = Color(0xFFFFB77C)
+internal val md_theme_dark_onTertiary = Color(0xFF4D2700)
+internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900)
+internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_dark_error = Color(0xFFFFB4AB)
+internal val md_theme_dark_errorContainer = Color(0xFF93000A)
+internal val md_theme_dark_onError = Color(0xFF690005)
+internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+internal val md_theme_dark_background = Color(0xFF221B00)
+internal val md_theme_dark_onBackground = Color(0xFFFFE264)
+internal val md_theme_dark_surface = Color(0xFF221B00)
+internal val md_theme_dark_onSurface = Color(0xFFFFE264)
+internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B)
+internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
+internal val md_theme_dark_outline = Color(0xFF899295)
+internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00)
+internal val md_theme_dark_inverseSurface = Color(0xFFFFE264)
+internal val md_theme_dark_inversePrimary = Color(0xFF00687A)
+internal val md_theme_dark_shadow = Color(0xFF000000)
+internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4)
+internal val md_theme_dark_outlineVariant = Color(0xFF3F484B)
+internal val md_theme_dark_scrim = Color(0xFF000000)
+
+// Seed color for dynamic theming
+internal val seed = Color(0xFF2C3639)
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
new file mode 100644
index 000000000..fc1f48a17
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Theme.kt
@@ -0,0 +1,104 @@
+package com.server.movile.xef.android.ui.themes
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.graphics.Color
+
+// Define custom colors
+val CustomButtonColor = Color(0xFF01A2D1)
+val CustomSliderThumbColor = Color(0xFF03DAC5)
+val CustomSliderTrackColor = Color(0xFF018786)
+
+private val LightColorScheme = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ 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
+)
+
+@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
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
new file mode 100644
index 000000000..637bb1a72
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/themes/Type.kt
@@ -0,0 +1,28 @@
+package com.server.movile.xef.android.ui.themes
+
+import androidx.compose.material3.Typography
+import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
+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 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
new file mode 100644
index 000000000..1bd88b147
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/AuthViewModel.kt
@@ -0,0 +1,149 @@
+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
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.xef.xefMobile.model.LoginRequest
+import com.xef.xefMobile.model.RegisterRequest
+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 {
+
+ private val dataStore = context.dataStore
+
+ private val _authToken = MutableLiveData()
+ override val authToken: LiveData = _authToken
+
+ private val _isLoading = MutableLiveData()
+ override val isLoading: LiveData = _isLoading
+
+ private val _errorMessage = MutableLiveData()
+ override val errorMessage: LiveData = _errorMessage
+
+ private val _userName = MutableLiveData()
+ override val userName: LiveData = _userName
+
+ init {
+ loadAuthToken()
+ }
+
+ private fun loadAuthToken() {
+ viewModelScope.launch {
+ val token = dataStore.data.map { preferences ->
+ preferences[stringPreferencesKey("authToken")]
+ }.firstOrNull()
+ _authToken.value = token
+
+
+ token?.let {
+ loadUserName()
+ }
+ }
+ }
+
+ private suspend fun loadUserName() {
+ withContext(Dispatchers.IO) {
+ val name = dataStore.data.map { preferences ->
+ preferences[stringPreferencesKey("userName")]
+ }.firstOrNull()
+ _userName.postValue(name)
+ }
+ }
+
+ 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}")
+ }
+ }
+
+ override fun logout() {
+ viewModelScope.launch {
+ try {
+ withContext(Dispatchers.IO) {
+ dataStore.edit { preferences ->
+ preferences.remove(stringPreferencesKey("authToken"))
+ preferences.remove(stringPreferencesKey("userName")) // Add this line
+ }
+ }
+ _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
new file mode 100644
index 000000000..ba203733a
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/IAuthViewModel.kt
@@ -0,0 +1,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()
+}
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
new file mode 100644
index 000000000..94328f41a
--- /dev/null
+++ b/server/xefMobile/composeApp/src/androidMain/kotlin/com/xef/xefMobile/ui/viewmodels/PathViewModel.kt
@@ -0,0 +1,43 @@
+package com.xef.xefMobile.ui.viewmodels
+
+import android.content.Context
+import android.net.Uri
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.xef.xefMobile.ui.composable.UriPathFinder
+import com.xef.xefMobile.ui.screens.FilePicker.PathScreenState
+import kotlinx.coroutines.launch
+
+class PathViewModel : ViewModel() {
+
+ var state by mutableStateOf(PathScreenState())
+ private set
+
+ 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 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)
+ }
+ }
+}
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/chat_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/chat_24px.xml
new file mode 100644
index 000000000..0163a0e13
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/chat_24px.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/home_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/home_24px.xml
new file mode 100644
index 000000000..bb05c6154
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/home_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml
new file mode 100644
index 000000000..f1c45b5a2
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml
new file mode 100644
index 000000000..0ce2444a2
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml
new file mode 100644
index 000000000..b7331d3e4
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml
@@ -0,0 +1,9 @@
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml
new file mode 100644
index 000000000..181067170
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/school_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/school_24px.xml
new file mode 100644
index 000000000..2d1038e2e
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/school_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/settings_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/settings_24px.xml
new file mode 100644
index 000000000..90bc79bea
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/settings_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/smart_toy_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/smart_toy_24px.xml
new file mode 100644
index 000000000..7382f2168
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/smart_toy_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/source_environment_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/source_environment_24px.xml
new file mode 100644
index 000000000..3707dc440
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/source_environment_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/support_agent_24px.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/support_agent_24px.xml
new file mode 100644
index 000000000..9c57a03f5
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/support_agent_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_icon.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_icon.xml
new file mode 100644
index 000000000..ca0f226f1
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name.xml
new file mode 100644
index 000000000..756bb5129
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name_white.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name_white.xml
new file mode 100644
index 000000000..db77eab35
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/drawable/xef_brand_name_white.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf b/server/xefMobile/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf
new file mode 100644
index 000000000..3774ef55d
Binary files /dev/null and b/server/xefMobile/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf differ
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/font/montserrat_bold.ttf b/server/xefMobile/composeApp/src/commonMain/composeResources/font/montserrat_bold.ttf
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/font/montserrat_regular.ttf b/server/xefMobile/composeApp/src/commonMain/composeResources/font/montserrat_regular.ttf
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/xefMobile/composeApp/src/commonMain/composeResources/values/strings.xml b/server/xefMobile/composeApp/src/commonMain/composeResources/values/strings.xml
new file mode 100644
index 000000000..b8d73e48d
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -0,0 +1,7 @@
+
+ Cyclone
+ Open github
+ Run
+ Stop
+ Theme
+
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Color.kt b/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Color.kt
new file mode 100644
index 000000000..849ee8d9d
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Color.kt
@@ -0,0 +1,70 @@
+package com.xef.xefMobile.theme.theme
+
+import androidx.compose.ui.graphics.Color
+
+// Light Theme Colors
+internal val md_theme_light_primary = Color(0xFF00687A)
+internal val md_theme_light_onPrimary = Color(0xFFFFFFFF)
+internal val md_theme_light_primaryContainer = Color(0xFFABEDFF)
+internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26)
+internal val md_theme_light_secondary = Color(0xFF00696E)
+internal val md_theme_light_onSecondary = Color(0xFFFFFFFF)
+internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_light_onSecondaryContainer = Color(0xFF002022)
+internal val md_theme_light_tertiary = Color(0xFF904D00)
+internal val md_theme_light_onTertiary = Color(0xFFFFFFFF)
+internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500)
+internal val md_theme_light_error = Color(0xFFBA1A1A)
+internal val md_theme_light_errorContainer = Color(0xFFFFDAD6)
+internal val md_theme_light_onError = Color(0xFFFFFFFF)
+internal val md_theme_light_onErrorContainer = Color(0xFF410002)
+internal val md_theme_light_background = Color(0xFFFFFBFF)
+internal val md_theme_light_onBackground = Color(0xFF221B00)
+internal val md_theme_light_surface = Color(0xFFFFFBFF)
+internal val md_theme_light_onSurface = Color(0xFF221B00)
+internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7)
+internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B)
+internal val md_theme_light_outline = Color(0xFF70797B)
+internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0)
+internal val md_theme_light_inverseSurface = Color(0xFF3A3000)
+internal val md_theme_light_inversePrimary = Color(0xFF55D6F4)
+internal val md_theme_light_shadow = Color(0xFF000000)
+internal val md_theme_light_surfaceTint = Color(0xFF00687A)
+internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB)
+internal val md_theme_light_scrim = Color(0xFF000000)
+
+// Dark Theme Colors
+internal val md_theme_dark_primary = Color(0xFF55D6F4)
+internal val md_theme_dark_onPrimary = Color(0xFF003640)
+internal val md_theme_dark_primaryContainer = Color(0xFF004E5C)
+internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF)
+internal val md_theme_dark_secondary = Color(0xFF4CD9E2)
+internal val md_theme_dark_onSecondary = Color(0xFF00373A)
+internal val md_theme_dark_secondaryContainer = Color(0xFF004F53)
+internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE)
+internal val md_theme_dark_tertiary = Color(0xFFFFB77C)
+internal val md_theme_dark_onTertiary = Color(0xFF4D2700)
+internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900)
+internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2)
+internal val md_theme_dark_error = Color(0xFFFFB4AB)
+internal val md_theme_dark_errorContainer = Color(0xFF93000A)
+internal val md_theme_dark_onError = Color(0xFF690005)
+internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
+internal val md_theme_dark_background = Color(0xFF221B00)
+internal val md_theme_dark_onBackground = Color(0xFFFFE264)
+internal val md_theme_dark_surface = Color(0xFF221B00)
+internal val md_theme_dark_onSurface = Color(0xFFFFE264)
+internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B)
+internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB)
+internal val md_theme_dark_outline = Color(0xFF899295)
+internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00)
+internal val md_theme_dark_inverseSurface = Color(0xFFFFE264)
+internal val md_theme_dark_inversePrimary = Color(0xFF00687A)
+internal val md_theme_dark_shadow = Color(0xFF000000)
+internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4)
+internal val md_theme_dark_outlineVariant = Color(0xFF3F484B)
+internal val md_theme_dark_scrim = Color(0xFF000000)
+
+// Seed color for dynamic theming
+internal val seed = Color(0xFF2C3639)
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
new file mode 100644
index 000000000..f16f5ba17
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonMain/kotlin/com/xef/xefMobile/theme/theme/Theme.kt
@@ -0,0 +1,107 @@
+package com.xef.xefMobile.theme.theme
+
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.graphics.Color
+
+// Define custom colors
+val CustomButtonColor = Color(0xFF01A2D1)
+val CustomSliderThumbColor = Color(0xFF03DAC5)
+val CustomSliderTrackColor = Color(0xFF018786)
+
+private val LightColorScheme = lightColorScheme(
+ primary = md_theme_light_primary,
+ onPrimary = md_theme_light_onPrimary,
+ primaryContainer = md_theme_light_primaryContainer,
+ onPrimaryContainer = md_theme_light_onPrimaryContainer,
+ secondary = md_theme_light_secondary,
+ onSecondary = md_theme_light_onSecondary,
+ secondaryContainer = md_theme_light_secondaryContainer,
+ onSecondaryContainer = md_theme_light_onSecondaryContainer,
+ tertiary = md_theme_light_tertiary,
+ onTertiary = md_theme_light_onTertiary,
+ tertiaryContainer = md_theme_light_tertiaryContainer,
+ onTertiaryContainer = md_theme_light_onTertiaryContainer,
+ error = md_theme_light_error,
+ errorContainer = md_theme_light_errorContainer,
+ onError = md_theme_light_onError,
+ onErrorContainer = md_theme_light_onErrorContainer,
+ background = md_theme_light_background,
+ onBackground = md_theme_light_onBackground,
+ surface = md_theme_light_surface,
+ onSurface = md_theme_light_onSurface,
+ surfaceVariant = md_theme_light_surfaceVariant,
+ onSurfaceVariant = md_theme_light_onSurfaceVariant,
+ outline = md_theme_light_outline,
+ inverseOnSurface = md_theme_light_inverseOnSurface,
+ inverseSurface = md_theme_light_inverseSurface,
+ inversePrimary = md_theme_light_inversePrimary,
+ surfaceTint = md_theme_light_surfaceTint,
+ outlineVariant = md_theme_light_outlineVariant,
+ scrim = md_theme_light_scrim,
+)
+
+private val DarkColorScheme = darkColorScheme(
+ primary = md_theme_dark_primary,
+ onPrimary = md_theme_dark_onPrimary,
+ primaryContainer = md_theme_dark_primaryContainer,
+ onPrimaryContainer = md_theme_dark_onPrimaryContainer,
+ secondary = md_theme_dark_secondary,
+ onSecondary = md_theme_dark_onSecondary,
+ secondaryContainer = md_theme_dark_secondaryContainer,
+ onSecondaryContainer = md_theme_dark_onSecondaryContainer,
+ tertiary = md_theme_dark_tertiary,
+ onTertiary = md_theme_dark_onTertiary,
+ tertiaryContainer = md_theme_dark_tertiaryContainer,
+ onTertiaryContainer = md_theme_dark_onTertiaryContainer,
+ error = md_theme_dark_error,
+ errorContainer = md_theme_dark_errorContainer,
+ onError = md_theme_dark_onError,
+ onErrorContainer = md_theme_dark_onErrorContainer,
+ background = md_theme_dark_background,
+ onBackground = md_theme_dark_onBackground,
+ surface = md_theme_dark_surface,
+ onSurface = md_theme_dark_onSurface,
+ surfaceVariant = md_theme_dark_surfaceVariant,
+ onSurfaceVariant = md_theme_dark_onSurfaceVariant,
+ outline = md_theme_dark_outline,
+ inverseOnSurface = md_theme_dark_inverseOnSurface,
+ inverseSurface = md_theme_dark_inverseSurface,
+ inversePrimary = md_theme_dark_inversePrimary,
+ 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
+)
+
+@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) }
+ )
+ }
+}
+
+//@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
new file mode 100644
index 000000000..136caf75c
--- /dev/null
+++ b/server/xefMobile/composeApp/src/commonTest/kotlin/org/xef/xefMobile/ComposeTest.kt
@@ -0,0 +1,45 @@
+package org.xef.xefMobile
+
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+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.platform.testTag
+import androidx.compose.ui.test.ExperimentalTestApi
+import androidx.compose.ui.test.assertTextEquals
+import androidx.compose.ui.test.onNodeWithTag
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.runComposeUiTest
+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")
+ }
+ }
+ }
+
+ onNodeWithTag("t_button").apply {
+ repeat(3) { performClick() }
+ }
+ onNodeWithTag("t_text").assertTextEquals("Go...")
+ }
+}
\ No newline at end of file
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/chat_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/chat_24px.xml
new file mode 100644
index 000000000..0163a0e13
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/chat_24px.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/home_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/home_24px.xml
new file mode 100644
index 000000000..bb05c6154
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/home_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/logout_24dp_fill0_wght400_grad0_opsz24.xml b/server/xefMobile/composeApp/src/main/res/drawable/logout_24dp_fill0_wght400_grad0_opsz24.xml
new file mode 100644
index 000000000..65d9baa89
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/logout_24dp_fill0_wght400_grad0_opsz24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/school_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/school_24px.xml
new file mode 100644
index 000000000..2d1038e2e
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/school_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/settings_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/settings_24px.xml
new file mode 100644
index 000000000..90bc79bea
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/settings_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/smart_toy_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/smart_toy_24px.xml
new file mode 100644
index 000000000..7382f2168
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/smart_toy_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/source_environment_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/source_environment_24px.xml
new file mode 100644
index 000000000..3707dc440
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/source_environment_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/support_agent_24px.xml b/server/xefMobile/composeApp/src/main/res/drawable/support_agent_24px.xml
new file mode 100644
index 000000000..9c57a03f5
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/support_agent_24px.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_icon.xml b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_icon.xml
new file mode 100644
index 000000000..ca0f226f1
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_icon.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name.xml b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name.xml
new file mode 100644
index 000000000..756bb5129
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name_white.xml b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name_white.xml
new file mode 100644
index 000000000..db77eab35
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/drawable/xef_brand_name_white.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/xefMobile/composeApp/src/main/res/font/montserrat_bold.ttf b/server/xefMobile/composeApp/src/main/res/font/montserrat_bold.ttf
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/xefMobile/composeApp/src/main/res/font/montserrat_regular.ttf b/server/xefMobile/composeApp/src/main/res/font/montserrat_regular.ttf
new file mode 100644
index 000000000..e69de29bb
diff --git a/server/xefMobile/composeApp/src/main/res/xml/network_security_config.xml b/server/xefMobile/composeApp/src/main/res/xml/network_security_config.xml
new file mode 100644
index 000000000..f18e1f040
--- /dev/null
+++ b/server/xefMobile/composeApp/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/server/xefMobile/gradle.properties b/server/xefMobile/gradle.properties
new file mode 100644
index 000000000..8eee6ee5c
--- /dev/null
+++ b/server/xefMobile/gradle.properties
@@ -0,0 +1,19 @@
+#Gradle
+org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4G"
+org.gradle.caching=true
+org.gradle.configuration-cache=true
+org.gradle.daemon=true
+org.gradle.parallel=true
+
+#Kotlin
+kotlin.code.style=official
+kotlin.js.compiler=ir
+
+#Android
+android.useAndroidX=true
+android.nonTransitiveRClass=true
+
+#Compose
+org.jetbrains.compose.experimental.uikit.enabled=true
+org.jetbrains.compose.experimental.jscanvas.enabled=true
+org.jetbrains.compose.experimental.wasm.enabled=true
diff --git a/server/xefMobile/gradle/libs.versions.toml b/server/xefMobile/gradle/libs.versions.toml
new file mode 100644
index 000000000..68760a056
--- /dev/null
+++ b/server/xefMobile/gradle/libs.versions.toml
@@ -0,0 +1,52 @@
+[versions]
+
+kotlin = "2.0.0-RC2"
+compose = "1.6.10-rc01"
+agp = "8.2.2"
+androidx-activityCompose = "1.9.0"
+androidx-uiTest = "1.6.7"
+voyager = "1.0.0"
+composeImageLoader = "1.7.8"
+napier = "2.7.1"
+buildConfig = "4.1.1"
+kotlinx-coroutines = "1.8.1"
+ktor = "2.3.9"
+kotlinx-serialization = "1.6.3"
+multiplatformSettings = "1.1.1"
+compose-compiler = "1.5.10.2"
+navigationCompose = "2.7.7"
+
+[libraries]
+
+androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
+androidx-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" }
+androidx-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" }
+voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
+composeImageLoader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "composeImageLoader" }
+napier = { module = "io.github.aakira:napier", version.ref = "napier" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
+kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
+ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
+ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
+ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
+ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" }
+ktor-client-curl = { module = "io.ktor:ktor-client-curl", version.ref = "ktor" }
+ktor-client-winhttp = { module = "io.ktor:ktor-client-winhttp", version.ref = "ktor" }
+kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
+multiplatformSettings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" }
+compose-compiler = { module = "org.jetbrains.compose.compiler:compiler", version.ref = "compose-compiler" }
+ktor-client-serialization = { module = "io.ktor:ktor-client-serialization-jvm", version.ref = "ktor" }
+ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
+ktor-client-json = { module = "io.ktor:ktor-client-json", version.ref = "ktor" }
+androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" }
+
+[plugins]
+
+multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
+compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
+compose = { id = "org.jetbrains.compose", version.ref = "compose" }
+android-application = { id = "com.android.application", version.ref = "agp" }
+buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig" }
+kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
\ No newline at end of file
diff --git a/server/xefMobile/gradle/wrapper/gradle-wrapper.jar b/server/xefMobile/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..e6441136f
Binary files /dev/null and b/server/xefMobile/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/server/xefMobile/gradle/wrapper/gradle-wrapper.properties b/server/xefMobile/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..668e6a6b6
--- /dev/null
+++ b/server/xefMobile/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/server/xefMobile/gradlew b/server/xefMobile/gradlew
new file mode 100755
index 000000000..ad93365b1
--- /dev/null
+++ b/server/xefMobile/gradlew
@@ -0,0 +1,250 @@
+
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
+# and any embedded shellness will be escaped.
+# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
+# treated as '${Hostname}' itself on the command line.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/server/xefMobile/gradlew.bat b/server/xefMobile/gradlew.bat
new file mode 100644
index 000000000..5c2a58c72
--- /dev/null
+++ b/server/xefMobile/gradlew.bat
@@ -0,0 +1,93 @@
+
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/server/xefMobile/settings.gradle.kts b/server/xefMobile/settings.gradle.kts
new file mode 100644
index 000000000..799f08823
--- /dev/null
+++ b/server/xefMobile/settings.gradle.kts
@@ -0,0 +1,17 @@
+rootProject.name = "xefMobile"
+include(":composeApp")
+
+pluginManagement {
+ repositories {
+ google()
+ gradlePluginPortal()
+ mavenCentral()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}