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() + } +}