fixed inital Config
This commit is contained in:
1 parent
cd57e97cff
commit
e8d2408c8e
6 files changed
+125
-49
No files matched your search
@@ -19,6 +19,7 @@ repositories {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(compose.desktop.currentOs)
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation(compose.materialIconsExtended)
|
||||||
|
|
||||||
// Global hotkeys
|
// Global hotkeys
|
||||||
implementation("com.github.kwhat:jnativehook:2.2.2")
|
implementation("com.github.kwhat:jnativehook:2.2.2")
|
||||||
|
|||||||
@@ -65,19 +65,6 @@ fun main() {
|
|||||||
onSettings = { isSettingsVisible = true },
|
onSettings = { isSettingsVisible = true },
|
||||||
onExit = { exitApplication() },
|
onExit = { exitApplication() },
|
||||||
)
|
)
|
||||||
client.getSpaces()
|
|
||||||
.onSuccess { spaces ->
|
|
||||||
logger.info("Available spaces:")
|
|
||||||
spaces.forEach { logger.info("Space: ${it.id} ${it.name}") }
|
|
||||||
val space = spaces.find { space -> space.id == configResult.config.spaceId }
|
|
||||||
if (space == null) {
|
|
||||||
logger.error("Selected space not found: ${configResult.config.spaceId}")
|
|
||||||
exitProcess(-1)
|
|
||||||
} else {
|
|
||||||
logger.info("Selected space: ${space.name} (${space.id})")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onFailure { logger.error("Failed to load spaces: ${it.message}") }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
@@ -129,7 +116,6 @@ fun main() {
|
|||||||
}
|
}
|
||||||
SettingsWindow(
|
SettingsWindow(
|
||||||
config = configResult.config,
|
config = configResult.config,
|
||||||
client = client,
|
|
||||||
onSave = { newConfig ->
|
onSave = { newConfig ->
|
||||||
saveConfig(newConfig, configResult.sourceFile)
|
saveConfig(newConfig, configResult.sourceFile)
|
||||||
configResult = ConfigResult(newConfig, configResult.sourceFile)
|
configResult = ConfigResult(newConfig, configResult.sourceFile)
|
||||||
|
|||||||
@@ -1,32 +1,19 @@
|
|||||||
package ch.ergon.anytask.api
|
package ch.ergon.anytask.api
|
||||||
|
|
||||||
import ch.ergon.anytask.api.dto.CreateObjectRequest
|
import ch.ergon.anytask.api.dto.*
|
||||||
import ch.ergon.anytask.api.dto.CreateObjectResponse
|
import io.ktor.client.*
|
||||||
import ch.ergon.anytask.api.dto.ObjectType
|
import io.ktor.client.call.*
|
||||||
import ch.ergon.anytask.api.dto.SearchObject
|
import io.ktor.client.engine.cio.*
|
||||||
import ch.ergon.anytask.api.dto.SearchObjectsResponse
|
import io.ktor.client.plugins.*
|
||||||
import ch.ergon.anytask.api.dto.Space
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
import ch.ergon.anytask.api.dto.SpacesResponse
|
import io.ktor.client.request.*
|
||||||
import ch.ergon.anytask.api.dto.TypesResponse
|
import io.ktor.client.statement.*
|
||||||
import kotlinx.serialization.json.JsonElement
|
import io.ktor.http.*
|
||||||
import io.ktor.client.HttpClient
|
import io.ktor.http.content.OutgoingContent
|
||||||
import io.ktor.client.call.body
|
import io.ktor.http.content.TextContent
|
||||||
import io.ktor.client.engine.cio.CIO
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
import io.ktor.client.plugins.HttpSend
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
|
|
||||||
import io.ktor.client.plugins.plugin
|
|
||||||
import io.ktor.client.request.bearerAuth
|
|
||||||
import io.ktor.client.request.get
|
|
||||||
import io.ktor.client.request.header
|
|
||||||
import io.ktor.client.request.parameter
|
|
||||||
import io.ktor.client.request.post
|
|
||||||
import io.ktor.client.request.setBody
|
|
||||||
import io.ktor.client.statement.bodyAsText
|
|
||||||
import io.ktor.http.ContentType
|
|
||||||
import io.ktor.http.contentType
|
|
||||||
import io.ktor.http.isSuccess
|
|
||||||
import io.ktor.serialization.kotlinx.json.json
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.json.JsonElement
|
||||||
import org.apache.logging.log4j.LogManager
|
import org.apache.logging.log4j.LogManager
|
||||||
import kotlin.time.DurationUnit.MILLISECONDS
|
import kotlin.time.DurationUnit.MILLISECONDS
|
||||||
import kotlin.time.toDuration
|
import kotlin.time.toDuration
|
||||||
@@ -52,6 +39,14 @@ class AnyTypeApi(
|
|||||||
val method = request.method.value
|
val method = request.method.value
|
||||||
val url = request.url.buildString()
|
val url = request.url.buildString()
|
||||||
logger.info("→ $method $url")
|
logger.info("→ $method $url")
|
||||||
|
if (logger.isTraceEnabled) {
|
||||||
|
val bodyText = when (val body = request.body) {
|
||||||
|
is TextContent -> body.text
|
||||||
|
is OutgoingContent.ByteArrayContent -> body.bytes().decodeToString()
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
if (bodyText != null) logger.trace("Request body: $bodyText")
|
||||||
|
}
|
||||||
val start = System.currentTimeMillis()
|
val start = System.currentTimeMillis()
|
||||||
val call = execute(request)
|
val call = execute(request)
|
||||||
val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS)
|
val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS)
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.material.TextButton
|
|||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Refresh
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -40,12 +41,14 @@ import androidx.compose.runtime.rememberCoroutineScope
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.onFocusChanged
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ch.ergon.anytask.api.AnyTypeApi
|
import ch.ergon.anytask.api.AnyTypeApi
|
||||||
import ch.ergon.anytask.api.dto.ObjectType
|
import ch.ergon.anytask.api.dto.ObjectType
|
||||||
import ch.ergon.anytask.api.dto.Property
|
import ch.ergon.anytask.api.dto.Property
|
||||||
|
import ch.ergon.anytask.api.dto.Space
|
||||||
import ch.ergon.anytask.config.Configuration
|
import ch.ergon.anytask.config.Configuration
|
||||||
import ch.ergon.anytask.config.Connection
|
import ch.ergon.anytask.config.Connection
|
||||||
import ch.ergon.anytask.config.PropertyConfig
|
import ch.ergon.anytask.config.PropertyConfig
|
||||||
@@ -54,16 +57,19 @@ import kotlinx.coroutines.launch
|
|||||||
@Composable
|
@Composable
|
||||||
fun SettingsWindow(
|
fun SettingsWindow(
|
||||||
config: Configuration,
|
config: Configuration,
|
||||||
client: AnyTypeApi,
|
|
||||||
onSave: (Configuration) -> Unit,
|
onSave: (Configuration) -> Unit,
|
||||||
onCancel: () -> Unit,
|
onCancel: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
var apiKey by remember { mutableStateOf(config.connection.apiKey) }
|
var apiKey by remember { mutableStateOf(config.connection.apiKey) }
|
||||||
|
var loadedApiKey by remember { mutableStateOf(config.connection.apiKey) }
|
||||||
|
var selectedSpaceId by remember { mutableStateOf(config.spaceId) }
|
||||||
var selectedTypeKey by remember { mutableStateOf(config.taskTypeKey) }
|
var selectedTypeKey by remember { mutableStateOf(config.taskTypeKey) }
|
||||||
val propertyConfigs = remember { mutableStateListOf<PropertyConfig>().also { it.addAll(config.properties) } }
|
val propertyConfigs = remember { mutableStateListOf<PropertyConfig>().also { it.addAll(config.properties) } }
|
||||||
|
|
||||||
|
var spaces by remember { mutableStateOf<List<Space>>(emptyList()) }
|
||||||
|
var isLoadingSpaces by remember { mutableStateOf(false) }
|
||||||
var types by remember { mutableStateOf<List<ObjectType>>(emptyList()) }
|
var types by remember { mutableStateOf<List<ObjectType>>(emptyList()) }
|
||||||
var isLoadingTypes by remember { mutableStateOf(false) }
|
var isLoadingTypes by remember { mutableStateOf(false) }
|
||||||
var errorMessage by remember { mutableStateOf<String?>(null) }
|
var errorMessage by remember { mutableStateOf<String?>(null) }
|
||||||
@@ -71,21 +77,48 @@ fun SettingsWindow(
|
|||||||
// Property edit state
|
// Property edit state
|
||||||
var editingIndex by remember { mutableStateOf<Int?>(null) } // null=none, -1=new
|
var editingIndex by remember { mutableStateOf<Int?>(null) } // null=none, -1=new
|
||||||
|
|
||||||
// Load types on start
|
suspend fun loadSpaces(key: String) {
|
||||||
LaunchedEffect(Unit) {
|
spaces = emptyList()
|
||||||
|
isLoadingSpaces = true
|
||||||
|
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key)
|
||||||
|
try {
|
||||||
|
tempClient.getSpaces()
|
||||||
|
.onSuccess { spaces = it }
|
||||||
|
.onFailure { errorMessage = "Failed to load spaces: ${it.message}" }
|
||||||
|
} finally {
|
||||||
|
tempClient.close()
|
||||||
|
}
|
||||||
|
isLoadingSpaces = false
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun loadTypes(key: String, spaceId: String) {
|
||||||
|
types = emptyList()
|
||||||
isLoadingTypes = true
|
isLoadingTypes = true
|
||||||
client.getTypes()
|
val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId)
|
||||||
|
try {
|
||||||
|
tempClient.getTypes()
|
||||||
.onSuccess { types = it }
|
.onSuccess { types = it }
|
||||||
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
|
.onFailure { errorMessage = "Failed to load types: ${it.message}" }
|
||||||
|
} finally {
|
||||||
|
tempClient.close()
|
||||||
|
}
|
||||||
isLoadingTypes = false
|
isLoadingTypes = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initial load
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
loadSpaces(apiKey)
|
||||||
|
if (selectedSpaceId.isNotEmpty()) {
|
||||||
|
loadTypes(apiKey, selectedSpaceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val selectedType = types.find { it.key == selectedTypeKey }
|
val selectedType = types.find { it.key == selectedTypeKey }
|
||||||
val availableProperties = selectedType?.properties.orEmpty()
|
val availableProperties = selectedType?.properties.orEmpty()
|
||||||
|
|
||||||
fun buildConfig(): Configuration = Configuration(
|
fun buildConfig(): Configuration = Configuration(
|
||||||
connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl),
|
connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl),
|
||||||
spaceId = config.spaceId,
|
spaceId = selectedSpaceId,
|
||||||
taskTypeKey = selectedTypeKey,
|
taskTypeKey = selectedTypeKey,
|
||||||
properties = propertyConfigs.toList(),
|
properties = propertyConfigs.toList(),
|
||||||
)
|
)
|
||||||
@@ -96,13 +129,56 @@ fun SettingsWindow(
|
|||||||
Text("Settings", style = MaterialTheme.typography.h6)
|
Text("Settings", style = MaterialTheme.typography.h6)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
|
||||||
// ApiKey
|
// ApiKey + reload button
|
||||||
|
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = apiKey,
|
value = apiKey,
|
||||||
onValueChange = { apiKey = it },
|
onValueChange = { apiKey = it },
|
||||||
label = { Text("API Key") },
|
label = { Text("API Key") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.weight(1f).onFocusChanged { focusState ->
|
||||||
|
if (!focusState.isFocused && apiKey != loadedApiKey) {
|
||||||
|
scope.launch {
|
||||||
|
loadedApiKey = apiKey
|
||||||
|
selectedSpaceId = ""
|
||||||
|
selectedTypeKey = ""
|
||||||
|
propertyConfigs.clear()
|
||||||
|
editingIndex = null
|
||||||
|
types = emptyList()
|
||||||
|
loadSpaces(apiKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
IconButton(onClick = {
|
||||||
|
scope.launch {
|
||||||
|
loadedApiKey = apiKey
|
||||||
|
selectedSpaceId = ""
|
||||||
|
selectedTypeKey = ""
|
||||||
|
propertyConfigs.clear()
|
||||||
|
editingIndex = null
|
||||||
|
types = emptyList()
|
||||||
|
loadSpaces(apiKey)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Icon(Icons.Default.Refresh, contentDescription = "Reload spaces")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
// Space dropdown
|
||||||
|
DropdownSelector(
|
||||||
|
label = "Space",
|
||||||
|
selectedValue = spaces.find { it.id == selectedSpaceId }?.name ?: selectedSpaceId,
|
||||||
|
options = spaces.map { it.id to it.name },
|
||||||
|
onSelect = { id ->
|
||||||
|
selectedSpaceId = id
|
||||||
|
selectedTypeKey = ""
|
||||||
|
propertyConfigs.clear()
|
||||||
|
editingIndex = null
|
||||||
|
scope.launch { loadTypes(apiKey, id) }
|
||||||
|
},
|
||||||
|
isLoading = isLoadingSpaces,
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(12.dp))
|
Spacer(modifier = Modifier.height(12.dp))
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Configuration status="WARN">
|
||||||
|
<Appenders>
|
||||||
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
|
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
||||||
|
</Console>
|
||||||
|
</Appenders>
|
||||||
|
<Loggers>
|
||||||
|
<Root level="info">
|
||||||
|
<AppenderRef ref="Console"/>
|
||||||
|
</Root>
|
||||||
|
</Loggers>
|
||||||
|
</Configuration>
|
||||||
@@ -1,12 +1,17 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<Configuration status="WARN">
|
<Configuration status="WARN">
|
||||||
|
<Properties>
|
||||||
|
<Property name="CLR">{FATAL=bright red, ERROR=red, WARN=bright yellow, INFO=Normal, DEBUG=white, TRACE=white}</Property>
|
||||||
|
<Property name="MSG_CLR">{FATAL=bright red, ERROR=Normal, WARN=Normal, INFO=Normal, DEBUG=Normal, TRACE=white}</Property>
|
||||||
|
<Property name="LOG_PATTERN">%d{yy-MM-dd HH:mm:ss.SSS} %style{[%t]}{magenta} %highlight{%-5level}${CLR} %style{%logger{36}}{cyan} %highlight{: %msg%n}${MSG_CLR}</Property>
|
||||||
|
</Properties>
|
||||||
<Appenders>
|
<Appenders>
|
||||||
<Console name="Console" target="SYSTEM_OUT">
|
<Console name="Console" target="SYSTEM_OUT">
|
||||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
|
<PatternLayout pattern="${LOG_PATTERN}" disableAnsi="false"/>
|
||||||
</Console>
|
</Console>
|
||||||
</Appenders>
|
</Appenders>
|
||||||
<Loggers>
|
<Loggers>
|
||||||
<Root level="info">
|
<Root level="trace">
|
||||||
<AppenderRef ref="Console"/>
|
<AppenderRef ref="Console"/>
|
||||||
</Root>
|
</Root>
|
||||||
</Loggers>
|
</Loggers>
|
||||||
|
|||||||