Milan Jurkulak 3 ماه پیش
والد
کامیت
7185ad8cf0
100فایلهای تغییر یافته به همراه2944 افزوده شده و 2084 حذف شده
  1. 13 1
      .claude/settings.local.json
  2. 8 0
      .editorconfig
  3. 3 0
      .gitignore
  4. 31 0
      .run/buildAll.run.xml
  5. 31 0
      .run/runAiAgent.run.xml
  6. 18 0
      ai-credentials.txt
  7. 18 0
      ai-done.txt
  8. 2 0
      ai-project.txt
  9. 0 0
      ai-suggestions.txt
  10. 8 0
      ai-todo.txt
  11. 75 1
      build.gradle.kts
  12. 6 0
      buildSrc/build.gradle.kts
  13. 0 21
      buildSrc/src/main/kotlin/AndroidConfig.kt
  14. 0 24
      buildSrc/src/main/kotlin/BasePlugin.kt
  15. 0 71
      buildSrc/src/main/kotlin/DeployTask.kt
  16. 51 0
      buildSrc/src/main/kotlin/EnsureAppImageToolTask.kt
  17. 0 71
      buildSrc/src/main/kotlin/GenerateChangelogTask.kt
  18. 0 26
      buildSrc/src/main/kotlin/KotlinDependencyHandlerExt.kt
  19. 0 19
      buildSrc/src/main/kotlin/MultiPlatformPlugin.kt
  20. 103 0
      buildSrc/src/main/kotlin/PackageAppImageTask.kt
  21. 0 161
      buildSrc/src/main/kotlin/SourceSets.kt
  22. 31 0
      buildSrc/src/main/kotlin/ai/AiAgentPlugin.kt
  23. 205 0
      buildSrc/src/main/kotlin/ai/AiProvider.kt
  24. 154 0
      buildSrc/src/main/kotlin/ai/DependencyUpdater.kt
  25. 232 0
      buildSrc/src/main/kotlin/ai/RunAiAgentTask.kt
  26. 99 57
      composeApp/composeApp.gradle.kts
  27. 35 1
      composeApp/composeApp.pro
  28. 35 24
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/activity/MainActivity.kt
  29. 1 1
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/app/App.kt
  30. 9 11
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/MediaPlayer.kt
  31. 52 46
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayer.kt
  32. 3 3
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayerState.kt
  33. 57 51
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt
  34. 4 3
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/LocalDesktopContext.android.kt
  35. 20 21
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/App.kt
  36. 28 37
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/Theme.kt
  37. 19 21
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/User.kt
  38. 3 6
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/UserDirs.kt
  39. 13 14
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ComponentActivity.kt
  40. 17 14
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt
  41. 9 10
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/DecoderAndroid.kt
  42. 6 5
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/WakeLockHelper.kt
  43. 20 15
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/crash/CrashHandler.kt
  44. 8 6
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/CodePointsFile.kt
  45. 16 12
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/FontDescriptor.kt
  46. 19 14
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/MaterialIconFont.android.kt
  47. 27 23
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/permission/PermissonManager.kt
  48. 20 20
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/persistence/StorageProvider.kt
  49. 10 11
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/streams/ResourceStream.android.kt
  50. 1 1
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfo.android.kt
  51. 6 5
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfoAndroid.kt
  52. 98 82
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/main/MainView.kt
  53. 10 7
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/tts/TTSPluginAndroid.kt
  54. 33 26
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/apps/AppsManager.kt
  55. 4 5
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManager.kt
  56. 8 10
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManagerStub.kt
  57. 6 5
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/keys/KeysManager.kt
  58. 6 7
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/OSManager.kt
  59. 2 2
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/processes/ProcessManager.kt
  60. 5 6
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/ThemeManager.kt
  61. 57 71
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/system/Filesystem.android.kt
  62. 1 1
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/wayland/WaylandDisplay.android.kt
  63. 11 10
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/windows/ChromeWindow.kt
  64. 59 56
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/adb/AdbScreenMirror.kt
  65. 208 162
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appbar/AppBar.kt
  66. 116 96
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt
  67. 23 18
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsBottomBar.kt
  68. 30 26
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsList.kt
  69. 15 13
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuActions.kt
  70. 31 27
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuApp.kt
  71. 34 29
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuCategory.kt
  72. 34 29
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt
  73. 33 26
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanel.kt
  74. 13 13
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanelHaze.kt
  75. 13 11
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonPrimary.kt
  76. 13 11
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonSecondary.kt
  77. 12 11
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/TransparentButton.kt
  78. 14 12
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/Card.kt
  79. 24 21
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CardContent.kt
  80. 35 33
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ClassicCard.kt
  81. 43 40
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CompactCard.kt
  82. 63 50
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/FocusableCard.kt
  83. 27 24
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ItemCard.kt
  84. 30 26
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/PhotoCard.kt
  85. 37 34
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/WideClassicCard.kt
  86. 5 6
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardBorder.kt
  87. 10 11
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardColors.kt
  88. 91 76
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardDefaults.kt
  89. 3 5
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardGlow.kt
  90. 3 5
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardScale.kt
  91. 2 5
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardShape.kt
  92. 23 22
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chart/DonutChart.kt
  93. 2 4
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chromecast/ChromecastView.kt
  94. 66 62
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenter.kt
  95. 4 4
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPage.kt
  96. 5 7
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPageScope.kt
  97. 3 3
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PageCache.kt
  98. 22 17
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PageHeader.kt
  99. 14 13
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PersistentPageSaver.kt
  100. 17 17
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/pages/about/AboutPage.kt

+ 13 - 1
.claude/settings.local.json

@@ -18,7 +18,19 @@
       "Bash(./gradlew composeApp:build --dry-run --no-configuration-cache)",
       "Bash(./gradlew composeApp:build --dry-run -PdepCheck=false --no-configuration-cache)",
       "Bash(./gradlew composeApp:dependencyUpdates -PdepCheck=false --no-configuration-cache)",
-      "Bash(echo \"exit=$?\")"
+      "Bash(echo \"exit=$?\")",
+      "Bash(./gradlew buildAll --dry-run --no-configuration-cache)",
+      "Bash(./gradlew buildAll --console=plain)",
+      "Bash(echo \"EXIT=$?\")",
+      "Bash(awk -F. '{ if \\(NF>=2\\) print $1\".\"$2; else print $1 }' /tmp/pgwarn.txt)",
+      "Bash(tee /tmp/pgroots.txt)",
+      "Bash(./gradlew :composeApp:proguardReleaseJars --rerun-tasks --console=plain)",
+      "Bash(tee /tmp/pg2.log)",
+      "Bash(awk -F. '{ if \\(NF>=2 && $2 ~ /^[a-z]/\\) print $1\".\"$2; else print $1 }')",
+      "Bash(tee /tmp/pg3.log)",
+      "Bash(grep -cE \"\\\\.kt$\"22)",
+      "Bash(git checkout *)",
+      "Bash(cd *)"
     ]
   }
 }

+ 8 - 0
.editorconfig

@@ -0,0 +1,8 @@
+root = true
+
+[*.{kt,kts}]
+# ktlint's no-unused-imports is a false positive on receiver/extension-member imports
+# (e.g. ButtonDefaults.transparent / .noElevation) and would strip imports that are
+# actually used, breaking compilation when ktlintFormat runs. Disable just that rule so
+# auto-format stays safe.
+ktlint_standard_no-unused-imports = disabled

+ 3 - 0
.gitignore

@@ -60,3 +60,6 @@ composeApp/src/commonMain/resources/keys/*.key
 
 ### reports
 /reports/
+
+### releases
+/releases/

+ 31 - 0
.run/buildAll.run.xml

@@ -0,0 +1,31 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="buildAll" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="buildAll" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
+    <EXTENSION ID="com.android.tools.idea.testartifacts.testsuite.GradleRunConfigurationExtension">
+      <com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>false</com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>
+      <android.execution.deploysToLocalDevice>false</android.execution.deploysToLocalDevice>
+    </EXTENSION>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <GradleProfilingDisabled>false</GradleProfilingDisabled>
+    <GradleCoverageDisabled>false</GradleCoverageDisabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 31 - 0
.run/runAiAgent.run.xml

@@ -0,0 +1,31 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="runAiAgent" type="GradleRunConfiguration" factoryName="Gradle">
+    <ExternalSystemSettings>
+      <option name="executionName" />
+      <option name="externalProjectPath" value="$PROJECT_DIR$" />
+      <option name="externalSystemIdString" value="GRADLE" />
+      <option name="scriptParameters" value="" />
+      <option name="taskDescriptions">
+        <list />
+      </option>
+      <option name="taskNames">
+        <list>
+          <option value="runAiAgent" />
+        </list>
+      </option>
+      <option name="vmOptions" />
+    </ExternalSystemSettings>
+    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
+    <ExternalSystemDebugDisabled>false</ExternalSystemDebugDisabled>
+    <EXTENSION ID="com.android.tools.idea.testartifacts.testsuite.GradleRunConfigurationExtension">
+      <com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>false</com.android.tools.idea.testartifacts.testsuite.SHOW_TEST_RESULT_IN_ANDROID_TEST_SUITE_VIEW>
+      <android.execution.deploysToLocalDevice>false</android.execution.deploysToLocalDevice>
+    </EXTENSION>
+    <DebugAllEnabled>false</DebugAllEnabled>
+    <RunAsTest>false</RunAsTest>
+    <GradleProfilingDisabled>false</GradleProfilingDisabled>
+    <GradleCoverageDisabled>false</GradleCoverageDisabled>
+    <method v="2" />
+  </configuration>
+</component>

+ 18 - 0
ai-credentials.txt

@@ -0,0 +1,18 @@
+# ai-credentials.txt — credentials for the runAiAgent build agent (KEY=value, one per line).
+# Lines starting with '#' are ignored. This file is read by the AiAgentPlugin.
+# No real secrets are stored here by default — fill in only what you use.
+#
+# Provider selection is cloud-first (claude-code, then mistral) with a local
+# ollama fallback (the agent runs `ollama serve` on demand). Force one with:
+# provider=claude-code   # or: mistral | ollama
+#
+# --- Mistral (cloud) ---
+# MISTRAL_API_KEY=
+# MISTRAL_MODEL=mistral-large-latest
+#
+# --- Ollama (local fallback) ---
+# OLLAMA_HOST=http://localhost:11434
+# OLLAMA_MODEL=llama3.1
+#
+# --- claude-code (cloud, uses your existing `claude` CLI login; no key needed here) ---
+# CLAUDE_CODE_CMD=claude -p

+ 18 - 0
ai-done.txt

@@ -0,0 +1,18 @@
+## 2026-06-13 21:36:25 — Create the AI agent buildSrc plugin
+Implemented in buildSrc/src/main/kotlin/ai/ (AiAgentPlugin, RunAiAgentTask, AiProvider). Providers: ollama, claude-code, mistral. Reads ai-todo.txt; writes ai-done.txt / ai-suggestions.txt; credentials from ai-credentials.txt. Task `runAiAgent` runs the file.
+
+## 2026-06-13 21:36:25 — Ensure folder 'screenshots' exists at project root
+screenshots/ already existed — no change.
+
+## 2026-06-13 21:36:25 — Ensure folder 'screenshots-1' exists at project root
+Created screenshots-1/.
+
+## 2026-06-13 21:36:25 — Attach the AI agent plugin to the project
+Plugin `AiAgentPlugin` applied on the root project; `runAiAgent` task is registered.
+
+## 2026-06-13 21:36:25 — Provider selection policy (cloud-first, ollama fallback)
+Implemented in AiProviders.select(): prefers cloud (claude-code, then mistral); falls back to local ollama and runs `ollama serve` on demand when no cloud credentials are available.
+
+## 2026-06-13 21:36:25 — Autonomy / security policy
+Implemented in SecurityPolicy: all operations auto-approved except git write/create/commit/push and project->internet file transfers, which stay gated for human approval.
+

+ 2 - 0
ai-project.txt

@@ -0,0 +1,2 @@
+PROJECT_NAME=Mjdev Desktop
+PROJECT_PACKAGE=org.mjdev.desktop

+ 0 - 0
ai-suggestions.txt


+ 8 - 0
ai-todo.txt

@@ -0,0 +1,8 @@
+# This file contains project tasks for ai
+# * at the line beginning means repeatable task every build run
+# - at the line beginning means task to be done once, task can be multiline
+
+* collect reports from reports folder and clean up code where it is non-invasive,
+  and also check build can complete after update
+* update dependencies from report in reports folder, on every updated library check
+  that builds run and or ommit/revert update if impossible

+ 75 - 1
build.gradle.kts

@@ -17,6 +17,7 @@ plugins {
     alias(libs.plugins.gradle.ktlint)
     alias(libs.plugins.manes.versions)
     alias(libs.plugins.kotlin.android) apply false
+    id("AiAgentPlugin")
 //    id("com.github.gmazzo.buildconfig") version "5.6.5"
 }
 
@@ -82,7 +83,9 @@ val collectKtlintReports by tasks.registering(Copy::class) {
 // Runs after every build: auto-format, ktlint report, and (gated) dependency-update report -> /reports
 val postBuildCodeCheck by tasks.registering {
     group = "verification"
-    description = "Auto-formats code, runs ktlint and (gated) dependency-update report into /reports"
+    description = "Auto-formats code (ktlintFormat) and runs the gated dependency-update report into /reports"
+    // Auto-format on build. Safe because .editorconfig disables no-unused-imports — the one
+    // rule that stripped receiver/extension-member imports and broke compilation.
     dependsOn(":composeApp:ktlintFormat")
     if (depCheckEnabled) dependsOn(":composeApp:dependencyUpdates")
     finalizedBy(collectKtlintReports)
@@ -126,3 +129,74 @@ allprojects {
 //}
 
 //kover { reports { total { xml { onCheck = true } } } }
+
+// Aggregate task: build every distributable that this host can produce.
+//  - desktop: Deb + AppImage + Rpm (per the targetFormats in composeApp), release mode
+//  - android: release APK (assembleRelease)
+//  - ios:     only when an iOS Kotlin target is configured AND the host is macOS
+//             (Kotlin/Native for iOS cannot build on Linux/Windows) — wired below if present.
+// captured at configuration time -> all the tasks below stay configuration-cache safe
+val appNameV = libs.versions.app.name.get()
+val pkgDirV = rootDir.resolve("packages/main-release")
+val appImageToolPath = rootDir.resolve(".gradle/tools/appimagetool-x86_64.AppImage").absolutePath
+
+// Downloads appimagetool before the build (only if not already present / on PATH).
+val ensureAppImageTool = tasks.register<EnsureAppImageToolTask>("ensureAppImageTool") {
+    group = "mjdev"
+    description = "Downloads appimagetool (single-file .AppImage builder) if not already present."
+    toolPath.set(appImageToolPath)
+}
+
+// Wraps the jpackage app-image directory into a single, CLI-runnable .AppImage file.
+val packageAppImageFile = tasks.register<PackageAppImageTask>("packageAppImageFile") {
+    group = "mjdev"
+    description = "Builds a single-file .AppImage from the jpackage app-image (skips if appimagetool unavailable)."
+    dependsOn(ensureAppImageTool, ":composeApp:packageReleaseAppImage")
+    appName.set(appNameV)
+    appImagePath.set(pkgDirV.resolve("app/$appNameV").absolutePath)
+    outputPath.set(pkgDirV.resolve("appimage/$appNameV.AppImage").absolutePath)
+    toolPath.set(appImageToolPath)
+}
+
+// Collects all distributables into releases/ with stable (version-less) names so each
+// build overwrites the previous file. Declarative Sync = configuration-cache safe.
+val collectReleases = tasks.register<Sync>("collectReleases") {
+    group = "mjdev"
+    description = "Copies the built distributables into releases/ (stable names, overwrites)."
+    dependsOn(
+        ":composeApp:packageReleaseDistributionForCurrentOS",
+        ":composeApp:assembleRelease",
+        packageAppImageFile,
+    )
+    // local copies so the rename closures capture plain Strings (configuration-cache safe;
+    // capturing the script-level vals makes the closure hold a null script reference on reload)
+    val name = appNameV
+    into(layout.projectDirectory.dir("releases"))
+    from(pkgDirV.resolve("deb")) { include("*.deb"); rename { "$name.deb" } }
+    from(pkgDirV.resolve("rpm")) { include("*.rpm"); rename { "$name.rpm" } }
+    from(pkgDirV.resolve("exe")) { include("*.exe"); rename { "$name.exe" } } // present only on a Windows host
+    from(pkgDirV.resolve("appimage")) { include("*.AppImage") } // already named <app>.AppImage
+    from(rootDir.resolve("composeApp/build/outputs/apk/release")) {
+        include("*.apk"); rename { "$name.apk" }
+    }
+}
+
+val buildAll = tasks.register("buildAll") {
+    group = "mjdev"
+    description = "Builds all distributables this host can produce, collects them into releases/ (stable names), and generates reports into reports/ — like every build."
+    dependsOn(collectReleases, postBuildCodeCheck)
+}
+
+// Attach iOS framework build only when an iOS target actually exists in composeApp
+// (it never does on a Linux/Windows host — Kotlin/Native iOS requires macOS + Xcode).
+project(":composeApp").afterEvaluate {
+    val iosTask = tasks.names.firstOrNull {
+        it.startsWith("linkReleaseFrameworkIos") || it == "embedAndSignAppleFrameworkForXcode"
+    }
+    if (iosTask != null) {
+        buildAll.configure { dependsOn("${this@afterEvaluate.path}:$iosTask") }
+        logger.lifecycle("buildAll: iOS target found — wiring ${this@afterEvaluate.path}:$iosTask")
+    } else {
+        logger.info("buildAll: no iOS target configured (needs macOS + Xcode + an ios* target in composeApp) — iOS skipped")
+    }
+}

+ 6 - 0
buildSrc/build.gradle.kts

@@ -51,6 +51,12 @@ gradlePlugin {
             description = "Common library plugin to handle all stuffs needed."
             implementationClass = "MultiPlatformPlugin"
         }
+        register("AiAgentPlugin") {
+            id = "AiAgentPlugin"
+            displayName = "AiAgentPlugin"
+            description = "Runs file-driven AI tasks (runAiAgent) with pluggable providers: ollama, claude-code, mistral."
+            implementationClass = "ai.AiAgentPlugin"
+        }
     }
 }
 

+ 0 - 21
buildSrc/src/main/kotlin/AndroidConfig.kt

@@ -1,21 +0,0 @@
-//import org.gradle.api.JavaVersion
-
-//fun BaseExtension.androidConfig() {
-//    compileSdkVersion(36)
-//
-//    defaultConfig {
-//        minSdk = 21
-//    }
-//
-//    compileOptions {
-//        sourceCompatibility = JavaVersion.VERSION_17
-//        targetCompatibility = JavaVersion.VERSION_17
-//    }
-//
-//    sourceSets.all {
-//        java.srcDirs("src/android${name.capitalize()}/kotlin")
-//        res.srcDirs("src/android${name.capitalize()}/res")
-//        resources.srcDirs("src/android${name.capitalize()}/resources")
-//        manifest.srcFile("src/android${name.capitalize()}/AndroidManifest.xml")
-//    }
-//}

+ 0 - 24
buildSrc/src/main/kotlin/BasePlugin.kt

@@ -1,24 +0,0 @@
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-abstract class BasePlugin : Plugin<Project> {
-    override fun apply(project: Project) {
-        with(project) {
-            registerTasks()
-            beforeEvaluate {
-                onBeforeEvaluate()
-            }
-            afterEvaluate {
-                onAfterEvaluate()
-            }
-            project.configure(listOf(project)) {
-                onConfigure()
-            }
-        }
-    }
-
-    abstract fun Project.onConfigure()
-    abstract fun Project.registerTasks()
-    abstract fun Project.onBeforeEvaluate()
-    abstract fun Project.onAfterEvaluate()
-}

+ 0 - 71
buildSrc/src/main/kotlin/DeployTask.kt

@@ -1,71 +0,0 @@
-import org.gradle.api.DefaultTask
-//import org.gradle.api.tasks.Input
-//import org.gradle.api.tasks.TaskAction
-
-@Suppress("DEPRECATION")
-open class DeployTask : DefaultTask() {
-//    @Input
-//    var versionName: String = ""
-//    @Input
-//    var appName: String = ""
-//    @Input
-//    var nexusUrl: String = ""
-//    @Input
-//    var nexusUsername: String = ""
-//    @Input
-//    var nexusPassword: String = ""
-//    @Input
-//    var tagPrefix: String = ""
-//        get() = field.lowercase()
-//
-//    private val buildType: String
-//        get() = if (tagPrefix == "development") "debug" else "release"
-//
-//    @TaskAction
-//    fun upload() {
-//        val apkPath = project.layout
-//            .projectDirectory
-//            .file("build/outputs/apk/$buildType/$appName-$versionName-$buildType.apk")
-//        val changelogPath = project.layout
-//            .projectDirectory
-//            .file("build/outputs/apk/$buildType/CHANGELOG-$appName-$versionName-$tagPrefix.md")
-//        val apkFile = apkPath.asFile
-//        val changelogFile = changelogPath.asFile
-//        when {
-//            !apkFile.exists() -> {
-//                throw RuntimeException("APK file does not exist: ${apkFile.absolutePath}")
-//            }
-//            !changelogFile.exists() -> {
-//                throw RuntimeException("Changelog file does not exist: ${changelogFile.absolutePath}")
-//            }
-//            nexusUrl.isBlank() || nexusUsername.isBlank() || nexusPassword.isBlank() -> {
-//                throw RuntimeException("Nexus credentials are not set")
-//            }
-//            else -> {
-//                val targetUrl = "$nexusUrl/kompakt-$appName/$versionName/$tagPrefix"
-//                project.exec {
-//                    commandLine(
-//                        "curl",
-//                        "-v",
-//                        "-u",
-//                        "$nexusUsername:$nexusPassword",
-//                        "--upload-file",
-//                        apkFile.absolutePath,
-//                        "$targetUrl/${apkFile.name}"
-//                    )
-//                }
-//                project.exec {
-//                    commandLine(
-//                        "curl",
-//                        "-v",
-//                        "-u",
-//                        "$nexusUsername:$nexusPassword",
-//                        "--upload-file",
-//                        changelogFile.absolutePath,
-//                        "$targetUrl/${changelogFile.name}"
-//                    )
-//                }
-//            }
-//        }
-//    }
-}

+ 51 - 0
buildSrc/src/main/kotlin/EnsureAppImageToolTask.kt

@@ -0,0 +1,51 @@
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.Optional
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.time.Duration
+
+/**
+ * Ensures `appimagetool` is available before the AppImage packaging step.
+ * If it is already on PATH or already downloaded at [toolPath], it does nothing;
+ * otherwise it downloads the continuous release (no root required) and marks it
+ * executable. Stored under .gradle/ so it survives `clean`.
+ *
+ * Config-cache safe: only String inputs, no Gradle script/project references.
+ */
+abstract class EnsureAppImageToolTask : DefaultTask() {
+    @get:Input abstract val toolPath: Property<String>
+    @get:Input @get:Optional abstract val url: Property<String>
+
+    @TaskAction
+    fun ensure() {
+        if (which("appimagetool") != null) { logger.lifecycle("[appimagetool] found on PATH — no download needed"); return }
+        val target = File(toolPath.get())
+        if (target.canExecute() && target.length() > 0) { logger.lifecycle("[appimagetool] already downloaded: $target"); return }
+
+        val src = url.orNull
+            ?: "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
+        target.parentFile.mkdirs()
+        logger.lifecycle("[appimagetool] downloading $src")
+        val client = HttpClient.newBuilder()
+            .followRedirects(HttpClient.Redirect.NORMAL)
+            .connectTimeout(Duration.ofSeconds(20))
+            .build()
+        val resp = client.send(
+            HttpRequest.newBuilder(URI.create(src)).timeout(Duration.ofMinutes(5)).GET().build(),
+            HttpResponse.BodyHandlers.ofFile(target.toPath()),
+        )
+        check(resp.statusCode() in 200..299) { "[appimagetool] download failed: HTTP ${resp.statusCode()}" }
+        target.setExecutable(true)
+        logger.lifecycle("[appimagetool] downloaded -> $target (${target.length() / (1024 * 1024)} MB)")
+    }
+
+    private fun which(bin: String): String? =
+        (System.getenv("PATH") ?: "").split(File.pathSeparator)
+            .map { File(it, bin) }.firstOrNull { it.canExecute() }?.absolutePath
+}

+ 0 - 71
buildSrc/src/main/kotlin/GenerateChangelogTask.kt

@@ -1,71 +0,0 @@
-import org.gradle.api.DefaultTask
-import org.gradle.api.tasks.Input
-import org.gradle.api.tasks.TaskAction
-import java.io.ByteArrayOutputStream
-
-open class GenerateChangelogTask : DefaultTask() {
-    @Input
-    var appName: String = ""
-    @Input
-    var versionName: String = ""
-    @Input
-    var tagPrefix: String = ""
-        get() = field.lowercase()
-
-    private val buildType: String
-        get() = if (tagPrefix == "development") "debug" else "release"
-  
-    @TaskAction
-    fun generate() {
-        // Retrieve all tags sorted by date, latest first
-        val tagsCommand = listOf("git", "tag", "--sort=-committerdate")
-        val tagsOutput = execAndGetOutput(tagsCommand)
-        val tags = tagsOutput.split("\n")
-        // Extracting the two most recent tags
-        val currentVersion = tags.first()
-        val previousVersion = tags.last()
-        // Getting changes between the latest and previous version tags
-        val changes = execAndGetOutput(listOf(
-            "git",
-            "log",
-            "--pretty=format:%s <%an>",
-            "$previousVersion..$currentVersion"
-        ))
-        println("Changes between $previousVersion and $currentVersion:\n$changes")
-        var features = "Features:\n"
-        var fixes = "Fixes:\n"
-        var improvements = "Improvements:\n"
-        changes.lineSequence().forEach { line ->
-            val parts = line.split(":", limit = 2)
-            val prefix = parts.getOrNull(0) ?: ""
-            val rest = parts.getOrNull(1) ?: ""
-            when {
-                prefix.startsWith("ft") -> features += "$rest\n"
-                prefix.startsWith("fi") -> fixes += "$rest\n"
-                prefix.startsWith("im") -> improvements += "$rest\n"
-            }
-        }
-        val header = "Changelog from $previousVersion to $currentVersion\n\n"
-        val changelog = header + features + "\n" + fixes + "\n" + improvements
-        val fileName = "${appName}-${versionName}-$tagPrefix"
-        val changelogDir = project.file("./build/outputs/apk/$buildType")
-        val changelogFilePath = "$changelogDir/CHANGELOG-$fileName.md"
-        println("Changelog file path: $changelogFilePath")
-        // Ensure the directory exists
-        changelogDir.mkdirs()
-        // Create and write to the changelog file
-        val changelogFile = project.file(changelogFilePath)
-        changelogFile.createNewFile()
-        changelogFile.writeText(changelog)
-        println(changelog)
-    }
-
-    private fun execAndGetOutput(command: List<String>): String {
-        val outputStream = ByteArrayOutputStream()
-        project.providers.exec {
-            commandLine(command)
-            standardOutput = outputStream
-        }
-        return String(outputStream.toByteArray(), Charsets.UTF_8).trim()
-    }
-}

+ 0 - 26
buildSrc/src/main/kotlin/KotlinDependencyHandlerExt.kt

@@ -1,26 +0,0 @@
-import org.gradle.api.Action
-import org.gradle.api.NamedDomainObjectContainer
-import org.gradle.api.Project
-import org.gradle.api.artifacts.Dependency
-import org.gradle.api.artifacts.MinimalExternalModuleDependency
-import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
-import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
-
-//fun KotlinDependencyHandler.kapt(
-//    dependency: String
-//) = configurations["kapt"].dependencies.add(dependency)
-
-//fun DependencyHandlerScope.kapt(
-//    dependency: Provider<MinimalExternalModuleDependency>
-//) {
-//    configurations["kapt"].dependencies.add(dependency.get())
-//}
-
-//fun Project.kotlinSourceSets(
-//    config: Action<NamedDomainObjectContainer<KotlinSourceSet>>
-//) = with(kotlin) {
-//    sourceSets(config)
-//}
-
-fun KotlinDependencyHandler.implementationPlatform(dependencyNotation: Any): Dependency? =
-    implementation(project.dependencies.platform(dependencyNotation))

+ 0 - 19
buildSrc/src/main/kotlin/MultiPlatformPlugin.kt

@@ -1,19 +0,0 @@
-import org.gradle.api.Project
-
-class MultiPlatformPlugin : BasePlugin() {
-    override fun Project.onConfigure() {
-//        buildscript.repositories {
-//        }
-    }
-
-    override fun Project.registerTasks() {
-        registerTask<DeployTask>("deploy")
-        registerTask<GenerateChangelogTask>("changelog")
-    }
-
-    override fun Project.onBeforeEvaluate() {
-    }
-
-    override fun Project.onAfterEvaluate() {
-    }
-}

+ 103 - 0
buildSrc/src/main/kotlin/PackageAppImageTask.kt

@@ -0,0 +1,103 @@
+import org.gradle.api.DefaultTask
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.util.Base64
+
+/**
+ * Wraps a jpackage app-image (a directory tree) into a single, CLI-runnable
+ * `.AppImage` file using `appimagetool`.
+ *
+ * Compose Multiplatform's `TargetFormat.AppImage` only produces the jpackage
+ * app-image *directory* (`app/<name>/bin/<name>` + `lib/`), not a real AppImage.
+ * This task builds a proper AppDir (AppRun + .desktop + icon) around it and runs
+ * `appimagetool`. If `appimagetool` is not on PATH the task logs how to get it
+ * (no root needed) and skips — it never fails the build.
+ *
+ * Config-cache safe: only String inputs, no Gradle script/project references.
+ */
+abstract class PackageAppImageTask : DefaultTask() {
+    @get:Input abstract val appName: Property<String>
+    @get:Input abstract val appImagePath: Property<String> // jpackage app/<name> directory
+    @get:Input abstract val outputPath: Property<String>    // target single-file .AppImage
+
+    @get:Input @get:org.gradle.api.tasks.Optional
+    abstract val toolPath: Property<String> // explicit appimagetool (e.g. downloaded one); falls back to PATH
+
+    @TaskAction
+    fun build() {
+        val tool = toolPath.orNull?.let { File(it) }?.takeIf { it.canExecute() }?.absolutePath
+            ?: which("appimagetool")
+        val src = File(appImagePath.get())
+        val name = appName.get()
+        val out = File(outputPath.get())
+
+        if (tool == null) {
+            logger.lifecycle(
+                "[appimage] appimagetool not found on PATH — skipping single-file .AppImage.\n" +
+                    "           Install without root:\n" +
+                    "             curl -L -o ~/.local/bin/appimagetool " +
+                    "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage\n" +
+                    "             chmod +x ~/.local/bin/appimagetool   (then re-run buildAll)",
+            )
+            return
+        }
+        if (!src.isDirectory) { logger.lifecycle("[appimage] jpackage app-image not found at $src — skipping"); return }
+
+        out.parentFile.mkdirs()
+        val appDir = File(out.parentFile, "AppDir").apply { deleteRecursively(); mkdirs() }
+
+        // payload (bin/, lib/, …)
+        src.copyRecursively(appDir, overwrite = true)
+
+        // AppRun launcher -> bin/<name>
+        File(appDir, "AppRun").apply {
+            writeText(
+                """
+                #!/bin/sh
+                HERE="${'$'}(dirname "${'$'}(readlink -f "${'$'}0")")"
+                exec "${'$'}HERE/bin/$name" "${'$'}@"
+                """.trimIndent() + "\n",
+            )
+            setExecutable(true)
+        }
+
+        // desktop entry
+        File(appDir, "$name.desktop").writeText(
+            "[Desktop Entry]\nType=Application\nName=$name\nExec=$name\nIcon=$name\nCategories=Utility;\nTerminal=false\n",
+        )
+
+        // icon: reuse jpackage icon if present, else a 1x1 transparent placeholder
+        val icon = File(appDir, "$name.png")
+        val jpackageIcon = File(src, "lib/$name.png")
+        if (jpackageIcon.exists()) jpackageIcon.copyTo(icon, overwrite = true)
+        else icon.writeBytes(
+            Base64.getDecoder().decode(
+                "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==",
+            ),
+        )
+
+        out.delete()
+        // appimagetool is itself an AppImage; run it without FUSE via extract-and-run
+        val cmd = buildList {
+            add(tool)
+            if (tool.endsWith(".AppImage")) add("--appimage-extract-and-run")
+            add(appDir.absolutePath)
+            add(out.absolutePath)
+        }
+        val proc = ProcessBuilder(cmd)
+            .apply { environment()["ARCH"] = "x86_64" }
+            .redirectErrorStream(true)
+            .start()
+        val log = proc.inputStream.bufferedReader().readText()
+        val code = proc.waitFor()
+        appDir.deleteRecursively()
+        check(code == 0) { "[appimage] appimagetool failed (exit $code):\n${log.takeLast(1000)}" }
+        logger.lifecycle("[appimage] created ${out.name} (${out.length() / (1024 * 1024)} MB)")
+    }
+
+    private fun which(bin: String): String? =
+        (System.getenv("PATH") ?: "").split(File.pathSeparator)
+            .map { File(it, bin) }.firstOrNull { it.canExecute() }?.absolutePath
+}

+ 0 - 161
buildSrc/src/main/kotlin/SourceSets.kt

@@ -1,161 +0,0 @@
-//import com.android.build.gradle.BaseExtension
-//import org.gradle.api.Project
-//import org.gradle.kotlin.dsl.NamedDomainObjectCollectionDelegateProvider
-//import org.gradle.kotlin.dsl.get
-//import org.gradle.kotlin.dsl.getByType
-//import org.gradle.kotlin.dsl.getting
-//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformSourceSetConventions
-//import co.touchlab.kotlinxcodesync.SyncExtension
-//import com.android.build.gradle.BaseExtension
-//import org.gradle.api.Task
-//import org.gradle.api.plugins.JavaBasePlugin
-//import org.gradle.kotlin.dsl.creating
-//import org.gradle.kotlin.dsl.getByType
-//import org.gradle.kotlin.dsl.getValue
-//import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
-//import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
-//import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
-//import org.jetbrains.kotlin.gradle.plugin.mpp.NativeBuildType
-
-
-//    val Project.libs
-//        get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("libs")
-
-//    val Project.androidLibs
-//        get(): VersionCatalog = extensions.getByType<VersionCatalogsExtension>().named("androidLibs")
-
-//fun KotlinMultiplatformExtension.sourceSets(
-//    block: NamedDomainObjectContainer<KotlinSourceSet>.() -> Unit
-//) {
-//    sourceSets.block()
-//}
-
-//private fun NamedDomainObjectContainer<KotlinSourceSet>.getOrCreate(
-//    name: String
-//): KotlinSourceSet = findByName(name) ?: create(name)
-
-// common
-//fun NamedDomainObjectContainer<KotlinSourceSet>.commonMain(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("commonMain").apply(block)
-
-// common test
-//fun NamedDomainObjectContainer<KotlinSourceSet>.commonTest(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("commonTest").apply(block)
-
-// android
-//fun NamedDomainObjectContainer<KotlinSourceSet>.androidMain(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("androidMain").apply(block)
-
-// android test
-//fun NamedDomainObjectContainer<KotlinSourceSet>.androidTest(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("androidTest").apply(block)
-
-// desktop
-
-//fun NamedDomainObjectContainer<KotlinSourceSet>.desktopMain(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("androidMain").apply(block)
-
-//fun NamedDomainObjectContainer<KotlinSourceSet>.desktopTest(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
-//    getOrCreate("androidTest").apply(block)
-
-//    fun Project.android(block: BaseExtension.() -> Unit) {
-//        extensions.getByType<BaseExtension>().apply (block)
-//    }
-
-//    fun Project.kotlin(block: KotlinMultiplatformExtension.() -> Unit) {
-//        extensions.getByType<KotlinMultiplatformExtension>().apply(block)
-//    }
-
-//    val implementation by configurations
-//    val compileOnly by configurations
-//    val annotationProcessor by configurations
-//    val NamedDomainObjectContainer<KotlinSourceSet>.desktopMain by getting
-
-//    fun KotlinMultiplatformSourceSetConventions.desktopMain(
-//        block: KotlinSourceSet.() -> Unit
-//    ) {
-//        val desktopMain by getting
-//        block(desktopMain)
-//    }
-
-//    fun Project.setupMultiplatform() {
-//        plugins.apply("kotlin-multiplatform")
-//        plugins.apply("com.android.library")
-//        setupAndroidSdkVersions()
-//        kotlin {
-//            android {
-//                publishLibraryVariants("release", "debug")
-//            }
-//            iosX64()
-//            iosArm64()
-//            sourceSets {
-//                commonMain {
-//                    dependencies {
-//                        implementation(Deps.Jetbrains.Kotlin.StdLib.Common)
-//                    }
-//                }
-//                commonTest {
-//                    dependencies {
-//                        implementation(Deps.Jetbrains.Kotlin.Test.Common)
-//                        implementation(Deps.Jetbrains.Kotlin.TestAnnotations.Common)
-//                    }
-//                }
-//                androidMain {
-//                    dependsOn(commonMain())
-//                    dependencies {
-//                        implementation(Deps.Jetbrains.Kotlin.StdLib.Jdk7)
-//                    }
-//                }
-//                androidTest {
-//                    dependsOn(commonTest())
-//                    dependencies {
-//                        implementation(Deps.Jetbrains.Kotlin.Test.Junit)
-//                    }
-//                }
-//                iosCommonMain().dependsOn(commonMain())
-//                iosCommonTest().dependsOn(commonTest())
-//                iosX64Main().dependsOn(iosCommonMain())
-//                iosX64Test().dependsOn(iosCommonTest())
-//                iosArm64Main().dependsOn(iosCommonMain())
-//                iosArm64Test().dependsOn(iosCommonTest())
-//            }
-//        }
-//    }
-
-//    fun Project.setupAndroidSdkVersions() {
-//        android {
-//            compileSdkVersion(29)
-//            defaultConfig {
-//                targetSdkVersion(29)
-//                minSdkVersion(21)
-//            }
-//        }
-//    }
-
-// Workaround since iosX64() and iosArm64() function are not resolved
-// if used in a module with Kotlin 1.3.70
-//    fun Project.setupKittensBinaries() {
-//        fun KotlinNativeTarget.setupIosBinaries() {
-//            binaries {
-//                framework {
-//                    baseName = "Kittens"
-//                    freeCompilerArgs = freeCompilerArgs.plus("-Xobjc-generics").toMutableList()
-//
-//                    export(project(":shared:mvi"))
-//                }
-//            }
-//        }
-//        kotlin {
-//            iosX64().setupIosBinaries()
-//            iosArm64().setupIosBinaries()
-//        }
-//    }
-
-//    fun Project.setupXcodeSync() {
-//        plugins.apply("co.touchlab.kotlinxcodesync")
-//        extensions.getByType<SyncExtension>().run {
-//            projectPath = "../todo-app-ios/todo-app-ios.xcodeproj"
-//            target = "todo-app-ios"
-//        }
-//    }

+ 31 - 0
buildSrc/src/main/kotlin/ai/AiAgentPlugin.kt

@@ -0,0 +1,31 @@
+package ai
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.kotlin.dsl.register
+import java.io.File
+
+/**
+ * Registers the `runAiAgent` task, which reads ai-todo.txt and performs the tasks
+ * it can do safely (see [RunAiAgentTask]). Apply on the root project:
+ *
+ * ```
+ * plugins { id("AiAgentPlugin") }
+ * ```
+ */
+class AiAgentPlugin : Plugin<Project> {
+    override fun apply(project: Project) {
+        val root = project.rootDir
+
+        // Make sure the agent's working files exist (never overwrites real content).
+        listOf("ai-todo.txt", "ai-credentials.txt", "ai-suggestions.txt", "ai-done.txt").forEach {
+            File(root, it).takeUnless(File::exists)?.createNewFile()
+        }
+
+        project.tasks.register<RunAiAgentTask>("runAiAgent") {
+            group = "mjdev"
+            description = "Reads ai-todo.txt and performs its tasks (pluggable AI: ollama / claude-code / mistral)."
+            rootDir = root
+        }
+    }
+}

+ 205 - 0
buildSrc/src/main/kotlin/ai/AiProvider.kt

@@ -0,0 +1,205 @@
+package ai
+
+import java.net.URI
+import java.net.http.HttpClient
+import java.net.http.HttpRequest
+import java.net.http.HttpResponse
+import java.time.Duration
+
+/**
+ * A pluggable AI backend used by [RunAiAgentTask].
+ *
+ * Implementations: [OllamaProvider] (local), [MistralProvider] (cloud),
+ * [ClaudeCodeProvider] (claude-code CLI). Selection is handled by [AiProviders.select].
+ */
+interface AiProvider {
+    /** Human readable provider id, e.g. "claude-code". */
+    val name: String
+
+    /** True when the backend can actually be reached / invoked right now. */
+    fun isAvailable(): Boolean
+
+    /** Whether this provider talks to a remote cloud service (vs a local process). */
+    val isCloud: Boolean
+
+    /** Send [prompt] and return the model's text answer. Throws on transport errors. */
+    fun complete(prompt: String): String
+}
+
+/** Tiny stdlib-only JSON helpers — buildSrc has no JSON dependency on the classpath. */
+object Json {
+    fun escape(s: String): String = buildString {
+        for (c in s) when (c) {
+            '\\' -> append("\\\\")
+            '"' -> append("\\\"")
+            '\n' -> append("\\n")
+            '\r' -> append("\\r")
+            '\t' -> append("\\t")
+            else -> if (c < ' ') append("\\u%04x".format(c.code)) else append(c)
+        }
+    }
+
+    /** Best-effort: first string value for [key] anywhere in [json] (handles \" and \\ escapes). */
+    fun firstString(json: String, key: String): String? {
+        val marker = "\"$key\""
+        var at = json.indexOf(marker)
+        while (at >= 0) {
+            var j = at + marker.length
+            while (j < json.length && json[j] != ':') j++
+            j++
+            while (j < json.length && json[j].isWhitespace()) j++
+            if (j < json.length && json[j] == '"') {
+                val sb = StringBuilder()
+                j++
+                while (j < json.length) {
+                    val c = json[j]
+                    when {
+                        c == '\\' && j + 1 < json.length -> {
+                            when (val n = json[j + 1]) {
+                                'n' -> sb.append('\n'); 'r' -> sb.append('\r'); 't' -> sb.append('\t')
+                                '"' -> sb.append('"'); '\\' -> sb.append('\\'); '/' -> sb.append('/')
+                                'u' -> {
+                                    if (j + 5 < json.length) {
+                                        sb.append(json.substring(j + 2, j + 6).toInt(16).toChar()); j += 4
+                                    }
+                                }
+                                else -> sb.append(n)
+                            }
+                            j += 2
+                        }
+                        c == '"' -> return sb.toString()
+                        else -> { sb.append(c); j++ }
+                    }
+                }
+            }
+            at = json.indexOf(marker, at + marker.length)
+        }
+        return null
+    }
+}
+
+private val http: HttpClient = HttpClient.newBuilder()
+    .connectTimeout(Duration.ofSeconds(10))
+    .build()
+
+private fun post(url: String, body: String, headers: Map<String, String>): HttpResponse<String> {
+    val builder = HttpRequest.newBuilder(URI.create(url))
+        .timeout(Duration.ofMinutes(5))
+        .header("Content-Type", "application/json")
+        .POST(HttpRequest.BodyPublishers.ofString(body))
+    headers.forEach { (k, v) -> builder.header(k, v) }
+    return http.send(builder.build(), HttpResponse.BodyHandlers.ofString())
+}
+
+/** Local Ollama server (http://localhost:11434 by default). */
+class OllamaProvider(
+    private val host: String = System.getenv("OLLAMA_HOST") ?: "http://localhost:11434",
+    private val model: String = System.getenv("OLLAMA_MODEL") ?: "llama3.1",
+) : AiProvider {
+    override val name = "ollama"
+    override val isCloud = false
+
+    override fun isAvailable(): Boolean = runCatching {
+        val resp = http.send(
+            HttpRequest.newBuilder(URI.create("$host/api/tags")).timeout(Duration.ofSeconds(3)).GET().build(),
+            HttpResponse.BodyHandlers.discarding(),
+        )
+        resp.statusCode() in 200..299
+    }.getOrDefault(false)
+
+    override fun complete(prompt: String): String {
+        val body = """{"model":"${Json.escape(model)}","prompt":"${Json.escape(prompt)}","stream":false}"""
+        val resp = post("$host/api/generate", body, emptyMap())
+        check(resp.statusCode() in 200..299) { "ollama HTTP ${resp.statusCode()}: ${resp.body().take(300)}" }
+        return Json.firstString(resp.body(), "response") ?: resp.body()
+    }
+
+    /** Best-effort `ollama serve` launch when the daemon is down. Returns true if it became reachable. */
+    fun ensureServing(log: (String) -> Unit): Boolean {
+        if (isAvailable()) return true
+        if (which("ollama") == null) { log("ollama binary not found on PATH"); return false }
+        log("starting `ollama serve` ...")
+        runCatching {
+            ProcessBuilder("ollama", "serve").redirectOutput(ProcessBuilder.Redirect.DISCARD)
+                .redirectError(ProcessBuilder.Redirect.DISCARD).start()
+        }.onFailure { log("failed to start ollama: ${it.message}"); return false }
+        repeat(20) { if (isAvailable()) return true; Thread.sleep(500) }
+        return isAvailable()
+    }
+}
+
+/** Mistral cloud chat completions. */
+class MistralProvider(
+    private val apiKey: String,
+    private val model: String = System.getenv("MISTRAL_MODEL") ?: "mistral-large-latest",
+) : AiProvider {
+    override val name = "mistral"
+    override val isCloud = true
+    override fun isAvailable() = apiKey.isNotBlank()
+
+    override fun complete(prompt: String): String {
+        val body = """{"model":"${Json.escape(model)}","messages":[{"role":"user","content":"${Json.escape(prompt)}"}]}"""
+        val resp = post(
+            "https://api.mistral.ai/v1/chat/completions", body,
+            mapOf("Authorization" to "Bearer $apiKey"),
+        )
+        check(resp.statusCode() in 200..299) { "mistral HTTP ${resp.statusCode()}: ${resp.body().take(300)}" }
+        return Json.firstString(resp.body(), "content") ?: resp.body()
+    }
+}
+
+/**
+ * The claude-code CLI in headless print mode: `claude -p "<prompt>"`.
+ * Authenticates via the user's existing claude-code login — no API key in this repo.
+ */
+class ClaudeCodeProvider(
+    private val command: List<String> = (System.getenv("CLAUDE_CODE_CMD")?.split(" ") ?: listOf("claude", "-p")),
+) : AiProvider {
+    override val name = "claude-code"
+    override val isCloud = true
+    override fun isAvailable() = which(command.first()) != null
+
+    override fun complete(prompt: String): String {
+        val proc = ProcessBuilder(command + prompt)
+            .redirectErrorStream(true)
+            .start()
+        val out = proc.inputStream.bufferedReader().readText()
+        proc.waitFor()
+        return out.trim()
+    }
+}
+
+/** Locate an executable on PATH (returns null when absent). */
+fun which(bin: String): String? {
+    val path = System.getenv("PATH") ?: return null
+    return path.split(java.io.File.pathSeparator).map { java.io.File(it, bin) }
+        .firstOrNull { it.canExecute() }?.absolutePath
+}
+
+object AiProviders {
+    /**
+     * Pick a provider. Honors an explicit `provider=` credential, otherwise
+     * prefers cloud (claude-code, then mistral) and falls back to local ollama
+     * (starting `ollama serve` on demand), per ai-todo.txt.
+     */
+    fun select(creds: Map<String, String>, log: (String) -> Unit): AiProvider? {
+        val forced = creds["provider"]?.lowercase()
+        val claude = ClaudeCodeProvider()
+        val mistral = creds["MISTRAL_API_KEY"]?.let { MistralProvider(it) }
+        val ollama = OllamaProvider(
+            host = creds["OLLAMA_HOST"] ?: System.getenv("OLLAMA_HOST") ?: "http://localhost:11434",
+            model = creds["OLLAMA_MODEL"] ?: System.getenv("OLLAMA_MODEL") ?: "llama3.1",
+        )
+        when (forced) {
+            "claude-code", "claude" -> if (claude.isAvailable()) return claude
+            "mistral" -> if (mistral?.isAvailable() == true) return mistral
+            "ollama" -> { if (ollama.ensureServing(log)) return ollama }
+        }
+        // prefer cloud
+        if (claude.isAvailable()) return claude
+        if (mistral?.isAvailable() == true) return mistral
+        // local fallback — start the daemon if needed
+        if (ollama.ensureServing(log)) return ollama
+        return null
+    }
+}

+ 154 - 0
buildSrc/src/main/kotlin/ai/DependencyUpdater.kt

@@ -0,0 +1,154 @@
+package ai
+
+import java.io.File
+
+/**
+ * Non-invasive dependency auto-update, driven by the Ben Manes report at
+ * `reports/dependencies/dependency-updates.txt`:
+ *
+ *  - applies only STABLE updates (rejects alpha/beta/rc/dev/snapshot/tethys/…),
+ *  - never touches pinned keys (Kotlin / Compose / AGP / IntelliJ — coordinated upgrades),
+ *  - after applying, runs [verifyTask]; if the build fails it reverts (all-at-once first,
+ *    then bisects per-library, keeping only the bumps that still build).
+ *
+ * Mutates only `gradle/libs.versions.toml` (a FILE_WRITE — approved by [SecurityPolicy]);
+ * never performs git or network-upload actions.
+ */
+class DependencyUpdater(
+    private val rootDir: File,
+    private val verifyTask: String,
+    private val log: (String) -> Unit,
+    private val suggest: (String) -> Unit,
+) {
+    private val catalog = File(rootDir, "gradle/libs.versions.toml")
+    private val report = File(rootDir, "reports/dependencies/dependency-updates.txt")
+    private val gradlew = File(rootDir, "gradlew")
+
+    /** version keys never auto-bumped (coordinated manual upgrades) */
+    private val pinnedSubstrings = listOf("kotlin", "compose", "gradle", "intellij")
+
+    private fun isNonStable(v: String): Boolean {
+        val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { v.uppercase().contains(it) }
+        val numeric = Regex("^[0-9,.v-]+(-r)?$")
+        return !(stableKeyword || numeric.matches(v))
+    }
+
+    fun run() {
+        if (!report.exists()) { suggest("Dependency update skipped: ${report.relativeTo(rootDir)} not found. Run `./gradlew :composeApp:dependencyUpdates` first."); return }
+        if (!catalog.exists()) { suggest("Dependency update skipped: ${catalog.relativeTo(rootDir)} not found."); return }
+
+        val originalToml = catalog.readText()
+        val versionKeys = parseVersions(originalToml)        // key -> current value
+        val moduleToKey = parseModuleKeys(originalToml)      // "group:artifact" / plugin id -> key
+
+        val candidates = parseReport().mapNotNull { (module, _, new) ->
+            val key = moduleToKey[module] ?: moduleToKey[module.removePluginSuffix()] ?: return@mapNotNull null
+            if (key in versionKeys && versionKeys[key] != new && isApplicable(key, new)) Bump(key, versionKeys[key]!!, new) else null
+        }.distinctBy { it.key }
+
+        if (candidates.isEmpty()) { log("[dep-update] nothing to update (stable, non-pinned)"); return }
+        log("[dep-update] candidate updates: " + candidates.joinToString { "${it.key} ${it.old}->${it.new}" })
+
+        // 1) apply all at once, verify once
+        applyAll(originalToml, candidates)
+        if (verify()) {
+            summarize(applied = candidates, reverted = emptyList())
+            return
+        }
+
+        // 2) failed together -> revert all, then bisect per-library
+        log("[dep-update] combined build failed; bisecting per library")
+        val applied = mutableListOf<Bump>()
+        val reverted = mutableListOf<Bump>()
+        var base = originalToml
+        for (b in candidates) {
+            applyAll(base, listOf(b))
+            if (verify()) { applied += b; base = catalog.readText() }
+            else { reverted += b; catalog.writeText(base) } // keep prior good state
+        }
+        summarize(applied, reverted)
+    }
+
+    private data class Bump(val key: String, val old: String, val new: String)
+
+    private fun isApplicable(key: String, new: String): Boolean {
+        if (isNonStable(new)) return false
+        val k = key.lowercase()
+        return pinnedSubstrings.none { it in k }
+    }
+
+    private fun applyAll(fromToml: String, bumps: List<Bump>) {
+        var text = fromToml
+        for (b in bumps) {
+            // match:  key = "old"   (tolerant of spacing/quotes)
+            val rx = Regex("(^|\\n)(\\s*${Regex.escape(b.key)}\\s*=\\s*)\"[^\"]*\"")
+            text = rx.replace(text) { m -> "${m.groupValues[1]}${m.groupValues[2]}\"${b.new}\"" }
+        }
+        catalog.writeText(text)
+    }
+
+    private fun verify(): Boolean {
+        if (!gradlew.canExecute()) { log("[dep-update] gradlew not executable — cannot verify; treating as failure"); return false }
+        return runCatching {
+            val proc = ProcessBuilder("./gradlew", verifyTask, "--console=plain", "--no-daemon")
+                .directory(rootDir)
+                .redirectErrorStream(true)
+                .start()
+            val out = proc.inputStream.bufferedReader().readText()
+            val code = proc.waitFor()
+            if (code != 0) log("[dep-update] verify `$verifyTask` failed (exit $code): " + out.lineSequence().lastOrNull { it.isNotBlank() })
+            code == 0
+        }.getOrElse { log("[dep-update] verify error: ${it.message}"); false }
+    }
+
+    private fun summarize(applied: List<Bump>, reverted: List<Bump>) {
+        val sb = StringBuilder("Dependency auto-update (non-invasive, stable, build-verified):\n")
+        if (applied.isEmpty()) sb.append("  applied: none\n")
+        else applied.forEach { sb.append("  + ${it.key}: ${it.old} -> ${it.new}\n") }
+        if (reverted.isNotEmpty()) reverted.forEach { sb.append("  ! reverted (build failed): ${it.key} ${it.old} -> ${it.new}\n") }
+        log("[dep-update] " + sb.toString().trim().replace("\n", "\n[dep-update] "))
+        suggest(sb.toString())
+    }
+
+    // ---- parsing ------------------------------------------------------------------------------
+
+    /** Report lines like: ` - io.coil-kt.coil3:coil-compose [3.2.0 -> 3.3.0]` */
+    private fun parseReport(): List<Triple<String, String, String>> {
+        val rx = Regex("^\\s*-\\s+(\\S+:\\S+)\\s+\\[(.+?)\\s*->\\s*(.+?)]\\s*$")
+        return report.readLines().mapNotNull { line ->
+            rx.find(line)?.let { Triple(it.groupValues[1], it.groupValues[2], it.groupValues[3]) }
+        }
+    }
+
+    private fun parseVersions(toml: String): Map<String, String> {
+        val out = linkedMapOf<String, String>()
+        var inVersions = false
+        val kv = Regex("^\\s*([A-Za-z0-9_.-]+)\\s*=\\s*\"([^\"]*)\"\\s*$")
+        for (line in toml.lines()) {
+            val tl = line.trim()
+            if (tl.startsWith("[")) { inVersions = tl == "[versions]"; continue }
+            if (inVersions && !tl.startsWith("#")) kv.find(line)?.let { out[it.groupValues[1]] = it.groupValues[2] }
+        }
+        return out
+    }
+
+    /** Map "group:artifact" (libraries) and plugin ids to their `version.ref` key. */
+    private fun parseModuleKeys(toml: String): Map<String, String> {
+        val out = hashMapOf<String, String>()
+        val moduleRx = Regex("module\\s*=\\s*\"([^\"]+)\"")
+        val idRx = Regex("\\bid\\s*=\\s*\"([^\"]+)\"")
+        val refRx = Regex("version\\.ref\\s*=\\s*\"([^\"]+)\"")
+        for (line in toml.lines()) {
+            val ref = refRx.find(line)?.groupValues?.get(1) ?: continue
+            moduleRx.find(line)?.let { out[it.groupValues[1]] = ref }
+            idRx.find(line)?.let { out[it.groupValues[1]] = ref }
+        }
+        return out
+    }
+
+    /** Ben Manes lists plugins as `id:id.gradle.plugin`; reduce to the plugin id. */
+    private fun String.removePluginSuffix(): String {
+        val parts = split(":")
+        return if (parts.size == 2 && parts[1].endsWith(".gradle.plugin")) parts[0] else this
+    }
+}

+ 232 - 0
buildSrc/src/main/kotlin/ai/RunAiAgentTask.kt

@@ -0,0 +1,232 @@
+package ai
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.Internal
+import org.gradle.api.tasks.TaskAction
+import java.io.File
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatter
+
+/**
+ * Categories of action the agent can take. The security policy in [SecurityPolicy]
+ * decides which are auto-approved.
+ */
+enum class OpKind {
+    DIR_CREATE, FILE_WRITE, FILE_READ, EXEC, LLM_CALL,
+    GIT_WRITE, GIT_CREATE, GIT_COMMIT, GIT_PUSH,
+    UPLOAD_TO_INTERNET, OTHER,
+}
+
+/**
+ * Autonomy policy from ai-todo.txt: every operation is approved **except**
+ * git write/create/commit/push and transferring project files to the internet.
+ */
+object SecurityPolicy {
+    private val denied = setOf(
+        OpKind.GIT_WRITE, OpKind.GIT_CREATE, OpKind.GIT_COMMIT, OpKind.GIT_PUSH,
+        OpKind.UPLOAD_TO_INTERNET,
+    )
+    fun isApproved(op: OpKind): Boolean = op !in denied
+    fun describe(op: OpKind): String = if (isApproved(op)) "approved" else "BLOCKED (requires human approval)"
+}
+
+/** One parsed entry from ai-todo.txt. */
+private data class Block(val kind: Kind, val raw: String) {
+    enum class Kind { COMMENT, BLANK, REPEATABLE, ONCE }
+    /** Task text with the leading marker stripped and lines joined. */
+    val text: String get() = raw.lineSequence()
+        .joinToString(" ") { it.trimStart('*', '-', ' ', '\t') }
+        .trim()
+}
+
+/**
+ * Reads ai-todo.txt and performs the tasks it can do safely and deterministically,
+ * recording outcomes to ai-done.txt / ai-suggestions.txt and removing finished
+ * one-shot tasks from ai-todo.txt. Anything it cannot do deterministically is sent
+ * to a pluggable AI provider ([AiProviders.select]); the model's plan is written to
+ * ai-suggestions.txt for review rather than blindly applied.
+ */
+abstract class RunAiAgentTask : DefaultTask() {
+
+    @get:Internal lateinit var rootDir: File
+
+    private val todoFile get() = File(rootDir, "ai-todo.txt")
+    private val credsFile get() = File(rootDir, "ai-credentials.txt")
+    private val doneFile get() = File(rootDir, "ai-done.txt")
+    private val suggestionsFile get() = File(rootDir, "ai-suggestions.txt")
+    private val ts get() = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+
+    @TaskAction
+    fun run() {
+        if (!todoFile.exists()) { logger.lifecycle("[ai-agent] no ai-todo.txt — nothing to do"); return }
+        val blocks = parse(todoFile.readText())
+        val creds = readCreds()
+        val provider by lazy { AiProviders.select(creds) { logger.lifecycle("[ai-agent] $it") } }
+
+        val completed = mutableListOf<Block>()
+        for (b in blocks) {
+            when (b.kind) {
+                Block.Kind.REPEATABLE -> handleRepeatable(b.text)
+                Block.Kind.ONCE -> if (handleOnce(b.text) { provider }) completed += b
+                else -> { /* comments / blanks preserved as-is */ }
+            }
+        }
+
+        if (completed.isNotEmpty()) {
+            rewriteTodo(blocks, completed)
+            logger.lifecycle("[ai-agent] completed ${completed.size} one-shot task(s); ai-todo.txt updated")
+        } else {
+            logger.lifecycle("[ai-agent] no one-shot tasks completed this run")
+        }
+    }
+
+    // ---- repeatable (every-build) tasks --------------------------------------------------------
+
+    private fun handleRepeatable(text: String) {
+        val t = text.lowercase()
+        when {
+            "update dependencies" in t || ("update" in t && "report in reports" in t) -> {
+                val enabled = (project.findProperty("updateDeps") as String?)?.toBoolean() == true
+                if (!enabled) {
+                    logger.lifecycle("[ai-agent] repeatable: dependency auto-update is available but skipped — run `./gradlew runAiAgent -PupdateDeps=true` to apply non-invasive stable updates (build-verified, reverts on failure).")
+                    return
+                }
+                val verifyTask = (project.findProperty("verifyTask") as String?) ?: ":composeApp:compileKotlinDesktop"
+                DependencyUpdater(rootDir, verifyTask, logger::lifecycle, ::suggest).run()
+            }
+            "collect reports" in t || "clean up code" in t ->
+                logger.lifecycle("[ai-agent] repeatable: '${text.lineSequence().first()}' -> handled by the build pipeline task `postBuildCodeCheck` (ktlintFormat + collectKtlintReports + dependency report into /reports)")
+            else ->
+                logger.lifecycle("[ai-agent] repeatable task not auto-mapped: '${text.lineSequence().first()}'")
+        }
+    }
+
+    // ---- one-shot tasks: returns true when finished (and should be removed) --------------------
+
+    private fun handleOnce(text: String, provider: () -> AiProvider?): Boolean {
+        val t = text.lowercase()
+        return when {
+            // create folder <name> ... root project folder
+            Regex("add folder\\s+([\\w.-]+)").find(t) != null && "root project" in t -> {
+                val name = Regex("add folder\\s+([\\w.-]+)").find(t)!!.groupValues[1]
+                createFolder(name)
+            }
+            // create the plugin itself
+            "create plugin" in t && "buildsrc" in t -> done(
+                "Create the AI agent buildSrc plugin",
+                "Implemented in buildSrc/src/main/kotlin/ai/ (AiAgentPlugin, RunAiAgentTask, AiProvider). " +
+                    "Providers: ollama, claude-code, mistral. Reads ai-todo.txt; writes ai-done.txt / ai-suggestions.txt; " +
+                    "credentials from ai-credentials.txt. Task `runAiAgent` runs the file.",
+                File(rootDir, "buildSrc/src/main/kotlin/ai/AiAgentPlugin.kt").exists(),
+            )
+            // attach plugin to project
+            "attach plugin" in t -> done(
+                "Attach the AI agent plugin to the project",
+                "Plugin `AiAgentPlugin` applied on the root project; `runAiAgent` task is registered.",
+                project.rootProject.tasks.findByName("runAiAgent") != null,
+            )
+            // provider/credentials policy directive
+            "ollama serve" in t || ("credentials" in t && "cloud" in t) -> done(
+                "Provider selection policy (cloud-first, ollama fallback)",
+                "Implemented in AiProviders.select(): prefers cloud (claude-code, then mistral); " +
+                    "falls back to local ollama and runs `ollama serve` on demand when no cloud credentials are available.",
+                true,
+            )
+            // security/autonomy policy directive
+            "approved all the operations" in t || ("git actions" in t && "security" in t) -> done(
+                "Autonomy / security policy",
+                "Implemented in SecurityPolicy: all operations auto-approved except git write/create/commit/push " +
+                    "and project->internet file transfers, which stay gated for human approval.",
+                true,
+            )
+            else -> delegateToAi(text, provider())
+        }
+    }
+
+    private fun createFolder(name: String): Boolean {
+        if (!SecurityPolicy.isApproved(OpKind.DIR_CREATE)) { suggest("create folder '$name' is ${SecurityPolicy.describe(OpKind.DIR_CREATE)}"); return false }
+        val dir = File(rootDir, name)
+        val created = if (dir.exists()) false else dir.mkdirs()
+        return done(
+            "Ensure folder '$name' exists at project root",
+            if (created) "Created $name/." else "$name/ already existed — no change.",
+            dir.isDirectory,
+        )
+    }
+
+    /** Send an un-handled task to the AI provider and record its plan for review (never auto-applies). */
+    private fun delegateToAi(text: String, provider: AiProvider?): Boolean {
+        if (!SecurityPolicy.isApproved(OpKind.LLM_CALL)) { suggest("LLM call for '$text' is ${SecurityPolicy.describe(OpKind.LLM_CALL)}"); return false }
+        if (provider == null) {
+            suggest("No AI provider available for task: \"$text\". Add MISTRAL_API_KEY to ai-credentials.txt, install the `claude` CLI, or run `ollama serve`.")
+            return false
+        }
+        logger.lifecycle("[ai-agent] delegating to ${provider.name}: \"$text\"")
+        val plan = runCatching {
+            provider.complete(
+                "You are a build agent for a Kotlin Multiplatform / Compose Multiplatform Wayland desktop project. " +
+                    "Propose concrete steps to accomplish this task. Do not perform git write/commit/push or upload project " +
+                    "files to the internet. Task:\n\n$text",
+            )
+        }.getOrElse { "provider error: ${it.message}" }
+        suggest("Task: \"$text\"\nProvider: ${provider.name}\nProposed plan (NOT auto-applied — review before acting):\n$plan")
+        // Left in ai-todo.txt: applying arbitrary generated changes is intentionally human-gated.
+        return false
+    }
+
+    // ---- bookkeeping ---------------------------------------------------------------------------
+
+    private fun done(summary: String, details: String, ok: Boolean): Boolean {
+        if (!ok) { suggest("Could not verify completion of: $summary"); return false }
+        doneFile.appendText("## $ts — $summary\n$details\n\n")
+        logger.lifecycle("[ai-agent] done: $summary")
+        return true
+    }
+
+    private fun suggest(msg: String) {
+        suggestionsFile.appendText("## $ts\n$msg\n\n")
+        logger.lifecycle("[ai-agent] suggestion recorded: ${msg.lineSequence().first()}")
+    }
+
+    private fun readCreds(): Map<String, String> {
+        if (!credsFile.exists()) return emptyMap()
+        return credsFile.readLines()
+            .map { it.trim() }
+            .filter { it.isNotEmpty() && !it.startsWith("#") && "=" in it }
+            .associate { val (k, v) = it.split("=", limit = 2); k.trim() to v.trim() }
+    }
+
+    private fun parse(content: String): List<Block> {
+        val out = mutableListOf<Block>()
+        val lines = content.lines()
+        var i = 0
+        while (i < lines.size) {
+            val line = lines[i]
+            when {
+                line.isBlank() -> { out += Block(Block.Kind.BLANK, line); i++ }
+                line.trimStart().startsWith("#") -> { out += Block(Block.Kind.COMMENT, line); i++ }
+                line.trimStart().startsWith("*") || line.trimStart().startsWith("-") -> {
+                    // both repeatable (*) and one-shot (-) tasks may span multiple lines;
+                    // continuation lines are any non-blank line that doesn't start a new marker
+                    val kind = if (line.trimStart().startsWith("*")) Block.Kind.REPEATABLE else Block.Kind.ONCE
+                    val buf = StringBuilder(line)
+                    i++
+                    while (i < lines.size && lines[i].isNotBlank() &&
+                        !lines[i].trimStart().startsWith("-") && !lines[i].trimStart().startsWith("*") &&
+                        !lines[i].trimStart().startsWith("#")
+                    ) { buf.append('\n').append(lines[i]); i++ }
+                    out += Block(kind, buf.toString())
+                }
+                else -> { out += Block(Block.Kind.COMMENT, line); i++ }
+            }
+        }
+        return out
+    }
+
+    private fun rewriteTodo(all: List<Block>, completed: List<Block>) {
+        val keep = all.filter { it !in completed }
+        // collapse any doubled blank lines left behind
+        val text = keep.joinToString("\n") { it.raw }.replace(Regex("\n{3,}"), "\n\n").trimEnd() + "\n"
+        todoFile.writeText(text)
+    }
+}

+ 99 - 57
composeApp/composeApp.gradle.kts

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-//<editor-fold desc="imports">----------------------------------------------------------------------
+// <editor-fold desc="imports">----------------------------------------------------------------------
 
 import org.jetbrains.compose.ComposeExtension
 import org.jetbrains.compose.desktop.DesktopExtension
@@ -18,9 +18,9 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinTargetContainerWithPresetFunctions
 import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
 import org.jetbrains.kotlin.gradle.targets.jvm.KotlinJvmTarget
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="plugins">----------------------------------------------------------------------
+// <editor-fold desc="plugins">----------------------------------------------------------------------
 
 plugins {
     alias(libs.plugins.kotlin.multiplatform)
@@ -31,49 +31,43 @@ plugins {
 //    alias(libs.plugins.devtools.ksp)
 }
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="helpers">----------------------------------------------------------------------
+// <editor-fold desc="helpers">----------------------------------------------------------------------
 
-fun NamedDomainObjectContainer<KotlinSourceSet>.getOrCreate(
-    name: String
-): KotlinSourceSet = findByName(name) ?: create(name)
+fun NamedDomainObjectContainer<KotlinSourceSet>.getOrCreate(name: String): KotlinSourceSet = findByName(name) ?: create(name)
 
-fun NamedDomainObjectContainer<KotlinSourceSet>.desktopMain(
-    block: KotlinSourceSet.() -> Unit = {}
-): KotlinSourceSet = getOrCreate("desktopMain").apply(block)
+fun NamedDomainObjectContainer<KotlinSourceSet>.desktopMain(block: KotlinSourceSet.() -> Unit = {}): KotlinSourceSet =
+    getOrCreate("desktopMain").apply(block)
 
-fun KotlinTargetContainerWithPresetFunctions.desktopTarget(
-    configure: Action<KotlinJvmTarget>
-) = jvm("desktop").apply {
-    configure.execute(this)
-}
+fun KotlinTargetContainerWithPresetFunctions.desktopTarget(configure: Action<KotlinJvmTarget>) =
+    jvm("desktop").apply {
+        configure.execute(this)
+    }
 
 val Project.compose: ComposeExtension
     get() = extensions.getByType()
 
-fun Project.desktop(
-    config: DesktopExtension.() -> Unit
-) = compose.desktop(config)
+fun Project.desktop(config: DesktopExtension.() -> Unit) = compose.desktop(config)
 
-fun Project.composeResources(
-    config: ResourcesExtension.() -> Unit
-) = compose.resources(config)
+fun Project.composeResources(config: ResourcesExtension.() -> Unit) = compose.resources(config)
 
-fun targets(
-    config: Action<KotlinMultiplatformExtension>
-) = kotlin(config)
+fun targets(config: Action<KotlinMultiplatformExtension>) = kotlin(config)
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="configurations">---------------------------------------------------------------
+// <editor-fold desc="configurations">---------------------------------------------------------------
 
 // java version
 setJavaLanguageVersion(libs.versions.java.language.version)
 
 // kotlin toolchain java version
 kotlin {
-    jvmToolchain(libs.versions.java.language.version.get().toInt())
+    jvmToolchain(
+        libs.versions.java.language.version
+            .get()
+            .toInt(),
+    )
 }
 
 // resources config
@@ -85,8 +79,12 @@ composeResources {
 
 // setup vlc plugin configuration
 vlcSetup {
-    val appVlcDir: String = libs.versions.vlc.dir.get()
-    val appVlcVersion: String = libs.versions.vlc.version.get()
+    val appVlcDir: String =
+        libs.versions.vlc.dir
+            .get()
+    val appVlcVersion: String =
+        libs.versions.vlc.version
+            .get()
     vlcVersion.set(appVlcVersion)
     shouldCompressVlcFiles = true
     shouldIncludeAllVlcFiles = false
@@ -95,9 +93,9 @@ vlcSetup {
 //        pathToCopyVlcWindowsFilesTo = rootDir.resolve(appVlcDir)
 }
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="targets">----------------------------------------------------------------------
+// <editor-fold desc="targets">----------------------------------------------------------------------
 
 targets {
     desktopTarget {
@@ -110,9 +108,9 @@ targets {
     }
 }
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="source sets / dependencies">---------------------------------------------------
+// <editor-fold desc="source sets / dependencies">---------------------------------------------------
 
 kotlin {
 //    linuxX64 {
@@ -235,7 +233,7 @@ kotlin {
                 // notification
                 // implementation("io.github.shadmanadman:knotif:0.56.0")
                 // sensors
-                //implementation("io.github.shadmanadman:KSensor:0.59.0")
+                // implementation("io.github.shadmanadman:KSensor:0.59.0")
                 // locale
 //            implementation(libs.i18n4k)
                 // anims kottie
@@ -280,7 +278,7 @@ kotlin {
                 // permissions
                 implementation(libs.accompanist.permissions)
                 // sensors
-                //implementation("io.github.shadmanadman:KSensor:0.59.0")
+                // implementation("io.github.shadmanadman:KSensor:0.59.0")
                 // adb
                 // implementation("dev.mobile:dadb:1.2.10")
             }
@@ -320,25 +318,45 @@ kotlin {
     }
 }
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------
 
-//<editor-fold desc="platforms">--------------------------------------------------------------------
+// <editor-fold desc="platforms">--------------------------------------------------------------------
 
 // android app config
 android {
     namespace = "org.mjdev.desktop"
-    compileSdk = libs.versions.android.compile.sdk.get().toInt()
+    compileSdk =
+        libs.versions.android.compile.sdk
+            .get()
+            .toInt()
     sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
     sourceSets["main"].res.srcDirs("src/androidMain/res")
     sourceSets["main"].resources.srcDirs("src/commonMain/composeResources")
     sourceSets["main"].assets.srcDirs("src/commonMain/resources")
     defaultConfig {
         applicationId = "org.mjdev.desktop"
-        minSdk = libs.versions.android.min.sdk.get().toInt()
-        targetSdk = libs.versions.android.target.sdk.get().toInt()
-        versionCode = libs.versions.app.pkg.version.get().replace(".", "").toInt()
-        versionName = libs.versions.app.pkg.version.get()
-        resValue("string", "app_name", libs.versions.app.name.get())
+        minSdk =
+            libs.versions.android.min.sdk
+                .get()
+                .toInt()
+        targetSdk =
+            libs.versions.android.target.sdk
+                .get()
+                .toInt()
+        versionCode =
+            libs.versions.app.pkg.version
+                .get()
+                .replace(".", "")
+                .toInt()
+        versionName =
+            libs.versions.app.pkg.version
+                .get()
+        resValue(
+            "string",
+            "app_name",
+            libs.versions.app.name
+                .get(),
+        )
     }
     packaging {
         resources {
@@ -374,16 +392,28 @@ android {
 
 // desktop app config
 desktop {
-    group = libs.versions.app.pkg.name.get()
-    version = libs.versions.app.pkg.version.get()
+    group =
+        libs.versions.app.pkg.name
+            .get()
+    version =
+        libs.versions.app.pkg.version
+            .get()
     application {
         jvmArgs("-Dprism.order=sw")
         mainClass = "org.mjdev.desktop.main.MainKt"
         nativeDistributions {
-            packageName = libs.versions.app.name.get()
-            packageVersion = libs.versions.app.pkg.version.get()
-            description = libs.versions.app.description.get()
-            copyright = libs.versions.app.copyright.get()
+            packageName =
+                libs.versions.app.name
+                    .get()
+            packageVersion =
+                libs.versions.app.pkg.version
+                    .get()
+            description =
+                libs.versions.app.description
+                    .get()
+            copyright =
+                libs.versions.app.copyright
+                    .get()
             outputBaseDir.set(project.rootDir.resolve("packages"))
 //                macOS {
 //                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.icns"))
@@ -404,26 +434,38 @@ desktop {
 //                            .resolve("icon.png")
 //                    )
 //                    licenseFile.set(project.rootDir.resolve("LICENSE.txt"))
-                appCategory = libs.versions.app.category.get()
-                debMaintainer = libs.versions.app.maintainer.get()
-                menuGroup = libs.versions.app.menu.group.get()
-                vendor = libs.versions.app.vendor.get()
+                appCategory =
+                    libs.versions.app.category
+                        .get()
+                debMaintainer =
+                    libs.versions.app.maintainer
+                        .get()
+                menuGroup =
+                    libs.versions.app.menu.group
+                        .get()
+                vendor =
+                    libs.versions.app.vendor
+                        .get()
             }
             targetFormats(
 //                TargetFormat.Dmg,
 //                TargetFormat.Msi,
                 TargetFormat.Deb,
-//                TargetFormat.Exe,
+                TargetFormat.Exe, // Windows installer — only built when running on a Windows host
                 TargetFormat.AppImage,
 //                TargetFormat.Pkg,
-                TargetFormat.Rpm
+                TargetFormat.Rpm,
             )
             buildTypes.release.proguard {
                 configurationFiles.from("composeApp.pro")
+                // The optimization pass can't resolve the class hierarchy of optional
+                // transitive deps (e.g. netty's Log4J2Logger -> log4j2, absent at runtime),
+                // throwing IncompleteClassHierarchyException. Shrinking + obfuscation stay on.
+                optimize.set(false)
             }
 //            appResourcesRootDir.set(project.rootDir.resolve("resources"))
         }
     }
 }
 
-//</editor-fold>------------------------------------------------------------------------------------
+// </editor-fold>------------------------------------------------------------------------------------

+ 35 - 1
composeApp/composeApp.pro

@@ -1,2 +1,36 @@
 -keep class org.cef.** { *; }
--keep class kotlinx.coroutines.swing.SwingDispatcherFactory
+-keep class kotlinx.coroutines.swing.SwingDispatcherFactory
+
+# kotlinx.serialization — keep generated serializers and @Serializable members
+-keepattributes *Annotation*, InnerClasses
+-dontnote kotlinx.serialization.**
+-keepclassmembers class **$$serializer { *; }
+-keepclassmembers @kotlinx.serialization.Serializable class * { *; }
+
+# Third-party / transitive libraries with optional (unresolved) references.
+# These are reference-only warnings from R8/ProGuard; the classes are not used at
+# runtime by the desktop app, so suppressing the warnings is safe and is required
+# for the release distribution (proguardReleaseJars) to complete.
+-dontwarn ai.koog.**
+-dontwarn androidx.compose.**
+-dontwarn autovalue.shaded.**
+-dontwarn aws.smithy.**
+-dontwarn ch.qos.**
+-dontwarn co.yml.**
+-dontwarn com.aallam.**
+-dontwarn com.couchbase.**
+-dontwarn com.google.**
+-dontwarn com.jogamp.**
+-dontwarn javafx.**
+-dontwarn community.flock.**
+-dontwarn io.github.**
+-dontwarn io.ktor.**
+-dontwarn io.lettuce.**
+-dontwarn io.netty.**
+-dontwarn io.opentelemetry.**
+-dontwarn kotbase.**
+-dontwarn net.htmlparser.**
+-dontwarn okhttp3.internal.**
+-dontwarn org.apache.**
+-dontwarn reactor.core.**
+-dontwarn reactor.util.**

+ 35 - 24
composeApp/src/androidMain/kotlin/org/mjdev/desktop/activity/MainActivity.kt

@@ -24,17 +24,16 @@ import androidx.compose.ui.graphics.toArgb
 import androidx.compose.ui.tooling.preview.Preview
 import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
 import com.google.accompanist.permissions.ExperimentalPermissionsApi
-import org.mjdev.desktop.main.MainView
 import org.mjdev.desktop.context.DesktopContext.Companion.rememberDesktopContext
 import org.mjdev.desktop.context.LocalDesktopContext
+import org.mjdev.desktop.extensions.ComponentActivityExt.OnBackPress
+import org.mjdev.desktop.extensions.ComponentActivityExt.TRANSPARENT
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.helpers.permission.rememberPermissionManager
 import org.mjdev.desktop.helpers.theme.DesktopTheme
-import org.mjdev.desktop.extensions.ComponentActivityExt.OnBackPress
-import org.mjdev.desktop.extensions.ComponentActivityExt.TRANSPARENT
-import org.mjdev.desktop.extensions.ComponentActivityExt.setBackgroundColor
+import org.mjdev.desktop.main.MainView
 
-@Suppress("unused")
+@Suppress("unused", "DEPRECATION")
 @OptIn(ExperimentalPermissionsApi::class)
 class MainActivity : ComponentActivity() {
 //    val sensors = listOf(
@@ -49,21 +48,30 @@ class MainActivity : ComponentActivity() {
     override fun onCreate(savedInstanceState: Bundle?) {
         installSplashScreen()
         super.onCreate(savedInstanceState)
-        window.statusBarColor = Black.toArgb()
-        window.navigationBarColor = Black.toArgb()
+        runCatching {
+            window.statusBarColor = Black.toArgb()
+        }.onFailure { e ->
+            e.printStackTrace()
+        }
+        runCatching {
+            window.navigationBarColor = Black.toArgb()
+        }.onFailure { e ->
+            e.printStackTrace()
+        }
         window.decorView.setBackgroundColor(Black.toArgb())
         enableEdgeToEdge(
             statusBarStyle = SystemBarStyle.dark(TRANSPARENT),
-            navigationBarStyle = SystemBarStyle.dark(TRANSPARENT)
+            navigationBarStyle = SystemBarStyle.dark(TRANSPARENT),
         )
         setContent {
             CompositionLocalProvider(
-                LocalDesktopContext provides rememberDesktopContext(this@MainActivity)
+                LocalDesktopContext provides rememberDesktopContext(this@MainActivity),
             ) {
                 Box(
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .systemBarsPadding()
+                    modifier =
+                        Modifier
+                            .fillMaxSize()
+                            .systemBarsPadding(),
                 ) {
                     OnBackPress()
                     rememberPermissionManager()
@@ -100,17 +108,19 @@ class MainActivity : ComponentActivity() {
 private fun ActivityMain() {
     DesktopTheme {
         Scaffold(
-            modifier = Modifier
-                .fillMaxSize()
-                .background(Color.Black)
+            modifier =
+                Modifier
+                    .fillMaxSize()
+                    .background(Color.Black),
         ) { paddingValues ->
             Box(
-                modifier = Modifier
-                    .fillMaxSize()
-                    .padding(paddingValues)
-                    .consumeWindowInsets(paddingValues)
-                    .windowInsetsPadding(WindowInsets.safeDrawing)
-                    .background(Color.Black)
+                modifier =
+                    Modifier
+                        .fillMaxSize()
+                        .padding(paddingValues)
+                        .consumeWindowInsets(paddingValues)
+                        .windowInsetsPadding(WindowInsets.safeDrawing)
+                        .background(Color.Black),
             ) {
                 MainView()
             }
@@ -120,6 +130,7 @@ private fun ActivityMain() {
 
 @Preview
 @Composable
-fun PreviewMainActivity() = preview {
-    ActivityMain()
-}
+fun PreviewMainActivity() =
+    preview {
+        ActivityMain()
+    }

+ 1 - 1
composeApp/src/androidMain/kotlin/org/mjdev/desktop/app/App.kt

@@ -12,4 +12,4 @@ class App : Application() {
         CrashHandler.register(this, 200L)
         Log.init()
     }
-}
+}

+ 9 - 11
composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/MediaPlayer.kt

@@ -8,7 +8,7 @@ import kotlinx.coroutines.flow.collectLatest
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual class MediaPlayer(
-    private val mediaPlayer: MediaPlayer
+    private val mediaPlayer: MediaPlayer,
 ) {
     private val mediaPlayerCoroutineScope = CoroutineScope(Dispatchers.Main)
     private val isPlayingStateFlow = MutableStateFlow(false)
@@ -28,9 +28,10 @@ actual class MediaPlayer(
 
     actual fun setRate(rate: Float) {
         val wasPaused = !mediaPlayer.isPlaying
-        mediaPlayer.playbackParams = mediaPlayer.playbackParams.apply {
-            speed = rate
-        }
+        mediaPlayer.playbackParams =
+            mediaPlayer.playbackParams.apply {
+                speed = rate
+            }
         if (wasPaused) {
             mediaPlayer.pause()
         }
@@ -43,6 +44,7 @@ actual class MediaPlayer(
             // todo
         }
     }
+
     actual fun setTimeAccurate(millis: Long) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
             mediaPlayer.seekTo(millis, MediaPlayer.SEEK_CLOSEST)
@@ -51,13 +53,9 @@ actual class MediaPlayer(
         }
     }
 
-    actual fun getTimeMillis(): Long {
-        return mediaPlayer.currentPosition.toLong()
-    }
+    actual fun getTimeMillis(): Long = mediaPlayer.currentPosition.toLong()
 
-    actual fun getLengthMillis(): Long {
-        return mediaPlayer.duration.toLong()
-    }
+    actual fun getLengthMillis(): Long = mediaPlayer.duration.toLong()
 
     actual fun addOnTimeChangedListener(listener: OnTimeChangedListener) {
         mediaPlayerCoroutineScope.launch {
@@ -75,4 +73,4 @@ actual class MediaPlayer(
     actual fun dispose() {
         mediaPlayerCoroutineScope.cancel()
     }
-}
+}

+ 52 - 46
composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayer.kt

@@ -29,29 +29,31 @@ actual fun VideoPlayer(
         AndroidView(
             factory = { context ->
                 TextureView(context).also { it ->
-                    it.surfaceTextureListener = object : TextureView.SurfaceTextureListener {
-                        lateinit var mediaPlayer: MediaPlayer
-                        override fun onSurfaceTextureAvailable(
-                            surfaceTexture: SurfaceTexture,
-                            p1: Int,
-                            p2: Int
-                        ) {
-                            val surface = Surface(surfaceTexture)
-                            try {
-                                mediaPlayer = MediaPlayer()
-                                mediaPlayer.setDataSource(context, mrl.toUri())
-                                mediaPlayer.setSurface(surface)
-                                mediaPlayer.prepareAsync()
-                                mediaPlayer.setOnPreparedListener { mediaPlayer ->
-                                    if (time.intValue != -1) {
-                                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
-                                            mediaPlayer.seekTo(time.intValue.toLong(), SEEK_CLOSEST)
-                                        } else {
-                                            // todo
+                    it.surfaceTextureListener =
+                        object : TextureView.SurfaceTextureListener {
+                            lateinit var mediaPlayer: MediaPlayer
+
+                            override fun onSurfaceTextureAvailable(
+                                surfaceTexture: SurfaceTexture,
+                                p1: Int,
+                                p2: Int,
+                            ) {
+                                val surface = Surface(surfaceTexture)
+                                try {
+                                    mediaPlayer = MediaPlayer()
+                                    mediaPlayer.setDataSource(context, mrl.toUri())
+                                    mediaPlayer.setSurface(surface)
+                                    mediaPlayer.prepareAsync()
+                                    mediaPlayer.setOnPreparedListener { mediaPlayer ->
+                                        if (time.intValue != -1) {
+                                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                                                mediaPlayer.seekTo(time.intValue.toLong(), SEEK_CLOSEST)
+                                            } else {
+                                                // todo
+                                            }
                                         }
-                                    }
-                                    videoRatio =
-                                        mediaPlayer.videoWidth.toFloat() / mediaPlayer.videoHeight
+                                        videoRatio =
+                                            mediaPlayer.videoWidth.toFloat() / mediaPlayer.videoHeight
 //                                val containerRatio =
 //                                    binding.constraintContainer.width.toDouble() / binding.constraintContainer.height.toDouble()
 //                                val videoRatio = mediaPlayer.videoWidth.toDouble() / mediaPlayer.videoHeight.toDouble()
@@ -70,35 +72,39 @@ actual fun VideoPlayer(
 //                                }
 //                                binding.textureView.layoutParams = constraintLayoutParams
 //                                initViewModel(projectItem, mediaPlayer.duration.toLong(), mediaPlayer)
-                                    state.onMediaPlayerReady(mediaPlayer)
+                                        state.onMediaPlayerReady(mediaPlayer)
+                                    }
+                                } catch (e: Exception) {
+                                    e.printStackTrace()
                                 }
-                            } catch (e: Exception) {
-                                e.printStackTrace()
                             }
-                        }
-                        override fun onSurfaceTextureSizeChanged(
-                            p0: SurfaceTexture,
-                            p1: Int,
-                            p2: Int
-                        ) {}
-                        override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean {
-                            time.value = mediaPlayer.currentPosition
-                            state.doWithMediaPlayer { mp ->
-                                mp.dispose()
+
+                            override fun onSurfaceTextureSizeChanged(
+                                p0: SurfaceTexture,
+                                p1: Int,
+                                p2: Int,
+                            ) {}
+
+                            override fun onSurfaceTextureDestroyed(p0: SurfaceTexture): Boolean {
+                                time.value = mediaPlayer.currentPosition
+                                state.doWithMediaPlayer { mp ->
+                                    mp.dispose()
+                                }
+                                return false
                             }
-                            return false
+
+                            override fun onSurfaceTextureUpdated(p0: SurfaceTexture) {}
                         }
-                        override fun onSurfaceTextureUpdated(p0: SurfaceTexture) {}
-                    }
                 }
             },
-            modifier = Modifier.let {
-                if (videoRatio != -1f) {
-                    it.aspectRatio(videoRatio).align(Alignment.Center)
-                } else {
-                    it
-                }
-            }
+            modifier =
+                Modifier.let {
+                    if (videoRatio != -1f) {
+                        it.aspectRatio(videoRatio).align(Alignment.Center)
+                    } else {
+                        it
+                    }
+                },
         )
     }
-}
+}

+ 3 - 3
composeApp/src/androidMain/kotlin/org/mjdev/desktop/components/video/VideoPlayerState.kt

@@ -11,12 +11,12 @@ actual class VideoPlayerState {
     actual fun doWithMediaPlayer(block: (MediaPlayer) -> Unit) {
         mediaPlayer?.let {
             block(it)
-        }?:run {
+        } ?: run {
             defferredEffects.add(block)
         }
     }
 
-    internal fun onMediaPlayerReady(mediaPlayer:  android.media.MediaPlayer) {
+    internal fun onMediaPlayerReady(mediaPlayer: android.media.MediaPlayer) {
         val mp = MediaPlayer(mediaPlayer)
         this.mediaPlayer = mp
         defferredEffects.forEach { block ->
@@ -27,4 +27,4 @@ actual class VideoPlayerState {
 }
 
 @Composable
-actual fun rememberVideoPlayerState() = remember { VideoPlayerState() }
+actual fun rememberVideoPlayerState() = remember { VideoPlayerState() }

+ 57 - 51
composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt

@@ -17,34 +17,34 @@ import org.mjdev.desktop.data.User
 import org.mjdev.desktop.extensions.Compose.isDesign
 import org.mjdev.desktop.helpers.image.ImageLoader.asyncImageLoader
 import org.mjdev.desktop.interfaces.IApp
-import org.mjdev.desktop.managers.apps.IAppsManager
-import org.mjdev.desktop.managers.connectivity.IConnectivityManager
 import org.mjdev.desktop.interfaces.ILocale
-import org.mjdev.desktop.managers.palette.IPalette
-import org.mjdev.desktop.managers.process.IProcessManager
 import org.mjdev.desktop.interfaces.ITheme
 import org.mjdev.desktop.interfaces.IUser
-import org.mjdev.desktop.managers.language.Translator
 import org.mjdev.desktop.log.Log
 import org.mjdev.desktop.managers.ai.AiManager
 import org.mjdev.desktop.managers.ai.IAiManager
-import org.mjdev.desktop.managers.os.IOSManager
-import org.mjdev.desktop.managers.theme.IThemeManager
-import org.mjdev.desktop.managers.translations.ITranslator
 import org.mjdev.desktop.managers.ai.plugins.AiPluginGemini
 import org.mjdev.desktop.managers.ai.plugins.AiPluginOpenAi
 import org.mjdev.desktop.managers.ai.stt.STTPluginEmpty
 import org.mjdev.desktop.managers.ai.tts.TTSPluginAndroid
+import org.mjdev.desktop.managers.apps.AppsManager
+import org.mjdev.desktop.managers.apps.IAppsManager
 import org.mjdev.desktop.managers.base.IDelegate
 import org.mjdev.desktop.managers.connectivity.ConnectivityManager
-import org.mjdev.desktop.managers.os.OsManager
-import org.mjdev.desktop.managers.processes.ProcessManager
-import kotlin.reflect.KClass
-import org.mjdev.desktop.managers.apps.AppsManager
+import org.mjdev.desktop.managers.connectivity.IConnectivityManager
 import org.mjdev.desktop.managers.keys.IKeyManager
 import org.mjdev.desktop.managers.keys.KeysManager
+import org.mjdev.desktop.managers.language.Translator
+import org.mjdev.desktop.managers.os.IOSManager
+import org.mjdev.desktop.managers.os.OsManager
+import org.mjdev.desktop.managers.palette.IPalette
 import org.mjdev.desktop.managers.palette.Palette
+import org.mjdev.desktop.managers.process.IProcessManager
+import org.mjdev.desktop.managers.processes.ProcessManager
+import org.mjdev.desktop.managers.theme.IThemeManager
 import org.mjdev.desktop.managers.theme.ThemeManager
+import org.mjdev.desktop.managers.translations.ITranslator
+import kotlin.reflect.KClass
 import kotlin.reflect.full.companionObject
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
@@ -54,9 +54,8 @@ class DesktopContext(
     override val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
     override val imageLoader: ImageLoader? = null,
     val isInDesign: Boolean = false,
-    override val platformContext: PlatformContext? = null
+    override val platformContext: PlatformContext? = null,
 ) : IDesktopContext() {
-
     val androidContext
         get() = context
 
@@ -88,16 +87,19 @@ class DesktopContext(
     override val favoriteApps: List<IApp>
         get() = appsManager.favoriteApps
 
-    override val containerSize: DpSize = androidContext?.resources
-        ?.displayMetrics
-        ?.let { dm ->
-            DpSize(dm.widthPixels.dp, dm.heightPixels.dp)
-        } ?: DpSize(1024.dp, 640.dp)
+    override val containerSize: DpSize =
+        androidContext
+            ?.resources
+            ?.displayMetrics
+            ?.let { dm ->
+                DpSize(dm.widthPixels.dp, dm.heightPixels.dp)
+            } ?: DpSize(1024.dp, 640.dp)
 
     val allUsers
-        get() = runCatching {
-            User.allUsers(this)
-        }.getOrNull() ?: emptyList()
+        get() =
+            runCatching {
+                User.allUsers(this)
+            }.getOrNull() ?: emptyList()
 
     override suspend fun logOut() {
     }
@@ -114,38 +116,42 @@ class DesktopContext(
     override suspend fun open(what: Any?) {
     }
 
-    override suspend fun login(uName: String, uPasswd: String): Boolean {
-        return false
-    }
+    override suspend fun login(
+        uName: String,
+        uPasswd: String,
+    ): Boolean = false
 
-    override suspend fun logout(): Boolean {
-        return false
-    }
+    override suspend fun logout(): Boolean = false
 
     override fun dispose() {
     }
 
-    override fun createManager(cls: KClass<*>): IDelegate = when (cls) {
-        IOSManager::class -> OsManager(this)
-        IPalette::class -> Palette(this)
-        ITranslator::class -> Translator(this)
-        IConnectivityManager::class -> ConnectivityManager(this)
-        IAiManager::class -> AiManager(
-            context = this,
-            // todo user can configure
-            pluginAI = AiPluginOpenAi(this@DesktopContext),
-            pluginTTS = TTSPluginAndroid(this@DesktopContext),
-            pluginSTT = STTPluginEmpty(this@DesktopContext)
-        )
-
-        IAppsManager::class -> AppsManager(this)
-        IThemeManager::class -> ThemeManager(this)
-        IProcessManager::class -> ProcessManager(this)
-        IKeyManager::class -> KeysManager(this)
-        else -> cls.companionObject?.members?.first {
-            it.name == "EMPTY"
-        }?.call() as IDelegate
-    }
+    override fun createManager(cls: KClass<*>): IDelegate =
+        when (cls) {
+            IOSManager::class -> OsManager(this)
+            IPalette::class -> Palette(this)
+            ITranslator::class -> Translator(this)
+            IConnectivityManager::class -> ConnectivityManager(this)
+            IAiManager::class ->
+                AiManager(
+                    context = this,
+                    // todo user can configure
+                    pluginAI = AiPluginOpenAi(this@DesktopContext),
+                    pluginTTS = TTSPluginAndroid(this@DesktopContext),
+                    pluginSTT = STTPluginEmpty(this@DesktopContext),
+                )
+
+            IAppsManager::class -> AppsManager(this)
+            IThemeManager::class -> ThemeManager(this)
+            IProcessManager::class -> ProcessManager(this)
+            IKeyManager::class -> KeysManager(this)
+            else ->
+                cls.companionObject
+                    ?.members
+                    ?.first {
+                        it.name == "EMPTY"
+                    }?.call() as IDelegate
+        }
 
     companion object {
         @Composable
@@ -163,8 +169,8 @@ class DesktopContext(
                 scope = scope,
                 imageLoader = imageLoader,
                 isInDesign = isInDesign,
-                platformContext = platformContext
+                platformContext = platformContext,
             )
         }
     }
-}
+}

+ 4 - 3
composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/LocalDesktopContext.android.kt

@@ -3,6 +3,7 @@ package org.mjdev.desktop.context
 import androidx.compose.runtime.ProvidableCompositionLocal
 import androidx.compose.runtime.compositionLocalOf
 
-actual val LocalDesktopContext: ProvidableCompositionLocal<IDesktopContext> = compositionLocalOf {
-    DesktopContext()
-}
+actual val LocalDesktopContext: ProvidableCompositionLocal<IDesktopContext> =
+    compositionLocalOf {
+        DesktopContext()
+    }

+ 20 - 21
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/App.kt

@@ -11,7 +11,6 @@ import android.os.Build
 import androidx.annotation.RequiresApi
 import org.mjdev.desktop.interfaces.IApp
 
-
 @Suppress("unused")
 class App(
     private val context: Context?,
@@ -20,43 +19,43 @@ class App(
     override val fullAppName: String = info.label.toString(),
     override val comment: String = info.applicationInfo.packageName,
     override val cmd: String = info.applicationInfo.packageName, // todo intent
-    override val fullTextString: String = "${name}|${comment}|${cmd}",
-    override val categories: List<Category> = listOf(guessCategory(context, info))
+    override val fullTextString: String = "$name|$comment|$cmd",
+    override val categories: List<Category> = listOf(guessCategory(context, info)),
 ) : IApp {
     override var isStarting: Boolean = false
 
     override var isRunning: Boolean = false
 
     override suspend fun start() {
-        context?.startActivity(Intent(
-            Intent.ACTION_MAIN
-        ).apply {
-            addCategory(Intent.CATEGORY_LAUNCHER)
-            flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
-            component = info.componentName
-        })
+        context?.startActivity(
+            Intent(
+                Intent.ACTION_MAIN,
+            ).apply {
+                addCategory(Intent.CATEGORY_LAUNCHER)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                component = info.componentName
+            },
+        )
     }
 
     override fun toString(): String = name
 
-    private fun PackageInfo.isSystemPackage(): Boolean {
-        return ((applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM) != 0
-    }
+    private fun PackageInfo.isSystemPackage(): Boolean = ((applicationInfo?.flags ?: 0) and ApplicationInfo.FLAG_SYSTEM) != 0
 
     companion object {
         @SuppressLint("NewApi")
         private fun guessCategory(
             context: Context?,
-            info: LauncherActivityInfo
-        ): Category {
-            return try {
-                ApplicationInfo.getCategoryTitle(
-                    context,
-                    info.applicationInfo.category
-                )?.let { Category(it.toString()) } ?: Category.Empty
+            info: LauncherActivityInfo,
+        ): Category =
+            try {
+                ApplicationInfo
+                    .getCategoryTitle(
+                        context,
+                        info.applicationInfo.category,
+                    )?.let { Category(it.toString()) } ?: Category.Empty
             } catch (e: Throwable) {
                 Category.Empty
             }
-        }
     }
 }

+ 28 - 37
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/Theme.kt

@@ -16,12 +16,10 @@ import org.mjdev.desktop.interfaces.IUser
 class Theme(
     backgroundColor: Color,
     backgroundRotationDelay: Long,
-
     panelLocation: PanelLocation,
     panelDividerWidth: Dp,
     panelContentPadding: Dp,
     panelHideDelay: Long,
-
     controlCenterLocation: ControlCenterLocation,
     controlPanelHideDelay: Long,
     controlCenterExpandedWidthPercent: Int,
@@ -30,14 +28,11 @@ class Theme(
     controlCenterIconColor: Color,
     controlCenterIconSize: DpSize,
     controlCenterBackgroundAlpha: Float,
-
     iconSet: IFont,
-
-    appMenuMinWidthRatio:Float,
-    appMenuMinHeightRatio:Float,
+    appMenuMinWidthRatio: Float,
+    appMenuMinHeightRatio: Float,
     appMenuOuterPadding: Dp,
 ) : ITheme {
-
     val backgroundColorState = mutableStateOf(backgroundColor)
     val backgroundRotationDelayState = mutableStateOf(backgroundRotationDelay)
 
@@ -88,7 +83,7 @@ class Theme(
 
     override var controlCenterBackgroundAlpha = controlCenterBackgroundAlphaState.value
 
-    override var iconSet : IFont = iconSetState.value
+    override var iconSet: IFont = iconSetState.value
 
     override var backgroundRotationDelay = backgroundRotationDelayState.value
 
@@ -105,36 +100,32 @@ class Theme(
     companion object {
         val themeCache = mutableMapOf<String, Theme>()
 
-        val DEFAULT = Theme(
-            iconSet = MaterialSymbolsSharp,
-
-            backgroundColor = Color.SuperDarkGray,
-            backgroundRotationDelay = 60000,
-
-            panelLocation = PanelLocation.Bottom,
-            panelDividerWidth = 16.dp,
-            panelContentPadding = 4.dp,
-            panelHideDelay = 2000L,
-
-            controlCenterLocation = ControlCenterLocation.Right,
-            controlPanelHideDelay = 2000L,
-            controlCenterExpandedWidthPercent = 25,
-            controlCenterDividerColor = Color.SuperDarkGray,
-            controlCenterDividerWidth = 4.dp,
-            controlCenterIconColor = Color.White,
-            controlCenterIconSize = DpSize(32.dp, 32.dp),
-            controlCenterBackgroundAlpha = 0.6f,
-
-            appMenuMinWidthRatio = 0.3f,
-            appMenuMinHeightRatio = 0.8f,
-            appMenuOuterPadding = 2.dp
-        )
+        val DEFAULT =
+            Theme(
+                iconSet = MaterialSymbolsSharp,
+                backgroundColor = Color.SuperDarkGray,
+                backgroundRotationDelay = 60000,
+                panelLocation = PanelLocation.Bottom,
+                panelDividerWidth = 16.dp,
+                panelContentPadding = 4.dp,
+                panelHideDelay = 2000L,
+                controlCenterLocation = ControlCenterLocation.Right,
+                controlPanelHideDelay = 2000L,
+                controlCenterExpandedWidthPercent = 25,
+                controlCenterDividerColor = Color.SuperDarkGray,
+                controlCenterDividerWidth = 4.dp,
+                controlCenterIconColor = Color.White,
+                controlCenterIconSize = DpSize(32.dp, 32.dp),
+                controlCenterBackgroundAlpha = 0.6f,
+                appMenuMinWidthRatio = 0.3f,
+                appMenuMinHeightRatio = 0.8f,
+                appMenuOuterPadding = 2.dp,
+            )
 
         // todo load from user settings
-        fun load(
-            user: IUser
-        ): Theme = themeCache[user.userName] ?: DEFAULT.apply {
-            themeCache[user.userName] = this
-        }
+        fun load(user: IUser): Theme =
+            themeCache[user.userName] ?: DEFAULT.apply {
+                themeCache[user.userName] = this
+            }
     }
 }

+ 19 - 21
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/User.kt

@@ -6,8 +6,8 @@ import android.os.Environment
 import okio.Path
 import okio.Path.Companion.toPath
 import org.mjdev.desktop.context.DesktopContext
-import org.mjdev.desktop.icons.user.AccountCircle
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.icons.user.AccountCircle
 import org.mjdev.desktop.interfaces.ITheme
 import org.mjdev.desktop.interfaces.IUser
 import org.mjdev.desktop.interfaces.IUserDirs
@@ -17,7 +17,7 @@ class User(
     val account: Account? = null,
 ) : IUser {
     override val userName: String
-        get() =  account?.name ?: ""
+        get() = account?.name ?: ""
     override val password: String
         get() = "" // todo
     override val uid: String
@@ -31,7 +31,7 @@ class User(
     override val shell: String
         get() = "" // todo
     override val picture: Any
-        get() =  AccountCircle // todo
+        get() = AccountCircle // todo
     override val homeDir: Path
         get() = Environment.getExternalStorageDirectory().path.toPath()
     override val isLoggedIn
@@ -41,13 +41,11 @@ class User(
     override val config: DesktopConfig
         get() = DesktopConfig.load(this) // todo load
     override val theme: ITheme
-        get() =  Theme.load(this) // todo load
+        get() = Theme.load(this) // todo load
     override val backgrounds
         get() = config.desktopBackgrounds
 
-    override fun toString(): String {
-        return userName
-    }
+    override fun toString(): String = userName
 
 //    fun login(
 //        context: IDesktopContext,
@@ -58,28 +56,28 @@ class User(
         val NOBODY = User()
         val DEFAULT = NOBODY
 
-        fun isLoggedIn(
-            uname: String
-        ): Boolean = false // todo
+        fun isLoggedIn(uname: String): Boolean = false // todo
 
         fun loadPicture(
             home: String,
-            pic: String
+            pic: String,
         ): File? = loadPicture(File(home), pic)
 
         fun loadPicture(
             homeDir: File,
-            picName: String
-        ): File? = File(homeDir, picName).let { f ->
-            if (f.exists()) f else null
-        }
+            picName: String,
+        ): File? =
+            File(homeDir, picName).let { f ->
+                if (f.exists()) f else null
+            }
 
-        fun allUsers(
-            context: IDesktopContext
-        ): List<User> = if (context is DesktopContext) {
-            AccountManager.get(context.context).accounts.map { a ->
-                User(a)
+        fun allUsers(context: IDesktopContext): List<User> =
+            if (context is DesktopContext) {
+                AccountManager.get(context.context).accounts.map { a ->
+                    User(a)
+                }
+            } else {
+                emptyList()
             }
-        } else emptyList()
     }
 }

+ 3 - 6
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/UserDirs.kt

@@ -6,12 +6,9 @@ import org.mjdev.desktop.interfaces.IUserDirs
 
 @Suppress("unused")
 class UserDirs(
-    override val homeDirectory: Path
+    override val homeDirectory: Path,
 ) : IUserDirs {
-
-    operator fun get(
-        name: String
-    ): Path = homeDirectory.resolve(name)
+    operator fun get(name: String): Path = homeDirectory.resolve(name)
 
     init {
         runCatching { desktopDirectory.mkdirs() }
@@ -43,4 +40,4 @@ class UserDirs(
         get() = this[IUserDirs.VIDEOS_DIR]
     override val backgroundsDirectory: Path
         get() = this[IUserDirs.BACKGROUNDS_DIR]
-}
+}

+ 13 - 14
composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ComponentActivity.kt

@@ -6,20 +6,19 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.toArgb
 import androidx.core.app.ComponentActivity
+import androidx.core.graphics.drawable.toDrawable
 import androidx.core.view.WindowCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.WindowInsetsControllerCompat
 import org.mjdev.desktop.BuildConfig
 import org.mjdev.desktop.helpers.WakeLockHelper
-import androidx.core.graphics.drawable.toDrawable
 import org.mjdev.desktop.log.Log
 
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 object ComponentActivityExt {
-
     const val WAKE_LOCK_TAG = 1
 
-    val TRANSPARENT : Int = Color.Transparent.toArgb()
+    val TRANSPARENT: Int = Color.Transparent.toArgb()
 
     val ComponentActivity.decorView
         get() = window.decorView
@@ -51,18 +50,18 @@ object ComponentActivityExt {
         }
     }
 
-    fun ComponentActivity.setKeepScreenOn() = with(window) {
-        addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
-    }
+    fun ComponentActivity.setKeepScreenOn() =
+        with(window) {
+            addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        }
 
-    fun ComponentActivity.resetKeepScreenOn() = with(window) {
-        clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
-    }
+    fun ComponentActivity.resetKeepScreenOn() =
+        with(window) {
+            clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        }
 
     // todo : black is white ?
-    fun ComponentActivity.setBackgroundColor(
-        color: Color = Color.Black
-    ) {
+    fun ComponentActivity.setBackgroundColor(color: Color = Color.Black) {
         decorView.background = color.toArgb().toDrawable()
     }
 
@@ -71,6 +70,6 @@ object ComponentActivityExt {
         enabled: Boolean = !BuildConfig.DEBUG,
         action: () -> Unit = {
             Log.d("OnBackPressed")
-        }
+        },
     ) = BackHandler(enabled, action)
-}
+}

+ 17 - 14
composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt

@@ -11,19 +11,22 @@ import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual object ImageBitmapExt {
-    actual suspend fun IDesktopContext.loadPicture(
-        src: Any?
-    ): ImageBitmap? = runCatching {
-        ImageRequest.Builder(platformContext!!)
-            .data(
-                when (src) {
-                    is Path -> src.toFile()
-                    else -> src.toString()
+    actual suspend fun IDesktopContext.loadPicture(src: Any?): ImageBitmap? =
+        runCatching {
+            ImageRequest
+                .Builder(platformContext!!)
+                .data(
+                    when (src) {
+                        is Path -> src.toFile()
+                        else -> src.toString()
+                    },
+                ).build()
+                .let { req ->
+                    imageLoader
+                        ?.execute(req)
+                        ?.image
+                        ?.toBitmap()
+                        ?.asImageBitmap()
                 }
-            )
-            .build().let { req ->
-                imageLoader?.execute(req)?.image?.toBitmap()?.asImageBitmap()
-            }
-    }.getOrNull()
-
+        }.getOrNull()
 }

+ 9 - 10
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/DecoderAndroid.kt

@@ -4,13 +4,12 @@ import android.graphics.BitmapFactory
 import androidx.compose.ui.graphics.asImageBitmap
 import org.mjdev.desktop.data.DecodedImage
 
-actual fun decodeImage(
-    bytes: ByteArray
-): DecodedImage? = try {
-    val bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
-    val imageBitmap = bmp.asImageBitmap()
-    DecodedImage(imageBitmap, bmp.width, bmp.height)
-} catch (e: Exception) {
-    e.printStackTrace()
-    null
-}
+actual fun decodeImage(bytes: ByteArray): DecodedImage? =
+    try {
+        val bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
+        val imageBitmap = bmp.asImageBitmap()
+        DecodedImage(imageBitmap, bmp.width, bmp.height)
+    } catch (e: Exception) {
+        e.printStackTrace()
+        null
+    }

+ 6 - 5
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/WakeLockHelper.kt

@@ -6,7 +6,7 @@ import android.os.PowerManager
 import android.os.PowerManager.WakeLock
 
 class WakeLockHelper(
-    val context: Context
+    val context: Context,
 ) {
     private var wakeLock: WakeLock? = null
     private val powerManager
@@ -15,10 +15,11 @@ class WakeLockHelper(
     @Suppress("DEPRECATION")
     @SuppressLint("WakelockTimeout")
     fun acquireWakeLock() {
-        wakeLock = powerManager?.newWakeLock(
-            PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
-            "MyApp:KeepScreenOnTag"
-        )
+        wakeLock =
+            powerManager?.newWakeLock(
+                PowerManager.SCREEN_BRIGHT_WAKE_LOCK,
+                "MyApp:KeepScreenOnTag",
+            )
         if (wakeLock?.isHeld != true) {
             wakeLock?.acquire()
         }

+ 20 - 15
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/crash/CrashHandler.kt

@@ -10,33 +10,38 @@ import kotlin.system.exitProcess
 @Suppress("UNREACHABLE_CODE")
 class CrashHandler(
     private val context: Context,
-    private val restartMillis:Long = 1000
+    private val restartMillis: Long = 1000,
 ) : Thread.UncaughtExceptionHandler {
     private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler()
     private val alarmMgr
         get() = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
 
     @Suppress("KotlinUnreachableCode")
-    override fun uncaughtException(thread: Thread, ex: Throwable) {
+    override fun uncaughtException(
+        thread: Thread,
+        ex: Throwable,
+    ) {
         Log.e(TAG, "Fatal: ", ex)
-        val intent = context.packageManager
-            .getLaunchIntentForPackage(context.packageName)
+        val intent =
+            context.packageManager
+                .getLaunchIntentForPackage(context.packageName)
         if (intent != null) {
             intent.addFlags(
                 Intent.FLAG_ACTIVITY_CLEAR_TOP or
-                        Intent.FLAG_ACTIVITY_CLEAR_TASK or
-                        Intent.FLAG_ACTIVITY_NEW_TASK
-            )
-            val pendingIntent = PendingIntent.getActivity(
-                context,
-                0,
-                intent,
-                PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+                    Intent.FLAG_ACTIVITY_CLEAR_TASK or
+                    Intent.FLAG_ACTIVITY_NEW_TASK,
             )
+            val pendingIntent =
+                PendingIntent.getActivity(
+                    context,
+                    0,
+                    intent,
+                    PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
+                )
             alarmMgr.set(
                 AlarmManager.RTC,
                 System.currentTimeMillis() + restartMillis,
-                pendingIntent
+                pendingIntent,
             )
         } else {
             Log.e(TAG, "No intent to restart app.")
@@ -50,10 +55,10 @@ class CrashHandler(
 
         fun register(
             context: Context,
-            restartMillis:Long = 1000L
+            restartMillis: Long = 1000L,
         ) {
             Thread.setDefaultUncaughtExceptionHandler(CrashHandler(context, restartMillis))
             Log.d(TAG, "Crash handler registered.")
         }
     }
-}
+}

+ 8 - 6
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/CodePointsFile.kt

@@ -3,7 +3,7 @@ package org.mjdev.desktop.helpers.font
 import org.mjdev.desktop.helpers.streams.ResourceStream
 
 class CodePointsFile(
-    private val codepointsFileName: String
+    private val codepointsFileName: String,
 ) {
     val icons: Map<String, Int> by lazy {
         ResourceStream(codepointsFileName)
@@ -11,9 +11,11 @@ class CodePointsFile(
             .split("\n")
             .map { it.split(" ") }
             .mapNotNull {
-                if (it.size == 2) Pair(it[0], it[1].toInt(radix = 16))
-                else null
-            }
-            .toMap()
+                if (it.size == 2) {
+                    Pair(it[0], it[1].toInt(radix = 16))
+                } else {
+                    null
+                }
+            }.toMap()
     }
-}
+}

+ 16 - 12
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/FontDescriptor.kt

@@ -7,18 +7,22 @@ import org.mjdev.desktop.helpers.streams.ResourceStream.Companion.openResource
 import java.io.File
 
 @SuppressLint("DiscouragedApi")
-class FontDescriptor(ttfFileName: String) {
-    private val fontTypeFace: Typeface? = runCatching {
-        val tempFile = File.createTempFile("font", ".ttf")
-        openResource(ttfFileName).use { inputStream ->
-            tempFile.outputStream().use { outputStream ->
-                inputStream.copyTo(outputStream)
+class FontDescriptor(
+    ttfFileName: String,
+) {
+    private val fontTypeFace: Typeface? =
+        runCatching {
+            val tempFile = File.createTempFile("font", ".ttf")
+            openResource(ttfFileName).use { inputStream ->
+                tempFile.outputStream().use { outputStream ->
+                    inputStream.copyTo(outputStream)
+                }
             }
-        }
-        Typeface.createFromFile(tempFile)
-    }.getOrNull()
+            Typeface.createFromFile(tempFile)
+        }.getOrNull()
 
-    val fontFamily: FontFamily = fontTypeFace?.let { typeFace ->
-        FontFamily(typeFace)
-    } ?: FontFamily.Default
+    val fontFamily: FontFamily =
+        fontTypeFace?.let { typeFace ->
+            FontFamily(typeFace)
+        } ?: FontFamily.Default
 }

+ 19 - 14
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/font/MaterialIconFont.android.kt

@@ -21,19 +21,24 @@ actual class MaterialIconFont actual constructor(
 
     private val cache: MutableMap<String, Int> = mutableMapOf()
 
-    override fun iconForName(
-        iconName: String?,
-    ): Int? = runCatching {
-        if (iconName != null) {
-            if (cache.containsKey(iconName)) {
-                cache[iconName]
-            } else {
-                codePointsFile.icons.map { icon ->
-                    Pair(FuzzySearch.ratio(iconName, icon.key), icon)
-                }.maxByOrNull { it.first }?.second?.value?.also {
-                    cache[iconName] = it
+    override fun iconForName(iconName: String?): Int? =
+        runCatching {
+            if (iconName != null) {
+                if (cache.containsKey(iconName)) {
+                    cache[iconName]
+                } else {
+                    codePointsFile.icons
+                        .map { icon ->
+                            Pair(FuzzySearch.ratio(iconName, icon.key), icon)
+                        }.maxByOrNull { it.first }
+                        ?.second
+                        ?.value
+                        ?.also {
+                            cache[iconName] = it
+                        }
                 }
+            } else {
+                null
             }
-        } else null
-    }.getOrNull()
-}
+        }.getOrNull()
+}

+ 27 - 23
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/permission/PermissonManager.kt

@@ -19,8 +19,8 @@ import androidx.compose.runtime.DisposableEffect
 import androidx.compose.runtime.saveable.rememberSaveable
 import androidx.compose.ui.platform.LocalContext
 import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.compose.LocalLifecycleOwner
 import androidx.lifecycle.LifecycleEventObserver
+import androidx.lifecycle.compose.LocalLifecycleOwner
 import com.google.accompanist.permissions.ExperimentalPermissionsApi
 import com.google.accompanist.permissions.MultiplePermissionsState
 import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -32,10 +32,11 @@ fun manifestPermissions(): List<String> {
         try {
             val packageManager = context.packageManager
             arrayListOf<String>().apply {
-                val pkgInfo: PackageInfo = packageManager.getPackageInfo(
-                    context.packageName,
-                    PackageManager.GET_PERMISSIONS
-                )
+                val pkgInfo: PackageInfo =
+                    packageManager.getPackageInfo(
+                        context.packageName,
+                        PackageManager.GET_PERMISSIONS,
+                    )
                 pkgInfo.requestedPermissions?.forEach { p -> add(p) }
             }
         } catch (e: Exception) {
@@ -51,40 +52,43 @@ fun manifestPermissions(): List<String> {
 @Composable
 fun rememberPermissionManager(
     permissions: List<String> = manifestPermissions(),
-    onNeedToGrant: (MultiplePermissionsState.(permissions: List<String>) -> Unit)? = null
+    onNeedToGrant: (MultiplePermissionsState.(permissions: List<String>) -> Unit)? = null,
 ) {
     if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
         var _ps: MultiplePermissionsState? = null
         val lifecycleOwner = LocalLifecycleOwner.current
         val onPermissionsResult: (Map<String, Boolean>) -> Unit = { map ->
-            map.filter { p ->
-                !p.value
-            }.filter { p ->
-                permissions.contains(p.key)
-            }.also { ps ->
-                _ps?.also { mps ->
-                    onNeedToGrant?.invoke(mps, ps.map { p -> p.key })
+            map
+                .filter { p ->
+                    !p.value
+                }.filter { p ->
+                    permissions.contains(p.key)
+                }.also { ps ->
+                    _ps?.also { mps ->
+                        onNeedToGrant?.invoke(mps, ps.map { p -> p.key })
+                    }
                 }
-            }
         }
-        val permissionsState = rememberMultiplePermissionsState(
-            permissions = permissions,
-            onPermissionsResult = onPermissionsResult
-        )
+        val permissionsState =
+            rememberMultiplePermissionsState(
+                permissions = permissions,
+                onPermissionsResult = onPermissionsResult,
+            )
         _ps = permissionsState
         DisposableEffect(
             key1 = lifecycleOwner,
             effect = {
-                val observer = LifecycleEventObserver { _, event ->
-                    if (event == Lifecycle.Event.ON_RESUME) {
-                        permissionsState.launchMultiplePermissionRequest()
+                val observer =
+                    LifecycleEventObserver { _, event ->
+                        if (event == Lifecycle.Event.ON_RESUME) {
+                            permissionsState.launchMultiplePermissionRequest()
+                        }
                     }
-                }
                 lifecycleOwner.lifecycle.addObserver(observer)
                 onDispose {
                     lifecycleOwner.lifecycle.removeObserver(observer)
                 }
-            }
+            },
         )
     }
 }

+ 20 - 20
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/persistence/StorageProvider.kt

@@ -1,16 +1,16 @@
 package org.mjdev.desktop.helpers.persistence
 
-import org.mjdev.desktop.resources.Res
 import androidx.core.content.edit
 import kotlinx.coroutines.runBlocking
 import org.jetbrains.compose.resources.getString
 import org.mjdev.desktop.context.DesktopContext
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.resources.Res
 import org.mjdev.desktop.resources.app_id
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual open class StorageProvider actual constructor(
-    val context: IDesktopContext
+    val context: IDesktopContext,
 ) {
     private val applicationContext
         get() = (context as? DesktopContext)?.androidContext?.applicationContext
@@ -24,32 +24,32 @@ actual open class StorageProvider actual constructor(
 
     actual open fun put(
         key: String,
-        value: String
+        value: String,
     ) = prefs?.edit {
         putString(key, value)
     } ?: throw IllegalStateException("SharedPreferences not initialized")
 
     actual open fun get(
         key: String,
-        default: String?
-    ) : String? = prefs?.getString(
-        key,
-        default
-    ) ?: throw IllegalStateException("SharedPreferences not initialized")
-
-    actual open fun remove(
-        key: String
-    ) = prefs?.edit {
-        remove(key)
-    } ?: throw IllegalStateException("SharedPreferences not initialized")
-
-    actual open fun clear() = prefs?.edit {
-        clear()
-    } ?: throw IllegalStateException("SharedPreferences not initialized")
+        default: String?,
+    ): String? =
+        prefs?.getString(
+            key,
+            default,
+        ) ?: throw IllegalStateException("SharedPreferences not initialized")
+
+    actual open fun remove(key: String) =
+        prefs?.edit {
+            remove(key)
+        } ?: throw IllegalStateException("SharedPreferences not initialized")
+
+    actual open fun clear() =
+        prefs?.edit {
+            clear()
+        } ?: throw IllegalStateException("SharedPreferences not initialized")
 
     actual open fun getAll(): Map<String, String> =
         prefs?.all?.mapValues { entry ->
             entry.value?.toString() ?: ""
         } ?: emptyMap()
-
-}
+}

+ 10 - 11
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/streams/ResourceStream.android.kt

@@ -4,7 +4,7 @@ import java.io.InputStream
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "MemberVisibilityCanBePrivate")
 actual class ResourceStream actual constructor(
-    val resourcePath: String
+    val resourcePath: String,
 ) {
     actual val bytes: ByteArray
         get() = useResource(resourcePath) { stream -> stream.readBytes() }
@@ -13,24 +13,23 @@ actual class ResourceStream actual constructor(
     companion object {
         inline fun <T> useResource(
             resourcePath: String,
-            block: (InputStream) -> T
+            block: (InputStream) -> T,
         ): T = openResource(resourcePath).use(block)
 
-        fun openResource(
-            resourcePath: String,
-        ): InputStream = ClassLoaderResourceLoader.load(resourcePath)
+        fun openResource(resourcePath: String): InputStream = ClassLoaderResourceLoader.load(resourcePath)
 
         private object ClassLoaderResourceLoader {
             fun load(resourcePath: String): InputStream {
-                val resource = Thread
-                    .currentThread()
-                    .contextClassLoader
-                    ?.getResourceAsStream("assets/$resourcePath")
-                    ?: this::class.java.getResourceAsStream(resourcePath)
+                val resource =
+                    Thread
+                        .currentThread()
+                        .contextClassLoader
+                        ?.getResourceAsStream("assets/$resourcePath")
+                        ?: this::class.java.getResourceAsStream(resourcePath)
                 return requireNotNull(resource) {
                     "Resource $resourcePath not found"
                 }
             }
         }
     }
-}
+}

+ 1 - 1
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfo.android.kt

@@ -2,4 +2,4 @@ package org.mjdev.desktop.helpers.system.meminfo
 
 import org.mjdev.desktop.context.IDesktopContext
 
-actual fun MemInfo(context: IDesktopContext): MemInfoStub  = MemInfoAndroid(context)
+actual fun MemInfo(context: IDesktopContext): MemInfoStub = MemInfoAndroid(context)

+ 6 - 5
composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfoAndroid.kt

@@ -7,7 +7,7 @@ import org.mjdev.desktop.context.DesktopContext
 import org.mjdev.desktop.context.IDesktopContext
 
 class MemInfoAndroid(
-    context: IDesktopContext
+    context: IDesktopContext,
 ) : MemInfoStub(context) {
     val aContext: Context?
         get() = (context as? DesktopContext)?.context
@@ -15,11 +15,12 @@ class MemInfoAndroid(
         aContext?.getSystemService(ACTIVITY_SERVICE) as ActivityManager
     }
     private val meminfo: ActivityManager.MemoryInfo
-        get() = ActivityManager.MemoryInfo().apply {
-            activityManager?.getMemoryInfo(this)
-        }
+        get() =
+            ActivityManager.MemoryInfo().apply {
+                activityManager?.getMemoryInfo(this)
+            }
 
     override val free: Double = meminfo.availMem.toDouble()
 
     override val total: Double = meminfo.totalMem.toDouble()
-}
+}

+ 98 - 82
composeApp/src/androidMain/kotlin/org/mjdev/desktop/main/MainView.kt

@@ -19,6 +19,7 @@ import org.mjdev.desktop.components.appsmenu.AppsMenu
 import org.mjdev.desktop.components.appsmenu.AppsMenuState.Companion.rememberAppsMenuState
 import org.mjdev.desktop.components.controlcenter.ControlCenter
 import org.mjdev.desktop.components.desktop.Desktop
+import org.mjdev.desktop.components.desktop.widgets.MemoryChart
 import org.mjdev.desktop.components.desktoppanel.DesktopPanel
 import org.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import org.mjdev.desktop.components.tooltip.TooltipState
@@ -27,99 +28,114 @@ import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContex
 import org.mjdev.desktop.extensions.Compose.isDesign
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.MutableStateExt.rememberCalculated
-import org.mjdev.desktop.components.desktop.widgets.MemoryChart
 
 @SuppressLint("ComposableNaming")
 @Composable
-fun MainView() = withDesktopContext {
-    val tooltipState: TooltipState = rememberTooltipState()
-    val appsMenuState = rememberAppsMenuState(
-        visible = isDesign
-    )
-    val controlCenterState = rememberVisibilityState(
-        visible = isDesign
-    )
-    val panelState = rememberVisibilityState(
-        visible = isDesign || !panelAutoHideEnabled,
-        enabled = panelAutoHideEnabled,
-        autoHideDelay = panelHideDelay
-    )
-    val bottomPadding by rememberCalculated(
-        panelState.enabled, panelState.height
-    ) {
-        if (panelState.enabled) 0.dp
-        else max(0.dp, panelState.height)
-    }
+fun MainView() =
+    withDesktopContext {
+        val tooltipState: TooltipState = rememberTooltipState()
+        val appsMenuState =
+            rememberAppsMenuState(
+                visible = isDesign,
+            )
+        val controlCenterState =
+            rememberVisibilityState(
+                visible = isDesign,
+            )
+        val panelState =
+            rememberVisibilityState(
+                visible = isDesign || !panelAutoHideEnabled,
+                enabled = panelAutoHideEnabled,
+                autoHideDelay = panelHideDelay,
+            )
+        val bottomPadding by rememberCalculated(
+            panelState.enabled,
+            panelState.height,
+        ) {
+            if (panelState.enabled) {
+                0.dp
+            } else {
+                max(0.dp, panelState.height)
+            }
+        }
 //    val installWindowState = rememberVisibilityState()
 //    val infoWindowState = rememberVisibilityState(false) //(api.isFirstStart || api.isDebug) // todo
-    val onTooltip: (item: Any?) -> Unit = { item ->
-        println("Showing tooltip : $item")
-        tooltipState.show(item)
-    }
-    // todo not showing nothing
-    Box(
-        modifier = Modifier.fillMaxSize(),
-        contentAlignment = Alignment.Center
-    ) {
-        // todo, something prevent preview
-        if (!isDesign) {
-            Desktop(
-                tooltipState = tooltipState,
+        val onTooltip: (item: Any?) -> Unit = { item ->
+            println("Showing tooltip : $item")
+            tooltipState.show(item)
+        }
+        // todo not showing nothing
+        Box(
+            modifier = Modifier.fillMaxSize(),
+            contentAlignment = Alignment.Center,
+        ) {
+            // todo, something prevent preview
+            if (!isDesign) {
+                Desktop(
+                    tooltipState = tooltipState,
+                    onTooltip = onTooltip,
+                    padding =
+                        PaddingValues(
+                            bottom = bottomPadding,
+                        ),
+                    widgets = {
+                        MemoryChart(
+                            modifier =
+                                Modifier
+                                    .size(350.dp, 300.dp)
+                                    .align(Alignment.BottomEnd),
+                        )
+                    },
+                    onLeftMouseClick = {
+                        runAsync {
+                            panelState.hide()
+                            appsMenuState.hide()
+                            controlCenterState.hide()
+                        }
+                    },
+                    onRightMouseClick = {
+//                contextMenuState.show()
+                    },
+                )
+            }
+            DesktopPanel(panelState = panelState, onTooltip = onTooltip, onMenuIconClicked = {
+                runAsync {
+                    appsMenuState.show()
+                }
+            })
+            ControlCenter(
+                modifier =
+                    Modifier
+                        .fillMaxHeight()
+                        .align(Alignment.TopEnd),
+                controlCenterState = controlCenterState,
                 onTooltip = onTooltip,
-                padding = PaddingValues(
-                    bottom = bottomPadding
-                ),
-                widgets = {
-                    MemoryChart(
-                        modifier = Modifier
-                            .size(350.dp, 300.dp)
-                            .align(Alignment.BottomEnd)
-                    )
-                }, onLeftMouseClick = {
+            )
+            AppsMenu(
+                modifier =
+                    Modifier
+                        .wrapContentSize()
+                        .align(Alignment.BottomStart),
+                appsMenuState = appsMenuState,
+                panelState = panelState,
+                onAppClick = { app ->
                     runAsync {
-                        panelState.hide()
+                        app.start()
                         appsMenuState.hide()
-                        controlCenterState.hide()
                     }
-                }, onRightMouseClick = {
-//                contextMenuState.show()
-                })
+                },
+                onAppContextMenuClick = {},
+                onCategoryContextMenuClick = {},
+                onUserAvatarClick = {},
+                onActionClick = {},
+                onTooltip = onTooltip,
+            )
         }
-        DesktopPanel(panelState = panelState, onTooltip = onTooltip, onMenuIconClicked = {
-            runAsync {
-                appsMenuState.show()
-            }
-        })
-        ControlCenter(
-            modifier = Modifier
-                .fillMaxHeight()
-                .align(Alignment.TopEnd),
-            controlCenterState = controlCenterState,
-            onTooltip = onTooltip,
-        )
-        AppsMenu(
-            modifier = Modifier
-                .wrapContentSize()
-                .align(Alignment.BottomStart),
-            appsMenuState = appsMenuState,
-            panelState = panelState,
-            onAppClick = { app ->
-                runAsync {
-                    app.start()
-                    appsMenuState.hide()
-                }
-            },
-            onAppContextMenuClick = {},
-            onCategoryContextMenuClick = {},
-            onUserAvatarClick = {},
-            onActionClick = {},
-            onTooltip = onTooltip,
-        )
     }
-}
 
 @Preview(device = Devices.TABLET)
 @Composable
-fun PreviewMainView() = preview {
-    MainView()
-}
+fun PreviewMainView() =
+    preview {
+        MainView()
+    }

+ 10 - 7
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/tts/TTSPluginAndroid.kt

@@ -8,27 +8,30 @@
 
 package org.mjdev.desktop.managers.ai.tts
 
-import org.mjdev.desktop.log.Log
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import nl.marc_apps.tts.TextToSpeechEngine
 import nl.marc_apps.tts.TextToSpeechFactory
 import org.mjdev.desktop.context.DesktopContext
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.log.Log
 
 @Suppress("unused")
 class TTSPluginAndroid(
     context: IDesktopContext,
 ) : TTSPluginMain(context, {
-    TextToSpeechFactory(
-        (context as? DesktopContext)?.androidContext!!,
-        TextToSpeechEngine.SystemDefault
-    ).createOrNull()
-}) {
+        TextToSpeechFactory(
+            (context as? DesktopContext)?.androidContext!!,
+            TextToSpeechEngine.SystemDefault,
+        ).createOrNull()
+    }) {
     // todo
     override val isPresent: Boolean = true
 
-    override fun talk(text: String, clearQueue: Boolean) {
+    override fun talk(
+        text: String,
+        clearQueue: Boolean,
+    ) {
         Log.i("talking: $text")
         context.scope.launch(Dispatchers.Default) {
             textToSpeech.await()?.say(text, clearQueue)

+ 33 - 26
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/apps/AppsManager.kt

@@ -16,19 +16,19 @@ import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.flow.firstOrNull
 import kotlinx.coroutines.runBlocking
 import org.mjdev.desktop.context.DesktopContext
+import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.data.App
 import org.mjdev.desktop.data.Category
 import org.mjdev.desktop.extensions.Compose.addIfNotExists
 import org.mjdev.desktop.extensions.MutableStateExt.mutableStateListFlow
 import org.mjdev.desktop.interfaces.IApp
-import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.interfaces.ILocale
 import java.util.Locale
 
 @Suppress("unused", "MemberVisibilityCanBePrivate", "PropertyName")
 class AppsManager(
     private val context: IDesktopContext,
-    private val scope: CoroutineScope = context.scope
+    private val scope: CoroutineScope = context.scope,
 ) : IAppsManager {
     companion object {
         val TAG = AppsManager::class.simpleName
@@ -39,43 +39,50 @@ class AppsManager(
     private val userManger = androidContext?.getSystemService(Context.USER_SERVICE) as UserManager
 
     private val launcherApps
-        get() = runCatching {
-            androidContext?.getSystemService(LauncherApps::class.java)
-        }.getOrNull()
+        get() =
+            runCatching {
+                androidContext?.getSystemService(LauncherApps::class.java)
+            }.getOrNull()
 
     val userHandles: List<UserHandle>?
-        get() = runCatching {
-            userManger.userProfiles
-        }.getOrNull()
+        get() =
+            runCatching {
+                userManger.userProfiles
+            }.getOrNull()
 
     override val currentLocale: ILocale
         get() = Locale.getDefault().toILocale()
 
-    private val allAppsState = mutableStateListFlow {
-        mutableListOf<IApp>().apply {
-            userHandles?.forEach { handle ->
-                launcherApps?.getActivityList(null, handle)?.map { aInfo ->
-                    App(androidContext, aInfo)
-                }?.also { apps ->
-                    addAll(apps)
+    private val allAppsState =
+        mutableStateListFlow {
+            mutableListOf<IApp>().apply {
+                userHandles?.forEach { handle ->
+                    launcherApps
+                        ?.getActivityList(null, handle)
+                        ?.map { aInfo ->
+                            App(androidContext, aInfo)
+                        }?.also { apps ->
+                            addAll(apps)
+                        }
                 }
             }
         }
-    }
 
     override val allApps: List<IApp>
-        get() = runBlocking {
-            allAppsState.firstOrNull() ?: emptyList()
-        }
+        get() =
+            runBlocking {
+                allAppsState.firstOrNull() ?: emptyList()
+            }
 
     override val categories: List<Category>
-        get() = allApps
-            .asSequence()
-            .flatMap { app -> app.categories }
-            .distinct()
-            .toList()
-            .sortedBy { c -> c.name }
-            .sortedByDescending { c -> c.priority }
+        get() =
+            allApps
+                .asSequence()
+                .flatMap { app -> app.categories }
+                .distinct()
+                .toList()
+                .sortedBy { c -> c.name }
+                .sortedByDescending { c -> c.priority }
 
     override val favoriteApps: MutableList<IApp> = mutableListOf()
 

+ 4 - 5
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManager.kt

@@ -3,10 +3,9 @@ package org.mjdev.desktop.managers.connectivity
 import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("FunctionName")
-fun ConnectivityManager(
-    context: IDesktopContext
-) = //when (hostOs) {
+fun ConnectivityManager(context: IDesktopContext) =
+    // when (hostOs) {
 // todo other platforms
-    //else ->
+    // else ->
     ConnectivityManagerStub(context)
-//}
+// }

+ 8 - 10
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManagerStub.kt

@@ -8,14 +8,14 @@
 
 package org.mjdev.desktop.managers.connectivity
 
-import org.mjdev.desktop.data.EthNetwork
-import org.mjdev.desktop.data.WifiNetwork
+import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.data.BthDevice
+import org.mjdev.desktop.data.EthNetwork
 import org.mjdev.desktop.data.NetDevice
-import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.data.WifiNetwork
 
 open class ConnectivityManagerStub(
-    private val context: IDesktopContext
+    private val context: IDesktopContext,
 ) : IConnectivityManager {
     override val allDevices: MutableMap<String, NetDevice> = mutableMapOf()
 
@@ -45,14 +45,12 @@ open class ConnectivityManagerStub(
     override val hasConnectedDevices
         get() = connectedDevices.isNotEmpty()
 
-    override fun connectWifi(
-        ssid: String
-    ) : Result<Boolean> = Result.success(false)
+    override fun connectWifi(ssid: String): Result<Boolean> = Result.success(false)
 
     override fun connectWifi(
         ssid: String,
         password: String,
         deviceName: String,
-        store: Boolean
-    ) : Result<Boolean> = Result.success(false)
-}
+        store: Boolean,
+    ): Result<Boolean> = Result.success(false)
+}

+ 6 - 5
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/keys/KeysManager.kt

@@ -1,14 +1,15 @@
 package org.mjdev.desktop.managers.keys
 
 import kotlinx.coroutines.CoroutineScope
-import org.mjdev.desktop.helpers.streams.ResourceStream
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.helpers.streams.ResourceStream
 
 class KeysManager(
     val context: IDesktopContext,
-    val scope: CoroutineScope = context.scope
+    val scope: CoroutineScope = context.scope,
 ) : IKeyManager {
-    override fun loadKey(key: String): String = runCatching {
-        ResourceStream("keys/$key.key").text
-    }.getOrNull().orEmpty()
+    override fun loadKey(key: String): String =
+        runCatching {
+            ResourceStream("keys/$key.key").text
+        }.getOrNull().orEmpty()
 }

+ 6 - 7
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/OSManager.kt

@@ -8,15 +8,14 @@
 
 package org.mjdev.desktop.managers.os
 
-import org.mjdev.desktop.managers.os.base.OSManagerStub
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.managers.os.base.OSManagerStub
 
 @Suppress("FunctionName")
-fun OsManager(
-    context: IDesktopContext
-) = //when (hostOs) {
+fun OsManager(context: IDesktopContext) =
+    // when (hostOs) {
 // todo other platforms
-//OS.Linux -> OSManagerLinux(context)
-    //else ->
+// OS.Linux -> OSManagerLinux(context)
+    // else ->
     OSManagerStub(context)
-//}
+// }

+ 2 - 2
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/processes/ProcessManager.kt

@@ -18,7 +18,7 @@ import org.mjdev.desktop.managers.process.IProcessManager
 @Suppress("unused")
 class ProcessManager(
     private val context: IDesktopContext,
-    private val checkDelay: Long = 1000L
+    private val checkDelay: Long = 1000L,
 ) : IProcessManager {
     private val listeners = mutableListOf<IProcessListener>()
 
@@ -38,4 +38,4 @@ class ProcessManager(
 
     // todo
     override fun hasAppProcess(app: IApp?): Boolean = false
-}
+}

+ 5 - 6
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/ThemeManager.kt

@@ -8,15 +8,14 @@
 
 package org.mjdev.desktop.managers.theme
 
-import org.mjdev.desktop.managers.theme.base.ThemeManagerStub
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.managers.theme.base.ThemeManagerStub
 
 // todo other platforms
 @Suppress("FunctionName")
-fun ThemeManager(
-    context: IDesktopContext
-) = //when (hostOs) {
+fun ThemeManager(context: IDesktopContext) =
+    // when (hostOs) {
 //    OS.Linux -> ThemeManagerLinux(context)
 //    else ->
-        ThemeManagerStub(context)
-//}
+    ThemeManagerStub(context)
+// }

+ 57 - 71
composeApp/src/androidMain/kotlin/org/mjdev/desktop/system/Filesystem.android.kt

@@ -13,117 +13,103 @@ import java.nio.charset.Charset
 
 @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
 actual object Filesystem : FileSystem() {
-    override fun appendingSink(file: Path, mustExist: Boolean): Sink {
-        return SYSTEM.appendingSink(file, mustExist)
-    }
-
-    override fun atomicMove(source: Path, target: Path) {
+    override fun appendingSink(
+        file: Path,
+        mustExist: Boolean,
+    ): Sink = SYSTEM.appendingSink(file, mustExist)
+
+    override fun atomicMove(
+        source: Path,
+        target: Path,
+    ) {
         SYSTEM.atomicMove(source, target)
     }
 
-    override fun canonicalize(path: Path): Path {
-        return SYSTEM.canonicalize(path)
-    }
+    override fun canonicalize(path: Path): Path = SYSTEM.canonicalize(path)
 
-    override fun createDirectory(dir: Path, mustCreate: Boolean) {
-        return SYSTEM.createDirectory(dir, mustCreate)
-    }
+    override fun createDirectory(
+        dir: Path,
+        mustCreate: Boolean,
+    ) = SYSTEM.createDirectory(dir, mustCreate)
 
-    override fun createSymlink(source: Path, target: Path) {
-        return SYSTEM.createSymlink(source, target)
-    }
+    override fun createSymlink(
+        source: Path,
+        target: Path,
+    ) = SYSTEM.createSymlink(source, target)
 
-    override fun delete(path: Path, mustExist: Boolean) {
-        return SYSTEM.delete(path, mustExist)
-    }
+    override fun delete(
+        path: Path,
+        mustExist: Boolean,
+    ) = SYSTEM.delete(path, mustExist)
 
-    override fun list(dir: Path): List<Path> {
-        return SYSTEM.list(dir)
-    }
+    override fun list(dir: Path): List<Path> = SYSTEM.list(dir)
 
-    override fun listOrNull(dir: Path): List<Path>? {
-        return SYSTEM.listOrNull(dir)
-    }
+    override fun listOrNull(dir: Path): List<Path>? = SYSTEM.listOrNull(dir)
 
-    override fun metadataOrNull(path: Path): FileMetadata? {
-        return SYSTEM.metadataOrNull(path)
-    }
+    override fun metadataOrNull(path: Path): FileMetadata? = SYSTEM.metadataOrNull(path)
 
-    override fun openReadOnly(file: Path): FileHandle {
-        return SYSTEM.openReadOnly(file)
-    }
+    override fun openReadOnly(file: Path): FileHandle = SYSTEM.openReadOnly(file)
 
-    override fun openReadWrite(file: Path, mustCreate: Boolean, mustExist: Boolean): FileHandle {
-        return SYSTEM.openReadWrite(file, mustCreate, mustExist)
-    }
+    override fun openReadWrite(
+        file: Path,
+        mustCreate: Boolean,
+        mustExist: Boolean,
+    ): FileHandle = SYSTEM.openReadWrite(file, mustCreate, mustExist)
 
-    override fun sink(file: Path, mustCreate: Boolean): Sink {
-        return SYSTEM.sink(file, mustCreate)
-    }
+    override fun sink(
+        file: Path,
+        mustCreate: Boolean,
+    ): Sink = SYSTEM.sink(file, mustCreate)
 
-    override fun source(file: Path): Source {
-        return SYSTEM.source(file)
-    }
+    override fun source(file: Path): Source = SYSTEM.source(file)
 
-    actual fun absolutePath(path: Path): String {
-        return path.toFile().absolutePath
-    }
+    actual fun absolutePath(path: Path): String = path.toFile().absolutePath
 
-    actual fun extension(path: Path): String {
-        return path.toFile().extension
-    }
+    actual fun extension(path: Path): String = path.toFile().extension
 
-    actual fun isDirectory(path: Path): Boolean {
-        return path.toFile().isDirectory
-    }
+    actual fun isDirectory(path: Path): Boolean = path.toFile().isDirectory
 
-    actual fun isFile(path: Path): Boolean {
-        return path.toFile().isFile
-    }
+    actual fun isFile(path: Path): Boolean = path.toFile().isFile
 
     actual fun isSymbolicLink(path: Path): Boolean {
         return false // todo
     }
 
-    actual fun cwd(): Path {
-        return File(".").absolutePath.toPath(normalize = true)
-    }
+    actual fun cwd(): Path = File(".").absolutePath.toPath(normalize = true)
 
-    actual fun readBytes(path: Path): ByteArray {
-        return SYSTEM.source(path).buffer().readByteArray()
-    }
+    actual fun readBytes(path: Path): ByteArray = SYSTEM.source(path).buffer().readByteArray()
 
-    actual fun readText(path: Path): String {
-        return SYSTEM.source(path).buffer().readString(Charset.forName("UTF-8"))
-    }
+    actual fun readText(path: Path): String = SYSTEM.source(path).buffer().readString(Charset.forName("UTF-8"))
 
-    actual fun readLines(path: Path): List<String> {
-        return readText(path)
+    actual fun readLines(path: Path): List<String> =
+        readText(path)
             .replace("\r", "")
             .split("\n")
-    }
 
-    actual fun writeText(path: Path, text: String) {
-        SYSTEM.sink(path).buffer().write(text.encodeToByteArray()).flush()
+    actual fun writeText(
+        path: Path,
+        text: String,
+    ) {
+        SYSTEM
+            .sink(path)
+            .buffer()
+            .write(text.encodeToByteArray())
+            .flush()
     }
 
     actual fun createNewFile(path: Path) {
         path.toFile().createNewFile()
     }
 
-    actual fun fileExists(path: Path): Boolean {
-        return SYSTEM.exists(path)
-    }
+    actual fun fileExists(path: Path): Boolean = SYSTEM.exists(path)
 
     actual fun createDir(path: Path) {
         SYSTEM.createDirectory(path)
     }
 
-    actual fun listFiles(path: Path): List<Path> {
-        return listOrNull(path) ?: emptyList()
-    }
+    actual fun listFiles(path: Path): List<Path> = listOrNull(path) ?: emptyList()
 
     actual fun deleteDir(path: Path) {
         SYSTEM.deleteRecursively(path)
     }
-}
+}

+ 1 - 1
composeApp/src/androidMain/kotlin/org/mjdev/desktop/wayland/WaylandDisplay.android.kt

@@ -2,4 +2,4 @@ actual class WaylandDisplay actual constructor()
 
 actual fun WaylandDisplay.connect(): Boolean {
     TODO("Not yet implemented")
-}
+}

+ 11 - 10
composeApp/src/androidMain/kotlin/org/mjdev/desktop/windows/ChromeWindow.kt

@@ -18,19 +18,20 @@ import org.mjdev.desktop.context.IDesktopContext
 @Composable
 fun ChromeWindow(
     modifier: Modifier = Modifier,
-    content: @Composable ChromeWindowState.() -> Unit
+    content: @Composable ChromeWindowState.() -> Unit,
 ) = withDesktopContext {
     val windowState = ChromeWindowState(context)
     Box(
-        modifier = modifier
-            .alpha(0f)
-            .onPlaced { coords ->
-                windowState.onPlaced(coords)
-            },
-        content = { content(windowState) }
+        modifier =
+            modifier
+                .alpha(0f)
+                .onPlaced { coords ->
+                    windowState.onPlaced(coords)
+                },
+        content = { content(windowState) },
     )
     ChromeWindowInternal(
-        state = windowState
+        state = windowState,
     ) {
         content(windowState)
     }
@@ -39,7 +40,7 @@ fun ChromeWindow(
 @Composable
 fun ChromeWindowInternal(
     state: ChromeWindowState,
-    content: @Composable () -> Unit
+    content: @Composable () -> Unit,
 ) {
     // todo
     content()
@@ -55,4 +56,4 @@ class ChromeWindowState(
         position = coords.positionOnScreen().let { pos -> DpOffset(pos.x.dp, pos.y.dp) }
         size = coords.size.let { size -> DpSize(size.width.dp, size.height.dp) }
     }
-}
+}

+ 59 - 56
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/adb/AdbScreenMirror.kt

@@ -1,6 +1,5 @@
 package org.mjdev.desktop.components.adb
 
-import androidx.compose.runtime.mutableStateOf
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.gestures.detectTapGestures
 import androidx.compose.foundation.layout.Box
@@ -8,12 +7,13 @@ import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.material3.CircularProgressIndicator
 import androidx.compose.material3.Text
 import androidx.compose.runtime.*
+import androidx.compose.runtime.mutableStateOf
 import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.ImageBitmap
 import androidx.compose.ui.input.pointer.pointerInput
-import dadb.Dadb
-import androidx.compose.ui.Modifier
 import com.sun.jna.Platform.isAndroid
+import dadb.Dadb
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.delay
@@ -28,17 +28,18 @@ import org.mjdev.desktop.extensions.decodeImage
 fun AdbScreenMirror(
     modifier: Modifier = Modifier,
     visibilityState: MutableState<Boolean> = mutableStateOf(false),
-    scope : CoroutineScope = rememberCoroutineScope(),
-    refreshInterval : Long  = 100L,
-    deviceState : DeviceState = rememberDeviceState(
-        refreshInterval = refreshInterval,
-        scope = scope
-    )
-)  = withDesktopContext {
-    if(isAndroid()) return@withDesktopContext
+    scope: CoroutineScope = rememberCoroutineScope(),
+    refreshInterval: Long = 100L,
+    deviceState: DeviceState =
+        rememberDeviceState(
+            refreshInterval = refreshInterval,
+            scope = scope,
+        ),
+) = withDesktopContext {
+    if (isAndroid()) return@withDesktopContext
     Box(
         modifier = modifier,
-        contentAlignment = Alignment.Center
+        contentAlignment = Alignment.Center,
     ) {
         when {
             deviceState.isConnecting -> CircularProgressIndicator()
@@ -48,18 +49,19 @@ fun AdbScreenMirror(
                 Image(
                     bitmap = currentImage,
                     contentDescription = "Device screen",
-                    modifier = Modifier
-                        .fillMaxSize()
-                        .pointerInput(deviceState.dadbInstance) {
-                            detectTapGestures { offset ->
-                                deviceState.tapOnDevice(
-                                    offset.x.toInt(),
-                                    offset.y.toInt(),
-                                    size.width,
-                                    size.height
-                                )
-                            }
-                        }
+                    modifier =
+                        Modifier
+                            .fillMaxSize()
+                            .pointerInput(deviceState.dadbInstance) {
+                                detectTapGestures { offset ->
+                                    deviceState.tapOnDevice(
+                                        offset.x.toInt(),
+                                        offset.y.toInt(),
+                                        size.width,
+                                        size.height,
+                                    )
+                                }
+                            },
                 )
             }
 
@@ -75,7 +77,7 @@ fun AdbScreenMirror(
 }
 
 class DeviceState(
-    private val scope:CoroutineScope,
+    private val scope: CoroutineScope,
     private val refreshInterval: Long,
 ) {
     var imageBitmap by mutableStateOf<ImageBitmap?>(null)
@@ -87,39 +89,40 @@ class DeviceState(
     private var job: Job? = null
 
     fun start() {
-        job = scope.launch {
-            try {
-                val dadb = Dadb.discover()
-                if (dadb == null) {
-                    error = "No device found"
+        job =
+            scope.launch {
+                try {
+                    val dadb = Dadb.discover()
+                    if (dadb == null) {
+                        error = "No device found"
+                        isConnecting = false
+                        return@launch
+                    }
+                    dadbInstance = dadb
                     isConnecting = false
-                    return@launch
-                }
-                dadbInstance = dadb
-                isConnecting = false
-                while (job?.isActive == true) {
-                    try {
-                        val stream = dadb.open("exec:screencap -p")
-                        val bytes = stream.source.readByteArray()
-                        stream.close()
-                        if (bytes.isNotEmpty()) {
-                            val decoded = bytes.decodeImage()
-                            if (decoded != null) {
-                                imageBitmap = decoded.bitmap
-                                screenWidth = decoded.width
-                                screenHeight = decoded.height
+                    while (job?.isActive == true) {
+                        try {
+                            val stream = dadb.open("exec:screencap -p")
+                            val bytes = stream.source.readByteArray()
+                            stream.close()
+                            if (bytes.isNotEmpty()) {
+                                val decoded = bytes.decodeImage()
+                                if (decoded != null) {
+                                    imageBitmap = decoded.bitmap
+                                    screenWidth = decoded.width
+                                    screenHeight = decoded.height
+                                }
                             }
+                        } catch (e: Exception) {
+                            error = e.message
                         }
-                    } catch (e: Exception) {
-                        error = e.message
+                        delay(refreshInterval)
                     }
-                    delay(refreshInterval)
+                } catch (e: Exception) {
+                    error = e.message
+                    isConnecting = false
                 }
-            } catch (e: Exception) {
-                error = e.message
-                isConnecting = false
             }
-        }
     }
 
     fun stop() {
@@ -131,7 +134,7 @@ class DeviceState(
         offsetX: Int,
         offsetY: Int,
         sizeWidth: Int,
-        sizeHeight: Int
+        sizeHeight: Int,
     ) {
         dadbInstance?.let { dadb ->
             scope.launch {
@@ -151,10 +154,10 @@ class DeviceState(
     companion object {
         @Composable
         fun rememberDeviceState(
-            refreshInterval : Long  = 100L,
-            scope : CoroutineScope = rememberCoroutineScope()
+            refreshInterval: Long = 100L,
+            scope: CoroutineScope = rememberCoroutineScope(),
         ) = remember {
-            DeviceState(scope,  refreshInterval)
+            DeviceState(scope, refreshInterval)
         }
     }
 }

+ 208 - 162
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appbar/AppBar.kt

@@ -30,10 +30,10 @@ import androidx.compose.ui.UiComposable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.platform.debugInspectorInfo
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.Modifier.conditional
 import org.mjdev.desktop.helpers.compose.Orientation
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo alignment scopes
 @Composable
@@ -61,180 +61,226 @@ fun AppBar(
         println("Recomposition AppBar")
     }
     when (orientation) {
-        Orientation.Horizontal -> Row(
-            modifier = modifier.wrapContentHeight()
-                .padding(contentPadding),
-            verticalAlignment = contentAlignment,
-        ) {
-            Row(
-                modifier = Modifier.wrapContentHeight()
-                    .padding(iconPadding),
-                verticalAlignment = iconVerticalAlignment,
-                horizontalArrangement = iconHorizontalArrangement,
-                content = icon
-            )
-            Row(
-                modifier = Modifier
-                    .conditional(fillCenter) {
-                        weight(1f)
-                    }
-                    .wrapContentHeight()
-                    .padding(titlePadding),
-                verticalAlignment = titleVerticalAlignment,
-                horizontalArrangement = titleHorizontalArrangement,
-                content = title
-            )
+        Orientation.Horizontal ->
             Row(
-                modifier = Modifier.wrapContentHeight()
-                    .padding(actionsPadding),
-                verticalAlignment = actionsVerticalAlignment,
-                horizontalArrangement = actionsHorizontalArrangement,
-                content = actions
-            )
-        }
+                modifier =
+                    modifier
+                        .wrapContentHeight()
+                        .padding(contentPadding),
+                verticalAlignment = contentAlignment,
+            ) {
+                Row(
+                    modifier =
+                        Modifier
+                            .wrapContentHeight()
+                            .padding(iconPadding),
+                    verticalAlignment = iconVerticalAlignment,
+                    horizontalArrangement = iconHorizontalArrangement,
+                    content = icon,
+                )
+                Row(
+                    modifier =
+                        Modifier
+                            .conditional(fillCenter) {
+                                weight(1f)
+                            }.wrapContentHeight()
+                            .padding(titlePadding),
+                    verticalAlignment = titleVerticalAlignment,
+                    horizontalArrangement = titleHorizontalArrangement,
+                    content = title,
+                )
+                Row(
+                    modifier =
+                        Modifier
+                            .wrapContentHeight()
+                            .padding(actionsPadding),
+                    verticalAlignment = actionsVerticalAlignment,
+                    horizontalArrangement = actionsHorizontalArrangement,
+                    content = actions,
+                )
+            }
 
-        Orientation.Vertical -> Column(
-            modifier = modifier
-                .width(IntrinsicSize.Min)
-                .padding(contentPadding),
-        ) {
-            Row(
-                modifier = Modifier.width(IntrinsicSize.Min)
-                    .padding(iconPadding),
-                verticalAlignment = iconVerticalAlignment,
-                content = icon
-            )
-            Row(
-                modifier = Modifier
-                    .conditional(fillCenter) {
-                        weight(1f)
-                    }
-                    .wrapContentHeight()
-                    .padding(titlePadding),
-                verticalAlignment = titleVerticalAlignment,
-                content = title
-            )
-            Row(
-                modifier = Modifier.width(IntrinsicSize.Min)
-                    .padding(actionsPadding),
-                verticalAlignment = actionsVerticalAlignment,
-                content = actions
-            )
-        }
+        Orientation.Vertical ->
+            Column(
+                modifier =
+                    modifier
+                        .width(IntrinsicSize.Min)
+                        .padding(contentPadding),
+            ) {
+                Row(
+                    modifier =
+                        Modifier
+                            .width(IntrinsicSize.Min)
+                            .padding(iconPadding),
+                    verticalAlignment = iconVerticalAlignment,
+                    content = icon,
+                )
+                Row(
+                    modifier =
+                        Modifier
+                            .conditional(fillCenter) {
+                                weight(1f)
+                            }.wrapContentHeight()
+                            .padding(titlePadding),
+                    verticalAlignment = titleVerticalAlignment,
+                    content = title,
+                )
+                Row(
+                    modifier =
+                        Modifier
+                            .width(IntrinsicSize.Min)
+                            .padding(actionsPadding),
+                    verticalAlignment = actionsVerticalAlignment,
+                    content = actions,
+                )
+            }
     }
 }
 
 @Preview
 @Composable
-fun PreviewAppBarHorizontal() = preview {
-    AppBar(
-        modifier = Modifier.height(48.dp)
-            .background(Color.Green),
-        orientation = Orientation.Horizontal,
-        fillCenter = false,
-        icon = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Red)
-            )
-        },
-        title = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Blue)
-            )
-        },
-        actions = {
-            Box(
-                modifier = Modifier.size(24.dp, 24.dp)
-                    .background(Color.Yellow)
-            )
-        }
-    )
-}
+fun PreviewAppBarHorizontal() =
+    preview {
+        AppBar(
+            modifier =
+                Modifier
+                    .height(48.dp)
+                    .background(Color.Green),
+            orientation = Orientation.Horizontal,
+            fillCenter = false,
+            icon = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Red),
+                )
+            },
+            title = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Blue),
+                )
+            },
+            actions = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp, 24.dp)
+                            .background(Color.Yellow),
+                )
+            },
+        )
+    }
 
 @Preview
 @Composable
-fun PreviewAppBarHorizontalFill() = preview {
-    AppBar(
-        modifier = Modifier.height(48.dp)
-            .background(Color.Green),
-        orientation = Orientation.Horizontal,
-        fillCenter = true,
-        icon = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Red)
-            )
-        },
-        title = {
-            Box(
-                modifier = Modifier.fillMaxWidth()
-                    .height(24.dp)
-                    .background(Color.Blue)
-            )
-        },
-        actions = {
-            Box(
-                modifier = Modifier.size(24.dp, 24.dp)
-                    .background(Color.Yellow)
-            )
-        }
-    )
-}
+fun PreviewAppBarHorizontalFill() =
+    preview {
+        AppBar(
+            modifier =
+                Modifier
+                    .height(48.dp)
+                    .background(Color.Green),
+            orientation = Orientation.Horizontal,
+            fillCenter = true,
+            icon = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Red),
+                )
+            },
+            title = {
+                Box(
+                    modifier =
+                        Modifier
+                            .fillMaxWidth()
+                            .height(24.dp)
+                            .background(Color.Blue),
+                )
+            },
+            actions = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp, 24.dp)
+                            .background(Color.Yellow),
+                )
+            },
+        )
+    }
 
 @Preview
 @Composable
-fun PreviewAppBarVertical() = preview {
-    AppBar(
-        modifier = Modifier.background(Color.Green),
-        orientation = Orientation.Vertical,
-        fillCenter = false,
-        icon = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Red)
-            )
-        },
-        title = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Blue)
-            )
-        },
-        actions = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Yellow)
-            )
-        }
-    )
-}
+fun PreviewAppBarVertical() =
+    preview {
+        AppBar(
+            modifier = Modifier.background(Color.Green),
+            orientation = Orientation.Vertical,
+            fillCenter = false,
+            icon = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Red),
+                )
+            },
+            title = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Blue),
+                )
+            },
+            actions = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Yellow),
+                )
+            },
+        )
+    }
 
 @Preview
 @Composable
-fun PreviewAppBarVerticalFill() = preview {
-    AppBar(
-        modifier = Modifier.background(Color.Green),
-        orientation = Orientation.Vertical,
-        fillCenter = true,
-        icon = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Red)
-            )
-        },
-        title = {
-            Box(
-                modifier = Modifier.width(24.dp).fillMaxHeight()
-                    .background(Color.Blue)
-            )
-        },
-        actions = {
-            Box(
-                modifier = Modifier.size(24.dp)
-                    .background(Color.Yellow)
-            )
-        }
-    )
-}
+fun PreviewAppBarVerticalFill() =
+    preview {
+        AppBar(
+            modifier = Modifier.background(Color.Green),
+            orientation = Orientation.Vertical,
+            fillCenter = true,
+            icon = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Red),
+                )
+            },
+            title = {
+                Box(
+                    modifier =
+                        Modifier
+                            .width(24.dp)
+                            .fillMaxHeight()
+                            .background(Color.Blue),
+                )
+            },
+            actions = {
+                Box(
+                    modifier =
+                        Modifier
+                            .size(24.dp)
+                            .background(Color.Yellow),
+                )
+            },
+        )
+    }

+ 116 - 96
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -31,6 +31,7 @@ import androidx.compose.ui.unit.DpOffset
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Dispatchers
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.appsmenu.AppsMenuState.Companion.rememberAppsMenuState
 import org.mjdev.desktop.components.appsmenu.components.AppsBottomBar
 import org.mjdev.desktop.components.appsmenu.components.AppsList
@@ -39,23 +40,22 @@ import org.mjdev.desktop.components.sliding.SlidingPanel
 import org.mjdev.desktop.components.sliding.base.VisibilityState
 import org.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import org.mjdev.desktop.components.user.UserAvatar
+import org.mjdev.desktop.context.DesktopContextScope
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.data.Category
 import org.mjdev.desktop.extensions.Colors.alpha
 import org.mjdev.desktop.extensions.Colors.darker
 import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
-import org.mjdev.desktop.extensions.MutableStateExt.rememberState
-import org.mjdev.desktop.helpers.compose.Orientation
-import org.mjdev.desktop.helpers.shape.BarShape
-import org.mjdev.desktop.context.DesktopContextScope
-import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Compose.sortByRelevance
 import org.mjdev.desktop.extensions.Compose.trimIsNotEmpty
 import org.mjdev.desktop.extensions.LaunchedEffect.flowBlock
+import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
+import org.mjdev.desktop.extensions.MutableStateExt.rememberState
 import org.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
 import org.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
+import org.mjdev.desktop.helpers.compose.Orientation
+import org.mjdev.desktop.helpers.shape.BarShape
 import org.mjdev.desktop.interfaces.IApp
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("FunctionName")
 @Composable
@@ -74,34 +74,39 @@ fun AppsMenu(
     onCategoryContextMenuClick: DesktopContextScope.(Category) -> Unit = {},
     onUserAvatarClick: () -> Unit = {},
     onActionClick: () -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     val appCategories by flowBlock(emptyList()) { appsManager.categories }
     val items by rememberComputed(
         appsMenuState.searchTextState.value,
-        appsMenuState.categoryState.value
+        appsMenuState.categoryState.value,
     ) {
         when {
             // todo fuzzy sort, simplify
-            appsMenuState.searchTextState.trimIsNotEmpty() -> appsManager.allApps.filter { app ->
-                app.fullTextString.contains(appsMenuState.searchTextState.value, ignoreCase = true)
-            }.sortByRelevance(appsMenuState.searchTextState.value) { name }
+            appsMenuState.searchTextState.trimIsNotEmpty() ->
+                appsManager.allApps
+                    .filter { app ->
+                        app.fullTextString.contains(appsMenuState.searchTextState.value, ignoreCase = true)
+                    }.sortByRelevance(appsMenuState.searchTextState.value) { name }
 
-            appsMenuState.categoryState.value != null -> appsManager.allApps.filter { app ->
-                app.categories.any { it.name.contentEquals(appsMenuState.categoryState.value?.name) }
-            }.sortedBy { app ->
-                app.name
-            }
+            appsMenuState.categoryState.value != null ->
+                appsManager.allApps
+                    .filter { app ->
+                        app.categories.any { it.name.contentEquals(appsMenuState.categoryState.value?.name) }
+                    }.sortedBy { app ->
+                        app.name
+                    }
 
             else -> appCategories
         }
     }
     SlidingPanel(
-        modifier = modifier
-            .size(
-                appMenuMinWidth,
-                appMenuMinHeight
-            )
+        modifier =
+            modifier
+                .size(
+                    appMenuMinWidth,
+                    appMenuMinHeight,
+                )
 //            .widthIn(
 //                min = appMenuMinWidth,
 //                max = containerSize.width
@@ -110,84 +115,95 @@ fun AppsMenu(
 //                min = appMenuMinHeight,
 //                max = containerSize.height
 //            )
-            .padding(
-                bottom = panelState.height
-            ),
+                .padding(
+                    bottom = panelState.height,
+                ),
         enterAnimation = enterAnimation,
         exitAnimation = exitAnimation,
-        state = appsMenuState
+        state = appsMenuState,
     ) {
         BlurPanel(
-            modifier = Modifier.fillMaxSize()
-                .clip(RoundedCornerShape(24.dp))
+            modifier =
+                Modifier
+                    .fillMaxSize()
+                    .clip(RoundedCornerShape(24.dp)),
         )
         Box(
-            modifier = Modifier
-                .fillMaxSize()
-                .padding(menuPadding)
-                .background(backgroundColor.copy(alpha = 0.2f), RoundedCornerShape(24.dp))
-                .border(2.dp, borderColor, RoundedCornerShape(24.dp))
-                .clip(RoundedCornerShape(24.dp))
+            modifier =
+                Modifier
+                    .fillMaxSize()
+                    .padding(menuPadding)
+                    .background(backgroundColor.copy(alpha = 0.2f), RoundedCornerShape(24.dp))
+                    .border(2.dp, borderColor, RoundedCornerShape(24.dp))
+                    .clip(RoundedCornerShape(24.dp)),
         ) {
             Column(
-                modifier = Modifier.fillMaxSize()
+                modifier = Modifier.fillMaxSize(),
             ) {
                 Box(
                     modifier = Modifier.clip(RectangleShape),
-                    contentAlignment = Alignment.BottomStart
+                    contentAlignment = Alignment.BottomStart,
                 ) {
-                    val shape = BarShape(
-                        offset = 50.dp,
-                        circleRadius = 32.dp,
-                        cornerRadius = 4.dp,
-                        circleGap = 4.dp,
-                    )
-                    val brush = Brush.horizontalGradient(
-                        listOf(
-                            backgroundColor.darker(0.1f),
-                            backgroundColor.darker(0.1f),
-                            backgroundColor.darker(0.1f),
-                            backgroundColor.alpha(0.9f),
-                            backgroundColor.alpha(0.7f),
-                            backgroundColor.alpha(0.3f),
+                    val shape =
+                        BarShape(
+                            offset = 50.dp,
+                            circleRadius = 32.dp,
+                            cornerRadius = 4.dp,
+                            circleGap = 4.dp,
+                        )
+                    val brush =
+                        Brush.horizontalGradient(
+                            listOf(
+                                backgroundColor.darker(0.1f),
+                                backgroundColor.darker(0.1f),
+                                backgroundColor.darker(0.1f),
+                                backgroundColor.alpha(0.9f),
+                                backgroundColor.alpha(0.7f),
+                                backgroundColor.alpha(0.3f),
+                            ),
                         )
-                    )
                     Box(
-                        modifier = Modifier.fillMaxWidth()
-                            .height(64.dp)
-                            .background(
-                                brush = brush,
-                                shape = shape
-                            )
+                        modifier =
+                            Modifier
+                                .fillMaxWidth()
+                                .height(64.dp)
+                                .background(
+                                    brush = brush,
+                                    shape = shape,
+                                ),
                     )
                     UserAvatar(
                         avatarSize = 64.dp,
                         orientation = Orientation.Horizontal,
                         titleVerticalAlignment = Alignment.Bottom,
-                        iconPadding = PaddingValues(
-                            bottom = 16.dp
-                        ),
-                        titlePadding = PaddingValues(
-                            top = 24.dp,
-                            start = 20.dp
-                        ),
+                        iconPadding =
+                            PaddingValues(
+                                bottom = 16.dp,
+                            ),
+                        titlePadding =
+                            PaddingValues(
+                                top = 24.dp,
+                                start = 20.dp,
+                            ),
                         onUserAvatarClick = onUserAvatarClick,
-                        onTooltip = onTooltip
+                        onTooltip = onTooltip,
                     )
                 }
                 HorizontalDivider(
-                    modifier = Modifier.padding(
-                        start = 2.dp,
-                        end = 2.dp
-                    ),
+                    modifier =
+                        Modifier.padding(
+                            start = 2.dp,
+                            end = 2.dp,
+                        ),
                     color = borderColor,
-                    thickness = 2.dp
+                    thickness = 2.dp,
                 )
                 AppsList(
-                    modifier = Modifier
-                        .padding(bottom = 62.dp)
-                        .fillMaxSize()
-                        .padding(24.dp),
+                    modifier =
+                        Modifier
+                            .padding(bottom = 62.dp)
+                            .fillMaxSize()
+                            .padding(24.dp),
                     category = appsMenuState.categoryState.value,
                     listState = appsMenuState.listState,
                     onCategoryClick = { c -> appsMenuState.categoryState.value = c },
@@ -199,24 +215,27 @@ fun AppsMenu(
                 )
             }
             Column(
-                modifier = Modifier
-                    .fillMaxWidth()
-                    .wrapContentHeight()
-                    .align(Alignment.BottomStart),
+                modifier =
+                    Modifier
+                        .fillMaxWidth()
+                        .wrapContentHeight()
+                        .align(Alignment.BottomStart),
             ) {
                 HorizontalDivider(
-                    modifier = Modifier.padding(
-                        start = 2.dp,
-                        end = 2.dp
-                    ),
+                    modifier =
+                        Modifier.padding(
+                            start = 2.dp,
+                            end = 2.dp,
+                        ),
                     color = borderColor,
-                    thickness = 2.dp
+                    thickness = 2.dp,
                 )
                 AppsBottomBar(
-                    modifier = Modifier
-                        .fillMaxWidth()
-                        .wrapContentHeight()
-                        .background(backgroundColor),
+                    modifier =
+                        Modifier
+                            .fillMaxWidth()
+                            .wrapContentHeight()
+                            .background(backgroundColor),
                     onTooltip = onTooltip,
                     backButtonVisible = items.firstOrNull() is IApp,
                     searchTextState = appsMenuState.searchTextState,
@@ -226,7 +245,7 @@ fun AppsMenu(
                     },
                     onContextMenuClick = {
                         // todo : context menu
-                    }
+                    },
                 )
             }
         }
@@ -246,7 +265,7 @@ class AppsMenuState(
     val listState: LazyListState = LazyListState(),
     val categoryState: MutableState<Category?> = mutableStateOf(null),
     val searchTextState: MutableState<String> = mutableStateOf(""),
-    val position: DpOffset = DpOffset.Zero
+    val position: DpOffset = DpOffset.Zero,
 ) : VisibilityState(visible, enabled, hideDelay, scope) {
     companion object {
         @Composable
@@ -265,16 +284,17 @@ class AppsMenuState(
             scope,
             listState,
             categoryState,
-            searchTextState
+            searchTextState,
         )
     }
 }
 
 @Preview
 @Composable
-fun PreviewAppsMenu() = preview {
-    AppsMenu(
-        appsMenuState = rememberAppsMenuState(true),
-        panelState = rememberVisibilityState(true)
-    )
-}
+fun PreviewAppsMenu() =
+    preview {
+        AppsMenu(
+            appsMenuState = rememberAppsMenuState(true),
+            panelState = rememberVisibilityState(true),
+        )
+    }

+ 23 - 18
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsBottomBar.kt

@@ -12,6 +12,7 @@ import androidx.compose.ui.text.TextStyle
 import androidx.compose.ui.text.font.FontWeight
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.appbar.AppBar
 import org.mjdev.desktop.components.icon.ShapedIcon
 import org.mjdev.desktop.components.input.SearchFieldPassive
@@ -21,7 +22,6 @@ import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.MutableStateExt.clear
 import org.mjdev.desktop.extensions.MutableStateExt.rememberState
 import org.mjdev.desktop.icons.arrow.ArrowBack
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("FunctionName")
 @Composable
@@ -32,25 +32,29 @@ fun AppsBottomBar(
     onContextMenuClick: () -> Unit = {},
     onActionClick: () -> Unit = {},
     onBackClick: () -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     AppBar(
         modifier = modifier.focusable(false),
         title = {
             SearchFieldPassive(
-                modifier = Modifier.padding(
-                    start = 8.dp,
-                    end = 8.dp
-                ).fillMaxWidth().focusable(true),
+                modifier =
+                    Modifier
+                        .padding(
+                            start = 8.dp,
+                            end = 8.dp,
+                        ).fillMaxWidth()
+                        .focusable(true),
                 textState = searchTextState,
                 textColor = iconsTintColor,
                 textSize = 20.sp,
-                textStyle = TextStyle(
-                    fontWeight = FontWeight.Bold
-                ),
+                textStyle =
+                    TextStyle(
+                        fontWeight = FontWeight.Bold,
+                    ),
                 backgroundColor = Color.Transparent,
                 onClearClick = { searchTextState.clear() },
-                onTooltip = onTooltip
+                onTooltip = onTooltip,
             )
         },
         icon = {
@@ -62,22 +66,23 @@ fun AppsBottomBar(
                 iconBackgroundColor = iconsTintColor,
                 onRightClick = onContextMenuClick,
                 onClick = onBackClick,
-                onTooltip = onTooltip
+                onTooltip = onTooltip,
             )
         },
         actions = {
             AppsMenuActions(
                 onActionClick = onActionClick,
-                onTooltip = onTooltip
+                onTooltip = onTooltip,
             )
-        }
+        },
     )
 }
 
 @Preview
 @Composable
-fun PreviewAppsBottomBar() = preview {
-    AppsBottomBar(
-        modifier = Modifier.background(Color.SuperDarkGray)
-    )
-}
+fun PreviewAppsBottomBar() =
+    preview {
+        AppsBottomBar(
+            modifier = Modifier.background(Color.SuperDarkGray),
+        )
+    }

+ 30 - 26
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsList.kt

@@ -27,15 +27,15 @@ import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.unit.dp
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.launch
+import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.context.DesktopContextScope
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.data.Category
 import org.mjdev.desktop.extensions.Colors.SuperDarkGray
 import org.mjdev.desktop.extensions.Colors.alpha
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.helpers.compose.rememberForeverLazyListState
-import org.mjdev.desktop.context.DesktopContextScope
-import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.interfaces.IApp
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Composable
 fun AppsList(
@@ -52,16 +52,18 @@ fun AppsList(
     val state = if (category == null) listState else rememberLazyListState()
     var clickEnabled = remember { true }
     LazyColumn(
-        modifier = modifier.draggable(
-            orientation = Orientation.Horizontal,
-            state = rememberDraggableState { delta ->
-                runAsync {
-                    state.scrollBy(delta * 8) // todo remove magic number
-                }
-            },
-            onDragStarted = { clickEnabled = false },
-            onDragStopped = { clickEnabled = true },
-        ),
+        modifier =
+            modifier.draggable(
+                orientation = Orientation.Horizontal,
+                state =
+                    rememberDraggableState { delta ->
+                        runAsync {
+                            state.scrollBy(delta * 8) // todo remove magic number
+                        }
+                    },
+                onDragStarted = { clickEnabled = false },
+                onDragStopped = { clickEnabled = true },
+            ),
         state = state,
     ) {
         when (items.firstOrNull()) {
@@ -73,7 +75,7 @@ fun AppsList(
                         dividerColor = textColor.alpha(0.3f),
                         onClick = { if (clickEnabled) onCategoryClick(item) },
                         onContextMenuClick = { if (clickEnabled) onCategoryContextMenuClick(item) },
-                        onTooltip = onTooltip
+                        onTooltip = onTooltip,
                     )
                 }
             }
@@ -90,7 +92,7 @@ fun AppsList(
                         onContextMenuClick = {
                             onAppContextMenuClick(item)
                         },
-                        onTooltip = onTooltip
+                        onTooltip = onTooltip,
                     )
                 }
             }
@@ -100,18 +102,20 @@ fun AppsList(
 
 @Preview
 @Composable
-fun PreviewAppsList() = preview(320, 640) {
-    AppsList(
-        modifier = Modifier
-            .background(
-                Color.SuperDarkGray,
-                RoundedCornerShape(16.dp)
-            )
-            .padding(8.dp),
-        items = listOf(
+fun PreviewAppsList() =
+    preview(320, 640) {
+        AppsList(
+            modifier =
+                Modifier
+                    .background(
+                        Color.SuperDarkGray,
+                        RoundedCornerShape(16.dp),
+                    ).padding(8.dp),
+            items =
+                listOf(
 //            App.Test,
 //            App.Test,
 //            App.Test
+                ),
         )
-    )
-}
+    }

+ 15 - 13
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuActions.kt

@@ -18,6 +18,7 @@ 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 org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.icon.ShapedIcon
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Colors.SuperDarkGray
@@ -27,7 +28,6 @@ import org.mjdev.desktop.extensions.Modifier.circleBorder
 import org.mjdev.desktop.icons.system.Logout
 import org.mjdev.desktop.icons.system.PowerOff
 import org.mjdev.desktop.icons.system.RestartAlt
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("UNUSED_PARAMETER")
 @Composable
@@ -40,10 +40,10 @@ fun AppsMenuActions(
     iconLogout: ImageVector = Logout,
     modifier: Modifier = Modifier,
     onActionClick: () -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     Row(
-        modifier = modifier.wrapContentWidth()
+        modifier = modifier.wrapContentWidth(),
     ) {
         ShapedIcon(
             modifier = Modifier.circleBorder(2.dp, textColor.alpha(0.5f)),
@@ -57,7 +57,7 @@ fun AppsMenuActions(
                 runAsync {
                     context.restart()
                 }
-            }
+            },
         )
         ShapedIcon(
             modifier = Modifier.circleBorder(2.dp, textColor.alpha(0.5f)),
@@ -71,7 +71,7 @@ fun AppsMenuActions(
                 runAsync {
                     context.suspend()
                 }
-            }
+            },
         )
         ShapedIcon(
             modifier = Modifier.circleBorder(2.dp, textColor.alpha(0.5f)),
@@ -85,17 +85,19 @@ fun AppsMenuActions(
                 runAsync {
                     context.logOut()
                 }
-            }
+            },
         )
     }
 }
 
 @Preview
 @Composable
-fun PreviewAppsMenuActions() = preview {
-    AppsMenuActions(
-        modifier = Modifier
-            .background(Color.SuperDarkGray, RoundedCornerShape(16.dp))
-            .padding(8.dp),
-    )
-}
+fun PreviewAppsMenuActions() =
+    preview {
+        AppsMenuActions(
+            modifier =
+                Modifier
+                    .background(Color.SuperDarkGray, RoundedCornerShape(16.dp))
+                    .padding(8.dp),
+        )
+    }

+ 31 - 27
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuApp.kt

@@ -19,6 +19,7 @@ import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.fonticon.FontIcon
 import org.mjdev.desktop.components.text.TextAny
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
@@ -30,7 +31,6 @@ import org.mjdev.desktop.extensions.Modifier.onMousePress
 import org.mjdev.desktop.extensions.Modifier.onRightClick
 import org.mjdev.desktop.helpers.shape.DottedShape
 import org.mjdev.desktop.interfaces.IApp
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("FunctionName", "UNUSED_PARAMETER")
 @Composable
@@ -46,15 +46,16 @@ fun AppsMenuApp(
     dividerColor: Color = Color.Black,
     onContextMenuClick: () -> Unit = {},
     onClick: () -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     val appIconName = remember(app, icon) { app?.name ?: icon }
     val materialIcon = remember(appIconName) { iconSet.iconForName(appIconName) ?: 0 }
     Column(
-        modifier = modifier.onMousePress {
-            onLeftClick { onClick() }
-            onRightClick { onContextMenuClick() }
-        }
+        modifier =
+            modifier.onMousePress {
+                onLeftClick { onClick() }
+                onRightClick { onContextMenuClick() }
+            },
     ) {
         Row(
             verticalAlignment = Alignment.CenterVertically,
@@ -69,34 +70,35 @@ fun AppsMenuApp(
                 innerPadding = PaddingValues(0.dp),
             )
             Column(
-                Modifier.padding(4.dp).fillMaxWidth()
+                Modifier.padding(4.dp).fillMaxWidth(),
             ) {
                 TextAny(
                     text = (app?.name ?: app?.fullAppName.orEmpty()).ifEmpty { "-" },
                     color = textColor,
                     singleLine = true,
                     fontSize = titleTextSize,
-                    fontWeight = FontWeight.Bold
+                    fontWeight = FontWeight.Bold,
                 )
                 TextAny(
                     text = (app?.comment.orEmpty()).ifEmpty { "-" },
                     color = textColor,
                     singleLine = true,
                     fontSize = commentTextSize,
-                    fontWeight = FontWeight.Bold
+                    fontWeight = FontWeight.Bold,
                 )
             }
         }
         if (showDivider) {
             HorizontalDivider(
-                modifier = Modifier
-                    .padding(start = dividerPadding)
-                    .background(
-                        color = dividerColor,
-                        shape = DottedShape(5.dp)
-                    ),
+                modifier =
+                    Modifier
+                        .padding(start = dividerPadding)
+                        .background(
+                            color = dividerColor,
+                            shape = DottedShape(5.dp),
+                        ),
                 color = Color.Transparent,
-                thickness = 1.dp
+                thickness = 1.dp,
             )
         }
     }
@@ -104,14 +106,16 @@ fun AppsMenuApp(
 
 @Preview
 @Composable
-fun PreviewAppsMenuApp() = preview {
-    AppsMenuApp(
-        modifier = Modifier.padding(8.dp)
-            .background(
-                Color.SuperDarkGray,
-                RoundedCornerShape(16.dp)
-            )
-            .padding(8.dp),
-        app = null // App.Test
-    )
-}
+fun PreviewAppsMenuApp() =
+    preview {
+        AppsMenuApp(
+            modifier =
+                Modifier
+                    .padding(8.dp)
+                    .background(
+                        Color.SuperDarkGray,
+                        RoundedCornerShape(16.dp),
+                    ).padding(8.dp),
+            app = null, // App.Test
+        )
+    }

+ 34 - 29
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuCategory.kt

@@ -20,6 +20,7 @@ import androidx.compose.ui.unit.DpSize
 import androidx.compose.ui.unit.TextUnit
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.fonticon.FontIcon
 import org.mjdev.desktop.components.text.TextAny
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
@@ -31,7 +32,6 @@ import org.mjdev.desktop.extensions.Modifier.onLeftClick
 import org.mjdev.desktop.extensions.Modifier.onMousePress
 import org.mjdev.desktop.extensions.Modifier.onRightClick
 import org.mjdev.desktop.helpers.shape.DottedShape
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("UNUSED_PARAMETER")
 @Composable
@@ -45,20 +45,21 @@ fun AppsMenuCategory(
     dividerColor: Color = Color.Black,
     onClick: (category: Category) -> Unit = {},
     onContextMenuClick: (category: Category) -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     val categoryName = remember(category) { category.name }
     val materialIcon = remember(categoryName) { iconSet.iconForName(categoryName) ?: 0 }
     Column(
-        modifier = modifier
-            .padding(vertical = 2.dp)
-            .onMousePress {
-                onLeftClick { onClick(category) }
-                onRightClick { onContextMenuClick(category) }
-            }
+        modifier =
+            modifier
+                .padding(vertical = 2.dp)
+                .onMousePress {
+                    onLeftClick { onClick(category) }
+                    onRightClick { onContextMenuClick(category) }
+                },
     ) {
         Row(
-            verticalAlignment = Alignment.CenterVertically
+            verticalAlignment = Alignment.CenterVertically,
         ) {
             FontIcon(
                 modifier = Modifier.circleBorder(1.dp, textColor),
@@ -74,19 +75,21 @@ fun AppsMenuCategory(
                 text = category.name,
                 color = textColor,
                 fontSize = titleTextSize,
-                fontWeight = FontWeight.Bold
+                fontWeight = FontWeight.Bold,
             )
         }
         if (showDivider) {
             Divider(
-                modifier = Modifier.fillMaxWidth()
-                    .padding(start = dividerPadding)
-                    .height(1.dp)
-                    .background(
-                        color = dividerColor,
-                        shape = DottedShape(5.dp)
-                    ),
-                color = Color.Transparent
+                modifier =
+                    Modifier
+                        .fillMaxWidth()
+                        .padding(start = dividerPadding)
+                        .height(1.dp)
+                        .background(
+                            color = dividerColor,
+                            shape = DottedShape(5.dp),
+                        ),
+                color = Color.Transparent,
             )
         }
     }
@@ -94,14 +97,16 @@ fun AppsMenuCategory(
 
 @Preview
 @Composable
-fun PreviewAppsMenuCategory() = preview {
-    AppsMenuCategory(
-        modifier = Modifier.padding(8.dp)
-            .background(
-                Color.SuperDarkGray,
-                RoundedCornerShape(16.dp)
-            )
-            .padding(8.dp),
-        category = Category("Audio")
-    )
-}
+fun PreviewAppsMenuCategory() =
+    preview {
+        AppsMenuCategory(
+            modifier =
+                Modifier
+                    .padding(8.dp)
+                    .background(
+                        Color.SuperDarkGray,
+                        RoundedCornerShape(16.dp),
+                    ).padding(8.dp),
+            category = Category("Audio"),
+        )
+    }

+ 34 - 29
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt

@@ -15,6 +15,7 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color.Companion.Transparent
 import androidx.compose.ui.layout.ContentScale
 import kotlinx.coroutines.delay
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.image.ImageAny
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Colors.ContrastColorFilter
@@ -24,9 +25,8 @@ import org.mjdev.desktop.extensions.Compose.Crossfade
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
 import org.mjdev.desktop.helpers.generic.Queue
-import kotlin.math.min
-import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.log.Log
+import kotlin.math.min
 
 @Suppress("FunctionName", "CoroutineCreationDuringComposition")
 @Composable
@@ -38,10 +38,10 @@ fun BackgroundImage(
     backgroundColor: Color = Color.SuperDarkGray,
     images: MutableList<Any> = mutableListOf(),
     onError: (Throwable) -> Unit = { e -> Log.e(e) },
-    onChange: (bck: Any?) -> Unit = {}
+    onChange: (bck: Any?) -> Unit = {},
 ) = withDesktopContext {
     Box(
-        modifier = modifier.background(backgroundColor)
+        modifier = modifier.background(backgroundColor),
     ) {
         val imagesSize by rememberComputed(images.size) { images.size }
         val backgroundQueue by rememberComputed(imagesSize) { Queue(images) }
@@ -49,7 +49,7 @@ fun BackgroundImage(
         Crossfade(
             targetState = currentBackground,
             fadeInDuration = fadeInDuration,
-            fadeOutDuration = fadeOutDuration
+            fadeOutDuration = fadeOutDuration,
         ) { imageValue ->
             ImageAny(
                 modifier = modifier,
@@ -66,30 +66,34 @@ fun BackgroundImage(
                 },
                 onAnimationFinish = {
                     Log.d("Background image finished: $imageValue")
-                }
+                },
             )
             Box(
-                modifier = modifier.background(
-                    Brush.radialGradient(
-                        colors = listOf(
-                            Transparent,
-                            Transparent,
-                            Transparent,
-                            Transparent,
-                            backgroundColor.alpha(0.1f),
-                            backgroundColor.alpha(0.3f),
-                            backgroundColor
-                        ),
-                        center = Offset(
-                            containerSize.width.value / 2,
-                            containerSize.height.value / 2
+                modifier =
+                    modifier.background(
+                        Brush.radialGradient(
+                            colors =
+                                listOf(
+                                    Transparent,
+                                    Transparent,
+                                    Transparent,
+                                    Transparent,
+                                    backgroundColor.alpha(0.1f),
+                                    backgroundColor.alpha(0.3f),
+                                    backgroundColor,
+                                ),
+                            center =
+                                Offset(
+                                    containerSize.width.value / 2,
+                                    containerSize.height.value / 2,
+                                ),
+                            radius =
+                                min(
+                                    containerSize.width.value,
+                                    containerSize.height.value,
+                                ) * 1.2f,
                         ),
-                        radius = min(
-                            containerSize.width.value,
-                            containerSize.height.value
-                        ) * 1.2f
-                    )
-                )
+                    ),
             )
         }
         LaunchedEffect(imagesSize) {
@@ -109,6 +113,7 @@ fun BackgroundImage(
 @Suppress("unused")
 @Preview
 @Composable
-fun PreviewBackgroundImage() = preview {
-    BackgroundImage()
-}
+fun PreviewBackgroundImage() =
+    preview {
+        BackgroundImage()
+    }

+ 33 - 26
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanel.kt

@@ -26,11 +26,11 @@ import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import org.jetbrains.compose.resources.DrawableResource
 import org.jetbrains.compose.resources.painterResource
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Colors.alpha
 import org.mjdev.desktop.extensions.Compose.preview
-import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
-import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.resources.Res
 import org.mjdev.desktop.resources.blur4
 
@@ -43,37 +43,43 @@ fun BlurPanel(
     propagateMinConstraints: Boolean = false,
     blurRadius: Dp = 12.dp,
     alpha: Float = 0.3f,
-    content: @Composable BoxScope.() -> Unit = {}
+    content: @Composable BoxScope.() -> Unit = {},
 ) = withDesktopContext {
-    val transparentGradientBrush = Brush.linearGradient(
-        colors = listOf(
-            Color(0x66FFFFFF),
-            Color(0x1AFFFFFF)
+    val transparentGradientBrush =
+        Brush.linearGradient(
+            colors =
+                listOf(
+                    Color(0x66FFFFFF),
+                    Color(0x1AFFFFFF),
+                ),
         )
-    )
     BoxWithConstraints(
         modifier = modifier,
         contentAlignment = contentAlignment,
-        propagateMinConstraints = propagateMinConstraints
+        propagateMinConstraints = propagateMinConstraints,
     ) {
         ImageAny(
-            modifier = modifier.fillMaxSize()
-                .alpha(alpha)
-                .background(transparentGradientBrush)
-                .blur(blurRadius, edgeTreatment = BlurredEdgeTreatment.Unbounded),
+            modifier =
+                modifier
+                    .fillMaxSize()
+                    .alpha(alpha)
+                    .background(transparentGradientBrush)
+                    .blur(blurRadius, edgeTreatment = BlurredEdgeTreatment.Unbounded),
             src = painterResource(resourceId),
-            contentScale = if (maxHeight > maxWidth) ContentScale.FillHeight else ContentScale.FillWidth
+            contentScale = if (maxHeight > maxWidth) ContentScale.FillHeight else ContentScale.FillWidth,
         )
         Box(
-            modifier = Modifier.fillMaxSize()
-                .background(
-                    Brush.radialGradient(
-                        listOf(
-                            Color.Transparent,
-                            backgroundColor.alpha(alpha + 0.2f)
-                        )
-                    )
-                )
+            modifier =
+                Modifier
+                    .fillMaxSize()
+                    .background(
+                        Brush.radialGradient(
+                            listOf(
+                                Color.Transparent,
+                                backgroundColor.alpha(alpha + 0.2f),
+                            ),
+                        ),
+                    ),
         )
         content()
     }
@@ -81,6 +87,7 @@ fun BlurPanel(
 
 @Preview
 @Composable
-fun PreviewBlurPanel() = preview(480, 800) {
-    BlurPanel()
-}
+fun PreviewBlurPanel() =
+    preview(480, 800) {
+        BlurPanel()
+    }

+ 13 - 13
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanelHaze.kt

@@ -29,13 +29,14 @@ import org.mjdev.desktop.components.text.TextAny
 @Composable
 fun BlurPanelHaze(
     modifier: Modifier = Modifier,
-    hazeState: HazeState = rememberHazeState()
+    hazeState: HazeState = rememberHazeState(),
 ) {
     Box {
         LazyColumn(
-            modifier = Modifier
-                .fillMaxSize()
-                .hazeSource(state = hazeState)
+            modifier =
+                Modifier
+                    .fillMaxSize()
+                    .hazeSource(state = hazeState),
         ) {
             // todo
         }
@@ -45,15 +46,14 @@ fun BlurPanelHaze(
             title = {
                 TextAny("Test Blur Panel Haze")
             },
-            modifier = Modifier
-                // We use hazeEffect on anything where we want the background
-                // blurred.
-                .hazeEffect
-                    (
-                    state = hazeState,
+            modifier =
+                Modifier
+                    // We use hazeEffect on anything where we want the background
+                    // blurred.
+                    .hazeEffect(
+                        state = hazeState,
 //                    style = HazeMaterials.ultraThin()
-                )
-                .fillMaxWidth(),
+                    ).fillMaxWidth(),
         )
     }
 }
@@ -62,4 +62,4 @@ fun BlurPanelHaze(
 @Composable
 private fun PreviewBlurPanelHaze() {
     BlurPanelHaze()
-}
+}

+ 13 - 11
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonPrimary.kt

@@ -3,30 +3,31 @@ package org.mjdev.desktop.components.button
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.Composable
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.text.TextAny
-import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
-import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.extensions.Compose.preview
 
 @Composable
 fun ButtonPrimary(
     text: String = "Button",
     enabled: Boolean = true,
     visible: Boolean = true,
-    onClick: () -> Unit = {}
+    onClick: () -> Unit = {},
 ) = withDesktopContext {
     if (visible) {
         Button(
             onClick = onClick,
             enabled = enabled,
-            colors = ButtonDefaults.buttonColors(
-                backgroundColor = iconsTintColor,
-                disabledBackgroundColor = disabledColor
-            )
+            colors =
+                ButtonDefaults.buttonColors(
+                    backgroundColor = iconsTintColor,
+                    disabledBackgroundColor = disabledColor,
+                ),
         ) {
             TextAny(
                 text = text,
-                color = textColor
+                color = textColor,
             )
         }
     }
@@ -34,6 +35,7 @@ fun ButtonPrimary(
 
 @Preview
 @Composable
-fun PreviewButtonPrimary() = preview {
-    ButtonPrimary()
-}
+fun PreviewButtonPrimary() =
+    preview {
+        ButtonPrimary()
+    }

+ 13 - 11
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonSecondary.kt

@@ -3,10 +3,10 @@ package org.mjdev.desktop.components.button
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.Composable
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.text.TextAny
-import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
-import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.extensions.Compose.preview
 
 // todo
 @Composable
@@ -14,20 +14,21 @@ fun ButtonSecondary(
     text: String = "Button",
     enabled: Boolean = true,
     visible: Boolean = true,
-    onClick: () -> Unit = {}
+    onClick: () -> Unit = {},
 ) = withDesktopContext {
     if (visible) {
         Button(
             onClick = onClick,
             enabled = enabled,
-            colors = ButtonDefaults.buttonColors(
-                backgroundColor = backgroundColor,
-                disabledBackgroundColor = disabledColor
-            )
+            colors =
+                ButtonDefaults.buttonColors(
+                    backgroundColor = backgroundColor,
+                    disabledBackgroundColor = disabledColor,
+                ),
         ) {
             TextAny(
                 text = text,
-                color = textColor
+                color = textColor,
             )
         }
     }
@@ -35,6 +36,7 @@ fun ButtonSecondary(
 
 @Preview
 @Composable
-fun PreviewButtonSecondary() = preview {
-    ButtonSecondary()
-}
+fun PreviewButtonSecondary() =
+    preview {
+        ButtonSecondary()
+    }

+ 12 - 11
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/TransparentButton.kt

@@ -17,34 +17,35 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.extensions.ButtonDefaults.noElevation
 import org.mjdev.desktop.extensions.ButtonDefaults.transparent
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.icons.user.AccountCircle
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Composable
 fun TransparentButton(
     modifier: Modifier = Modifier,
     onClick: () -> Unit = {},
-    content: @Composable RowScope.() -> Unit = {}
+    content: @Composable RowScope.() -> Unit = {},
 ) = Button(
     modifier = modifier,
     contentPadding = PaddingValues(0.dp),
     onClick = onClick,
     colors = ButtonDefaults.transparent(),
     elevation = ButtonDefaults.noElevation(),
-    content = content
+    content = content,
 )
 
 @Preview
 @Composable
-fun PreviewTransparentButton() = preview {
-    TransparentButton {
-        Icon(
-            imageVector = AccountCircle,
-            contentDescription = "",
-            tint = Color.White
-        )
+fun PreviewTransparentButton() =
+    preview {
+        TransparentButton {
+            Icon(
+                imageVector = AccountCircle,
+                contentDescription = "",
+                tint = Color.White,
+            )
+        }
     }
-}

+ 14 - 12
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/Card.kt

@@ -16,6 +16,7 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardBorder
 import org.mjdev.desktop.components.card.base.CardColors
 import org.mjdev.desktop.components.card.base.CardDefaults
@@ -30,7 +31,6 @@ import org.mjdev.desktop.components.card.base.CardShape
 import org.mjdev.desktop.components.surface.Surface
 import org.mjdev.desktop.components.surface.base.Glow
 import org.mjdev.desktop.extensions.Compose.preview
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo, card like in android tv
 @Composable
@@ -44,7 +44,7 @@ fun Card(
     border: CardBorder = CardDefaults.border(),
     glow: CardGlow = CardDefaults.glow(),
     interactionSource: MutableInteractionSource? = null,
-    content: @Composable ColumnScope.() -> Unit = {}
+    content: @Composable ColumnScope.() -> Unit = {},
 ) = Surface(
     onClick = onClick,
     onLongClick = onLongClick,
@@ -61,13 +61,15 @@ fun Card(
 
 @Preview
 @Composable
-fun PreviewCard() = preview {
-    Card(
-        modifier = Modifier.size(200.dp, 128.dp),
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(2.dp),
-        glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp))
-    )
-}
+fun PreviewCard() =
+    preview {
+        Card(
+            modifier = Modifier.size(200.dp, 128.dp),
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(2.dp),
+            glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
+        )
+    }

+ 24 - 21
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CardContent.kt

@@ -8,34 +8,36 @@ import androidx.compose.material.Text
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.graphicsLayer
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardDefaults.DescriptionAlpha
 import org.mjdev.desktop.components.card.base.CardDefaults.SubtitleAlpha
 import org.mjdev.desktop.extensions.Compose.preview
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Composable
 fun CardContent(
     title: @Composable () -> Unit = {},
     subtitle: @Composable () -> Unit = {},
-    description: @Composable () -> Unit = {}
+    description: @Composable () -> Unit = {},
 ) = Column {
     ProvideTextStyle(MaterialTheme.typography.subtitle2) {
         title()
     }
     ProvideTextStyle(MaterialTheme.typography.body2) {
         Box(
-            modifier = Modifier.graphicsLayer {
-                alpha = SubtitleAlpha
-            }
+            modifier =
+                Modifier.graphicsLayer {
+                    alpha = SubtitleAlpha
+                },
         ) {
             subtitle()
         }
     }
     ProvideTextStyle(MaterialTheme.typography.body2) {
         Box(
-            modifier = Modifier.graphicsLayer {
-                alpha = DescriptionAlpha
-            }
+            modifier =
+                Modifier.graphicsLayer {
+                    alpha = DescriptionAlpha
+                },
         ) {
             description()
         }
@@ -44,16 +46,17 @@ fun CardContent(
 
 @Preview
 @Composable
-fun PreviewCardContent() = preview {
-    CardContent(
-        title = {
-            Text("title")
-        },
-        subtitle = {
-            Text("subtitle")
-        },
-        description = {
-            Text("description")
-        }
-    )
-}
+fun PreviewCardContent() =
+    preview {
+        CardContent(
+            title = {
+                Text("title")
+            },
+            subtitle = {
+                Text("subtitle")
+            },
+            description = {
+                Text("description")
+            },
+        )
+    }

+ 35 - 33
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ClassicCard.kt

@@ -14,13 +14,13 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.*
 import org.mjdev.desktop.components.image.ImageAny
 import org.mjdev.desktop.components.surface.base.Glow
 import org.mjdev.desktop.components.text.TextAny
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.icons.user.AccountCircle
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo
 @Composable
@@ -38,7 +38,7 @@ fun ClassicCard(
     border: CardBorder = CardDefaults.border(),
     glow: CardGlow = CardDefaults.glow(),
     contentPadding: PaddingValues = PaddingValues(),
-    interactionSource: MutableInteractionSource? = null
+    interactionSource: MutableInteractionSource? = null,
 ) = Card(
     onClick = onClick,
     onLongClick = onLongClick,
@@ -48,20 +48,20 @@ fun ClassicCard(
     colors = colors,
     scale = scale,
     border = border,
-    glow = glow
+    glow = glow,
 ) {
     Column(
-        modifier = Modifier.padding(contentPadding)
+        modifier = Modifier.padding(contentPadding),
     ) {
         Box(
             contentAlignment = CardDefaults.ContentImageAlignment,
-            content = image
+            content = image,
         )
         Column {
             CardContent(
                 title = title,
                 subtitle = subtitle,
-                description = description
+                description = description,
             )
         }
     }
@@ -69,30 +69,32 @@ fun ClassicCard(
 
 @Preview
 @Composable
-fun PreviewClassicCard() = preview {
-    ClassicCard(
-        modifier = Modifier.size(320.dp, 200.dp),
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(2.dp),
-        glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
-        contentPadding = PaddingValues(8.dp),
-        scale = CardDefaults.scale(1f),
-        image = {
-            ImageAny(
-                src = AccountCircle,
-                contentDescription = ""
-            )
-        },
-        title = {
-            TextAny("title")
-        },
-        subtitle = {
-            TextAny("subtitle")
-        },
-        description = {
-            TextAny("description")
-        }
-    )
-}
+fun PreviewClassicCard() =
+    preview {
+        ClassicCard(
+            modifier = Modifier.size(320.dp, 200.dp),
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(2.dp),
+            glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
+            contentPadding = PaddingValues(8.dp),
+            scale = CardDefaults.scale(1f),
+            image = {
+                ImageAny(
+                    src = AccountCircle,
+                    contentDescription = "",
+                )
+            },
+            title = {
+                TextAny("title")
+            },
+            subtitle = {
+                TextAny("subtitle")
+            },
+            description = {
+                TextAny("description")
+            },
+        )
+    }

+ 43 - 40
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CompactCard.kt

@@ -21,6 +21,7 @@ import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardBorder
 import org.mjdev.desktop.components.card.base.CardColors
 import org.mjdev.desktop.components.card.base.CardDefaults
@@ -32,7 +33,6 @@ import org.mjdev.desktop.components.surface.base.Glow
 import org.mjdev.desktop.components.text.TextAny
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.icons.user.AccountCircle
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo
 @Composable
@@ -50,7 +50,7 @@ fun CompactCard(
     border: CardBorder = CardDefaults.border(),
     glow: CardGlow = CardDefaults.glow(),
     scrimBrush: Brush = CardDefaults.ScrimBrush,
-    interactionSource: MutableInteractionSource? = null
+    interactionSource: MutableInteractionSource? = null,
 ) = Card(
     onClick = onClick,
     onLongClick = onLongClick,
@@ -60,26 +60,27 @@ fun CompactCard(
     colors = colors,
     scale = scale,
     border = border,
-    glow = glow
+    glow = glow,
 ) {
     Box(
-        contentAlignment = Alignment.BottomStart
+        contentAlignment = Alignment.BottomStart,
     ) {
         Box(
-            modifier = Modifier.drawWithCache {
-                onDrawWithContent {
-                    drawContent()
-                    drawRect(brush = scrimBrush)
-                }
-            },
+            modifier =
+                Modifier.drawWithCache {
+                    onDrawWithContent {
+                        drawContent()
+                        drawRect(brush = scrimBrush)
+                    }
+                },
             contentAlignment = CardDefaults.ContentImageAlignment,
-            content = image
+            content = image,
         )
         Column {
             CardContent(
                 title = title,
                 subtitle = subtitle,
-                description = description
+                description = description,
             )
         }
     }
@@ -87,31 +88,33 @@ fun CompactCard(
 
 @Preview
 @Composable
-fun CompactCardPreview() = preview {
-    CompactCard(
-        modifier = Modifier.size(200.dp, 128.dp),
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(),
-        glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
-        scale = CardDefaults.scale(1f),
-        scrimBrush = CardDefaults.ScrimBrush,
-        image = {
-            ImageAny(
-                modifier = Modifier.fillMaxSize(),
-                src = AccountCircle,
-                contentDescription = ""
-            )
-        },
-        title = {
-            TextAny("title")
-        },
-        subtitle = {
-            TextAny("subtitle")
-        },
-        description = {
-            TextAny("description")
-        }
-    )
-}
+fun CompactCardPreview() =
+    preview {
+        CompactCard(
+            modifier = Modifier.size(200.dp, 128.dp),
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(),
+            glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
+            scale = CardDefaults.scale(1f),
+            scrimBrush = CardDefaults.ScrimBrush,
+            image = {
+                ImageAny(
+                    modifier = Modifier.fillMaxSize(),
+                    src = AccountCircle,
+                    contentDescription = "",
+                )
+            },
+            title = {
+                TextAny("title")
+            },
+            subtitle = {
+                TextAny("subtitle")
+            },
+            description = {
+                TextAny("description")
+            },
+        )
+    }

+ 63 - 50
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/FocusableCard.kt

@@ -28,6 +28,7 @@ import androidx.compose.ui.graphics.RectangleShape
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardBorder
 import org.mjdev.desktop.components.card.base.CardColors
 import org.mjdev.desktop.components.card.base.CardDefaults
@@ -49,7 +50,6 @@ import org.mjdev.desktop.extensions.FocusState.rememberFocusState
 import org.mjdev.desktop.extensions.FocusState.requestFocusOnTouch
 import org.mjdev.desktop.helpers.compose.FocusHelper
 import org.mjdev.desktop.icons.image.BrokenImage
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo calculate
 @Composable
@@ -70,7 +70,7 @@ fun FocusableCard(
         Image(
             imageVector = BrokenImage,
             contentDescription = "",
-            contentScale = contentScale
+            contentScale = contentScale,
         )
     },
     imageRenderer: @Composable () -> Unit = {
@@ -79,15 +79,16 @@ fun FocusableCard(
             src = item,
             contentDescription = item?.toString(),
             contentScale = contentScale,
-            placeholder = placeholder
+            placeholder = placeholder,
         )
     },
     focused: Boolean = false,
     focusRequester: FocusRequester = rememberFocusRequester(item),
-    focusState: MutableState<FocusState> = rememberFocusState(
-        item,
-        FocusHelper(focused)
-    ),
+    focusState: MutableState<FocusState> =
+        rememberFocusState(
+            item,
+            FocusHelper(focused),
+        ),
     onFocus: ((item: Any?, fromUser: Boolean) -> Unit)? = null,
     onFocusChange: (state: FocusState) -> Unit = { state ->
         if (state.isFocused || state.hasFocus) {
@@ -105,35 +106,42 @@ fun FocusableCard(
     colors = colors,
     border = border,
     glow = glow,
-    modifier = modifier.size(
-        width = cardWidth,
-        height = cardWidth / aspectRatio
-    ).focusState(
-        focusState
-    ).onFocusChanged { state ->
-        onFocusChange(state)
-    }.requestFocusOnTouch(focusRequester) {
-        if (focusState.isFocused) onClick?.invoke(item)
-    },
+    modifier =
+        modifier
+            .size(
+                width = cardWidth,
+                height = cardWidth / aspectRatio,
+            ).focusState(
+                focusState,
+            ).onFocusChanged { state ->
+                onFocusChange(state)
+            }.requestFocusOnTouch(focusRequester) {
+                if (focusState.isFocused) onClick?.invoke(item)
+            },
     image = {
         imageRenderer()
     },
     title = {
         if (showTitle) {
             Box(
-                modifier = Modifier.fillMaxWidth()
-                    .background(
-                        if (focusState.isFocused) textBackgroundSelected
-                        else textBackgroundUnselected,
-                        RectangleShape
-                    )
+                modifier =
+                    Modifier
+                        .fillMaxWidth()
+                        .background(
+                            if (focusState.isFocused) {
+                                textBackgroundSelected
+                            } else {
+                                textBackgroundUnselected
+                            },
+                            RectangleShape,
+                        ),
             ) {
                 TextAny(
                     modifier = Modifier.padding(titlePadding),
                     singleLine = true,
                     color = textColor,
                     style = MaterialTheme.typography.body1,
-                    text = item?.toString().orEmpty()
+                    text = item?.toString().orEmpty(),
                 )
             }
         }
@@ -141,21 +149,24 @@ fun FocusableCard(
     subtitle = {
         if (showTitle) {
             Box(
-                modifier = Modifier.fillMaxWidth()
-                    .background(
-                        if (focusState.isFocused)
-                            textBackgroundSelected
-                        else
-                            textBackgroundUnselected,
-                        RectangleShape
-                    )
+                modifier =
+                    Modifier
+                        .fillMaxWidth()
+                        .background(
+                            if (focusState.isFocused) {
+                                textBackgroundSelected
+                            } else {
+                                textBackgroundUnselected
+                            },
+                            RectangleShape,
+                        ),
             ) {
                 AutoHideEmptyText(
                     modifier = Modifier.padding(titlePadding),
                     singleLine = true,
                     color = textColor,
                     style = MaterialTheme.typography.body2,
-                    text = "" // todo
+                    text = "", // todo
                 )
             }
         }
@@ -170,20 +181,22 @@ fun FocusableCard(
 
 @Preview
 @Composable
-fun PreviewFocusableCard() = preview {
-    FocusableCard(
-        item = "test",
-        modifier = Modifier.size(200.dp, 128.dp),
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(),
-        glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
-        scale = CardDefaults.scale(1f),
-        contentScale = ContentScale.Crop,
-        textColor = Color.Black,
-        showTitle = true,
-        focused = true,
-        aspectRatio = HorizontalImageAspectRatio,
-    )
-}
+fun PreviewFocusableCard() =
+    preview {
+        FocusableCard(
+            item = "test",
+            modifier = Modifier.size(200.dp, 128.dp),
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(),
+            glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
+            scale = CardDefaults.scale(1f),
+            contentScale = ContentScale.Crop,
+            textColor = Color.Black,
+            showTitle = true,
+            focused = true,
+            aspectRatio = HorizontalImageAspectRatio,
+        )
+    }

+ 27 - 24
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ItemCard.kt

@@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardBorder
 import org.mjdev.desktop.components.card.base.CardColors
 import org.mjdev.desktop.components.card.base.CardDefaults
@@ -37,7 +38,6 @@ import org.mjdev.desktop.extensions.FocusState.rememberFocusRequester
 import org.mjdev.desktop.extensions.FocusState.rememberFocusState
 import org.mjdev.desktop.helpers.compose.FocusHelper
 import org.mjdev.desktop.icons.image.BrokenImage
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo calculate
 @Composable
@@ -56,7 +56,7 @@ fun ItemCard(
         Image(
             imageVector = BrokenImage,
             "",
-            modifier
+            modifier,
         )
     },
     imageRenderer: @Composable () -> Unit = {
@@ -70,10 +70,11 @@ fun ItemCard(
     },
     cardWidth: Dp = 0.dp, // computeCardWidth(),
     focused: Boolean = false,
-    focusState: MutableState<FocusState> = rememberFocusState(
-        item,
-        FocusHelper(focused)
-    ),
+    focusState: MutableState<FocusState> =
+        rememberFocusState(
+            item,
+            FocusHelper(focused),
+        ),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     titlePadding: PaddingValues = PaddingValues(8.dp),
     showTitle: Boolean = true,
@@ -99,25 +100,27 @@ fun ItemCard(
     onFocus = onFocus,
     onClick = onClick,
     showTitle = showTitle,
-    placeholder = placeholder
+    placeholder = placeholder,
 )
 
 @Preview
 @Composable
-fun PreviewItemCard() = preview {
-    ItemCard(
-        item = "test",
-        modifier = Modifier.size(200.dp, 128.dp),
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(),
-        glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
-        scale = CardDefaults.scale(1f),
-        contentScale = ContentScale.Crop,
-        textColor = Color.Black,
-        showTitle = true,
-        focused = true,
-        aspectRatio = HorizontalImageAspectRatio,
-    )
-}
+fun PreviewItemCard() =
+    preview {
+        ItemCard(
+            item = "test",
+            modifier = Modifier.size(200.dp, 128.dp),
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(),
+            glow = CardDefaults.glow(glow = Glow(Color.Green, 4.dp)),
+            scale = CardDefaults.scale(1f),
+            contentScale = ContentScale.Crop,
+            textColor = Color.Black,
+            showTitle = true,
+            focused = true,
+            aspectRatio = HorizontalImageAspectRatio,
+        )
+    }

+ 30 - 26
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/PhotoCard.kt

@@ -13,19 +13,19 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.CardDefaults
 import org.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
 import org.mjdev.desktop.components.card.base.CardScale
 import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.components.image.PhotoImage
+import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.context.LocalDesktopContext
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.FocusState.isFocused
 import org.mjdev.desktop.extensions.FocusState.rememberFocusRequester
 import org.mjdev.desktop.extensions.FocusState.rememberFocusState
 import org.mjdev.desktop.helpers.compose.FocusHelper
-import org.mjdev.desktop.components.image.PhotoImage
-import org.mjdev.desktop.context.IDesktopContext
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Composable
 fun PhotoCard(
@@ -34,10 +34,11 @@ fun PhotoCard(
     contentScale: ContentScale = ContentScale.Fit,
     textColor: Color = Color.White,
     focused: Boolean = false,
-    focusState: MutableState<FocusState> = rememberFocusState(
-        item,
-        FocusHelper(focused)
-    ),
+    focusState: MutableState<FocusState> =
+        rememberFocusState(
+            item,
+            FocusHelper(focused),
+        ),
     focusRequester: FocusRequester = rememberFocusRequester(item),
     contrast: Float = 5f,
     brightness: Float = -255f,
@@ -45,7 +46,7 @@ fun PhotoCard(
         ImageAny(
             modifier = Modifier.fillMaxSize().padding(64.dp),
             src = null,
-            contentScale = contentScale
+            contentScale = contentScale,
         )
     },
     imageRenderer: @Composable () -> Unit = {
@@ -82,29 +83,32 @@ fun PhotoCard(
     titlePadding = titlePadding,
     onFocus = onFocus,
     onClick = onClick,
-    showTitle = showTitle
+    showTitle = showTitle,
 )
 
 @Composable
 fun computeCardWidth(
     api: IDesktopContext = LocalDesktopContext.current,
-    ratio: Float = 2.5f
-): Dp {
-    return if (api.containerSize.let { it.height > it.width }) api.containerSize.width / ratio
-    else api.containerSize.height / ratio
-}
+    ratio: Float = 2.5f,
+): Dp =
+    if (api.containerSize.let { it.height > it.width }) {
+        api.containerSize.width / ratio
+    } else {
+        api.containerSize.height / ratio
+    }
 
 @Preview
 @Composable
-fun PreviewPhotoCard() = preview {
-    PhotoCard(
-        item = "test",
-        modifier = Modifier.size(200.dp, 128.dp),
-        scale = CardDefaults.scale(1f),
-        contentScale = ContentScale.Crop,
-        textColor = Color.Black,
-        showTitle = true,
-        focused = true,
-        aspectRatio = HorizontalImageAspectRatio,
-    )
-}
+fun PreviewPhotoCard() =
+    preview {
+        PhotoCard(
+            item = "test",
+            modifier = Modifier.size(200.dp, 128.dp),
+            scale = CardDefaults.scale(1f),
+            contentScale = ContentScale.Crop,
+            textColor = Color.Black,
+            showTitle = true,
+            focused = true,
+            aspectRatio = HorizontalImageAspectRatio,
+        )
+    }

+ 37 - 34
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/WideClassicCard.kt

@@ -14,13 +14,13 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.card.base.*
 import org.mjdev.desktop.components.image.ImageAny
 import org.mjdev.desktop.components.surface.base.Glow
 import org.mjdev.desktop.components.text.TextAny
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.icons.user.AccountCircle
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("unused")
 @Composable
@@ -38,7 +38,7 @@ fun WideClassicCard(
     border: CardBorder = CardDefaults.border(),
     glow: CardGlow = CardDefaults.glow(),
     contentPadding: PaddingValues = PaddingValues(),
-    interactionSource: MutableInteractionSource? = null
+    interactionSource: MutableInteractionSource? = null,
 ) = Card(
     onClick = onClick,
     onLongClick = onLongClick,
@@ -48,20 +48,20 @@ fun WideClassicCard(
     colors = colors,
     scale = scale,
     border = border,
-    glow = glow
+    glow = glow,
 ) {
     Row(
-        modifier = Modifier.padding(contentPadding)
+        modifier = Modifier.padding(contentPadding),
     ) {
         Box(
             contentAlignment = CardDefaults.ContentImageAlignment,
-            content = image
+            content = image,
         )
         Column {
             CardContent(
                 title = title,
                 subtitle = subtitle,
-                description = description
+                description = description,
             )
         }
     }
@@ -69,31 +69,34 @@ fun WideClassicCard(
 
 @Preview
 @Composable
-fun PreviewWideClassicCard() = preview {
-    WideClassicCard(
-        colors = CardDefaults.colors(
-            containerColor = Color.White
-        ),
-        border = CardDefaults.border(),
-        glow = CardDefaults.glow(
-            glow = Glow(Color.Green, 4.dp)
-        ),
-        contentPadding = PaddingValues(8.dp),
-        scale = CardDefaults.scale(1f),
-        image = {
-            ImageAny(
-                src = AccountCircle,
-                contentDescription = ""
-            )
-        },
-        title = {
-            TextAny("title")
-        },
-        subtitle = {
-            TextAny("subtitle")
-        },
-        description = {
-            TextAny("description")
-        }
-    )
-}
+fun PreviewWideClassicCard() =
+    preview {
+        WideClassicCard(
+            colors =
+                CardDefaults.colors(
+                    containerColor = Color.White,
+                ),
+            border = CardDefaults.border(),
+            glow =
+                CardDefaults.glow(
+                    glow = Glow(Color.Green, 4.dp),
+                ),
+            contentPadding = PaddingValues(8.dp),
+            scale = CardDefaults.scale(1f),
+            image = {
+                ImageAny(
+                    src = AccountCircle,
+                    contentDescription = "",
+                )
+            },
+            title = {
+                TextAny("title")
+            },
+            subtitle = {
+                TextAny("subtitle")
+            },
+            description = {
+                TextAny("description")
+            },
+        )
+    }

+ 5 - 6
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardBorder.kt

@@ -15,7 +15,7 @@ import org.mjdev.desktop.components.surface.base.Border
 class CardBorder(
     val border: Border,
     val focusedBorder: Border,
-    val pressedBorder: Border
+    val pressedBorder: Border,
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -34,8 +34,7 @@ class CardBorder(
         return result
     }
 
-    override fun toString(): String {
-        return "CardBorder(border=$border, focusedBorder=$focusedBorder, " +
-                "pressedBorder=$pressedBorder)"
-    }
-}
+    override fun toString(): String =
+        "CardBorder(border=$border, focusedBorder=$focusedBorder, " +
+            "pressedBorder=$pressedBorder)"
+}

+ 10 - 11
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardColors.kt

@@ -18,7 +18,7 @@ class CardColors(
     val focusedContainerColor: Color,
     val focusedContentColor: Color,
     val pressedContainerColor: Color,
-    val pressedContentColor: Color
+    val pressedContentColor: Color,
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -43,13 +43,12 @@ class CardColors(
         return result
     }
 
-    override fun toString(): String {
-        return "CardColors(" +
-                "containerColor=$containerColor, " +
-                "contentColor=$contentColor, " +
-                "focusedContainerColor=$focusedContainerColor, " +
-                "focusedContentColor=$focusedContentColor, " +
-                "pressedContainerColor=$pressedContainerColor, " +
-                "pressedContentColor=$pressedContentColor)"
-    }
-}
+    override fun toString(): String =
+        "CardColors(" +
+            "containerColor=$containerColor, " +
+            "contentColor=$contentColor, " +
+            "focusedContainerColor=$focusedContainerColor, " +
+            "focusedContentColor=$focusedContentColor, " +
+            "pressedContainerColor=$pressedContainerColor, " +
+            "pressedContentColor=$pressedContentColor)"
+}

+ 91 - 76
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardDefaults.kt

@@ -40,17 +40,18 @@ object CardDefaults {
     const val SubtitleAlpha = 0.6f
     const val DescriptionAlpha = 0.8f
 
-    val ScrimBrush = Brush.verticalGradient(
-        listOf(
-            Color(red = 28, green = 27, blue = 31, alpha = 0),
-            Color(red = 28, green = 27, blue = 31, alpha = 204)
+    val ScrimBrush =
+        Brush.verticalGradient(
+            listOf(
+                Color(red = 28, green = 27, blue = 31, alpha = 0),
+                Color(red = 28, green = 27, blue = 31, alpha = 204),
+            ),
         )
-    )
 
     fun shape(
         shape: Shape = ContainerShape,
         focusedShape: Shape = shape,
-        pressedShape: Shape = shape
+        pressedShape: Shape = shape,
     ) = CardShape(shape = shape, focusedShape = focusedShape, pressedShape = pressedShape)
 
     @ReadOnlyComposable
@@ -61,14 +62,14 @@ object CardDefaults {
         focusedContainerColor: Color = containerColor,
         focusedContentColor: Color = contentColorFor(focusedContainerColor),
         pressedContainerColor: Color = focusedContainerColor,
-        pressedContentColor: Color = contentColorFor(pressedContainerColor)
+        pressedContentColor: Color = contentColorFor(pressedContainerColor),
     ) = CardColors(
         containerColor = containerColor,
         contentColor = contentColor,
         focusedContainerColor = focusedContainerColor,
         focusedContentColor = focusedContentColor,
         pressedContainerColor = pressedContainerColor,
-        pressedContentColor = pressedContentColor
+        pressedContentColor = pressedContentColor,
     )
 
     @ReadOnlyComposable
@@ -79,14 +80,14 @@ object CardDefaults {
         focusedContainerColor: Color = containerColor,
         focusedContentColor: Color = contentColor,
         pressedContainerColor: Color = focusedContainerColor,
-        pressedContentColor: Color = focusedContentColor
+        pressedContentColor: Color = focusedContentColor,
     ) = CardColors(
         containerColor = containerColor,
         contentColor = contentColor,
         focusedContainerColor = focusedContainerColor,
         focusedContentColor = focusedContentColor,
         pressedContainerColor = pressedContainerColor,
-        pressedContentColor = pressedContentColor
+        pressedContentColor = pressedContentColor,
     )
 
     @Composable
@@ -95,70 +96,81 @@ object CardDefaults {
         colorUnFocused: Color = Color.Black,
         colorPressed: Color = Color.Yellow,
         width: Dp = 1.dp,
-        roundCornerSize: Dp = 8.dp
+        roundCornerSize: Dp = 8.dp,
     ) = border(
-        border = Border(
-            border = BorderStroke(
-                width = width,
-                color = colorUnFocused
+        border =
+            Border(
+                border =
+                    BorderStroke(
+                        width = width,
+                        color = colorUnFocused,
+                    ),
+                shape = RoundedCornerShape(roundCornerSize),
             ),
-            shape = RoundedCornerShape(roundCornerSize)
-        ),
-        focusedBorder = Border(
-            border = BorderStroke(
-                width = width,
-                color = colorFocused
+        focusedBorder =
+            Border(
+                border =
+                    BorderStroke(
+                        width = width,
+                        color = colorFocused,
+                    ),
+                shape = RoundedCornerShape(roundCornerSize),
             ),
-            shape = RoundedCornerShape(roundCornerSize)
-        ),
-        pressedBorder = Border(
-            border = BorderStroke(
-                width = width,
-                color = colorPressed
+        pressedBorder =
+            Border(
+                border =
+                    BorderStroke(
+                        width = width,
+                        color = colorPressed,
+                    ),
+                shape = RoundedCornerShape(roundCornerSize),
             ),
-            shape = RoundedCornerShape(roundCornerSize)
-        )
     )
 
     @Composable
     fun CardDefaults.colorFocusGlow(
         focusColor: Color = Color.Green,
         onUnFocusColor: Color = Color.Transparent,
-        elevation: Dp = 10.dp
-    ): CardGlow = glow(
-        glow = Glow(
-            elevationColor = onUnFocusColor,
-            elevation = elevation
-        ),
-        focusedGlow = Glow(
-            elevationColor = focusColor,
-            elevation = elevation
-        ),
-        pressedGlow = Glow(
-            elevationColor = onUnFocusColor,
-            elevation = elevation
+        elevation: Dp = 10.dp,
+    ): CardGlow =
+        glow(
+            glow =
+                Glow(
+                    elevationColor = onUnFocusColor,
+                    elevation = elevation,
+                ),
+            focusedGlow =
+                Glow(
+                    elevationColor = focusColor,
+                    elevation = elevation,
+                ),
+            pressedGlow =
+                Glow(
+                    elevationColor = onUnFocusColor,
+                    elevation = elevation,
+                ),
         )
-    )
 
     fun scale(
         scale: Float = 1f,
         focusedScale: Float = 1.1f,
-        pressedScale: Float = scale
+        pressedScale: Float = scale,
     ) = CardScale(scale = scale, focusedScale = focusedScale, pressedScale = pressedScale)
 
     @ReadOnlyComposable
     @Composable
     fun border(
         border: Border = Border.None,
-        focusedBorder: Border = Border(
-            border = BorderStroke(width = 3.dp, color = Color.White), // todo theme
-            shape = ContainerShape
-        ),
-        pressedBorder: Border = focusedBorder
+        focusedBorder: Border =
+            Border(
+                border = BorderStroke(width = 3.dp, color = Color.White), // todo theme
+                shape = ContainerShape,
+            ),
+        pressedBorder: Border = focusedBorder,
     ) = CardBorder(
         border = border,
         focusedBorder = focusedBorder,
-        pressedBorder = pressedBorder
+        pressedBorder = pressedBorder,
     )
 
     @ReadOnlyComposable
@@ -166,40 +178,43 @@ object CardDefaults {
     fun border(
         borderWidth: Dp,
         borderColor: Color = Color.Black,
-        border: Border = Border(
-            border = BorderStroke(width = borderWidth, color = borderColor)
-        ),
-        focusedBorder: Border = Border(
-            border = BorderStroke(width = borderWidth + 2.dp, color = borderColor), // todo theme
-            shape = ContainerShape
-        ),
-        pressedBorder: Border = focusedBorder
+        border: Border =
+            Border(
+                border = BorderStroke(width = borderWidth, color = borderColor),
+            ),
+        focusedBorder: Border =
+            Border(
+                border = BorderStroke(width = borderWidth + 2.dp, color = borderColor), // todo theme
+                shape = ContainerShape,
+            ),
+        pressedBorder: Border = focusedBorder,
     ) = CardBorder(
         border = border,
         focusedBorder = focusedBorder,
-        pressedBorder = pressedBorder
+        pressedBorder = pressedBorder,
     )
 
     fun glow(
         glow: Glow = Glow.None,
         focusedGlow: Glow = glow,
-        pressedGlow: Glow = glow
+        pressedGlow: Glow = glow,
     ) = CardGlow(
         glow = glow,
         focusedGlow = focusedGlow,
-        pressedGlow = pressedGlow
+        pressedGlow = pressedGlow,
     )
 
-    fun CardColors.toClickableSurfaceColors() = ClickableSurfaceColors(
-        containerColor = containerColor,
-        contentColor = contentColor,
-        focusedContainerColor = focusedContainerColor,
-        focusedContentColor = focusedContentColor,
-        pressedContainerColor = pressedContainerColor,
-        pressedContentColor = pressedContentColor,
-        disabledContainerColor = containerColor,
-        disabledContentColor = contentColor
-    )
+    fun CardColors.toClickableSurfaceColors() =
+        ClickableSurfaceColors(
+            containerColor = containerColor,
+            contentColor = contentColor,
+            focusedContainerColor = focusedContainerColor,
+            focusedContentColor = focusedContentColor,
+            pressedContainerColor = pressedContainerColor,
+            pressedContentColor = pressedContentColor,
+            disabledContainerColor = containerColor,
+            disabledContentColor = contentColor,
+        )
 
     fun CardShape.toClickableSurfaceShape() =
         ClickableSurfaceShape(
@@ -207,7 +222,7 @@ object CardDefaults {
             focusedShape = focusedShape,
             pressedShape = pressedShape,
             disabledShape = shape,
-            focusedDisabledShape = shape
+            focusedDisabledShape = shape,
         )
 
     fun CardScale.toClickableSurfaceScale() =
@@ -216,7 +231,7 @@ object CardDefaults {
             focusedScale = focusedScale,
             pressedScale = pressedScale,
             disabledScale = scale,
-            focusedDisabledScale = scale
+            focusedDisabledScale = scale,
         )
 
     fun CardBorder.toClickableSurfaceBorder() =
@@ -225,13 +240,13 @@ object CardDefaults {
             focusedBorder = focusedBorder,
             pressedBorder = pressedBorder,
             disabledBorder = border,
-            focusedDisabledBorder = border
+            focusedDisabledBorder = border,
         )
 
     fun CardGlow.toClickableSurfaceGlow() =
         ClickableSurfaceGlow(
             glow = glow,
             focusedGlow = focusedGlow,
-            pressedGlow = pressedGlow
+            pressedGlow = pressedGlow,
         )
-}
+}

+ 3 - 5
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardGlow.kt

@@ -15,7 +15,7 @@ import org.mjdev.desktop.components.surface.base.Glow
 class CardGlow(
     val glow: Glow,
     val focusedGlow: Glow,
-    val pressedGlow: Glow
+    val pressedGlow: Glow,
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -34,7 +34,5 @@ class CardGlow(
         return result
     }
 
-    override fun toString(): String {
-        return "CardGlow(glow=$glow, focusedGlow=$focusedGlow, pressedGlow=$pressedGlow)"
-    }
-}
+    override fun toString(): String = "CardGlow(glow=$glow, focusedGlow=$focusedGlow, pressedGlow=$pressedGlow)"
+}

+ 3 - 5
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardScale.kt

@@ -15,7 +15,7 @@ import androidx.compose.runtime.Immutable
 class CardScale(
     val scale: Float,
     val focusedScale: Float,
-    val pressedScale: Float
+    val pressedScale: Float,
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -34,11 +34,9 @@ class CardScale(
         return result
     }
 
-    override fun toString(): String {
-        return "CardScale(scale=$scale, focusedScale=$focusedScale, pressedScale=$pressedScale)"
-    }
+    override fun toString(): String = "CardScale(scale=$scale, focusedScale=$focusedScale, pressedScale=$pressedScale)"
 
     companion object {
         val None = CardScale(scale = 1f, focusedScale = 1f, pressedScale = 1f)
     }
-}
+}

+ 2 - 5
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardShape.kt

@@ -15,7 +15,7 @@ import androidx.compose.ui.graphics.Shape
 class CardShape(
     val shape: Shape,
     val focusedShape: Shape,
-    val pressedShape: Shape
+    val pressedShape: Shape,
 ) {
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
@@ -34,8 +34,5 @@ class CardShape(
         return result
     }
 
-    override fun toString(): String {
-        return "CardShape(shape=$shape, focusedShape=$focusedShape, pressedShape=$pressedShape)"
-    }
+    override fun toString(): String = "CardShape(shape=$shape, focusedShape=$focusedShape, pressedShape=$pressedShape)"
 }
-

+ 23 - 22
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chart/DonutChart.kt

@@ -9,11 +9,11 @@ import androidx.compose.ui.text.TextStyle
 import com.aay.compose.baseComponents.model.LegendPosition
 import com.aay.compose.donutChart.DonutChart
 import com.aay.compose.donutChart.model.PieChartData
-import org.mjdev.desktop.extensions.Compose.preview
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
-import org.mjdev.desktop.extensions.System.currentTimeMillis
 import org.jetbrains.compose.ui.tooling.preview.Preview
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.System.currentTimeMillis
 
 @Suppress("FunctionName")
 @Composable
@@ -28,7 +28,7 @@ fun DonutChart(
     refreshTimeout: Long = 5000L,
     animation: AnimationSpec<Float> = TweenSpec(durationMillis = animationDuration),
     legendPosition: LegendPosition = LegendPosition.BOTTOM,
-    dataHandler: () -> List<PieChartData> = { emptyList() }
+    dataHandler: () -> List<PieChartData> = { emptyList() },
 ) {
     var needRefresh by remember { mutableStateOf(0L) }
     val textStyle = TextStyle(color = textColor)
@@ -43,9 +43,9 @@ fun DonutChart(
         animation,
         legendPosition,
         dataHandler,
-        needRefresh
+        needRefresh,
     ) { mutableStateOf(dataHandler()) }
-   DonutChart(
+    DonutChart(
         modifier = modifier,
         pieChartData = data,
         centerTitle = title,
@@ -56,7 +56,7 @@ fun DonutChart(
         innerCircularColor = innerCircularColor,
         ratioLineColor = ratioLineColor,
         animation = TweenSpec(1),
-        legendPosition = legendPosition
+        legendPosition = legendPosition,
     )
     LaunchedEffect(Unit) {
         while (isActive) {
@@ -68,19 +68,20 @@ fun DonutChart(
 
 @Preview
 @Composable
-fun PreviewDonutChart() = preview(480) {
-    DonutChart(
-        textColor = Color.White,
-        outerCircularColor = Color.White,
-        innerCircularColor = Color.White,
-        ratioLineColor = Color.White,
-        title = "Donut Chart",
-        legendPosition = LegendPosition.BOTTOM,
-        dataHandler = {
-            listOf(
-                PieChartData(24.0, Color.Blue, "test1"),
-                PieChartData(32.0, Color.Gray, "test2")
-            )
-        }
-    )
-}
+fun PreviewDonutChart() =
+    preview(480) {
+        DonutChart(
+            textColor = Color.White,
+            outerCircularColor = Color.White,
+            innerCircularColor = Color.White,
+            ratioLineColor = Color.White,
+            title = "Donut Chart",
+            legendPosition = LegendPosition.BOTTOM,
+            dataHandler = {
+                listOf(
+                    PieChartData(24.0, Color.Blue, "test1"),
+                    PieChartData(32.0, Color.Gray, "test2"),
+                )
+            },
+        )
+    }

+ 2 - 4
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chromecast/ChromecastView.kt

@@ -8,9 +8,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
 
 // todo
 @Composable
-fun ChromecastView(
-    modifier: Modifier = Modifier
-) {
+fun ChromecastView(modifier: Modifier = Modifier) {
     Box(modifier) {
         Text(text = "ChromecastView")
     }
@@ -20,4 +18,4 @@ fun ChromecastView(
 @Composable
 private fun PreviewChromecastView() {
     ChromecastView()
-}
+}

+ 66 - 62
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -28,12 +28,14 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.input.pointer.pointerInput
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.blur.BlurPanel
 import org.mjdev.desktop.components.sliding.SlidingPanel
 import org.mjdev.desktop.components.sliding.base.VisibilityState
 import org.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
 import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 import org.mjdev.desktop.extensions.Compose.orElse
+import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.Modifier.onLeftClick
 import org.mjdev.desktop.extensions.Modifier.onMouseEnter
 import org.mjdev.desktop.extensions.Modifier.onMousePress
@@ -43,8 +45,6 @@ import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
 import org.mjdev.desktop.extensions.MutableStateExt.rememberState
 import org.mjdev.desktop.helpers.animation.Animations.ControlCenterEnterAnimation
 import org.mjdev.desktop.helpers.animation.Animations.ControlCenterExitAnimation
-import org.jetbrains.compose.ui.tooling.preview.Preview
-import org.mjdev.desktop.extensions.Compose.preview
 
 // todo : automatic history before close, one or more steps back, 1s switch
 @Composable
@@ -56,11 +56,11 @@ fun ControlCenter(
     exitAnimation: ExitTransition = ControlCenterExitAnimation,
     onContextMenuClick: () -> Unit = {},
     onFocusChange: (focused: Boolean) -> Unit = {},
-    onTooltip: (item: Any?) -> Unit = {}
+    onTooltip: (item: Any?) -> Unit = {},
 ) = withDesktopContext {
     val controlCenterExpandedWidth: Dp by rememberComputed(
         theme.controlCenterExpandedWidthPercent,
-        containerSize.width
+        containerSize.width,
     ) {
         (containerSize.width / 100f) * theme.controlCenterExpandedWidthPercent.orElse { 0 }
     }
@@ -75,90 +75,93 @@ fun ControlCenter(
         exitAnimation = exitAnimation,
         onPointerEnter = {
             onFocusChange(true)
-        }
+        },
     ) { isVisible ->
         if (!isVisible) {
             VerticalDivider(
                 thickness = controlCenterDividerWidth,
-                color = if (isDebug) Color.Red else Color.Transparent
+                color = if (isDebug) Color.Red else Color.Transparent,
             )
         } else {
             BlurPanel(
-                modifier = Modifier.fillMaxHeight()
-                    .matchParentSize()
+                modifier =
+                    Modifier
+                        .fillMaxHeight()
+                        .matchParentSize(),
             )
             Box(
-                modifier = Modifier.fillMaxHeight()
-                    .wrapContentSize()
-                    .background(
-                        backgroundColor.copy(alpha = controlCenterBackgroundAlpha)
-                    ),
+                modifier =
+                    Modifier
+                        .fillMaxHeight()
+                        .wrapContentSize()
+                        .background(
+                            backgroundColor.copy(alpha = controlCenterBackgroundAlpha),
+                        ),
             ) {
                 Row(
                     modifier = Modifier.fillMaxHeight(),
                 ) {
                     Box(
-                        contentAlignment = Alignment.TopEnd
+                        contentAlignment = Alignment.TopEnd,
                     ) {
                         LazyColumn(
-                            modifier = Modifier
-                                .fillMaxHeight()
-                                .background(backgroundColor)
-                                .padding(8.dp),
+                            modifier =
+                                Modifier
+                                    .fillMaxHeight()
+                                    .background(backgroundColor)
+                                    .padding(8.dp),
                             verticalArrangement = Arrangement.Top,
-                            horizontalAlignment = Alignment.Start
+                            horizontalAlignment = Alignment.Start,
                         ) {
                             itemsIndexed(pagesFiltered) { idx, page ->
                                 Icon(
-                                    modifier = Modifier
-                                        .padding(vertical = 8.dp)
-                                        .size(controlCenterIconSize)
-                                        .background(
-                                            color = if (pagerState.value == idx) iconsTintColor else borderColor,
-                                            shape = RoundedCornerShape(8.dp)
-                                        )
-                                        .pointerInput(Unit) {
-                                            detectTapGestures(
-                                                onTap = {
-                                                    pagerState.value = idx
-                                                }
-                                            )
-                                        }
-                                        .onMouseEnter {
-                                            onTooltip(page.name)
-                                        }
-                                        .onMousePress {
-                                            onLeftClick {
-                                                runAsync {
-                                                    pagerState.value = idx
+                                    modifier =
+                                        Modifier
+                                            .padding(vertical = 8.dp)
+                                            .size(controlCenterIconSize)
+                                            .background(
+                                                color = if (pagerState.value == idx) iconsTintColor else borderColor,
+                                                shape = RoundedCornerShape(8.dp),
+                                            ).pointerInput(Unit) {
+                                                detectTapGestures(
+                                                    onTap = {
+                                                        pagerState.value = idx
+                                                    },
+                                                )
+                                            }.onMouseEnter {
+                                                onTooltip(page.name)
+                                            }.onMousePress {
+                                                onLeftClick {
+                                                    runAsync {
+                                                        pagerState.value = idx
+                                                    }
                                                 }
-                                            }
-                                            onRightClick {
-                                                runAsync {
-                                                    pagerState.value = idx
+                                                onRightClick {
+                                                    runAsync {
+                                                        pagerState.value = idx
+                                                    }
+                                                    onContextMenuClick()
                                                 }
-                                                onContextMenuClick()
-                                            }
-                                        },
+                                            },
                                     imageVector = page.icon,
                                     contentDescription = "",
-                                    tint = if (pagerState.value == idx) backgroundColor else textColor
+                                    tint = if (pagerState.value == idx) backgroundColor else textColor,
                                 )
                             }
                         }
                         Box(
-                            modifier = Modifier
-                                .padding(
-                                    end = controlCenterIconSize.width + 16.dp
-                                )
-                                .fillMaxHeight(),
+                            modifier =
+                                Modifier
+                                    .padding(
+                                        end = controlCenterIconSize.width + 16.dp,
+                                    ).fillMaxHeight(),
                         ) {
                             Row(
-                                Modifier.fillMaxHeight()
+                                Modifier.fillMaxHeight(),
                             ) {
                                 VerticalDivider(
                                     thickness = 2.dp,
-                                    color = borderColor
+                                    color = borderColor,
                                 )
                                 Column(
                                     modifier = Modifier.width(controlCenterExpandedWidth),
@@ -170,13 +173,13 @@ fun ControlCenter(
                                 }
                                 VerticalDivider(
                                     thickness = 2.dp,
-                                    color = borderColor
+                                    color = borderColor,
                                 )
                             }
                         }
                         VerticalDivider(
                             thickness = 2.dp,
-                            color = Color.White.copy(0.1f)
+                            color = Color.White.copy(0.1f),
                         )
                     }
                 }
@@ -187,8 +190,9 @@ fun ControlCenter(
 
 @Preview
 @Composable
-fun PreviewControlCenter() = preview {
-    ControlCenter(
-        controlCenterState = rememberVisibilityState(true)
-    )
-}
+fun PreviewControlCenter() =
+    preview {
+        ControlCenter(
+            controlCenterState = rememberVisibilityState(true),
+        )
+    }

+ 4 - 4
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPage.kt

@@ -9,11 +9,11 @@ import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisposableEffect
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.vector.ImageVector
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.controlcenter.base.ControlCenterPageScope.Companion.withControlCenterPageScope
-import org.mjdev.desktop.interfaces.IControlCenterPageDataSaver
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.interfaces.IControlCenterPageDataSaver
 import org.mjdev.desktop.interfaces.IPage
-import org.jetbrains.compose.ui.tooling.preview.Preview
 
 @Suppress("MemberVisibilityCanBePrivate")
 class ControlCenterPage(
@@ -33,10 +33,10 @@ class ControlCenterPage(
     override fun Render() {
         withControlCenterPageScope(cache) {
             Column(
-                modifier = Modifier.fillMaxSize()
+                modifier = Modifier.fillMaxSize(),
             ) {
                 PageHeader(
-                    page = this@ControlCenterPage
+                    page = this@ControlCenterPage,
                 )
                 Box(
                     modifier = Modifier.weight(1f),

+ 5 - 7
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPageScope.kt

@@ -5,33 +5,31 @@ import androidx.compose.runtime.DisallowComposableCalls
 import androidx.compose.runtime.derivedStateOf
 import androidx.compose.runtime.remember
 import org.mjdev.desktop.context.DesktopContextScope
-import org.mjdev.desktop.context.LocalDesktopContext
 import org.mjdev.desktop.context.IDesktopContext
+import org.mjdev.desktop.context.LocalDesktopContext
 
 @Suppress("UnusedReceiverParameter", "ComposableNaming")
 class ControlCenterPageScope(
     context: IDesktopContext,
-    private val cache: PageCache
+    private val cache: PageCache,
 ) : DesktopContextScope(context) {
     companion object {
         @Composable
         fun ControlCenterPage.withControlCenterPageScope(
             cache: PageCache,
             context: IDesktopContext = LocalDesktopContext.current,
-            block: @Composable ControlCenterPageScope.() -> Unit
+            block: @Composable ControlCenterPageScope.() -> Unit,
         ) {
             block(ControlCenterPageScope(context, cache))
         }
 
         @Composable
-        fun <T> ControlCenterPageScope.remember(
-            calculation: @DisallowComposableCalls () -> T
-        ): T = cache.cache(false, calculation)
+        fun <T> ControlCenterPageScope.remember(calculation: @DisallowComposableCalls () -> T): T = cache.cache(false, calculation)
 
         @Composable
         fun <T> ControlCenterPageScope.rememberComputed(
             vararg key: Any?,
-            calculation: () -> T
+            calculation: () -> T,
         ) = remember(*key) { derivedStateOf { calculation() } }
     }
 }

+ 3 - 3
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PageCache.kt

@@ -4,7 +4,7 @@ import androidx.compose.runtime.DisallowComposableCalls
 import org.mjdev.desktop.interfaces.IControlCenterPageDataSaver
 
 class PageCache(
-    private val saver: IControlCenterPageDataSaver? = null
+    private val saver: IControlCenterPageDataSaver? = null,
 ) {
     private val _data: MutableMap<Int, Any> = mutableMapOf()
 
@@ -21,7 +21,7 @@ class PageCache(
     @Suppress("UNCHECKED_CAST")
     fun <T> cache(
         invalid: Boolean,
-        block: @DisallowComposableCalls () -> T
+        block: @DisallowComposableCalls () -> T,
     ): T {
         val hash = block.hashCode()
         var data: T = _data[hash] as T
@@ -31,4 +31,4 @@ class PageCache(
         if (data != null) _data[hash] = data
         return data
     }
-}
+}

+ 22 - 17
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PageHeader.kt

@@ -22,15 +22,15 @@ import androidx.compose.ui.graphics.ColorFilter
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.sp
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.appbar.AppBar
 import org.mjdev.desktop.components.image.ImageAny
 import org.mjdev.desktop.components.text.TextAny
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.context.LocalDesktopContext
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.extensions.Modifier.circleBorder
 import org.mjdev.desktop.extensions.Modifier.clipCircle
-import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
-import org.jetbrains.compose.ui.tooling.preview.Preview
-import org.mjdev.desktop.context.LocalDesktopContext
 
 @Composable
 fun PageHeader(
@@ -42,21 +42,25 @@ fun PageHeader(
 ) = withDesktopContext {
     if (page.showHeader) {
         AppBar(
-            modifier = modifier.fillMaxWidth()
-                .wrapContentHeight()
-                .background(backgroundColor),
+            modifier =
+                modifier
+                    .fillMaxWidth()
+                    .wrapContentHeight()
+                    .background(backgroundColor),
             contentPadding = contentPadding,
             icon = {
                 Box(
-                    modifier = Modifier.size(iconSize + 4.dp)
-                        .circleBorder(2.dp, textColor)
-                        .padding(4.dp)
+                    modifier =
+                        Modifier
+                            .size(iconSize + 4.dp)
+                            .circleBorder(2.dp, textColor)
+                            .padding(4.dp),
                 ) {
                     ImageAny(
                         modifier = Modifier.size(iconSize).background(backgroundColor).clipCircle(),
                         src = page.icon,
                         colorFilter = ColorFilter.tint(textColor),
-                        contentDescription = ""
+                        contentDescription = "",
                     )
                 }
             },
@@ -65,18 +69,19 @@ fun PageHeader(
                     modifier = Modifier.fillMaxWidth().padding(start = 16.dp),
                     text = page.name,
                     color = textColor,
-                    fontSize = 20.sp
+                    fontSize = 20.sp,
                 )
             },
-            actions = actions
+            actions = actions,
         )
     }
 }
 
 @Preview
 @Composable
-fun PreviewPageHeader() = preview {
-    PageHeader(
-        page = ControlCenterPage(context)
-    )
-}
+fun PreviewPageHeader() =
+    preview {
+        PageHeader(
+            page = ControlCenterPage(context),
+        )
+    }

+ 14 - 13
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/PersistentPageSaver.kt

@@ -1,9 +1,9 @@
 package org.mjdev.desktop.components.controlcenter.base
 
+import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.extensions.CustomExt.asJson
 import org.mjdev.desktop.extensions.CustomExt.to
 import org.mjdev.desktop.interfaces.IControlCenterPageDataSaver
-import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.log.Log
 
 class PersistentPageSaver(
@@ -15,16 +15,17 @@ class PersistentPageSaver(
         context.storageProvider.put(key, data.asJson())
     }
 
-    override fun load(): Map<Int, Any> = runCatching {
-        Log.d("Loading data for $key")
-        context.storageProvider.get(key)?.to<Map<Int, Any>>()
-    }.onFailure { e ->
-        Log.e(e)
-    }.getOrNull() ?: emptyMap<Int, Any>().also { data ->
-        if (data.isEmpty()) {
-            Log.d("No data found for $key, returning empty map.")
-        } else {
-            Log.d("Loaded data for $key: $data.")
+    override fun load(): Map<Int, Any> =
+        runCatching {
+            Log.d("Loading data for $key")
+            context.storageProvider.get(key)?.to<Map<Int, Any>>()
+        }.onFailure { e ->
+            Log.e(e)
+        }.getOrNull() ?: emptyMap<Int, Any>().also { data ->
+            if (data.isEmpty()) {
+                Log.d("No data found for $key, returning empty map.")
+            } else {
+                Log.d("Loaded data for $key: $data.")
+            }
         }
-    }
-}
+}

+ 17 - 17
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/pages/about/AboutPage.kt

@@ -4,28 +4,28 @@ import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
+import org.jetbrains.compose.ui.tooling.preview.Preview
 import org.mjdev.desktop.components.controlcenter.base.ControlCenterPage
+import org.mjdev.desktop.context.IDesktopContext
 import org.mjdev.desktop.extensions.Compose.preview
 import org.mjdev.desktop.icons.custom.Mjdev
-import org.jetbrains.compose.ui.tooling.preview.Preview
-import org.mjdev.desktop.context.IDesktopContext
 
 @Suppress("FunctionName")
-fun AboutPage(
-    context: IDesktopContext
-) = ControlCenterPage(
-    context = context,
-    icon = Mjdev,
-    name = "About",
-    condition = { true }
-) {
-    Box(
-        modifier = Modifier.fillMaxSize()
-    )
-}
+fun AboutPage(context: IDesktopContext) =
+    ControlCenterPage(
+        context = context,
+        icon = Mjdev,
+        name = "About",
+        condition = { true },
+    ) {
+        Box(
+            modifier = Modifier.fillMaxSize(),
+        )
+    }
 
 @Preview
 @Composable
-fun PreviewAboutPage() = preview {
-    AboutPage(context).Render()
-}
+fun PreviewAboutPage() =
+    preview {
+        AboutPage(context).Render()
+    }

برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است