Project init by claude

This commit is contained in:
Thomas Boksberger committed 2026-04-03 09:42:49 +02:00
1 parent 0f63311c6a
commit 830765cb13
7 files changed
+345

No files matched your search

+47
View File
@@ -0,0 +1,47 @@
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
plugins {
kotlin("jvm") version "2.1.0"
id("org.jetbrains.compose") version "1.7.1"
id("org.jetbrains.kotlin.plugin.compose") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}
repositories {
mavenCentral()
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
}
dependencies {
implementation(compose.desktop.currentOs)
// Global hotkeys
implementation("com.github.kwhat:jnativehook:2.2.2")
// AnyType API (HTTP)
val ktorVersion = "3.0.0"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
// Coroutines (Swing/AWT dispatcher)
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.9.0")
}
compose.desktop {
application {
mainClass = "ch.ergon.anytask.MainKt"
nativeDistributions {
targetFormats(TargetFormat.Dmg)
packageName = "AnyTask"
packageVersion = "1.0.0"
macOS {
bundleID = "ch.ergon.anytask"
}
}
}
}
+2
View File
@@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2g -Dfile.encoding=UTF-8
kotlin.daemon.jvm.options=-Xmx2g
+1
View File
@@ -0,0 +1 @@
rootProject.name = "AnyTask"
+87
View File
@@ -0,0 +1,87 @@
package ch.ergon.anytask
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.*
import ch.ergon.anytask.api.AnyTypeClient
import ch.ergon.anytask.hotkey.HotkeyManager
import ch.ergon.anytask.ui.TaskWindow
import java.awt.*
fun main() {
// Kein Dock-Icon auf macOS (Agent-App)
System.setProperty("apple.awt.UIElement", "true")
application {
var isVisible by remember { mutableStateOf(false) }
val windowState = rememberWindowState(
position = WindowPosition.Aligned(Alignment.Center),
size = DpSize(480.dp, 320.dp)
)
val client = remember { AnyTypeClient() }
LaunchedEffect(Unit) {
HotkeyManager.start(onHotkey = { isVisible = !isVisible })
setupSystemTray(onShow = { isVisible = true }, onExit = { exitApplication() })
}
DisposableEffect(Unit) {
onDispose { HotkeyManager.stop() }
}
Window(
onCloseRequest = { isVisible = false },
visible = isVisible,
state = windowState,
title = "AnyTask",
resizable = false,
alwaysOnTop = true,
undecorated = false,
) {
TaskWindow(
client = client,
onDismiss = { isVisible = false }
)
}
}
}
private fun setupSystemTray(onShow: () -> Unit, onExit: () -> Unit) {
if (!SystemTray.isSupported()) return
val size = SystemTray.getSystemTray().trayIconSize
val icon = createTrayIcon(size)
val menu = PopupMenu().apply {
add(MenuItem("Öffnen").also { it.addActionListener { onShow() } })
addSeparator()
add(MenuItem("Beenden").also { it.addActionListener { onExit() } })
}
val trayIcon = TrayIcon(icon, "AnyTask", menu).apply {
isImageAutoSize = true
addActionListener { onShow() } // Doppelklick
}
SystemTray.getSystemTray().add(trayIcon)
}
private fun createTrayIcon(size: Dimension): Image {
val img = java.awt.image.BufferedImage(size.width, size.height, java.awt.image.BufferedImage.TYPE_INT_ARGB)
val g = img.createGraphics().apply {
setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
color = Color(100, 149, 237) // Cornflower Blue
fillOval(1, 1, size.width - 2, size.height - 2)
color = Color.WHITE
font = Font("SansSerif", Font.BOLD, (size.height * 0.55).toInt())
val fm = getFontMetrics(font)
val label = "A"
drawString(label, (size.width - fm.stringWidth(label)) / 2, (size.height + fm.ascent - fm.descent) / 2)
}
g.dispose()
return img
}
@@ -0,0 +1,53 @@
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,55 @@
package ch.ergon.anytask.hotkey
import com.github.kwhat.jnativehook.GlobalScreen
import com.github.kwhat.jnativehook.NativeHookException
import com.github.kwhat.jnativehook.keyboard.NativeKeyEvent
import com.github.kwhat.jnativehook.keyboard.NativeKeyListener
import java.awt.EventQueue
import java.util.logging.Level
import java.util.logging.Logger
/**
* Lauscht auf eine systemweite Tastenkombination: Ctrl+Shift+Space
* Erfordert auf macOS die Berechtigung "Eingabegeräte überwachen" (Accessibility).
*/
object HotkeyManager {
private var listener: NativeKeyListener? = null
fun start(onHotkey: () -> Unit) {
// JNativeHook-Logging unterdrücken
Logger.getLogger(GlobalScreen::class.java.`package`.name).apply {
level = Level.OFF
useParentHandlers = false
}
try {
GlobalScreen.registerNativeHook()
} catch (e: NativeHookException) {
System.err.println("JNativeHook konnte nicht registriert werden: ${e.message}")
return
}
listener = object : NativeKeyListener {
override fun nativeKeyPressed(e: NativeKeyEvent) {
if (isHotkey(e)) {
// Callback auf den AWT-EDT dispatchen (Compose läuft dort)
EventQueue.invokeLater(onHotkey)
}
}
}.also { GlobalScreen.addNativeKeyListener(it) }
}
fun stop() {
listener?.let { GlobalScreen.removeNativeKeyListener(it) }
try {
GlobalScreen.unregisterNativeHook()
} catch (_: NativeHookException) {}
}
/** Ctrl+Shift+Space */
private fun isHotkey(e: NativeKeyEvent): Boolean =
e.keyCode == NativeKeyEvent.VC_SPACE &&
e.modifiers and NativeKeyEvent.CTRL_MASK != 0 &&
e.modifiers and NativeKeyEvent.SHIFT_MASK != 0
}
@@ -0,0 +1,100 @@
package ch.ergon.anytask.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.dp
import ch.ergon.anytask.api.AnyTypeClient
import kotlinx.coroutines.launch
@Composable
fun TaskWindow(client: AnyTypeClient, onDismiss: () -> Unit) {
val scope = rememberCoroutineScope()
var title by remember { mutableStateOf("") }
var details by remember { mutableStateOf("") }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val focusRequester = remember { FocusRequester() }
fun submit() {
if (title.isBlank()) return
scope.launch {
isLoading = true
errorMessage = null
client.createTask(title = title.trim(), details = details.trim())
.onSuccess {
title = ""
details = ""
onDismiss()
}
.onFailure { errorMessage = it.message }
isLoading = false
}
}
MaterialTheme {
Surface(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier
.padding(24.dp)
.onKeyEvent { event ->
when {
event.type == KeyEventType.KeyDown && event.key == Key.Escape -> {
onDismiss(); true
}
event.type == KeyEventType.KeyDown &&
event.key == Key.Enter &&
event.isMetaPressed -> {
submit(); true
}
else -> false
}
},
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text("Neue Aufgabe", style = MaterialTheme.typography.h6)
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Titel") },
singleLine = true,
modifier = Modifier
.fillMaxWidth()
.focusRequester(focusRequester),
)
OutlinedTextField(
value = details,
onValueChange = { details = it },
label = { Text("Details (optional)") },
modifier = Modifier.fillMaxWidth().height(100.dp),
maxLines = 4,
)
if (errorMessage != null) {
Text(errorMessage!!, color = MaterialTheme.colors.error, style = MaterialTheme.typography.caption)
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.End),
) {
TextButton(onClick = onDismiss) { Text("Abbrechen") }
Button(onClick = ::submit, enabled = title.isNotBlank() && !isLoading) {
if (isLoading) CircularProgressIndicator(modifier = Modifier.size(16.dp), strokeWidth = 2.dp)
else Text("Erstellen ⌘↵")
}
}
}
}
}
LaunchedEffect(Unit) { focusRequester.requestFocus() }
}
Reference in new issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.