diff --git a/build.gradle.kts b/build.gradle.kts index 5da2c82..4947b1b 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -19,6 +19,7 @@ repositories { dependencies { implementation(compose.desktop.currentOs) + implementation(compose.materialIconsExtended) // Global hotkeys implementation("com.github.kwhat:jnativehook:2.2.2") diff --git a/src/main/kotlin/ch/ergon/anytask/Main.kt b/src/main/kotlin/ch/ergon/anytask/Main.kt index 8608c13..dc2b153 100644 --- a/src/main/kotlin/ch/ergon/anytask/Main.kt +++ b/src/main/kotlin/ch/ergon/anytask/Main.kt @@ -65,19 +65,6 @@ fun main() { onSettings = { isSettingsVisible = true }, 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) { @@ -129,7 +116,6 @@ fun main() { } SettingsWindow( config = configResult.config, - client = client, onSave = { newConfig -> saveConfig(newConfig, configResult.sourceFile) configResult = ConfigResult(newConfig, configResult.sourceFile) diff --git a/src/main/kotlin/ch/ergon/anytask/api/AnyTypeApi.kt b/src/main/kotlin/ch/ergon/anytask/api/AnyTypeApi.kt index 178ec6b..9aee4ce 100644 --- a/src/main/kotlin/ch/ergon/anytask/api/AnyTypeApi.kt +++ b/src/main/kotlin/ch/ergon/anytask/api/AnyTypeApi.kt @@ -1,32 +1,19 @@ package ch.ergon.anytask.api -import ch.ergon.anytask.api.dto.CreateObjectRequest -import ch.ergon.anytask.api.dto.CreateObjectResponse -import ch.ergon.anytask.api.dto.ObjectType -import ch.ergon.anytask.api.dto.SearchObject -import ch.ergon.anytask.api.dto.SearchObjectsResponse -import ch.ergon.anytask.api.dto.Space -import ch.ergon.anytask.api.dto.SpacesResponse -import ch.ergon.anytask.api.dto.TypesResponse -import kotlinx.serialization.json.JsonElement -import io.ktor.client.HttpClient -import io.ktor.client.call.body -import io.ktor.client.engine.cio.CIO -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 ch.ergon.anytask.api.dto.* +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.cio.* +import io.ktor.client.plugins.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.client.statement.* +import io.ktor.http.* +import io.ktor.http.content.OutgoingContent +import io.ktor.http.content.TextContent +import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement import org.apache.logging.log4j.LogManager import kotlin.time.DurationUnit.MILLISECONDS import kotlin.time.toDuration @@ -52,6 +39,14 @@ class AnyTypeApi( val method = request.method.value val url = request.url.buildString() 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 call = execute(request) val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS) diff --git a/src/main/kotlin/ch/ergon/anytask/ui/SettingsWindow.kt b/src/main/kotlin/ch/ergon/anytask/ui/SettingsWindow.kt index ddeebba..bc5091e 100644 --- a/src/main/kotlin/ch/ergon/anytask/ui/SettingsWindow.kt +++ b/src/main/kotlin/ch/ergon/anytask/ui/SettingsWindow.kt @@ -30,6 +30,7 @@ import androidx.compose.material.TextButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -40,12 +41,14 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import ch.ergon.anytask.api.AnyTypeApi import ch.ergon.anytask.api.dto.ObjectType 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.Connection import ch.ergon.anytask.config.PropertyConfig @@ -54,16 +57,19 @@ import kotlinx.coroutines.launch @Composable fun SettingsWindow( config: Configuration, - client: AnyTypeApi, onSave: (Configuration) -> Unit, onCancel: () -> Unit, ) { val scope = rememberCoroutineScope() 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) } val propertyConfigs = remember { mutableStateListOf().also { it.addAll(config.properties) } } + var spaces by remember { mutableStateOf>(emptyList()) } + var isLoadingSpaces by remember { mutableStateOf(false) } var types by remember { mutableStateOf>(emptyList()) } var isLoadingTypes by remember { mutableStateOf(false) } var errorMessage by remember { mutableStateOf(null) } @@ -71,21 +77,48 @@ fun SettingsWindow( // Property edit state var editingIndex by remember { mutableStateOf(null) } // null=none, -1=new - // Load types on start - LaunchedEffect(Unit) { + suspend fun loadSpaces(key: String) { + 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 - client.getTypes() - .onSuccess { types = it } - .onFailure { errorMessage = "Failed to load types: ${it.message}" } + val tempClient = AnyTypeApi(baseUrl = config.connection.baseUrl, apiKey = key, spaceId = spaceId) + try { + tempClient.getTypes() + .onSuccess { types = it } + .onFailure { errorMessage = "Failed to load types: ${it.message}" } + } finally { + tempClient.close() + } isLoadingTypes = false } + // Initial load + LaunchedEffect(Unit) { + loadSpaces(apiKey) + if (selectedSpaceId.isNotEmpty()) { + loadTypes(apiKey, selectedSpaceId) + } + } + val selectedType = types.find { it.key == selectedTypeKey } val availableProperties = selectedType?.properties.orEmpty() fun buildConfig(): Configuration = Configuration( connection = Connection(apiKey = apiKey, baseUrl = config.connection.baseUrl), - spaceId = config.spaceId, + spaceId = selectedSpaceId, taskTypeKey = selectedTypeKey, properties = propertyConfigs.toList(), ) @@ -96,13 +129,56 @@ fun SettingsWindow( Text("Settings", style = MaterialTheme.typography.h6) Spacer(modifier = Modifier.height(16.dp)) - // ApiKey - OutlinedTextField( - value = apiKey, - onValueChange = { apiKey = it }, - label = { Text("API Key") }, - singleLine = true, - modifier = Modifier.fillMaxWidth(), + // ApiKey + reload button + Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { + OutlinedTextField( + value = apiKey, + onValueChange = { apiKey = it }, + label = { Text("API Key") }, + singleLine = true, + 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)) diff --git a/src/main/resources/log4j2-backup.xml b/src/main/resources/log4j2-backup.xml new file mode 100644 index 0000000..35a6a3c --- /dev/null +++ b/src/main/resources/log4j2-backup.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index 35a6a3c..472f3f1 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -1,12 +1,17 @@ + + {FATAL=bright red, ERROR=red, WARN=bright yellow, INFO=Normal, DEBUG=white, TRACE=white} + {FATAL=bright red, ERROR=Normal, WARN=Normal, INFO=Normal, DEBUG=Normal, TRACE=white} + %d{yy-MM-dd HH:mm:ss.SSS} %style{[%t]}{magenta} %highlight{%-5level}${CLR} %style{%logger{36}}{cyan} %highlight{: %msg%n}${MSG_CLR} + - + - +