Reworked and improved in fact almost all
This commit is contained in:
parent
fa96c879ae
commit
9d905564c2
|
@ -27,20 +27,24 @@ repositories {
|
|||
dependencies {
|
||||
implementation(project(":common"))
|
||||
implementation("androidx.activity:activity-compose:1.7.2")
|
||||
//implementation("androidx.activity:activity-compose:1.8.0") // for SDK Version 34
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdk = 33
|
||||
defaultConfig {
|
||||
applicationId = "technology.iatlas.spaceup.android"
|
||||
minSdk = 24
|
||||
targetSdk = 33
|
||||
minSdk = 28
|
||||
targetSdk = 33
|
||||
versionCode = 1
|
||||
versionName = "1.0-SNAPSHOT"
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
buildTypes {
|
||||
getByName("release") {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<application
|
||||
android:icon="@mipmap/launcher_icon"
|
||||
android:label="SpaceUp-NextUI"
|
||||
android:supportsRtl="true"
|
||||
android:usesCleartextTraffic="true"
|
||||
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
|
||||
android:theme="@style/Theme.AppCompat.DayNight">
|
||||
<activity android:name=".MainActivity" android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
|
@ -20,6 +21,5 @@
|
|||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<!--uses-permission android:name="android.permission.USE_BIOMETRIC" /-->
|
||||
</manifest>
|
|
@ -1,19 +1,26 @@
|
|||
package technology.iatlas.spaceup.android
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.compose.material3.Surface
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.core.view.WindowCompat
|
||||
import moe.tlaster.precompose.lifecycle.PreComposeActivity
|
||||
import moe.tlaster.precompose.lifecycle.setContent
|
||||
import moe.tlaster.precompose.PreComposeApp
|
||||
import technology.iatlas.spaceup.common.App
|
||||
|
||||
class MainActivity : PreComposeActivity() {
|
||||
class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// add FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS flag to the window
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
|
||||
|
||||
// Turn off the decor fitting system windows
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
setContent {
|
||||
App()
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false);
|
||||
setContent{
|
||||
PreComposeApp {
|
||||
App()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ plugins {
|
|||
kotlin("multiplatform")
|
||||
id("org.jetbrains.compose")
|
||||
id("com.android.library")
|
||||
kotlin("plugin.serialization") version "1.9.10"
|
||||
kotlin("plugin.serialization") version "1.9.20"
|
||||
}
|
||||
|
||||
group = "technology.iatlas.spaceup"
|
||||
|
@ -22,12 +22,15 @@ repositories {
|
|||
maven {
|
||||
url = uri("https://repo1.maven.org/maven2/")
|
||||
}
|
||||
maven { url = uri("https://jitpack.io") }
|
||||
}
|
||||
|
||||
kotlin {
|
||||
android()
|
||||
androidTarget {
|
||||
jvmToolchain(17)
|
||||
}
|
||||
jvm("desktop") {
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
}
|
||||
sourceSets {
|
||||
val commonMain by getting {
|
||||
|
@ -39,11 +42,19 @@ kotlin {
|
|||
api(compose.materialIconsExtended)
|
||||
|
||||
// ViewModel & Navigation
|
||||
api("moe.tlaster:precompose:1.3.15")
|
||||
val precomposeVersion = "1.5.7"
|
||||
api("moe.tlaster:precompose:$precomposeVersion")
|
||||
api("moe.tlaster:precompose-viewmodel:$precomposeVersion")
|
||||
api("moe.tlaster:precompose-koin:$precomposeVersion")
|
||||
api("io.insert-koin:koin-core:3.5.0")
|
||||
api("io.insert-koin:koin-compose:1.1.0")
|
||||
|
||||
// JWT
|
||||
implementation("io.github.nefilim.kjwt:kjwt-core:0.9.0")
|
||||
|
||||
// Google
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
|
||||
implementation("androidx.compose.material3:material3:1.1.1")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||
implementation("androidx.compose.material3:material3:1.1.2")
|
||||
implementation("androidx.compose.material:material-icons-extended:1.4.3")
|
||||
|
||||
val dataStoreVersion = "1.1.0-alpha04"
|
||||
|
@ -93,6 +104,17 @@ kotlin {
|
|||
implementation("io.ktor:ktor-client-json-jvm:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-okhttp:$ktorVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
|
||||
|
||||
implementation("org.jetbrains.skiko:skiko-android:0.7.80")
|
||||
|
||||
// Lets-Plot Kotlin API
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-kotlin-kernel:4.4.3")
|
||||
|
||||
// Lets-Plot Multiplatform
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-common:4.0.1")
|
||||
|
||||
// Lets-Plot Skia Frontend
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-compose:1.0.0")
|
||||
}
|
||||
}
|
||||
val androidUnitTest by getting {
|
||||
|
@ -107,6 +129,16 @@ kotlin {
|
|||
api(compose.material3)
|
||||
// https://mvnrepository.com/artifact/ch.qos.logback/logback-classic
|
||||
implementation("ch.qos.logback:logback-classic:1.4.11")
|
||||
|
||||
// Lets-Plot Kotlin API
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-kotlin-kernel:4.4.3")
|
||||
|
||||
// Lets-Plot Multiplatform
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-common:4.0.1")
|
||||
implementation("org.jetbrains.lets-plot:platf-awt:4.0.1")
|
||||
|
||||
// Lets-Plot Skia Frontend
|
||||
implementation("org.jetbrains.lets-plot:lets-plot-compose:1.0.0")
|
||||
}
|
||||
}
|
||||
val desktopTest by getting
|
||||
|
@ -117,11 +149,11 @@ android {
|
|||
compileSdk = 33
|
||||
sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
|
||||
defaultConfig {
|
||||
minSdk = 24
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
minSdk = 28
|
||||
}
|
||||
namespace = "technology.iatlas.spaceup.common"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation("io.ktor:ktor-client-logging-jvm:2.3.4")
|
||||
implementation("androidx.compose.ui:ui-geometry-android:1.5.4")
|
||||
}
|
||||
|
|
|
@ -8,6 +8,10 @@ actual fun getPlatformName(): String {
|
|||
}
|
||||
|
||||
@Composable
|
||||
actual fun openInBrowser(domain: String) {
|
||||
actual fun OpenInBrowser(domain: String) {
|
||||
LocalUriHandler.current.openUri(domain)
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun notify(title: String, body: String, type: String) {
|
||||
}
|
|
@ -6,11 +6,9 @@ import io.ktor.client.plugins.*
|
|||
import io.ktor.client.plugins.auth.*
|
||||
import io.ktor.client.plugins.auth.providers.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.serialization.gson.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
|
||||
actual fun httpClient(bearerToken: String): HttpClient {
|
||||
actual fun httpClient(token: String): HttpClient {
|
||||
return HttpClient(Android) {
|
||||
/*install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
|
@ -22,12 +20,13 @@ actual fun httpClient(bearerToken: String): HttpClient {
|
|||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
BearerTokens(bearerToken, "")
|
||||
BearerTokens(token, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 20000 // Webbackend list, Get domains take rather long
|
||||
// 60 secs
|
||||
requestTimeoutMillis = 60000 // Webbackend list, Get domains take rather long
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,94 +1,75 @@
|
|||
package technology.iatlas.spaceup.common
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Outline
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.LayoutDirection
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.set
|
||||
import io.github.nefilim.kjwt.JWT
|
||||
import moe.tlaster.precompose.navigation.NavHost
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
import moe.tlaster.precompose.navigation.RouteBuilder
|
||||
import moe.tlaster.precompose.navigation.rememberNavigator
|
||||
import moe.tlaster.precompose.navigation.transition.NavTransition
|
||||
import moe.tlaster.precompose.ui.LocalViewModelStoreOwner
|
||||
import moe.tlaster.precompose.ui.viewModel
|
||||
import moe.tlaster.precompose.viewmodel.ViewModelStoreOwner
|
||||
import technology.iatlas.spaceup.common.components.Drawer
|
||||
import technology.iatlas.spaceup.common.model.Routes
|
||||
import technology.iatlas.spaceup.common.model.SettingsConstants
|
||||
import technology.iatlas.spaceup.common.pages.DomainsView
|
||||
import technology.iatlas.spaceup.common.theme.AppTheme
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
import technology.iatlas.spaceup.common.views.Home
|
||||
import technology.iatlas.spaceup.common.views.HomeView
|
||||
import technology.iatlas.spaceup.common.views.Login
|
||||
import technology.iatlas.spaceup.common.views.SettingsView
|
||||
import java.util.*
|
||||
|
||||
@Composable
|
||||
fun App(useDarkTheme: Boolean = isSystemInDarkTheme()) {
|
||||
AppTheme(useDarkTheme = useDarkTheme) {
|
||||
val navigator = rememberNavigator()
|
||||
val viewModelStoreOwner = LocalViewModelStoreOwner.current
|
||||
|
||||
val serverViewModel = viewModel(ServerViewModel::class) {
|
||||
ServerViewModel()
|
||||
val settings = Settings()
|
||||
val accesstoken = settings.getString(SettingsConstants.ACCESS_TOKEN.toString(), "")
|
||||
var startPath by remember { mutableStateOf(Routes.LOGIN.path) }
|
||||
|
||||
var isValidJWT = false
|
||||
JWT.decode(accesstoken).tap { d ->
|
||||
d.expiresAt().tap { isValidJWT = it.isAfter(Date().toInstant()) }
|
||||
}
|
||||
startPath = if(isValidJWT) Routes.HOME.path else Routes.LOGIN.path
|
||||
|
||||
NavHost(
|
||||
navigator = navigator,
|
||||
initialRoute = if(serverViewModel.token.accessToken.isNotEmpty()) Routes.HOME.path else Routes.LOGIN.path
|
||||
initialRoute = startPath
|
||||
) {
|
||||
scene(route = Routes.LOGIN.path, navigation = navigator, viewModelStoreOwner = viewModelStoreOwner) {
|
||||
scene(route = Routes.LOGIN.path, navigation = navigator) {
|
||||
Login(it)
|
||||
}
|
||||
scene(route = Routes.LOGOUT.path, navigation = navigator, viewModelStoreOwner = viewModelStoreOwner) {
|
||||
scene(route = Routes.LOGOUT.path, navigation = navigator) {
|
||||
settings[SettingsConstants.ACCESS_TOKEN.toString()] = ""
|
||||
Login(it)
|
||||
}
|
||||
sceneWithDrawer(route = Routes.HOME.path, navigation = navigator, viewModelStoreOwner = viewModelStoreOwner) {
|
||||
Home(it)
|
||||
sceneWithDrawer(route = Routes.SETTINGS.path, navigation = navigator, canGoBack = true) {
|
||||
SettingsView()
|
||||
}
|
||||
sceneWithDrawer(route = Routes.SETTINGS.path, navigation = navigator, viewModelStoreOwner = viewModelStoreOwner) {
|
||||
SettingsView(it)
|
||||
sceneWithDrawer(
|
||||
route = Routes.HOME.path, navigation = navigator, canGoBack = false
|
||||
) {
|
||||
HomeView()
|
||||
}
|
||||
sceneWithDrawer(
|
||||
route = Routes.DOMAINS.path, navigation = navigator, canGoBack = true
|
||||
) {
|
||||
DomainsView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,185 +77,32 @@ fun App(useDarkTheme: Boolean = isSystemInDarkTheme()) {
|
|||
|
||||
fun RouteBuilder.scene(
|
||||
navigation: Navigator,
|
||||
viewModelStoreOwner: ViewModelStoreOwner,
|
||||
route: String,
|
||||
deepLinks: List<String> = emptyList(),
|
||||
navTransition: NavTransition? = null,
|
||||
content: @Composable (navigation: Navigator) -> Unit,
|
||||
) {
|
||||
scene(route = route, deepLinks = deepLinks, navTransition = navTransition) {
|
||||
CompositionLocalProvider(
|
||||
LocalViewModelStoreOwner provides viewModelStoreOwner,
|
||||
) {
|
||||
content(navigation)
|
||||
}
|
||||
content(navigation)
|
||||
}
|
||||
}
|
||||
|
||||
fun RouteBuilder.sceneWithDrawer(
|
||||
navigation: Navigator,
|
||||
viewModelStoreOwner: ViewModelStoreOwner,
|
||||
route: String,
|
||||
deepLinks: List<String> = emptyList(),
|
||||
navTransition: NavTransition? = null,
|
||||
canGoBack: Boolean,
|
||||
content: @Composable (navigation: Navigator) -> Unit,
|
||||
) {
|
||||
|
||||
) {
|
||||
scene(route = route, deepLinks = deepLinks, navTransition = navTransition) {
|
||||
CompositionLocalProvider(
|
||||
LocalViewModelStoreOwner provides viewModelStoreOwner,
|
||||
) {
|
||||
Drawer(navigation) {
|
||||
content(navigation)
|
||||
}
|
||||
Drawer(navigation, canGoBack) {
|
||||
content(navigation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Drawer(
|
||||
navigation: Navigator,
|
||||
content: @Composable (navigation: Navigator) -> Unit
|
||||
) {
|
||||
val coroutine = rememberCoroutineScope()
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
|
||||
val serverViewModel = viewModel(ServerViewModel::class) {
|
||||
ServerViewModel()
|
||||
}
|
||||
|
||||
val drawerList = Routes.values().toList()
|
||||
ModalNavigationDrawer(
|
||||
content = {
|
||||
Scaffold(
|
||||
//modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
alpha = 1.0f,
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Gray,
|
||||
Color( 0, 128, 128), // Teal
|
||||
Color.Gray
|
||||
),
|
||||
//startX = 10.0f,
|
||||
//endX = 20.0f
|
||||
)
|
||||
)
|
||||
.padding(0.dp),
|
||||
colors = TopAppBarDefaults.smallTopAppBarColors(containerColor = Color.Transparent),
|
||||
title = { Text("NextUI") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
coroutine.launch {
|
||||
drawerState.open()
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
|
||||
}) {
|
||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(modifier = Modifier
|
||||
//.fillMaxSize()
|
||||
.padding(top = it.calculateTopPadding())
|
||||
) {
|
||||
content.invoke(navigation)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(320.dp)
|
||||
.background(MaterialTheme.colorScheme.onPrimary)
|
||||
) {
|
||||
if(serverViewModel.token.accessToken.isNotEmpty()) {
|
||||
Card(
|
||||
shape = NavShape(0.dp, 1.0f),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
modifier = Modifier
|
||||
.height(30.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
serverViewModel.serverUrl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
Card(
|
||||
shape = NavShape(0.dp, 1.0f),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
modifier = Modifier
|
||||
.height(30.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
serverViewModel.expiresAsString,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.width(240.dp)
|
||||
.padding(start = 24.dp, top = 8.dp)
|
||||
.fillMaxHeight()
|
||||
//.padding(start = 24.dp, top = 48.dp),
|
||||
) {
|
||||
drawerList.forEach {
|
||||
val route = it.path
|
||||
val menuItem = it.title
|
||||
val drawerBehavior = it.drawerBehavior
|
||||
|
||||
it.apply {
|
||||
if (drawerBehavior.isVisible) {
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
try {
|
||||
navigation.navigate(route)
|
||||
} catch (ex: IllegalStateException) {
|
||||
// NOP Do nothing currently if there is no route
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
menuItem,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
if (drawerBehavior.hasAfterDivider) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
class NavShape(
|
||||
private val widthOffset: Dp,
|
||||
private val scale: Float
|
||||
|
|
|
@ -1,18 +1,21 @@
|
|||
package technology.iatlas.spaceup.common.components
|
||||
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.AlertDialog
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun Alert(openDialog: MutableState<Boolean>, errorMsg: String) {
|
||||
fun ErrorAlert(openDialog: MutableState<Boolean>, errorMsg: String) {
|
||||
val scroll = rememberScrollState(0)
|
||||
AlertDialog(
|
||||
backgroundColor = MaterialTheme.colorScheme.errorContainer,
|
||||
onDismissRequest = {
|
||||
openDialog.value = false
|
||||
},
|
||||
|
@ -32,7 +35,11 @@ fun Alert(openDialog: MutableState<Boolean>, errorMsg: String) {
|
|||
val baseMsg = "An error occurred: \n%s"
|
||||
val msg = if(errorMsg.isEmpty()) baseMsg.replace(": %s", "")
|
||||
else baseMsg.replace("%s", errorMsg)
|
||||
Text(msg)
|
||||
Text(
|
||||
text = msg,
|
||||
modifier = Modifier
|
||||
.verticalScroll(scroll)
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
|
|
@ -0,0 +1,207 @@
|
|||
package technology.iatlas.spaceup.common.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Search
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.DrawerValue
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberDrawerState
|
||||
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.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tlaster.precompose.koin.koinViewModel
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
import moe.tlaster.precompose.stateholder.LocalSavedStateHolder
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import technology.iatlas.spaceup.common.NavShape
|
||||
import technology.iatlas.spaceup.common.model.Routes
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Drawer(
|
||||
navigation: Navigator,
|
||||
canGoBack: Boolean = true,
|
||||
content: @Composable (navigation: Navigator) -> Unit
|
||||
) {
|
||||
val coroutine = rememberCoroutineScope()
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
|
||||
val stateHolder = LocalSavedStateHolder.current
|
||||
val serverViewModel = koinViewModel(ServerViewModel::class) { parametersOf(stateHolder) }
|
||||
|
||||
val serverUrl = serverViewModel.serverUrl
|
||||
val token = serverViewModel.token.value
|
||||
|
||||
val drawerList = Routes.entries
|
||||
ModalNavigationDrawer(
|
||||
content = {
|
||||
Scaffold(
|
||||
//modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
modifier = Modifier
|
||||
.background(
|
||||
alpha = 1.0f,
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
Color.Gray,
|
||||
Color(0, 128, 128), // Teal
|
||||
Color.Gray
|
||||
),
|
||||
//startX = 10.0f,
|
||||
//endX = 20.0f
|
||||
)
|
||||
)
|
||||
.padding(0.dp),
|
||||
colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent),
|
||||
title = { Text("NextUI") },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = {
|
||||
coroutine.launch {
|
||||
if (canGoBack) {
|
||||
navigation.popBackStack()
|
||||
} else {
|
||||
drawerState.open()
|
||||
}
|
||||
}
|
||||
}) {
|
||||
if (canGoBack) {
|
||||
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
|
||||
} else {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = {
|
||||
|
||||
}) {
|
||||
Icon(Icons.Default.Search, contentDescription = "Search")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
//.fillMaxSize()
|
||||
.padding(top = it.calculateTopPadding())
|
||||
) {
|
||||
content.invoke(navigation)
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.width(320.dp)
|
||||
) {
|
||||
Card(
|
||||
shape = NavShape(0.dp, 1.0f),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
serverUrl,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
if (token.isNotEmpty()) {
|
||||
Card(
|
||||
shape = NavShape(0.dp, 1.0f),
|
||||
elevation = CardDefaults.elevatedCardElevation(),
|
||||
modifier = Modifier
|
||||
.height(30.dp)
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
serverViewModel.expiresAsString,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center,
|
||||
modifier = Modifier
|
||||
.padding(10.dp, 0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.width(240.dp)
|
||||
.padding(start = 24.dp, top = 8.dp)
|
||||
.fillMaxHeight()
|
||||
//.padding(start = 24.dp, top = 48.dp),
|
||||
) {
|
||||
drawerList.forEach {
|
||||
val route = it.path
|
||||
val menuItem = it.title
|
||||
val drawerBehavior = it.drawerBehavior
|
||||
|
||||
it.apply {
|
||||
if (drawerBehavior.isVisible) {
|
||||
item {
|
||||
Spacer(Modifier.height(4.dp))
|
||||
TextButton(
|
||||
onClick = {
|
||||
try {
|
||||
navigation.navigate(route)
|
||||
} catch (ex: IllegalStateException) {
|
||||
// NOP Do nothing currently if there is no route
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
menuItem,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
if (drawerBehavior.hasAfterDivider) {
|
||||
Divider()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package technology.iatlas.spaceup.common.model
|
||||
|
||||
data class Hostname(val hostname: String)
|
|
@ -5,7 +5,7 @@ enum class Routes(
|
|||
val path: String,
|
||||
val drawerBehavior: DrawerBehavior = DrawerBehavior(),
|
||||
) {
|
||||
HOME("Home", "/home"),
|
||||
HOME("Home", "/home", DrawerBehavior(isVisible = false)),
|
||||
DOMAINS("Domains", "/domains"),
|
||||
SERVICES("Services", "/services"),
|
||||
WEBBACKENDS("Web backends", "/webbackends"),
|
||||
|
@ -14,16 +14,6 @@ enum class Routes(
|
|||
ABOUT("About", "/about", DrawerBehavior(hasAfterDivider = true)),
|
||||
LOGOUT("Logout", "/logout"),
|
||||
LOGIN("Login", "/login", DrawerBehavior(isVisible = false)),
|
||||
|
||||
// TODO DELETE me if test is successful
|
||||
// For testing Drawer behavior
|
||||
fake1("Logout", "/a"),
|
||||
fake2("Logout", "/b"),
|
||||
fake3("Logout", "/c"),
|
||||
fake4("Logout", "/d"),
|
||||
fake5("Logout", "/e"),
|
||||
fake6("Logout", "/f"),
|
||||
fake7("Logout", "/g"),
|
||||
}
|
||||
|
||||
data class DrawerBehavior(
|
||||
|
|
|
@ -10,7 +10,8 @@ enum class SettingsConstants(
|
|||
REMEMBER_CREDENTIALS("rememberCredentials", "Remember Credentials", "boolean"),
|
||||
USERNAME("username", "Username", "string"),
|
||||
PASSWORD("password", "Password", "string"),
|
||||
AUTO_LOGIN("autoLogin", "Auto Login", "boolean");
|
||||
AUTO_LOGIN("autoLogin", "Auto Login", "boolean"),
|
||||
ACCESS_TOKEN("token", "", "string");
|
||||
|
||||
override fun toString(): String {
|
||||
return naming
|
||||
|
|
|
@ -0,0 +1,280 @@
|
|||
package technology.iatlas.spaceup.common.pages
|
||||
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.drawText
|
||||
import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.min
|
||||
|
||||
@Composable
|
||||
fun DiskPlot() {
|
||||
// TODO use real data from API
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally),
|
||||
text = "Disk Plot"
|
||||
)
|
||||
DonutChart(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally),
|
||||
colors = listOf(MaterialTheme.colorScheme.inversePrimary, MaterialTheme.colorScheme.secondary),
|
||||
inputValues = listOf(10f, 20f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for creating Donut Chart
|
||||
* Slices are painted clockwise
|
||||
* e.g. 1st input value starts from top to the right, etc
|
||||
*/
|
||||
|
||||
private const val animationDuration = 800
|
||||
private const val chartDegrees = 360f
|
||||
private const val emptyIndex = -1
|
||||
private val defaultSliceWidth = 12.dp
|
||||
private val defaultSlicePadding = 5.dp
|
||||
private val defaultSliceClickPadding = 8.dp
|
||||
|
||||
@Composable
|
||||
internal fun DonutChart(
|
||||
modifier: Modifier = Modifier,
|
||||
colors: List<Color>,
|
||||
inputValues: List<Float>,
|
||||
sliceWidthDp: Dp = defaultSliceWidth,
|
||||
slicePaddingDp: Dp = defaultSlicePadding,
|
||||
sliceClickPaddingDp: Dp = defaultSliceClickPadding,
|
||||
animated: Boolean = true
|
||||
) {
|
||||
|
||||
val textMeasurer = rememberTextMeasurer()
|
||||
val maxDiskColor = MaterialTheme.colorScheme.primary
|
||||
val usedDiskSpaceColor = MaterialTheme.colorScheme.secondary
|
||||
val freeDiskSpaceColor = MaterialTheme.colorScheme.inverseSurface
|
||||
|
||||
assert(inputValues.isNotEmpty() && inputValues.size == colors.size) {
|
||||
"Input values count must be equal to colors size"
|
||||
}
|
||||
|
||||
// disk text
|
||||
val text = buildAnnotatedString {
|
||||
// Max disk space
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = maxDiskColor,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
) {
|
||||
append("Max disk space:\n")
|
||||
}
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = maxDiskColor,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
) {
|
||||
append("10GB\n")
|
||||
append("\n")
|
||||
}
|
||||
|
||||
// Used disk space
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = usedDiskSpaceColor,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
) {
|
||||
append("Used disk space:\n")
|
||||
}
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = usedDiskSpaceColor,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
) {
|
||||
append("8GB\n")
|
||||
}
|
||||
// Free disk space
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = freeDiskSpaceColor,
|
||||
fontSize = 22.sp
|
||||
)
|
||||
) {
|
||||
append("Free disk space: \n")
|
||||
}
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = freeDiskSpaceColor,
|
||||
fontSize = 18.sp
|
||||
)
|
||||
) {
|
||||
append("2GB\n")
|
||||
}
|
||||
}
|
||||
|
||||
// calculate each input percentage
|
||||
val proportions = inputValues.toPercent()
|
||||
|
||||
// calculate each input slice degrees
|
||||
val angleProgress = proportions.map { prop ->
|
||||
chartDegrees * prop / 100
|
||||
}
|
||||
|
||||
// start drawing clockwise (top to right)
|
||||
var startAngle = 270f
|
||||
|
||||
// used for animating each slice
|
||||
val pathPortion = remember {
|
||||
Animatable(initialValue = 0f)
|
||||
}
|
||||
|
||||
// clicked slice in chart
|
||||
var clickedItemIndex by remember {
|
||||
mutableStateOf(emptyIndex)
|
||||
}
|
||||
|
||||
// calculate each slice end point in degrees, for handling click position
|
||||
val progressSize = mutableListOf<Float>()
|
||||
|
||||
LaunchedEffect(angleProgress) {
|
||||
progressSize.add(angleProgress.first())
|
||||
for (x in 1 until angleProgress.size) {
|
||||
progressSize.add(angleProgress[x] + progressSize[x - 1])
|
||||
}
|
||||
}
|
||||
|
||||
val density = LocalDensity.current
|
||||
|
||||
//convert dp values to pixels
|
||||
val sliceWidthPx = with(density) { sliceWidthDp.toPx() }
|
||||
val slicePaddingPx = with(density) { slicePaddingDp.toPx() }
|
||||
val sliceClickPaddingPx = with(density) { sliceClickPaddingDp.toPx() }
|
||||
|
||||
// slice width when clicked
|
||||
val selectedSliceWidth = sliceWidthPx + sliceClickPaddingPx
|
||||
|
||||
// animate chart slices on composition
|
||||
LaunchedEffect(inputValues) {
|
||||
pathPortion.animateTo(1f, animationSpec = tween(if (animated) animationDuration else 0))
|
||||
}
|
||||
|
||||
BoxWithConstraints(modifier = modifier, contentAlignment = Alignment.Center) {
|
||||
|
||||
val canvasSize = min(constraints.maxWidth, constraints.maxHeight)
|
||||
val padding = canvasSize * slicePaddingPx / 100f
|
||||
val size = Size(canvasSize.toFloat() - padding, canvasSize.toFloat() - padding)
|
||||
val canvasSizeDp = with(density) { canvasSize.toDp() }
|
||||
|
||||
Canvas(
|
||||
modifier = Modifier
|
||||
.size(canvasSizeDp)
|
||||
.align(Alignment.Center)
|
||||
/*.pointerInput(inputValues) {
|
||||
|
||||
detectTapGestures { offset ->
|
||||
val clickedAngle = touchPointToAngle(
|
||||
width = canvasSize.toFloat(),
|
||||
height = canvasSize.toFloat(),
|
||||
touchX = offset.x,
|
||||
touchY = offset.y,
|
||||
chartDegrees = chartDegrees
|
||||
)
|
||||
progressSize.forEachIndexed { index, item ->
|
||||
if (clickedAngle <= item) {
|
||||
clickedItemIndex = index
|
||||
return@detectTapGestures
|
||||
}
|
||||
}
|
||||
}
|
||||
},*/
|
||||
) {
|
||||
|
||||
angleProgress.forEachIndexed { index, angle ->
|
||||
drawArc(
|
||||
color = colors[index],
|
||||
startAngle = startAngle,
|
||||
sweepAngle = angle * pathPortion.value,
|
||||
useCenter = false,
|
||||
size = size,
|
||||
style = Stroke(width = if (clickedItemIndex == index) selectedSliceWidth else sliceWidthPx),
|
||||
topLeft = Offset(padding / 2, padding / 2),
|
||||
)
|
||||
startAngle += angle
|
||||
}
|
||||
|
||||
val canvasWidth = size.width
|
||||
val canvasHeight = size.height
|
||||
val textLayoutResult = textMeasurer.measure(text)
|
||||
val textSize = textLayoutResult.size
|
||||
|
||||
drawText(
|
||||
textMeasurer = textMeasurer,
|
||||
text = text,
|
||||
topLeft = Offset(
|
||||
(canvasWidth - textSize.width) / 2f,
|
||||
(canvasHeight - textSize.height) / 2f
|
||||
)
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal fun List<Float>.toPercent(): List<Float> {
|
||||
return this.map { item ->
|
||||
item * 100 / this.sum()
|
||||
}
|
||||
}
|
||||
|
||||
internal fun touchPointToAngle(
|
||||
width: Float,
|
||||
height: Float,
|
||||
touchX: Float,
|
||||
touchY: Float,
|
||||
chartDegrees: Float
|
||||
): Double {
|
||||
val x = touchX - (width * 0.5f)
|
||||
val y = touchY - (height * 0.5f)
|
||||
var angle = Math.toDegrees(atan2(y.toDouble(), x.toDouble()) + Math.PI / 2)
|
||||
angle = if (angle < 0) angle + chartDegrees else angle
|
||||
return angle
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package technology.iatlas.spaceup.common.views
|
||||
package technology.iatlas.spaceup.common.pages
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
|
@ -32,13 +32,13 @@ import androidx.compose.runtime.getValue
|
|||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.russhwolf.settings.Settings
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
|
@ -46,29 +46,39 @@ import io.ktor.http.*
|
|||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
import moe.tlaster.precompose.ui.viewModel
|
||||
import technology.iatlas.spaceup.common.components.Alert
|
||||
import moe.tlaster.precompose.koin.koinViewModel
|
||||
import moe.tlaster.precompose.stateholder.LocalSavedStateHolder
|
||||
import technology.iatlas.spaceup.common.OpenInBrowser
|
||||
import technology.iatlas.spaceup.common.components.ErrorAlert
|
||||
import technology.iatlas.spaceup.common.components.FullscreenCircularLoader
|
||||
import technology.iatlas.spaceup.common.model.Domain
|
||||
import technology.iatlas.spaceup.common.openInBrowser
|
||||
import technology.iatlas.spaceup.common.model.SettingsConstants
|
||||
import technology.iatlas.spaceup.common.util.httpClient
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun Home(navigator: Navigator) {
|
||||
fun DomainsView() {
|
||||
val logger = KotlinLogging.logger {}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val cached = remember { mutableStateOf(false) }
|
||||
|
||||
val settings = Settings()
|
||||
|
||||
val cached = remember { mutableStateOf(true) }
|
||||
var isLoading by remember { mutableStateOf(false) }
|
||||
val domains = remember { mutableStateListOf<Domain>() }
|
||||
val openDialog = remember { mutableStateOf(false) }
|
||||
val errorMsg = remember { mutableStateOf("") }
|
||||
var isEnabled by remember { mutableStateOf(true) }
|
||||
|
||||
val serverViewModel = viewModel(ServerViewModel::class) {
|
||||
ServerViewModel()
|
||||
}
|
||||
val stateHolder = LocalSavedStateHolder.current
|
||||
val serverViewModel = koinViewModel(ServerViewModel::class) { org.koin.core.parameter.parametersOf(stateHolder) }
|
||||
val serverUrl = serverViewModel.serverUrl
|
||||
|
||||
val client = httpClient(settings.getString(SettingsConstants.ACCESS_TOKEN.toString(), ""))
|
||||
|
||||
// Create grid view layout
|
||||
|
||||
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
|
@ -81,8 +91,8 @@ fun Home(navigator: Navigator) {
|
|||
isLoading = true
|
||||
isEnabled = false
|
||||
try {
|
||||
val response = serverViewModel.client()
|
||||
.get("${serverViewModel.serverUrl}/api/domain/list?cached=${cached.value}")
|
||||
val response = client
|
||||
.get("${serverUrl}/api/domain/list?cached=${cached.value}")
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
val domainList = response.body<List<Domain>>()
|
||||
logger.info { "Received domains: $domainList" }
|
||||
|
@ -93,7 +103,7 @@ fun Home(navigator: Navigator) {
|
|||
}
|
||||
} else {
|
||||
// Show error
|
||||
errorMsg.value = response.body()
|
||||
errorMsg.value = response.status.description
|
||||
openDialog.value = true
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
@ -122,7 +132,7 @@ fun Home(navigator: Navigator) {
|
|||
}
|
||||
|
||||
if (openDialog.value) {
|
||||
Alert(openDialog, serverViewModel.serverUrl)
|
||||
ErrorAlert(openDialog, errorMsg.value)
|
||||
}
|
||||
|
||||
if (!cached.value && isLoading) {
|
||||
|
@ -135,10 +145,8 @@ fun Home(navigator: Navigator) {
|
|||
|
||||
LaunchedEffect(Unit) {
|
||||
try {
|
||||
// TODO move this to domainViewModel
|
||||
// "Authorization": 'Bearer $jwt'
|
||||
val response = serverViewModel.client()
|
||||
.get("${serverViewModel.serverUrl}/api/domain/list?cached=true")
|
||||
val response = client
|
||||
.get("${serverUrl}/api/domain/list?cached=${cached.value}")
|
||||
if (response.status == HttpStatusCode.OK) {
|
||||
response.body<List<Domain>>().forEach {
|
||||
if (!domains.contains(it)) {
|
||||
|
@ -147,7 +155,7 @@ fun Home(navigator: Navigator) {
|
|||
}
|
||||
} else {
|
||||
// Show error
|
||||
errorMsg.value = response.body()
|
||||
errorMsg.value = response.status.description
|
||||
openDialog.value = true
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
@ -172,7 +180,7 @@ fun MessageList(domains: List<Domain>) {
|
|||
items(domains.size) { index ->
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.height(60.dp)
|
||||
.height(35.dp)
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
|
@ -182,53 +190,44 @@ fun MessageList(domains: List<Domain>) {
|
|||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Start,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
Text(
|
||||
modifier = Modifier.padding(start = 12.dp),
|
||||
text = domains[index].url,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
IconButton(
|
||||
modifier = Modifier.align(Alignment.CenterVertically),
|
||||
onClick = {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
) {
|
||||
Text(
|
||||
text = domains[index].url,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
//modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
expanded.value = !expanded.value
|
||||
}
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
"More"
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded.value,
|
||||
onDismissRequest = { expanded.value = false },
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
"More"
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = expanded.value,
|
||||
onDismissRequest = { expanded.value = false },
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text("Open") },
|
||||
onClick = {
|
||||
domain = domains[index].url
|
||||
})
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
"Delete",
|
||||
style = TextStyle(color = MaterialTheme.colorScheme.error)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
// Delete Domain with Alert warning
|
||||
})
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text("Open") },
|
||||
onClick = {
|
||||
domain = domains[index].url
|
||||
})
|
||||
Divider()
|
||||
DropdownMenuItem(
|
||||
text = {
|
||||
Text(
|
||||
"Delete",
|
||||
style = TextStyle(color = MaterialTheme.colorScheme.error)
|
||||
)
|
||||
},
|
||||
onClick = {
|
||||
// Delete Domain with Alert warning
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -238,7 +237,7 @@ fun MessageList(domains: List<Domain>) {
|
|||
}
|
||||
|
||||
if(domain.isNotEmpty()) {
|
||||
openInBrowser("https://${domain}").also {
|
||||
OpenInBrowser("https://${domain}").also {
|
||||
domain = ""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,188 @@
|
|||
package technology.iatlas.spaceup.common.pages
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Api
|
||||
import androidx.compose.material.icons.filled.CloudCircle
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawBehind
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.drawscope.translate
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.set
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
import io.ktor.util.*
|
||||
import moe.tlaster.precompose.koin.koinViewModel
|
||||
import moe.tlaster.precompose.navigation.rememberNavigator
|
||||
import moe.tlaster.precompose.stateholder.LocalSavedStateHolder
|
||||
import technology.iatlas.spaceup.common.model.Hostname
|
||||
import technology.iatlas.spaceup.common.model.Routes
|
||||
import technology.iatlas.spaceup.common.model.SettingsConstants
|
||||
import technology.iatlas.spaceup.common.util.httpClient
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
import java.net.ConnectException
|
||||
|
||||
@OptIn(InternalAPI::class)
|
||||
@Composable
|
||||
fun Server() {
|
||||
val logger = KotlinLogging.logger { }
|
||||
|
||||
val settings = Settings()
|
||||
val navigator = rememberNavigator()
|
||||
|
||||
val serverVersion = remember { mutableStateOf("") }
|
||||
val hostname = remember { mutableStateOf("") }
|
||||
|
||||
val stateHolder = LocalSavedStateHolder.current
|
||||
val serverViewModel = koinViewModel(ServerViewModel::class) { org.koin.core.parameter.parametersOf(stateHolder) }
|
||||
|
||||
val serverUrl = serverViewModel.serverUrl
|
||||
|
||||
Column(
|
||||
modifier = Modifier.padding(vertical = 8.dp),
|
||||
verticalArrangement = Arrangement.SpaceBetween,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.height(35.dp)
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
TextLineIcon(
|
||||
text = "Server Version: ${serverVersion.value}",
|
||||
icon = Icons.Default.Api,
|
||||
iconTint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.height(35.dp)
|
||||
.padding(4.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
TextLineIcon(
|
||||
text = "Hostname: ${hostname.value}",
|
||||
icon = Icons.Default.CloudCircle,
|
||||
iconTint = MaterialTheme.colorScheme.secondary
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
val client = httpClient(settings.getString(SettingsConstants.ACCESS_TOKEN.toString(), ""))
|
||||
|
||||
try {
|
||||
val responseVersion = client
|
||||
.get("${serverUrl}/api/system/version")
|
||||
val content = responseVersion.body<String>()
|
||||
logger.info { "Version response: $responseVersion" }
|
||||
if(responseVersion.status == HttpStatusCode.OK) {
|
||||
logger.info { "Server version: $content" }
|
||||
serverVersion.value = content
|
||||
} else {
|
||||
logger.error { content }
|
||||
}
|
||||
|
||||
val responseHostname = client
|
||||
.get("${serverUrl}/api/system/hostname")
|
||||
val hostnameBody = responseHostname.body<Hostname>()
|
||||
if(responseVersion.status == HttpStatusCode.OK) {
|
||||
logger.info { "Hostname: $hostnameBody" }
|
||||
hostname.value = hostnameBody.hostname
|
||||
} else {
|
||||
logger.error { hostnameBody }
|
||||
}
|
||||
} catch (ex: ConnectException) {
|
||||
// unable to connect to host
|
||||
// clear server from history and remove JWT
|
||||
settings[SettingsConstants.SERVER_URL.toString()] = ""
|
||||
settings[SettingsConstants.ACCESS_TOKEN.toString()] = ""
|
||||
|
||||
// logout
|
||||
navigator.navigate(Routes.LOGIN.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TextLineIcon(
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
modifier: Modifier = Modifier,
|
||||
color: Color = Color.Unspecified,
|
||||
fontSize: TextUnit = TextUnit.Unspecified,
|
||||
fontWeight: FontWeight? = null,
|
||||
iconRightPadding: Dp = 0.dp,
|
||||
iconLine: Int = 0,
|
||||
iconTint: Color = MaterialTheme.colorScheme.secondary
|
||||
// etc
|
||||
) {
|
||||
val painter = rememberVectorPainter(image = icon)
|
||||
var lineTop = 0f
|
||||
var lineBottom = 0f
|
||||
var lineLeft = 0f
|
||||
with(LocalDensity.current) {
|
||||
val imageSize = Size(icon.defaultWidth.toPx(), icon.defaultHeight.toPx())
|
||||
val rightPadding = iconRightPadding.toPx()
|
||||
Text(
|
||||
text = text,
|
||||
color = color,
|
||||
fontSize = fontSize,
|
||||
fontWeight = fontWeight,
|
||||
onTextLayout = { layoutResult ->
|
||||
val nbLines = layoutResult.lineCount
|
||||
if (nbLines > iconLine) {
|
||||
lineTop = layoutResult.getLineTop(iconLine)
|
||||
lineBottom = layoutResult.getLineBottom(iconLine)
|
||||
lineLeft = layoutResult.getLineLeft(iconLine)
|
||||
}
|
||||
},
|
||||
modifier = modifier.drawBehind {
|
||||
with(painter) {
|
||||
translate(
|
||||
left = lineLeft - imageSize.width - rightPadding,
|
||||
top = lineTop + (lineBottom - lineTop) / 2 - imageSize.height / 2,
|
||||
) {
|
||||
//draw(painter.intrinsicSize, colorFilter = ColorFilter.tint(iconTint))
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,4 +1,11 @@
|
|||
package technology.iatlas.spaceup.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
expect fun getPlatformName(): String
|
||||
expect fun openInBrowser(domain: String)
|
||||
|
||||
@Composable
|
||||
expect fun OpenInBrowser(domain: String)
|
||||
|
||||
@Composable
|
||||
expect fun notify(title: String, body: String, type: String = "INFO")
|
|
@ -2,9 +2,15 @@ package technology.iatlas.spaceup.common.theme
|
|||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import moe.tlaster.precompose.PreComposeApp
|
||||
import moe.tlaster.precompose.stateholder.SavedStateHolder
|
||||
import org.koin.compose.KoinApplication
|
||||
import org.koin.dsl.module
|
||||
import technology.iatlas.spaceup.common.viewmodel.AuthenticationViewModel
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
|
||||
|
||||
private val LightColors = lightColorScheme(
|
||||
|
@ -75,16 +81,31 @@ private val DarkColors = darkColorScheme(
|
|||
@Composable
|
||||
fun AppTheme(
|
||||
useDarkTheme: Boolean = isSystemInDarkTheme(),
|
||||
content: @Composable () -> Unit
|
||||
appcontent: @Composable () -> Unit
|
||||
) {
|
||||
val colors = if (!useDarkTheme) {
|
||||
LightColors
|
||||
} else {
|
||||
DarkColors
|
||||
}
|
||||
val colors = if (!useDarkTheme) {
|
||||
LightColors
|
||||
} else {
|
||||
DarkColors
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
content = content
|
||||
)
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
content = {
|
||||
PreComposeApp {
|
||||
KoinApplication(
|
||||
application = {
|
||||
modules(
|
||||
module {
|
||||
factory { (savedStateHolder: SavedStateHolder) -> ServerViewModel(savedStateHolder) }
|
||||
single { AuthenticationViewModel() }
|
||||
}
|
||||
)
|
||||
}
|
||||
) {
|
||||
appcontent()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package technology.iatlas.spaceup.common.util
|
||||
|
||||
object Helper {
|
||||
fun getSystemProfile(): String = System.getProperty("profile") ?: ""
|
||||
}
|
||||
|
|
@ -2,4 +2,4 @@ package technology.iatlas.spaceup.common.util
|
|||
|
||||
import io.ktor.client.*
|
||||
|
||||
expect fun httpClient(bearerToken: String = ""): HttpClient
|
||||
expect fun httpClient(token: String): HttpClient
|
|
@ -6,13 +6,11 @@ import androidx.compose.runtime.setValue
|
|||
import com.google.gson.annotations.SerializedName
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.plugins.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.http.*
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
import moe.tlaster.precompose.viewmodel.ViewModel
|
||||
import technology.iatlas.spaceup.common.model.Authentication
|
||||
import technology.iatlas.spaceup.common.util.Helper.getSystemProfile
|
||||
import technology.iatlas.spaceup.common.util.httpClient
|
||||
|
||||
class AuthenticationViewModel : ViewModel() {
|
||||
|
@ -21,22 +19,23 @@ class AuthenticationViewModel : ViewModel() {
|
|||
var username by mutableStateOf("")
|
||||
var password by mutableStateOf("")
|
||||
|
||||
suspend fun login(navigator: Navigator, serverViewModel: ServerViewModel) {
|
||||
suspend fun login(serverUrl: String): Token {
|
||||
val authentication = Authentication(username.trim(), password.trim())
|
||||
val profile = getSystemProfile()
|
||||
logger.info {
|
||||
"Authenticate with $authentication to ${serverViewModel.serverUrl}"
|
||||
if(profile.lowercase().contains("dev")) {
|
||||
"Authenticate with $authentication to $serverUrl"
|
||||
} else {
|
||||
"Authenticate with ${authentication.password.replace(Regex("."), "*")} to $serverUrl"
|
||||
}
|
||||
}
|
||||
if(username.isNotEmpty() && password.isNotEmpty()) {
|
||||
val response = httpClient().post("${serverViewModel.serverUrl}/login") {
|
||||
val response = httpClient("").post("$serverUrl/login") {
|
||||
contentType(ContentType.Application.Json)
|
||||
timeout {
|
||||
|
||||
}
|
||||
setBody(authentication)
|
||||
}
|
||||
if(response.status == HttpStatusCode.OK) {
|
||||
serverViewModel.token = response.body() as Token
|
||||
navigator.navigate("/home")
|
||||
return response.body<Token>()
|
||||
} else {
|
||||
throw Exception("Something is wrong: Code ${response.status.value} Message: ${response.body<String>()}")
|
||||
}
|
||||
|
|
|
@ -6,26 +6,25 @@ import androidx.compose.runtime.setValue
|
|||
import com.russhwolf.settings.Settings
|
||||
import com.russhwolf.settings.get
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import io.ktor.client.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import moe.tlaster.precompose.stateholder.SavedStateHolder
|
||||
import moe.tlaster.precompose.viewmodel.ViewModel
|
||||
import technology.iatlas.spaceup.common.model.SettingsConstants
|
||||
import technology.iatlas.spaceup.common.util.httpClient
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class ServerViewModel : ViewModel() {
|
||||
class ServerViewModel(savedStateHolder: SavedStateHolder) : ViewModel() {
|
||||
private val logger = KotlinLogging.logger { }
|
||||
|
||||
// Contains base url
|
||||
var serverUrl by mutableStateOf("https://")
|
||||
var serverUrl by mutableStateOf("")
|
||||
|
||||
// Contains the JWT for authentication
|
||||
var token by mutableStateOf(Token("", "", 0))
|
||||
private val _token = MutableStateFlow(savedStateHolder.consumeRestored("token") as String? ?: "")
|
||||
val token: StateFlow<String> = _token
|
||||
|
||||
// For showing how long the JWT is valid
|
||||
var expiresAsString by mutableStateOf("")
|
||||
|
@ -33,12 +32,21 @@ class ServerViewModel : ViewModel() {
|
|||
init {
|
||||
val settings = Settings()
|
||||
serverUrl = settings[SettingsConstants.SERVER_URL.toString()] ?: ""
|
||||
|
||||
savedStateHolder.registerProvider("token") {
|
||||
token.value
|
||||
}
|
||||
}
|
||||
|
||||
fun updateToken(token: String) {
|
||||
logger.info { "Update token: $token" }
|
||||
val successful = _token.tryEmit(token)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
suspend fun checkExpiresIn() = runBlocking {
|
||||
CoroutineScope(Dispatchers.IO.limitedParallelism(1)).launch {
|
||||
while (true) {
|
||||
/*while (true) {
|
||||
if(token.expiresIn <= 0) {
|
||||
expiresAsString = "Session timed out." // TODO use it for re-login automatically
|
||||
delay(100)
|
||||
|
@ -48,9 +56,10 @@ class ServerViewModel : ViewModel() {
|
|||
logger.trace { "token expires in ${token.expiresIn.seconds}" }
|
||||
// 1 second
|
||||
delay(1000) // somehow more accurate than Kotlin delay(...)
|
||||
token.expiresIn = token.expiresIn - 1
|
||||
token.expiresIn -= 1
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,12 +68,7 @@ class ServerViewModel : ViewModel() {
|
|||
*
|
||||
* @return is valid JWT
|
||||
*/
|
||||
fun validToken(): Boolean {
|
||||
// TODO me
|
||||
return true
|
||||
}
|
||||
|
||||
fun client(): HttpClient {
|
||||
return httpClient(token.accessToken)
|
||||
suspend fun validToken(): Boolean {
|
||||
return false //token.expiresIn >= 0
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package technology.iatlas.spaceup.common.views
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import technology.iatlas.spaceup.common.pages.DiskPlot
|
||||
import technology.iatlas.spaceup.common.pages.Server
|
||||
|
||||
@Composable
|
||||
fun HomeView() {
|
||||
Server()
|
||||
DiskPlot()
|
||||
}
|
|
@ -47,16 +47,21 @@ import com.russhwolf.settings.Settings
|
|||
import com.russhwolf.settings.get
|
||||
import com.russhwolf.settings.set
|
||||
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import moe.tlaster.precompose.koin.koinViewModel
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
import moe.tlaster.precompose.ui.viewModel
|
||||
import technology.iatlas.spaceup.common.components.Alert
|
||||
import moe.tlaster.precompose.stateholder.LocalSavedStateHolder
|
||||
import org.koin.core.parameter.parametersOf
|
||||
import technology.iatlas.spaceup.common.components.ErrorAlert
|
||||
import technology.iatlas.spaceup.common.components.FullscreenCircularLoader
|
||||
import technology.iatlas.spaceup.common.model.Routes
|
||||
import technology.iatlas.spaceup.common.model.SettingsConstants
|
||||
import technology.iatlas.spaceup.common.viewmodel.AuthenticationViewModel
|
||||
import technology.iatlas.spaceup.common.viewmodel.ServerViewModel
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class)
|
||||
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class, DelicateCoroutinesApi::class)
|
||||
@Composable
|
||||
fun Login(navigator: Navigator) {
|
||||
/*
|
||||
|
@ -84,12 +89,12 @@ fun Login(navigator: Navigator) {
|
|||
settings.getBoolean(SettingsConstants.REMEMBER_CREDENTIALS.toString(), false)) }
|
||||
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
val serverViewModel = viewModel(ServerViewModel::class) {
|
||||
ServerViewModel()
|
||||
}
|
||||
val authenticationViewModel = viewModel(AuthenticationViewModel::class) {
|
||||
AuthenticationViewModel()
|
||||
}
|
||||
val stateHolder = LocalSavedStateHolder.current
|
||||
|
||||
val serverViewModel = koinViewModel(ServerViewModel::class) { parametersOf(stateHolder) }
|
||||
val serverUrl = serverViewModel.serverUrl
|
||||
|
||||
val authenticationViewModel = koinViewModel(AuthenticationViewModel::class)
|
||||
|
||||
Column(
|
||||
// modifier = Modifier.fillMaxSize(), // To make it center center
|
||||
|
@ -105,6 +110,7 @@ fun Login(navigator: Navigator) {
|
|||
.height(48.dp)
|
||||
) {
|
||||
Text(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 36.dp)
|
||||
.align(Alignment.CenterHorizontally),
|
||||
|
@ -126,19 +132,21 @@ fun Login(navigator: Navigator) {
|
|||
label = {
|
||||
Text("SpaceUp-Server")
|
||||
},
|
||||
value = serverViewModel.serverUrl,
|
||||
value = serverUrl,
|
||||
onValueChange = {
|
||||
val serverUrl = it.trim()
|
||||
serverViewModel.serverUrl = serverUrl
|
||||
val updatedServerUrl = it.trim()
|
||||
serverViewModel.serverUrl = updatedServerUrl
|
||||
if(rememberServer) {
|
||||
val serverConst = SettingsConstants.SERVER_URL.toString()
|
||||
settings[serverConst] = serverUrl
|
||||
}
|
||||
}
|
||||
)
|
||||
Row {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
checked = rememberServer,
|
||||
onCheckedChange = {
|
||||
rememberServer = it
|
||||
|
@ -146,13 +154,15 @@ fun Login(navigator: Navigator) {
|
|||
val serverConst = SettingsConstants.SERVER_URL.toString()
|
||||
settings[rememberServerConst] = rememberServer
|
||||
if(rememberServer) {
|
||||
settings[serverConst] = serverViewModel.serverUrl
|
||||
settings[serverConst] = serverUrl
|
||||
} else {
|
||||
settings[serverConst] = ""
|
||||
}
|
||||
}
|
||||
)
|
||||
Text("Remember server?", modifier = Modifier.padding(16.dp))
|
||||
Text(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
text = "Remember server?", modifier = Modifier.padding(start = 16.dp))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(15.dp))
|
||||
OutlinedTextField(
|
||||
|
@ -217,9 +227,11 @@ fun Login(navigator: Navigator) {
|
|||
}
|
||||
}
|
||||
)
|
||||
Row {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Checkbox(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
checked = rememberUserPassword,
|
||||
onCheckedChange = {
|
||||
rememberUserPassword = it
|
||||
|
@ -236,26 +248,34 @@ fun Login(navigator: Navigator) {
|
|||
}
|
||||
}
|
||||
)
|
||||
Text("Remember Credentials?", modifier = Modifier.padding(16.dp))
|
||||
Text(
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
text = "Remember Credentials?", modifier = Modifier.padding(start = 16.dp)
|
||||
)
|
||||
}
|
||||
Row {
|
||||
val formIsFilled = authenticationViewModel.username.isNotEmpty()
|
||||
&& authenticationViewModel.password.isNotEmpty() && serverViewModel.serverUrl.isNotEmpty()
|
||||
&& authenticationViewModel.password.isNotEmpty() && serverUrl.isNotEmpty()
|
||||
AnimatedVisibility(formIsFilled) {
|
||||
Button(
|
||||
onClick = {
|
||||
isLoading = true
|
||||
coroutineScope.launch {
|
||||
logger.info {
|
||||
"User ${authenticationViewModel.username} is trying to login."
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
logger.info {
|
||||
"User ${authenticationViewModel.username} is trying to login."
|
||||
}
|
||||
try {
|
||||
val token = authenticationViewModel.login(serverUrl)
|
||||
settings[SettingsConstants.ACCESS_TOKEN.toString()] = token.accessToken
|
||||
|
||||
//serverViewModel.updateToken(token.accessToken)
|
||||
logger.info { "Finished login" }
|
||||
if(token.username.isNotEmpty()) navigator.navigate("/home")
|
||||
} catch (ex: Exception) {
|
||||
openDialog.value = true
|
||||
errorMsg.value = ex.message ?: "Cannot login!"
|
||||
}
|
||||
}
|
||||
try {
|
||||
authenticationViewModel.login(navigator, serverViewModel)
|
||||
} catch (ex: Exception) {
|
||||
openDialog.value = true
|
||||
errorMsg.value = ex.message ?: "Cannot login!"
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
) {
|
||||
|
@ -280,6 +300,11 @@ fun Login(navigator: Navigator) {
|
|||
Text("Settings")
|
||||
}
|
||||
}
|
||||
|
||||
if (openDialog.value) {
|
||||
ErrorAlert(openDialog, errorMsg.value)
|
||||
}
|
||||
if(isLoading) FullscreenCircularLoader(isLoading)
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
|
@ -294,14 +319,9 @@ fun Login(navigator: Navigator) {
|
|||
|
||||
serverViewModel.checkExpiresIn()
|
||||
}
|
||||
|
||||
if (openDialog.value) {
|
||||
Alert(openDialog, errorMsg.value)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO Make this general usable for other components
|
||||
@OptIn(ExperimentalComposeUiApi::class)
|
||||
fun handleKeyEvents(
|
||||
navigator: Navigator,
|
||||
keyEvent: KeyEvent, // Get KeyEvents
|
||||
|
|
|
@ -6,31 +6,28 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Divider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import moe.tlaster.precompose.navigation.Navigator
|
||||
|
||||
@Composable
|
||||
fun SettingsView(navigator: Navigator) {
|
||||
fun SettingsView() {
|
||||
val settingsList = listOf(
|
||||
SettingItem("Einstellung 1", "Beschreibung 1", Icons.Filled.Settings, true) {},
|
||||
SettingItem("Einstellung 2", "Beschreibung 2", Icons.Filled.Info, false) {},
|
||||
SettingItem("Einstellung 3", "Beschreibung 3", Icons.Filled.Settings, true) {},
|
||||
SettingItem("Einstellung 4", "Beschreibung 4", Icons.Filled.Info, false) {},
|
||||
SettingItem("Einstellung 5", "Beschreibung 5", Icons.Filled.Settings, true) {}
|
||||
SettingItemState("Einstellung 1", "Beschreibung 1", Icons.Filled.Settings, mutableStateOf(false)) { },
|
||||
SettingItemState("Einstellung 2", "Beschreibung 2", Icons.Filled.Info, mutableStateOf(true)) {},
|
||||
SettingItemState("Einstellung 3", "Beschreibung 3", Icons.Filled.Settings, mutableStateOf(false)) {},
|
||||
SettingItemState("Einstellung 4", "Beschreibung 4", Icons.Filled.Info, mutableStateOf(true)) {},
|
||||
SettingItemState("Einstellung 5", "Beschreibung 5", Icons.Filled.Settings, mutableStateOf(false)) {}
|
||||
)
|
||||
|
||||
Column {
|
||||
|
@ -39,17 +36,17 @@ fun SettingsView(navigator: Navigator) {
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun Settings(settingsList: List<SettingItem>) {
|
||||
fun Settings(settingsList: List<SettingItemState>) {
|
||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
||||
items(settingsList.size) { index ->
|
||||
SettingItem(settingsList[index])
|
||||
Divider(color = Color.LightGray)
|
||||
//Divider(color = Color.LightGray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingItem(setting: SettingItem) {
|
||||
fun SettingItem(setting: SettingItemState) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
@ -61,14 +58,16 @@ fun SettingItem(setting: SettingItem) {
|
|||
Text(text = setting.title)
|
||||
Text(text = setting.description, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Checkbox(checked = setting.value, onCheckedChange = setting.action)
|
||||
when (setting.value) {
|
||||
is Boolean -> Checkbox(checked = !(setting.value as Boolean), onCheckedChange = setting.action)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SettingItem(
|
||||
data class SettingItemState(
|
||||
val title: String,
|
||||
val description: String,
|
||||
val icon: ImageVector,
|
||||
var value: Boolean,
|
||||
val action: (value: Boolean) -> Unit
|
||||
var value: Any,
|
||||
val action: (value: Any) -> Unit
|
||||
)
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
package technology.iatlas.spaceup.common
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.window.Notification
|
||||
import androidx.compose.ui.window.rememberNotification
|
||||
import androidx.compose.ui.window.rememberTrayState
|
||||
import java.awt.Desktop
|
||||
import java.net.URI
|
||||
|
||||
|
@ -7,6 +11,20 @@ actual fun getPlatformName(): String {
|
|||
return "Desktop"
|
||||
}
|
||||
|
||||
actual fun openInBrowser(domain: String) {
|
||||
@Composable
|
||||
actual fun OpenInBrowser(domain: String) {
|
||||
Desktop.getDesktop().browse(URI.create(domain))
|
||||
}
|
||||
|
||||
@Composable
|
||||
actual fun notify(title: String, body: String, type: String) {
|
||||
val trayState = rememberTrayState()
|
||||
val notifyType = when(type.uppercase()) {
|
||||
"INFO" -> Notification.Type.Info
|
||||
"ERROR" -> Notification.Type.Error
|
||||
"WARN" -> Notification.Type.Warning
|
||||
"WARNING" -> Notification.Type.Warning
|
||||
else -> {Notification.Type.Info}
|
||||
}
|
||||
trayState.sendNotification(rememberNotification(title, body, notifyType))
|
||||
}
|
|
@ -9,16 +9,22 @@ import io.ktor.client.plugins.auth.providers.*
|
|||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.plugins.logging.*
|
||||
import io.ktor.serialization.gson.*
|
||||
import technology.iatlas.spaceup.common.util.Helper.getSystemProfile
|
||||
|
||||
val profile = System.getProperty("nextui.profile") ?: ""
|
||||
val profile = getSystemProfile()
|
||||
|
||||
actual fun httpClient(bearerToken: String): HttpClient {
|
||||
actual fun httpClient(token: String): HttpClient {
|
||||
return HttpClient(CIO) {
|
||||
if(profile == "dev") {
|
||||
if (profile == "dev") {
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.ALL
|
||||
}
|
||||
} else {
|
||||
install(Logging) {
|
||||
logger = Logger.DEFAULT
|
||||
level = LogLevel.INFO
|
||||
}
|
||||
}
|
||||
install(ContentNegotiation) {
|
||||
gson {
|
||||
|
@ -28,12 +34,12 @@ actual fun httpClient(bearerToken: String): HttpClient {
|
|||
install(Auth) {
|
||||
bearer {
|
||||
loadTokens {
|
||||
BearerTokens(bearerToken, "")
|
||||
BearerTokens(token, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
install(HttpTimeout) {
|
||||
requestTimeoutMillis = 20000 // Webbackend list, Get domains take rather long
|
||||
requestTimeoutMillis = 60000 // Webbackend list, Get domains take rather long
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import org.jetbrains.compose.compose
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
|
||||
plugins {
|
||||
|
@ -11,7 +10,7 @@ version = "1.0-SNAPSHOT"
|
|||
|
||||
kotlin {
|
||||
jvm {
|
||||
jvmToolchain(11)
|
||||
jvmToolchain(17)
|
||||
withJava()
|
||||
}
|
||||
sourceSets {
|
||||
|
@ -21,7 +20,7 @@ kotlin {
|
|||
implementation(compose.desktop.currentOs) {
|
||||
exclude("org.jetbrains.compose.material")
|
||||
}
|
||||
implementation("com.bybutter.compose:compose-jetbrains-expui-theme:2.1.0")
|
||||
implementation("com.bybutter.compose:compose-jetbrains-expui-theme:2.2.0")
|
||||
}
|
||||
}
|
||||
val jvmTest by getting
|
||||
|
|
|
@ -2,10 +2,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
|
@ -33,24 +31,27 @@ import kotlinx.coroutines.flow.distinctUntilChanged
|
|||
import moe.tlaster.precompose.lifecycle.Lifecycle
|
||||
import moe.tlaster.precompose.lifecycle.LifecycleOwner
|
||||
import moe.tlaster.precompose.lifecycle.LifecycleRegistry
|
||||
import moe.tlaster.precompose.lifecycle.LocalLifecycleOwner
|
||||
import moe.tlaster.precompose.stateholder.LocalStateHolder
|
||||
import moe.tlaster.precompose.stateholder.StateHolder
|
||||
import moe.tlaster.precompose.ui.BackDispatcher
|
||||
import moe.tlaster.precompose.ui.BackDispatcherOwner
|
||||
import moe.tlaster.precompose.ui.LocalBackDispatcherOwner
|
||||
import moe.tlaster.precompose.ui.LocalLifecycleOwner
|
||||
import moe.tlaster.precompose.ui.LocalViewModelStoreOwner
|
||||
import moe.tlaster.precompose.viewmodel.ViewModelStore
|
||||
import moe.tlaster.precompose.viewmodel.ViewModelStoreOwner
|
||||
import technology.iatlas.spaceup.common.App
|
||||
import technology.iatlas.spaceup.common.util.Helper.getSystemProfile
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
val profile = System.getProperty("nextui.profile") ?: ""
|
||||
val profile = getSystemProfile()
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@OptIn(ExperimentalFoundationApi::class,
|
||||
ExperimentalMaterial3Api::class)
|
||||
fun main() = application {
|
||||
var isDark by remember { mutableStateOf(false) }
|
||||
var isAutoMode by remember { mutableStateOf(false) }
|
||||
var isAutoMode by remember { mutableStateOf(true) }
|
||||
|
||||
val isDarkMode = if(isAutoMode) {
|
||||
val painterIcon = ImageIO.read(this::class.java.getResourceAsStream("/spaceup_icon.png")).toPainter()
|
||||
|
||||
val isDarkMode = if (isAutoMode) {
|
||||
isSystemInDarkTheme()
|
||||
} else {
|
||||
isDark
|
||||
|
@ -63,8 +64,8 @@ fun main() = application {
|
|||
}
|
||||
|
||||
val state = rememberWindowState(
|
||||
size = DpSize(700.dp, 750.dp),
|
||||
position = WindowPosition(Alignment.BottomCenter)
|
||||
size = DpSize(600.dp, 650.dp),
|
||||
position = WindowPosition(Alignment.Center)
|
||||
)
|
||||
|
||||
val holder = remember {
|
||||
|
@ -85,17 +86,17 @@ fun main() = application {
|
|||
|
||||
ProvideDesktopCompositionLocals(holder) {
|
||||
JBWindow(
|
||||
icon = ImageIO.read(this::class.java.getResourceAsStream("/spaceup_icon.png")).toPainter(),
|
||||
icon = painterIcon,
|
||||
onCloseRequest = {
|
||||
holder.lifecycle.currentState = Lifecycle.State.Destroyed
|
||||
exitApplication()
|
||||
},
|
||||
title = "SpaceUp-NextUI ${if(profile.isNotEmpty()) profile.uppercase() else ""}",
|
||||
title = "SpaceUp-NextUI ${profile.uppercase()}",
|
||||
theme = currentTheme,
|
||||
state = state,
|
||||
mainToolBar = {
|
||||
Row(Modifier.mainToolBarItem(Alignment.End)) {
|
||||
if(!isAutoMode) {
|
||||
if (!isAutoMode) {
|
||||
Tooltip("Switch between dark and light mode,\ncurrently is ${if (isDark) "dark" else "light"} mode") {
|
||||
ActionButton(
|
||||
{ isDark = !isDark }, Modifier.size(40.dp), shape = RectangleShape
|
||||
|
@ -133,19 +134,19 @@ private fun ProvideDesktopCompositionLocals(
|
|||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalLifecycleOwner provides holder,
|
||||
LocalViewModelStoreOwner provides holder,
|
||||
LocalStateHolder provides holder.stateHolder,
|
||||
LocalBackDispatcherOwner provides holder,
|
||||
) {
|
||||
content.invoke()
|
||||
}
|
||||
}
|
||||
|
||||
private class PreComposeWindowHolder : LifecycleOwner, ViewModelStoreOwner, BackDispatcherOwner {
|
||||
private class PreComposeWindowHolder : LifecycleOwner, BackDispatcherOwner {
|
||||
override val lifecycle by lazy {
|
||||
LifecycleRegistry()
|
||||
}
|
||||
override val viewModelStore by lazy {
|
||||
ViewModelStore()
|
||||
val stateHolder by lazy {
|
||||
StateHolder()
|
||||
}
|
||||
override val backDispatcher by lazy {
|
||||
BackDispatcher()
|
||||
|
|
|
@ -3,8 +3,9 @@ android.useAndroidX=true
|
|||
|
||||
# Kotlin / Compose
|
||||
kotlin.code.style=official
|
||||
kotlin.version=1.9.0
|
||||
compose.version=1.4.3
|
||||
kotlin.experimental.tryK2=true
|
||||
kotlin.version=1.9.20
|
||||
compose.version=1.5.10
|
||||
|
||||
# AGP
|
||||
agp.version=8.0.0
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
#-------------------------------------------------------------------------------#
|
||||
# Qodana analysis is configured by qodana.yaml file #
|
||||
# https://www.jetbrains.com/help/qodana/qodana-yaml.html #
|
||||
#-------------------------------------------------------------------------------#
|
||||
version: "1.0"
|
||||
|
||||
#Specify inspection profile for code analysis
|
||||
profile:
|
||||
name: qodana.starter
|
||||
|
||||
#Enable inspections
|
||||
#include:
|
||||
# - name: <SomeEnabledInspectionId>
|
||||
|
||||
#Disable inspections
|
||||
#exclude:
|
||||
# - name: <SomeDisabledInspectionId>
|
||||
# paths:
|
||||
# - <path/where/not/run/inspection>
|
||||
|
||||
projectJDK: jbr-17 #(Applied in CI/CD pipeline)
|
||||
|
||||
#Execute shell command before Qodana execution (Applied in CI/CD pipeline)
|
||||
#bootstrap: sh ./prepare-qodana.sh
|
||||
|
||||
#Install IDE plugins before Qodana execution (Applied in CI/CD pipeline)
|
||||
#plugins:
|
||||
# - id: <plugin.id> #(plugin id can be found at https://plugins.jetbrains.com)
|
||||
|
||||
#Specify Qodana linter for analysis (Applied in CI/CD pipeline)
|
||||
linter: jetbrains/qodana-jvm:latest
|
Loading…
Reference in New Issue