first workting version
This commit is contained in:
1 parent
830765cb13
commit
d2048ea5b7
23 files changed
+292
-86
No files matched your search
@@ -13,3 +13,6 @@ out/
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# Http Client
|
||||||
|
httpClient/http-client.private.env.json
|
||||||
Generated
+6
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="21" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+15
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+7
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="externalSystemId" value="Gradle" />
|
||||||
|
<option name="version" value="2.1.0" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
Generated
+5
-1
@@ -1,6 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="temurin-21" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ProjectModuleManager">
|
<component name="ProjectModuleManager">
|
||||||
<modules>
|
<modules>
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/AnyTask.iml" filepath="$PROJECT_DIR$/.idea/AnyTask.iml" />
|
<module fileurl="file://$PROJECT_DIR$/.idea/modules/AnyTask.main.iml" filepath="$PROJECT_DIR$/.idea/modules/AnyTask.main.iml" />
|
||||||
</modules>
|
</modules>
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
Generated
+8
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module version="4">
|
||||||
|
<component name="AdditionalModuleElements">
|
||||||
|
<content url="file://$MODULE_DIR$/../../src/main" dumb="true">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/../../src/main/resources" type="java-resource" />
|
||||||
|
</content>
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
@@ -2,37 +2,47 @@
|
|||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
Always stage newly created files that permanently belong to the project with `git add <file>`.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
./gradlew run # App starten
|
./gradlew run # Run the app
|
||||||
./gradlew compileKotlin # Nur kompilieren
|
./gradlew compileKotlin # Compile only
|
||||||
./gradlew packageDmg # macOS DMG bauen
|
./gradlew packageDmg # Build macOS DMG
|
||||||
```
|
```
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
**AnyTask** ist eine Kotlin/Compose-Desktop-App, die im Hintergrund läuft und über eine globale Tastenkombination (Ctrl+Shift+Space) ein Fenster zum Erstellen von AnyType-Tasks zeigt.
|
**AnyTask** is a Kotlin/Compose desktop app that runs in the background and shows a task creation window via a global hotkey (Ctrl+Shift+Space).
|
||||||
|
|
||||||
### Komponenten
|
### Components
|
||||||
|
|
||||||
- **`Main.kt`** — Einstiegspunkt. Setzt `apple.awt.UIElement=true` (kein Dock-Icon), startet die Compose-`application {}`, richtet System-Tray und HotkeyManager ein.
|
- **`Main.kt`** — Entry point. Sets `apple.awt.UIElement=true` (no dock icon), starts the Compose `application {}`, sets up system tray and HotkeyManager.
|
||||||
- **`hotkey/HotkeyManager.kt`** — JNativeHook-Wrapper. Registriert Ctrl+Shift+Space als systemweiten Hotkey und dispatcht den Callback auf den AWT-EDT.
|
- **`hotkey/HotkeyManager.kt`** — JNativeHook wrapper. Registers Ctrl+Shift+Space as a system-wide hotkey and dispatches the callback on the AWT EDT.
|
||||||
- **`ui/TaskWindow.kt`** — Compose-UI mit Formular (Titel, Details). Escape schliesst, ⌘↵ erstellt.
|
- **`ui/TaskWindow.kt`** — Compose UI with a form (title, details). Escape closes, ⌘↵ creates.
|
||||||
- **`api/AnyTypeClient.kt`** — Ktor-HTTP-Client für die lokale AnyType-API (`localhost:31007`). Momentan Skeleton — API-Details müssen noch verifiziert werden.
|
- **`api/AnyTypeApi.kt`** — Ktor HTTP client for the local AnyType API (`localhost:31009`). Currently a skeleton — API details still need to be verified.
|
||||||
|
|
||||||
### Stack
|
### Stack
|
||||||
|
|
||||||
- Kotlin 2.1.0 / Compose Multiplatform 1.7.1
|
- Kotlin 2.1.0 / Compose Multiplatform 1.7.1
|
||||||
- JNativeHook 2.2.2 (globale Hotkeys)
|
- JNativeHook 2.2.2 (global hotkeys)
|
||||||
- Ktor 3.0.0 + kotlinx.serialization (AnyType API)
|
- Ktor 3.0.0 + kotlinx.serialization (AnyType API)
|
||||||
- Gradle 9.2.1
|
- Gradle 9.2.1
|
||||||
|
|
||||||
## macOS-Hinweise
|
## Code Style
|
||||||
|
|
||||||
- **Accessibility-Berechtigung** für JNativeHook: System Settings → Privacy & Security → Accessibility → App hinzufügen. Ohne diese Berechtigung funktioniert der globale Hotkey nicht.
|
- One class per file — file name matches the class name.
|
||||||
- Das Tray-Icon erscheint in der Menüleiste (kein Dock-Icon, da `apple.awt.UIElement=true`).
|
- No wildcard imports (`import foo.*`).
|
||||||
|
- Code, comments, and documentation in English.
|
||||||
|
|
||||||
|
## macOS Notes
|
||||||
|
|
||||||
|
- **Accessibility permission** for JNativeHook: System Settings → Privacy & Security → Accessibility → add the app. Without this permission the global hotkey will not work.
|
||||||
|
- The tray icon appears in the menu bar (no dock icon, since `apple.awt.UIElement=true`).
|
||||||
|
|
||||||
## AnyType API
|
## AnyType API
|
||||||
|
|
||||||
Die lokale AnyType-API läuft via `anytype-heart` auf `http://localhost:31007`. Der `appKey` und die `spaceId` in `AnyTypeClient` müssen noch konfigurierbar gemacht werden.
|
The local AnyType API runs via `anytype-heart` on `http://localhost:31007`. `apiKey` and `spaceId` are configured via `anytask.yaml`.
|
||||||
@@ -28,6 +28,13 @@ dependencies {
|
|||||||
|
|
||||||
// Coroutines (Swing/AWT dispatcher)
|
// Coroutines (Swing/AWT dispatcher)
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.9.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.9.0")
|
||||||
|
|
||||||
|
// YAML config
|
||||||
|
implementation("com.charleskorn.kaml:kaml:0.67.0")
|
||||||
|
|
||||||
|
// Logging
|
||||||
|
implementation("org.apache.logging.log4j:log4j-api:2.24.3")
|
||||||
|
implementation("org.apache.logging.log4j:log4j-core:2.24.3")
|
||||||
}
|
}
|
||||||
|
|
||||||
compose.desktop {
|
compose.desktop {
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
### Get Spaces
|
||||||
|
GET http://localhost:31009/v1/spaces
|
||||||
|
Authorization: Bearer {{apiKey}}
|
||||||
|
|
||||||
|
### Create Task
|
||||||
|
POST http://localhost:31009/v1/spaces/{{spaceId}}/objects
|
||||||
|
Authorization: Bearer {{apiKey}}
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Test Task II",
|
||||||
|
"body": "Created via HTTP client",
|
||||||
|
"type_key": "ot-task"
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"local": {
|
||||||
|
"apiKey": "<API-Key>",
|
||||||
|
"spaceId": "<Space-Id>"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,18 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.unit.DpSize
|
import androidx.compose.ui.unit.DpSize
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.*
|
import androidx.compose.ui.window.*
|
||||||
import ch.ergon.anytask.api.AnyTypeClient
|
import ch.ergon.anytask.api.AnyTypeApi
|
||||||
|
import ch.ergon.anytask.config.loadConfig
|
||||||
import ch.ergon.anytask.hotkey.HotkeyManager
|
import ch.ergon.anytask.hotkey.HotkeyManager
|
||||||
import ch.ergon.anytask.ui.TaskWindow
|
import ch.ergon.anytask.ui.TaskWindow
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
import java.awt.*
|
import java.awt.*
|
||||||
|
import kotlin.system.exitProcess
|
||||||
|
|
||||||
|
private val logger = LogManager.getLogger("Main")
|
||||||
|
|
||||||
fun main() {
|
fun main() {
|
||||||
// Kein Dock-Icon auf macOS (Agent-App)
|
// No dock icon on macOS (agent app)
|
||||||
System.setProperty("apple.awt.UIElement", "true")
|
System.setProperty("apple.awt.UIElement", "true")
|
||||||
|
|
||||||
application {
|
application {
|
||||||
@@ -22,11 +27,25 @@ fun main() {
|
|||||||
size = DpSize(480.dp, 320.dp)
|
size = DpSize(480.dp, 320.dp)
|
||||||
)
|
)
|
||||||
|
|
||||||
val client = remember { AnyTypeClient() }
|
val config = remember { loadConfig() }
|
||||||
|
val client = remember { AnyTypeApi(apiKey = config.apiKey, spaceId = config.spaceId) }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
HotkeyManager.start(onHotkey = { isVisible = !isVisible })
|
HotkeyManager.start(onHotkey = { isVisible = !isVisible })
|
||||||
setupSystemTray(onShow = { isVisible = true }, onExit = { exitApplication() })
|
setupSystemTray(onShow = { isVisible = 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 == config.spaceId }
|
||||||
|
if (space == null) {
|
||||||
|
logger.error("Selected space not found: ${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) {
|
||||||
@@ -57,14 +76,14 @@ private fun setupSystemTray(onShow: () -> Unit, onExit: () -> Unit) {
|
|||||||
val icon = createTrayIcon(size)
|
val icon = createTrayIcon(size)
|
||||||
|
|
||||||
val menu = PopupMenu().apply {
|
val menu = PopupMenu().apply {
|
||||||
add(MenuItem("Öffnen").also { it.addActionListener { onShow() } })
|
add(MenuItem("Open").also { it.addActionListener { onShow() } })
|
||||||
addSeparator()
|
addSeparator()
|
||||||
add(MenuItem("Beenden").also { it.addActionListener { onExit() } })
|
add(MenuItem("Quit").also { it.addActionListener { onExit() } })
|
||||||
}
|
}
|
||||||
|
|
||||||
val trayIcon = TrayIcon(icon, "AnyTask", menu).apply {
|
val trayIcon = TrayIcon(icon, "AnyTask", menu).apply {
|
||||||
isImageAutoSize = true
|
isImageAutoSize = true
|
||||||
addActionListener { onShow() } // Doppelklick
|
addActionListener { onShow() } // double click
|
||||||
}
|
}
|
||||||
|
|
||||||
SystemTray.getSystemTray().add(trayIcon)
|
SystemTray.getSystemTray().add(trayIcon)
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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.Space
|
||||||
|
import ch.ergon.anytask.api.dto.SpacesResponse
|
||||||
|
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.serialization.kotlinx.json.*
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
|
import kotlin.time.DurationUnit.MILLISECONDS
|
||||||
|
import kotlin.time.toDuration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Client for the local AnyType API (anytype-heart middleware).
|
||||||
|
* The API runs on http://localhost:31007 by default.
|
||||||
|
* apiKey and spaceId are loaded from anytask.yaml.
|
||||||
|
*/
|
||||||
|
class AnyTypeApi(
|
||||||
|
private val baseUrl: String = "http://localhost:31009",
|
||||||
|
private val apiKey: String = "",
|
||||||
|
private val spaceId: String = "",
|
||||||
|
) {
|
||||||
|
private val logger = LogManager.getLogger(AnyTypeApi::class.java)
|
||||||
|
|
||||||
|
private val http = HttpClient(CIO) {
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json { ignoreUnknownKeys = true })
|
||||||
|
}
|
||||||
|
}.also { client ->
|
||||||
|
client.plugin(HttpSend).intercept { request ->
|
||||||
|
val method = request.method.value
|
||||||
|
val url = request.url.buildString()
|
||||||
|
logger.info("→ $method $url")
|
||||||
|
val start = System.currentTimeMillis()
|
||||||
|
val call = execute(request)
|
||||||
|
val duration = (System.currentTimeMillis() - start).toDuration(MILLISECONDS)
|
||||||
|
val status = call.response.status
|
||||||
|
logger.info("← ${status.value} (${status.description}) $duration")
|
||||||
|
if (!status.isSuccess()) {
|
||||||
|
logger.error("Response body: ${call.response.bodyAsText()}")
|
||||||
|
}
|
||||||
|
call
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getSpaces(): Result<List<Space>> =
|
||||||
|
runCatching {
|
||||||
|
val response = http.get("$baseUrl/v1/spaces") {
|
||||||
|
header("Anytype-Version", "0.1")
|
||||||
|
bearerAuth(apiKey)
|
||||||
|
}
|
||||||
|
response.body<SpacesResponse>().data
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun createTask(title: String, details: String = ""): Result<String> =
|
||||||
|
runCatching {
|
||||||
|
val response = http.post("$baseUrl/v1/spaces/$spaceId/objects") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
header("Anytype-Version", "0.1")
|
||||||
|
bearerAuth(apiKey)
|
||||||
|
setBody(CreateObjectRequest(name = title, body = details, typeKey = "ot-task"))
|
||||||
|
}
|
||||||
|
response.body<CreateObjectResponse>().objectId
|
||||||
|
}
|
||||||
|
|
||||||
|
fun close() = http.close()
|
||||||
|
}
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
package ch.ergon.anytask.api
|
|
||||||
|
|
||||||
import io.ktor.client.*
|
|
||||||
import io.ktor.client.call.*
|
|
||||||
import io.ktor.client.engine.cio.*
|
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
|
||||||
import io.ktor.client.request.*
|
|
||||||
import io.ktor.http.*
|
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import kotlinx.serialization.json.Json
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Client für die lokale AnyType-API (anytype-heart Middleware).
|
|
||||||
* Die API läuft standardmässig auf http://localhost:31007.
|
|
||||||
*
|
|
||||||
* TODO: App-Key via Einstellungen konfigurierbar machen.
|
|
||||||
*/
|
|
||||||
class AnyTypeClient(
|
|
||||||
private val baseUrl: String = "http://localhost:31007",
|
|
||||||
private val appKey: String = "", // wird nach erstem Login gesetzt
|
|
||||||
) {
|
|
||||||
private val http = HttpClient(CIO) {
|
|
||||||
install(ContentNegotiation) {
|
|
||||||
json(Json { ignoreUnknownKeys = true })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun createTask(title: String, details: String = "", spaceId: String = ""): Result<String> =
|
|
||||||
runCatching {
|
|
||||||
val response = http.post("$baseUrl/v1/spaces/$spaceId/objects") {
|
|
||||||
contentType(ContentType.Application.Json)
|
|
||||||
header("Anytype-Version", "0.1")
|
|
||||||
bearerAuth(appKey)
|
|
||||||
setBody(CreateObjectRequest(title = title, details = details, typeKey = "ot-task"))
|
|
||||||
}
|
|
||||||
response.body<CreateObjectResponse>().objectId
|
|
||||||
}
|
|
||||||
|
|
||||||
fun close() = http.close()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class CreateObjectRequest(
|
|
||||||
val title: String,
|
|
||||||
val details: String,
|
|
||||||
val typeKey: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
|
||||||
private data class CreateObjectResponse(
|
|
||||||
val objectId: String,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package ch.ergon.anytask.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateObjectRequest(
|
||||||
|
val name: String,
|
||||||
|
val body: String,
|
||||||
|
@SerialName("type_key") val typeKey: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ch.ergon.anytask.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CreateObjectResponse(
|
||||||
|
val objectId: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package ch.ergon.anytask.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Space(
|
||||||
|
val id: String,
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package ch.ergon.anytask.api.dto
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SpacesResponse(
|
||||||
|
val data: List<Space>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package ch.ergon.anytask.config
|
||||||
|
|
||||||
|
import com.charleskorn.kaml.Yaml
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class AppConfig(
|
||||||
|
val apiKey: String = "",
|
||||||
|
val spaceId: String = "",
|
||||||
|
)
|
||||||
|
|
||||||
|
fun loadConfig(): AppConfig {
|
||||||
|
val yaml = Yaml.default
|
||||||
|
val serializer = AppConfig.serializer()
|
||||||
|
|
||||||
|
// External files take precedence (production use)
|
||||||
|
val externalLocations = listOf(
|
||||||
|
File(System.getProperty("user.home"), ".anytask/anytask.yaml"),
|
||||||
|
File("anytask.yaml"),
|
||||||
|
)
|
||||||
|
externalLocations.firstOrNull { it.exists() }?.let {
|
||||||
|
return yaml.decodeFromString(serializer, it.readText())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: classpath resource (development)
|
||||||
|
AppConfig::class.java.getResourceAsStream("/anytask.yaml")?.let {
|
||||||
|
return yaml.decodeFromString(serializer, it.bufferedReader().readText())
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppConfig()
|
||||||
|
}
|
||||||
@@ -4,20 +4,22 @@ import com.github.kwhat.jnativehook.GlobalScreen
|
|||||||
import com.github.kwhat.jnativehook.NativeHookException
|
import com.github.kwhat.jnativehook.NativeHookException
|
||||||
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
|
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
|
||||||
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener
|
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener
|
||||||
|
import org.apache.logging.log4j.LogManager
|
||||||
import java.awt.EventQueue
|
import java.awt.EventQueue
|
||||||
import java.util.logging.Level
|
import java.util.logging.Level
|
||||||
import java.util.logging.Logger
|
import java.util.logging.Logger
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lauscht auf eine systemweite Tastenkombination: Ctrl+Shift+Space
|
* Listens for a system-wide hotkey: Ctrl+Shift+Space.
|
||||||
* Erfordert auf macOS die Berechtigung "Eingabegeräte überwachen" (Accessibility).
|
* Requires the Accessibility permission on macOS.
|
||||||
*/
|
*/
|
||||||
object HotkeyManager {
|
object HotkeyManager {
|
||||||
|
|
||||||
|
private val logger = LogManager.getLogger(HotkeyManager::class.java)
|
||||||
private var listener: NativeKeyListener? = null
|
private var listener: NativeKeyListener? = null
|
||||||
|
|
||||||
fun start(onHotkey: () -> Unit) {
|
fun start(onHotkey: () -> Unit) {
|
||||||
// JNativeHook-Logging unterdrücken
|
// Suppress JNativeHook logging
|
||||||
Logger.getLogger(GlobalScreen::class.java.`package`.name).apply {
|
Logger.getLogger(GlobalScreen::class.java.`package`.name).apply {
|
||||||
level = Level.OFF
|
level = Level.OFF
|
||||||
useParentHandlers = false
|
useParentHandlers = false
|
||||||
@@ -26,14 +28,14 @@ object HotkeyManager {
|
|||||||
try {
|
try {
|
||||||
GlobalScreen.registerNativeHook()
|
GlobalScreen.registerNativeHook()
|
||||||
} catch (e: NativeHookException) {
|
} catch (e: NativeHookException) {
|
||||||
System.err.println("JNativeHook konnte nicht registriert werden: ${e.message}")
|
logger.error("Failed to register JNativeHook: ${e.message}")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
listener = object : NativeKeyListener {
|
listener = object : NativeKeyListener {
|
||||||
override fun nativeKeyPressed(e: NativeKeyEvent) {
|
override fun nativeKeyPressed(e: NativeKeyEvent) {
|
||||||
if (isHotkey(e)) {
|
if (isHotkey(e)) {
|
||||||
// Callback auf den AWT-EDT dispatchen (Compose läuft dort)
|
// Dispatch callback on the AWT EDT (where Compose runs)
|
||||||
EventQueue.invokeLater(onHotkey)
|
EventQueue.invokeLater(onHotkey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,11 +9,11 @@ import androidx.compose.ui.focus.FocusRequester
|
|||||||
import androidx.compose.ui.focus.focusRequester
|
import androidx.compose.ui.focus.focusRequester
|
||||||
import androidx.compose.ui.input.key.*
|
import androidx.compose.ui.input.key.*
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import ch.ergon.anytask.api.AnyTypeClient
|
import ch.ergon.anytask.api.AnyTypeApi
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun TaskWindow(client: AnyTypeClient, onDismiss: () -> Unit) {
|
fun TaskWindow(client: AnyTypeApi, onDismiss: () -> Unit) {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var title by remember { mutableStateOf("") }
|
var title by remember { mutableStateOf("") }
|
||||||
var details by remember { mutableStateOf("") }
|
var details by remember { mutableStateOf("") }
|
||||||
@@ -58,12 +58,12 @@ fun TaskWindow(client: AnyTypeClient, onDismiss: () -> Unit) {
|
|||||||
},
|
},
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp),
|
verticalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
) {
|
) {
|
||||||
Text("Neue Aufgabe", style = MaterialTheme.typography.h6)
|
Text("New Task", style = MaterialTheme.typography.h6)
|
||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = title,
|
value = title,
|
||||||
onValueChange = { title = it },
|
onValueChange = { title = it },
|
||||||
label = { Text("Titel") },
|
label = { Text("Title") },
|
||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -86,10 +86,10 @@ fun TaskWindow(client: AnyTypeClient, onDismiss: () -> Unit) {
|
|||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
|
||||||
) {
|
) {
|
||||||
TextButton(onClick = onDismiss) { Text("Abbrechen") }
|
TextButton(onClick = onDismiss) { Text("Cancel") }
|
||||||
Button(onClick = ::submit, enabled = title.isNotBlank() && !isLoading) {
|
Button(onClick = ::submit, enabled = title.isNotBlank() && !isLoading) {
|
||||||
if (isLoading) CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
if (isLoading) CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
|
||||||
else Text("Erstellen ⌘↵")
|
else Text("Create ⌘↵")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
apiKey: 2tkMSzFegWwzsQ+n9SpGwVjychEkCESWjV1/ftytXG4=
|
||||||
|
spaceId: bafyreiafzvfsrurvx4gvr2wwysgp5uugujzmbdg5hxnjlahfmx3ww6kxfy.172mmtcppmjtt
|
||||||
@@ -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>
|
||||||