Pārlūkot izejas kodu

multi-platform

multi-platform

multi-platform
Milan Jurkulak 1 gadu atpakaļ
vecāks
revīzija
e5d6cd6ec8
100 mainītis faili ar 3090 papildinājumiem un 880 dzēšanām
  1. 16 31
      .gitignore
  2. 3 3
      .run/mjdev-desktop [generateIcons].run.xml
  3. 3 3
      .run/mjdev-desktop [jvm].run.xml
  4. 0 24
      .run/mjdev-desktop [runDebug].run.xml
  5. 3 3
      .run/mjdev-desktop [wasm].run.xml
  6. 2 3
      README.md
  7. 8 287
      build.gradle.kts
  8. 15 19
      buildSrc/build.gradle.kts
  9. 8 0
      buildSrc/settings.gradle.kts
  10. 23 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/AndroidConfig.kt
  11. 0 70
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/DependencyExt.kt
  12. 15 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/KotlinDependencyHandlerExt.kt
  13. 6 82
      buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/ProjectExt.kt
  14. 0 28
      buildSrc/src/main/kotlin/org/mjdev/gradle/plugin/DesktopPlugin.kt
  15. 43 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/plugins/MultiPlatformPlugin.kt
  16. 73 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/tasks/DeployTask.kt
  17. 74 0
      buildSrc/src/main/kotlin/org/mjdev/gradle/tasks/GenerateChangelogTask.kt
  18. 266 0
      composeApp/build.gradle.kts
  19. 72 0
      composeApp/src/androidMain/AndroidManifest.xml
  20. 84 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/MainActivity.kt
  21. 123 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/MainView.kt
  22. 145 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt
  23. 9 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/LocalDesktopContext.android.kt
  24. 63 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/App.kt
  25. 140 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/Theme.kt
  26. 86 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/User.kt
  27. 46 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/UserDirs.kt
  28. 37 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt
  29. 13 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/JSonExt.kt
  30. 11 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/streams/ResourceStream.kt
  31. 5 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfo.android.kt
  32. 25 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/helpers/system/meminfo/MemInfoAndroid.kt
  33. 70 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/AIManager.kt
  34. 50 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginGemini.kt
  35. 37 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/tts/TTSPluginMain.kt
  36. 92 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/apps/AppsManager.kt
  37. 12 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManager.kt
  38. 58 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManagerStub.kt
  39. 11 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/keys/KeysManager.kt
  40. 22 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/OSManager.kt
  41. 31 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/base/OSManagerStub.kt
  42. 39 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/processes/ProcessManager.kt
  43. 22 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/ThemeManager.kt
  44. 21 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/base/ThemeManagerStub.kt
  45. 58 0
      composeApp/src/androidMain/kotlin/org/mjdev/desktop/windows/ChromeWindow.kt
  46. 9 0
      composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_banner.xml
  47. BIN
      composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
  48. BIN
      composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
  49. BIN
      composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
  50. BIN
      composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
  51. BIN
      composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
  52. BIN
      composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
  53. BIN
      composeApp/src/androidMain/res/mipmap-xhdpi/ic_banner.png
  54. BIN
      composeApp/src/androidMain/res/mipmap-xhdpi/ic_banner_foreground.png
  55. BIN
      composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
  56. BIN
      composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
  57. BIN
      composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
  58. BIN
      composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
  59. BIN
      composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
  60. BIN
      composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
  61. BIN
      composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
  62. BIN
      composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
  63. BIN
      composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
  64. 4 0
      composeApp/src/androidMain/res/values/ic_banner_background.xml
  65. 11 0
      composeApp/src/androidMain/res/values/splash.xml
  66. 9 0
      composeApp/src/androidMain/res/xml/network_security_config.xml
  67. BIN
      composeApp/src/commonMain/composeResources/drawable/blur4.webp
  68. BIN
      composeApp/src/commonMain/composeResources/drawable/icon.png
  69. 21 0
      composeApp/src/commonMain/composeResources/drawable/icon.svg
  70. 69 28
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appbar/AppBar.kt
  71. 275 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt
  72. 22 18
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsBottomBar.kt
  73. 32 24
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsList.kt
  74. 27 25
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuActions.kt
  75. 33 22
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuApp.kt
  76. 30 19
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuCategory.kt
  77. 105 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt
  78. 14 11
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanel.kt
  79. 7 7
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonPrimary.kt
  80. 7 7
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonSecondary.kt
  81. 8 8
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/TransparentButton.kt
  82. 17 12
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/Card.kt
  83. 40 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CardContent.kt
  84. 10 10
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ClassicCard.kt
  85. 22 46
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CompactCard.kt
  86. 33 24
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/FocusableCard.kt
  87. 22 17
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ItemCard.kt
  88. 16 16
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/PhotoCard.kt
  89. 9 10
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/WideClassicCard.kt
  90. 2 2
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardBorder.kt
  91. 1 1
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardColors.kt
  92. 8 2
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardDefaults.kt
  93. 2 2
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardGlow.kt
  94. 1 1
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardScale.kt
  95. 1 1
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardShape.kt
  96. 7 6
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chart/DonutChart.kt
  97. 23 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chromecast/ChromecastView.kt
  98. 191 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenter.kt
  99. 54 0
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPage.kt
  100. 8 8
      composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPageScope.kt

+ 16 - 31
.gitignore

@@ -1,36 +1,21 @@
+*.iml
+.kotlin
 .gradle
-build/
-/build
-/build/
-!gradle/wrapper/gradle-wrapper.jar
-**/src/main/**/build/
-**/src/test/**/build/
-
-### IntelliJ IDEA ###
+**/build/
+xcuserdata
+!src/**/build/
+local.properties
 .idea
-.idea/
-/.idea
-/.idea/
-*.iws
-*.iml
-*.ipr
-out/
-/out
-/out/
-!**/src/main/**/out/
-!**/src/test/**/out/
-
-### Eclipse ###
-.apt_generated
-.classpath
-.factorypath
-.project
-.settings
-.springBeans
-.sts4-cache
-bin/
-!**/src/main/**/bin/
-!**/src/test/**/bin/
+.DS_Store
+captures
+.externalNativeBuild
+.cxx
+*.xcodeproj/*
+!*.xcodeproj/project.pbxproj
+!*.xcodeproj/xcshareddata/
+!*.xcodeproj/project.xcworkspace/
+!*.xcworkspace/contents.xcworkspacedata
+**/xcshareddata/WorkspaceSettings.xcsettings
 
 ### Packages ###
 packages

+ 3 - 3
.run/mjdev-desktop [cleanUp].run.xml → .run/mjdev-desktop [generateIcons].run.xml

@@ -1,5 +1,5 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="mjdev-desktop [cleanUp]" type="GradleRunConfiguration" factoryName="Gradle" nameIsGenerated="true">
+  <configuration default="false" name="mjdev-desktop [generateIcons]" type="GradleRunConfiguration" factoryName="Gradle">
     <ExternalSystemSettings>
       <option name="executionName" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
@@ -10,12 +10,12 @@
       </option>
       <option name="taskNames">
         <list>
-          <option value="cleanUp" />
+          <option value=":generateIcons" />
         </list>
       </option>
       <option name="vmOptions" />
     </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <DebugAllEnabled>false</DebugAllEnabled>
     <RunAsTest>false</RunAsTest>

+ 3 - 3
.run/mjdev-desktop [createPackages].run.xml → .run/mjdev-desktop [jvm].run.xml

@@ -1,5 +1,5 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="mjdev-desktop [createPackages]" type="GradleRunConfiguration" factoryName="Gradle" nameIsGenerated="true">
+  <configuration default="false" name="mjdev-desktop [jvm]" type="GradleRunConfiguration" factoryName="Gradle">
     <ExternalSystemSettings>
       <option name="executionName" />
       <option name="externalProjectPath" value="$PROJECT_DIR$" />
@@ -10,12 +10,12 @@
       </option>
       <option name="taskNames">
         <list>
-          <option value="createPackages" />
+          <option value=":composeApp:run" />
         </list>
       </option>
       <option name="vmOptions" />
     </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
+    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
     <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
     <DebugAllEnabled>false</DebugAllEnabled>
     <RunAsTest>false</RunAsTest>

+ 0 - 24
.run/mjdev-desktop [runDebug].run.xml

@@ -1,24 +0,0 @@
-<component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="mjdev-desktop [runDebug]" type="GradleRunConfiguration" factoryName="Gradle">
-    <ExternalSystemSettings>
-      <option name="executionName" />
-      <option name="externalProjectPath" value="$PROJECT_DIR$" />
-      <option name="externalSystemIdString" value="GRADLE" />
-      <option name="scriptParameters" value="--stacktrace" />
-      <option name="taskDescriptions">
-        <list />
-      </option>
-      <option name="taskNames">
-        <list>
-          <option value="runDebug" />
-        </list>
-      </option>
-      <option name="vmOptions" value="--enable-preview" />
-    </ExternalSystemSettings>
-    <ExternalSystemDebugServerProcess>false</ExternalSystemDebugServerProcess>
-    <ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
-    <DebugAllEnabled>false</DebugAllEnabled>
-    <RunAsTest>false</RunAsTest>
-    <method v="2" />
-  </configuration>
-</component>

+ 3 - 3
.run/mjdev-desktop [run].run.xml → .run/mjdev-desktop [wasm].run.xml

@@ -1,16 +1,16 @@
 <component name="ProjectRunConfigurationManager">
-  <configuration default="false" name="mjdev-desktop [run]" type="GradleRunConfiguration" factoryName="Gradle" nameIsGenerated="true">
+  <configuration default="false" name="mjdev-desktop [wasm]" 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="scriptParameters" value="-t" />
       <option name="taskDescriptions">
         <list />
       </option>
       <option name="taskNames">
         <list>
-          <option value="run" />
+          <option value="wasmJsBrowserRun" />
         </list>
       </option>
       <option name="vmOptions" />

+ 2 - 3
README.md

@@ -1,9 +1,8 @@
 # Mjdev Desktop
 
 An kotlin compose desktop environment like DDE Desktop.
-New desktop environment with many functio
-nality i miss in another desktops.
-Build on compose kotlin and jvm.
+New desktop environment with many functionality i miss in another desktops.
+Build on compose kotlin and jvm, multiplatform, for android, web & desktop.
 
 (Currently only linux os is supported as main target)
 

+ 8 - 287
build.gradle.kts

@@ -1,288 +1,9 @@
-@file:Suppress("UnstableApiUsage")
-
-import org.jetbrains.compose.desktop.application.dsl.TargetFormat
-//import org.jetbrains.kotlin.config.JvmTarget
-import org.mjdev.gradle.extensions.ProjectExt.createTask
-import org.mjdev.gradle.extensions.ProjectExt.resolve
-
 plugins {
-//    java
-//    application
-
-//    kotlin("multiplatform") version "2.0.20"
-//    alias(libs.plugins.jetbrainsCompose) apply false
-//    alias(libs.plugins.compose.compiler) apply false
-
-    alias(libs.plugins.kotlin.jvm)
-    alias(libs.plugins.compose.desktop)
-    alias(libs.plugins.kotlin.serialization)
-    alias(libs.plugins.gradle.versions)
-//    DesktopPlugin
-//    alias(libs.plugins.kotlin.kapt)
-//    id("dev.datlag.sekret") version "2.0.0-alpha-07"
-//    id("com.bintray.gradle.plugins.bintray")
-}
-
-group = libs.versions.packageName.get()
-version = libs.versions.packageVersion.get()
-
-configurations.all {
-    resolutionStrategy.cacheChangingModulesFor(0, "seconds")
-}
-
-kotlin {
-//    jvm()
-//    linuxX64() {
-    /* Specify additional settings for the 'linux' target here */
-//    }
-    jvmToolchain {
-        languageVersion.set(JavaLanguageVersion.of(21))
-    }
-    jvmToolchain(21)
-}
-
-configurations {
-    "implementation" {
-        exclude(group = "androidx.compose.animation")
-        exclude(group = "androidx.compose.foundation")
-        exclude(group = "androidx.compose.material")
-        exclude(group = "androidx.compose.runtime")
-        exclude(group = "androidx.compose.ui")
-    }
-}
-
-allprojects {
-    repositories {
-        maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
-        maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental")
-        maven("https://androidx.dev/storage/compose-compiler/repository")
-        maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
-        maven("https://gitlab.com/api/v4/projects/38224197/packages/maven")
-        maven("https://oss.sonatype.org/content/repositories/snapshots")
-        maven("https://jogamp.org/deployment/maven")
-        maven("https://jitpack.io")
-        google()
-        gradlePluginPortal()
-        mavenCentral()
-    }
-    dependencies {
-        api(fileTree("src/main/libs") { include("*.jar") })
-        // reflection
-        implementation(kotlin("reflect"))
-        // coroutines
-        runtimeOnly(libs.kotlinx.coroutines.swing)
-        // compose desktop
-        implementation(compose.desktop.currentOs)
-        // preview
-        implementation(compose.runtime)
-        // material 3
-//        implementation(compose.material3)
-        // icons
-        implementation(compose.materialIconsExtended)
-        // foundation
-//        implementation(compose.foundation)
-        // preview
-//        implementation(compose.uiTooling)
-//        implementation(compose.preview)
-        // resources
-//        implementation(compose.components.resources)
-        // init & desktop files
-        implementation(libs.ini4j)
-        // http client
-        implementation(libs.okhttp.client)
-        // coil
-        implementation(libs.coil.compose.core)
-        implementation(libs.coil.compose)
-        implementation(libs.coil.mp)
-        implementation(libs.coil.svg)
-        implementation(libs.coil.network.okhttp)
-        // ffmpeg
-        implementation(libs.ffmpeg.platform)
-        // json
-        implementation(libs.google.gson)
-        // no log
-        implementation(libs.slf4j.nop)
-        // fuzzy search
-        implementation(libs.fuzzywuzzy)
-        // graphs
-        implementation(libs.charts)
-        // gemini ai
-        implementation(libs.google.generativeai)
-        // jna
-        implementation(libs.jna.platform)
-        // qr code
-        implementation(libs.qrose)
-        // tts
-        implementation(libs.tts)
-        // webview
-        implementation(libs.compose.webview)
-        // javascript
-        implementation(libs.mozilla.rhino)
-        // dbus
-        implementation(libs.dbus.java.core)
-        implementation(libs.dbus.java.transport.native.unixsocket)
-        // testing yet :
-        // gettext
-        implementation("org.gnu.gettext:libintl:0.18.3")
-        // custom components
-        implementation("com.composables:core:1.12.0")
-        // events
-        implementation("org.rationalityfrontline:kevent:2.3.1")
-        // components
-        implementation("com.composables:core:1.17.1")
-        // wayland
-//        implementation("org.freedesktop:wayland-client:1.5.1")
-        // paths
-//        implementation("me.sujanpoudel.multiplatform.utils:multiplatform-paths:0.2.2")
-        // files
-//        implementation("io.github.vinceglb:filekit-core:0.8.2")
-        // files with composable utilities
-//        implementation("io.github.vinceglb:filekit-compose:0.8.2")
-//        // images metadata
-//        implementation("com.ashampoo:kim:0.18.4")
-        // bottom sheet flexible
-//        implementation("com.github.skydoves:flexible-bottomsheet-material3:0.1.5")
-        // stt
-//        implementation("com.alphacephei:vosk:0.3.32+")
-        // fs watcher
-//        implementation("io.github.irgaly.kfswatch:kfswatch:1.3.0")
-        // lib-app-indicator
-//        implementation("org.purejava:libappindicator-gtk3-java-full:1.4.1")
-        // internationalization
-//        implementation("org.swiftshire:ji18n-core:1.0")
-        // sikulix
-//        implementation("com.sikulix:sikulixapi:2.0.5")
-        // blur
-//        implementation("dev.chrisbanes.haze:haze:0.7.3")
-        // java gnome
-//        implementation("com.github.bailuk:java-gtk:0.5.0")
-        // wayland
-//        implementation("org.freedesktop:wayland:1.4.0")
-        // wnck
-        // todo
-        // compose debugger
-//        implementation("io.github.theapache64:rebugger:1.0.0-rc03")
-        // music
-//        implementation(libs.korau)
-        // dbus
-//        implementation("org.freedesktop.dbus:dbus-java:2.7")
-//        implementation("org.freedesktop.dbus:dbus-java-annotations:2.7")
-        // bar code
-//        implementation("io.github.alexzhirkevich:qrose-oned:1.0.1")
-        // flow extensions
-//        implementation("io.github.hoc081098:FlowExt:1.0.0-RC")
-        // metadata
-//        implementation("com.ashampoo:kim:0.18.4")
-        // html
-//        implementation("com.github.Hamamas:Kotlin-Wasm-Html-Interop:1.0.1")
-        // sockets
-//        implementation("com.kohlschutter.junixsocket:junixsocket-core:2.3.2")
-        // x11 client
-//        implementation("com.github.moaxcp.x11:x11-client:0.18.2")
-        // jwm
-//        implementation("io.github.humbleui:jwm:0.4.8")
-        // hid
-//        implementation('org.hid4java:hid4java')
-    }
-}
-
-compose {
-    desktop {
-        application {
-            jvmArgs("--add-opens", "java.desktop/sun.awt=ALL-UNNAMED")
-            jvmArgs("--add-opens", "java.desktop/java.awt.peer=ALL-UNNAMED")
-            if (System.getProperty("os.name").contains("Mac")) {
-                jvmArgs("--add-opens", "java.desktop/sun.lwawt=ALL-UNNAMED")
-                jvmArgs("--add-opens", "java.desktop/sun.lwawt.macosx=ALL-UNNAMED")
-            }
-            mainClass = "eu.mjdev.desktop.MainKt"
-            nativeDistributions {
-                packageName = libs.versions.appName.get()
-                packageVersion = libs.versions.packageVersion.get()
-                description = libs.versions.appDescription.get()
-                copyright = libs.versions.appCopyright.get()
-                modules("jdk.unsupported")
-                outputBaseDir.set(project.rootDir.resolve("packages"))
-//                macOS {
-//                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.icns"))
-//                }
-//                windows {
-//                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.ico"))
-//                    exePackageVersion = "1.0.0"
-//                    msiPackageVersion = "1.0.0"
-//                }
-                linux {
-//                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.png"))
-//                    licenseFile.set(project.file("LICENSE.txt"))
-                    appCategory = libs.versions.appCategory.get()
-                    debMaintainer = libs.versions.appMaintainer.get()
-                    menuGroup = libs.versions.appMenuGroup.get()
-                    vendor = libs.versions.appVendor.get()
-                    modules("jdk.security.auth")
-                }
-                targetFormats(
-//                    TargetFormat.Dmg,
-//                    TargetFormat.Msi,
-                    TargetFormat.Deb,
-//                    TargetFormat.Exe,
-                    TargetFormat.AppImage,
-//                    TargetFormat.Pkg,
-                    TargetFormat.Rpm
-                )
-                buildTypes.release.proguard {
-                    configurationFiles.from("compose-desktop.pro")
-                }
-//                appResourcesRootDir.set(project.rootDir.resolve("resources"))
-            }
-        }
-    }
-}
-
-createTask(
-    name = "cleanUp",
-    group = "mjdev",
-    description = "Task cleans project."
-) {
-    resolve(".gradle").deleteRecursively()
-    resolve("build").deleteRecursively()
-    resolve("buildSrc").resolve("build").deleteRecursively()
-    resolve("buildSrc").resolve(".gradle").deleteRecursively()
-    resolve("packages").deleteRecursively()
-}
-
-createTask(
-    name = "createPackages",
-    group = "mjdev",
-    description = "Task packages project to be published."
-) {
-    dependsOn("packageAppImage")
-    dependsOn("packageDeb")
-//    dependsOn("packageDmg")
-//    dependsOn("packageExe")
-//    dependsOn("packageMsi")
-//    dependsOn("packagePkg")
-//    dependsOn("packageRpm")
-//    dependsOn("packageReleaseDeb")
-}
-
-createTask<JavaExec>(
-    name = "runDesktop",
-    group = "mjdev",
-    description = "Run app"
-) {
-    mainClass = "eu.mjdev.desktop.MainKt"
-    classpath = sourceSets.main.get().runtimeClasspath
-    jvmArgs("--enable-preview")
-}
-
-createTask<JavaExec>(
-    name = "runDebug",
-    group = "mjdev",
-    description = "Run app with debug parameter."
-) {
-    mainClass = "eu.mjdev.desktop.MainKt"
-    classpath = sourceSets.main.get().runtimeClasspath
-    args = arrayListOf("--debug")
-    jvmArgs("--enable-preview")
-    // dbus-run-session mutter --wayland --nested
-}
+    alias(libs.plugins.android.application) apply false
+    alias(libs.plugins.android.library) apply false
+    alias(libs.plugins.compose) apply false
+    alias(libs.plugins.compose.compiler) apply false
+    alias(libs.plugins.kotlin.jvm) apply false
+    alias(libs.plugins.kotlin.multiplatform) apply false
+    alias(libs.plugins.app.icon.generator) apply false
+}

+ 15 - 19
buildSrc/build.gradle.kts

@@ -1,30 +1,26 @@
-repositories {
-    google()
-    mavenCentral()
-    gradlePluginPortal()
-}
-
 plugins {
     `kotlin-dsl`
-//    java
-//    `java-gradle-plugin`
+}
+
+repositories {
+    mavenCentral()
 }
 
 dependencies {
-//    implementation(files(libs.javaClass.superclass.protectionDomain.codeSource.location))
     implementation(gradleApi())
     implementation(gradleKotlinDsl())
 //    implementation(libs.gradle)
 //    implementation(libs.gradle.api)
+//    implementation(libs.gradle.kotlin.plugin)
 }
 
-//gradlePlugin {
-//    plugins {
-//        register("DesktopPlugin") {
-//            id = "DesktopPlugin"
-//            displayName = "DesktopPlugin"
-//            description = "Common application plugin to handle all stuffs needed."
-//            implementationClass = "org.mjdev.gradle.plugin.DesktopPlugin"
-//        }
-//    }
-//}
+gradlePlugin {
+    plugins {
+        register("MultiPlatformPlugin") {
+            id = "MultiPlatformPlugin"
+            displayName = "MultiPlatformPlugin"
+            description = "Common library plugin to handle all stuffs needed."
+            implementationClass = "org.mjdev.gradle.plugins.MultiPlatformPlugin"
+        }
+    }
+}

+ 8 - 0
buildSrc/settings.gradle.kts

@@ -1,3 +1,11 @@
+/*
+ *  Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
 dependencyResolutionManagement {
     versionCatalogs {
         create("libs") {

+ 23 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/AndroidConfig.kt

@@ -0,0 +1,23 @@
+package org.mjdev.gradle.extensions
+
+//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 - 70
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/DependencyExt.kt

@@ -1,70 +0,0 @@
-package org.mjdev.gradle.extensions
-
-//import org.gradle.api.artifacts.Dependency
-//import org.gradle.api.artifacts.MinimalExternalModuleDependency
-//import org.gradle.api.internal.catalog.ExternalModuleDependencyFactory
-//import org.gradle.api.provider.Provider
-//import org.gradle.kotlin.dsl.DependencyHandlerScope
-
-//object DependencyExt {
-//    private const val IMPLEMENTATION = "implementation"
-//    private const val DEBUG_IMPLEMENTATION = "debugImplementation"
-//    private const val ANDROID_TEST_IMPLEMENTATION = "androidTestImplementation"
-//    private const val TEST_IMPLEMENTATION = "testImplementation"
-//    private const val KAPT_IMPLEMENTATION = "kapt"
-//    private const val KSP_IMPLEMENTATION = "ksp"
-//
-//    fun DependencyHandlerScope.implementation(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(IMPLEMENTATION, dependency.get())
-//    }
-//
-//    fun DependencyHandlerScope.implementation(
-//        dependency: ExternalModuleDependencyFactory.DependencyNotationSupplier
-//    ) {
-//        add(IMPLEMENTATION, dependency.asProvider().get())
-//    }
-//
-//    fun DependencyHandlerScope.implementation(
-//        dependency: Dependency
-//    ) {
-//        add(IMPLEMENTATION, dependency)
-//    }
-//
-//    fun DependencyHandlerScope.implementation(
-//        dependency: String
-//    ) {
-//        add(IMPLEMENTATION, dependency)
-//    }
-//
-//    fun DependencyHandlerScope.debugImplementation(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(DEBUG_IMPLEMENTATION, dependency.get())
-//    }
-//
-//    fun DependencyHandlerScope.androidTestImplementation(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(ANDROID_TEST_IMPLEMENTATION, dependency.get())
-//    }
-//
-//    fun DependencyHandlerScope.testImplementation(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(TEST_IMPLEMENTATION, dependency.get())
-//    }
-//
-//    fun DependencyHandlerScope.ksp(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(KSP_IMPLEMENTATION, dependency.get())
-//    }
-//
-//    fun DependencyHandlerScope.kapt(
-//        dependency: Provider<MinimalExternalModuleDependency>
-//    ) {
-//        add(KAPT_IMPLEMENTATION, dependency.get())
-//    }
-//}

+ 15 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/KotlinDependencyHandlerExt.kt

@@ -0,0 +1,15 @@
+package org.mjdev.gradle.extensions
+
+object KotlinDependencyHandlerExt {
+
+//    fun KotlinDependencyHandler.kapt(
+//        dependency:String
+//    ) = configurations["kapt"].dependencies.add(dependency)
+
+//    fun DependencyHandlerScope.kapt(
+//        dependency: Provider<MinimalExternalModuleDependency>
+//    ) {
+//        configurations["kapt"].dependencies.add(dependency.get())
+//    }
+
+}

+ 6 - 82
buildSrc/src/main/kotlin/org/mjdev/gradle/extensions/ProjectExt.kt

@@ -1,91 +1,15 @@
-/*
- * Copyright (c) Milan Jurkulák 2024.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
 package org.mjdev.gradle.extensions
 
-//import org.gradle.accessors.dm.LibrariesForLibs
-import org.gradle.api.*
-//import org.gradle.internal.impldep.jakarta.xml.bind.DatatypeConverter
-import org.gradle.kotlin.dsl.*
-import java.io.File
+import org.gradle.api.Project
+import org.gradle.api.Task
 
-@Suppress("unused", "MemberVisibilityCanBePrivate")
 object ProjectExt {
-    val Project.androidStudioVersion
-        get() = project.extra.properties["android.studio.version"]
-
-//    val Project.runningFromAndroidStudio
-//        get() = DatatypeConverter.parseBoolean(project.extra.properties["android.injected.invoked.from.ide"].toString())
-
-    val Project.isAndroidStudio
-        get() = project.extra.properties.keys.contains("android.studio.version")
-
-//    val Project.libs
-//        get() = the<LibrariesForLibs>()
-
-//    val Project.libs: LibrariesForLibs get() =
-//        (this as ExtensionAware).extensions.getByName("libs") as org.gradle.accessors.dm.LibrariesForLibs
-
-//    fun Project.libs(configure: Action<LibrariesForLibs>): Unit =
-//        (this as org.gradle.api.plugins.ExtensionAware).extensions.configure("libs", configure)
-
-//    val Project.libs : VersionCatalog
-//        get() = rootProject.extensions
-//            .getByType<VersionCatalogsExtension>().named("libs")
-
-//    inline fun <reified T : Plugin<Project>> Project.apply() =
-//        project.plugins.apply(T::class.java)
-
-//    fun Project.apply(plugin: Provider<*>) =
-//        project.plugins.apply(plugin.get().toString())
-
-//    fun Project.apply(plugin: String) =
-//        project.plugins.apply(plugin)
+    val GROUP = "mjdev"
 
-    inline fun <reified T> Project.configureIfExists(fn: T.() -> Unit) {
-        extensions.findByType(T::class.java)?.fn()
-    }
-
-    @Suppress("UNUSED_PARAMETER")
-    inline fun <reified T : Task> Project.createTask(
+    inline fun <reified T : Task> Project.registerTask(
         name: String,
-        group: String = "mjdev",
-        description: String = "",
-        className: String? = T::class.simpleName,
-        configureAction: T.() -> Unit
-    ) = tasks.create<T>(name).apply {
+        group: String = GROUP
+    ) = tasks.register(name, T::class.java) {
         this.group = group
-        this.description = description
-        configureAction.invoke(this)
     }
-
-    inline fun Project.createTask(
-        name: String,
-        group: String = "mjdev",
-        description: String = "",
-        configureAction: Task.() -> Unit
-    ) = tasks.create<Task>(name).apply {
-        this.group = group
-        this.description = description
-        configureAction.invoke(this)
-    }
-
-    fun Project.resolve(relative: String): File = rootDir.resolve(File(relative))
-
-//tasks.withType(Copy::class) {
-//    from {
-//        fileTree("native") {
-//            include "*.so", "*.dylib", "*.dll"
-//        }
-//    }
-//    into {
-//        configurations.runtimeClasspath.files.collect { it.absolutePath }
-//    }
-//}
-
 }

+ 0 - 28
buildSrc/src/main/kotlin/org/mjdev/gradle/plugin/DesktopPlugin.kt

@@ -1,28 +0,0 @@
-/*
- * Copyright (c) Milan Jurkulák 2024.
- *  Contact:
- *  e: mimoccc@gmail.com
- *  e: mj@mjdev.org
- *  w: https://mjdev.org
- */
-
-package org.mjdev.gradle.plugin
-
-import org.gradle.api.Plugin
-import org.gradle.api.Project
-
-class DesktopPlugin : Plugin<Project> {
-    override fun apply(project: Project): Unit = with(project) {
-        println("Main plugin started for $name")
-//        apply(libs.plugins.kotlin.jvm)
-//        apply(libs.plugins.compose.desktop)
-//        apply(libs.plugins.kotlin.serialization)
-//        configureIfExists<DesktopExtension> {
-//            application {
-//                jvmArgs += "--enable-preview"
-//            }
-//            nativeApplication {
-//            }
-//        }
-    }
-}

+ 43 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/plugins/MultiPlatformPlugin.kt

@@ -0,0 +1,43 @@
+package org.mjdev.gradle.plugins
+
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.mjdev.gradle.tasks.DeployTask
+import org.mjdev.gradle.tasks.GenerateChangelogTask
+import  org.mjdev.gradle.extensions.ProjectExt.registerTask
+
+class MultiPlatformPlugin : Plugin<Project> {
+    override fun apply(project: Project) {
+        with(project) {
+            registerTasks()
+            beforeEvaluate {
+                onBeforeEvaluate()
+            }
+            afterEvaluate {
+                onAfterEvaluate()
+            }
+            project.configure(listOf(project)) {
+                onConfigure()
+            }
+        }
+    }
+
+    private fun Project.onConfigure() {
+//        buildscript.repositories {
+//
+//        }
+    }
+
+    private fun Project.registerTasks() {
+        registerTask<DeployTask>("deploy")
+        registerTask<GenerateChangelogTask>("changelog")
+    }
+
+    private fun Project.onBeforeEvaluate() {
+
+    }
+
+    private fun Project.onAfterEvaluate() {
+
+    }
+}

+ 73 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/tasks/DeployTask.kt

@@ -0,0 +1,73 @@
+package org.mjdev.gradle.tasks
+
+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}"
+                    )
+                }
+            }
+        }
+    }
+}

+ 74 - 0
buildSrc/src/main/kotlin/org/mjdev/gradle/tasks/GenerateChangelogTask.kt

@@ -0,0 +1,74 @@
+package org.mjdev.gradle.tasks
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.TaskAction
+import java.io.ByteArrayOutputStream
+
+@Suppress("DEPRECATION")
+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()
+    }
+}

+ 266 - 0
composeApp/build.gradle.kts

@@ -0,0 +1,266 @@
+@file:Suppress("OPT_IN_USAGE")
+
+import org.jetbrains.compose.desktop.application.dsl.TargetFormat
+import org.jetbrains.kotlin.gradle.dsl.JvmTarget
+import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig
+
+plugins {
+    alias(libs.plugins.kotlin.multiplatform)
+    alias(libs.plugins.android.application)
+    alias(libs.plugins.compose)
+    alias(libs.plugins.compose.compiler)
+    MultiPlatformPlugin
+}
+
+kotlin {
+    jvm("desktop")
+    wasmJs {
+        moduleName = "composeApp"
+        browser {
+            commonWebpackConfig {
+                outputFileName = "composeApp.js"
+                devServer = (devServer ?: KotlinWebpackConfig.DevServer()).apply {
+                    static = (static ?: mutableListOf()).apply {
+                        // Serve sources to debug inside browser
+                        add(project.projectDir.path)
+                    }
+                }
+            }
+        }
+        binaries.executable()
+    }
+    androidTarget {
+        compilerOptions {
+            jvmTarget.set(JvmTarget.JVM_17)
+        }
+    }
+    sourceSets {
+        // common dependencies
+        commonMain {
+            dependencies {
+                // reflection
+                implementation(kotlin("reflect"))
+                // base
+                implementation(projects.shared)
+                implementation(compose.runtime)
+                implementation(compose.foundation)
+                implementation(compose.material)
+                implementation(compose.ui)
+                implementation(compose.animation)
+                implementation(compose.animationGraphics)
+                implementation(compose.components.resources)
+                implementation(libs.androidx.lifecycle.viewmodel)
+                implementation(libs.androidx.lifecycle.runtime.compose)
+                // network client/s
+//                implementation(libs.bundles.ktor.common)
+                // date time
+                implementation(libs.kotlinx.datetime)
+                // images
+                implementation(libs.coil.compose)
+                implementation(libs.coil.svg)
+                implementation(libs.coil.mp)
+                // charts
+                implementation(libs.charts)
+                // qr code
+                implementation(libs.qrose)
+                // json
+                implementation(libs.google.gson)
+                // desktop file linux todo move to desktop
+                implementation(libs.ini4j)
+                // tts
+                implementation(libs.tts)
+                // flowmvi
+//                implementation(libs.flowmvi.core)
+//                implementation(libs.flowmvi.compose)
+//                implementation(libs.flowmvi.android)
+//                implementation(libs.flowmvi.savedstate)
+//                implementation(libs.flowmvi.debugger)
+                // locale
+//            implementation(libs.i18n4k)
+                // mo po gettext
+                implementation(libs.gettext.lib)
+                // timeline
+                implementation(libs.jetlime)
+                // event bus
+//            implementation ("com.github.nort3x:event4k:1.0.1")
+                // anims kottie
+//            implementation("io.github.ismai117:kottie:2.0.1")
+                // kbd
+//            implementation("com.github.animeshz:keyboard-kt-jvm:0.3.3")
+                // mouse
+//            implementation("com.github.animeshz:mouse-kt-jvm:0.3.3")
+                // rss
+//            implementation("com.prof18.rssparser:rssparser:6.0.8")
+                // korlibs
+//            implementation("com.soywiz:korlibs-io:6.0.0")
+//            implementation("com.soywiz:korlibs-io-fs:6.0.0")
+                // rich text editor
+//            implementation("com.mohamedrejeb.richeditor:richeditor-compose:1.0.0-rc10")
+//                implementation (libs.popkorn)
+//                kapt(libs.popkorn.compiler)
+//                implementation("io.github.khubaibkhan4:mediaplayer-kmp:2.0.6")
+                // strings
+                implementation(libs.human.readable)
+                // alerts
+                implementation(libs.alert.kmp)
+                // tor
+                // implementation("io.matthewnelson.kmp-tor:runtime:2.1.0")
+                implementation("com.squareup.okio:okio:3.10.2")
+            }
+        }
+        // android dependencies
+        androidMain {
+            dependencies {
+                // reflection
+                implementation(kotlin("reflect"))
+                // preview
+                implementation(compose.preview)
+                // activity
+                implementation(libs.androidx.activity.compose)
+                // network client
+                implementation(libs.okhttp3.client)
+                // images
+                implementation(libs.coil.okhttp)
+                // splash
+                implementation(libs.androidx.core.splashscreen)
+                // charts
+                implementation(libs.charts)
+                // emoji
+                implementation(libs.androidx.emoji2)
+            }
+        }
+        // wasm dependencies
+        wasmJsMain {
+            dependencies {
+//                implementation(libs.coil.ktor.wasm.js)
+                // charts
+//                implementation(libs.charts)
+//                implementation("com.squareup.okio:okio-nodefilesystem:3.10.2")
+            }
+        }
+        // desktop dependencies
+        val desktopMain by getting
+        desktopMain.dependencies {
+            // reflection
+            implementation(kotlin("reflect"))
+            // preview
+            implementation(compose.preview)
+            // desktop
+            implementation(compose.desktop.currentOs)
+            // coroutines
+            implementation(libs.kotlinx.coroutines.swing)
+            // network
+//            implementation(libs.ktor.client.java)
+            implementation(libs.okhttp3.client)
+//            implementation(libs.coil.ktor)
+            implementation(libs.coil.okhttp)
+            // charts
+//            implementation(libs.charts)
+            // gemini ai
+            implementation(libs.google.generative.ai)
+            // chatgpt
+            implementation(libs.ychat)
+            // desktop files
+            implementation(libs.ini4j)
+            // charts
+            implementation(libs.charts)
+        }
+    }
+}
+
+// android app config
+android {
+    namespace = "org.mjdev.desktop"
+    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/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())
+    }
+    packaging {
+        resources {
+            excludes += "/META-INF/{AL2.0,LGPL2.1}"
+        }
+    }
+    buildTypes {
+        getByName("release") {
+            isMinifyEnabled = false
+        }
+    }
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_17
+        targetCompatibility = JavaVersion.VERSION_17
+    }
+    buildFeatures {
+        compose = true
+    }
+    dependencies {
+        debugImplementation(libs.androidx.ui.tooling)
+    }
+}
+
+//dependencies {
+//    implementation(libs.generativeai)
+//}
+
+
+// desktop app config
+compose {
+    desktop {
+        group = libs.versions.app.pkg.name.get()
+        version = libs.versions.app.pkg.version.get()
+        application {
+            mainClass = "org.mjdev.desktop.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()
+                outputBaseDir.set(project.rootDir.resolve("packages"))
+//                macOS {
+//                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.icns"))
+//                }
+//                windows {
+//                    iconFile.set(project.rootDir.resolve("icons").resolve("icon.ico"))
+//                    exePackageVersion = "1.0.0"
+//                    msiPackageVersion = "1.0.0"
+//                }
+                linux {
+//                    iconFile.set(
+//                        project.rootDir
+//                            .resolve("composeApp")
+//                            .resolve("src")
+//                            .resolve("commonMain")
+//                            .resolve("resources")
+//                            .resolve("drawable")
+//                            .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()
+                }
+                targetFormats(
+                    TargetFormat.Dmg,
+                    TargetFormat.Msi,
+                    TargetFormat.Deb,
+                    TargetFormat.Exe,
+                    TargetFormat.AppImage,
+                    TargetFormat.Pkg,
+                    TargetFormat.Rpm
+                )
+                buildTypes.release.proguard {
+                    configurationFiles.from("compose-desktop.pro")
+                }
+//                appResourcesRootDir.set(project.rootDir.resolve("resources"))
+            }
+        }
+    }
+}

+ 72 - 0
composeApp/src/androidMain/AndroidManifest.xml

@@ -0,0 +1,72 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    tools:ignore="SelectedPhotoAccess">
+
+    <uses-feature
+        android:name="android.software.leanback"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.touchscreen"
+        android:required="false" />
+
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission
+        android:name="android.permission.QUERY_ALL_PACKAGES"
+        tools:ignore="QueryAllPackagesPermission" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
+        tools:ignore="ScopedStorage" />
+    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
+    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
+    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
+
+    <application
+        android:allowBackup="true"
+        android:banner="@mipmap/ic_banner"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:networkSecurityConfig="@xml/network_security_config"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/Theme.SplashScreen.Splash"
+        android:usesCleartextTraffic="true">
+
+        <activity
+            android:name=".MainActivity"
+            android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|mnc|colorMode|density|fontScale|fontWeightAdjustment|keyboard|layoutDirection|locale|mcc|navigation|smallestScreenSize|touchscreen|uiMode"
+            android:exported="true">
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LAUNCHER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.LEANBACK_LAUNCHER" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+
+                <category android:name="android.intent.category.HOME" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+            <intent-filter>
+                <category android:name="android.intent.category.SECONDARY_HOME" />
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+
+        </activity>
+
+    </application>
+
+</manifest>

+ 84 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/MainActivity.kt

@@ -0,0 +1,84 @@
+package org.mjdev.desktop
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import androidx.activity.SystemBarStyle
+import androidx.activity.compose.setContent
+import androidx.activity.enableEdgeToEdge
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.WindowInsets
+import androidx.compose.foundation.layout.consumeWindowInsets
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.safeDrawing
+import androidx.compose.foundation.layout.windowInsetsPadding
+import androidx.compose.material.Scaffold
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.core.provider.FontRequest
+import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.core.view.WindowCompat
+import androidx.core.view.WindowInsetsCompat
+import androidx.core.view.WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+import androidx.emoji2.text.EmojiCompat
+import androidx.emoji2.text.FontRequestEmojiCompatConfig
+import org.mjdev.desktop.context.DesktopContext.Companion.rememberDesktopContext
+import org.mjdev.desktop.context.LocalDesktopContext
+import org.mjdev.desktop.helpers.theme.DesktopTheme
+
+class MainActivity : ComponentActivity() {
+    override fun onCreate(savedInstanceState: Bundle?) {
+        initEmojiCompat()
+        installSplashScreen()
+        super.onCreate(savedInstanceState)
+        enableEdgeToEdge(
+            statusBarStyle = SystemBarStyle.dark(
+                android.graphics.Color.TRANSPARENT,
+            ),
+            navigationBarStyle = SystemBarStyle.dark(
+                android.graphics.Color.TRANSPARENT,
+            )
+        )
+        WindowCompat.getInsetsController(window, window.decorView).apply {
+            hide(WindowInsetsCompat.Type.statusBars())
+            hide(WindowInsetsCompat.Type.navigationBars())
+            systemBarsBehavior = BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
+        }
+        setContent {
+            CompositionLocalProvider(
+                LocalDesktopContext provides rememberDesktopContext(baseContext)
+            ) {
+                DesktopTheme {
+                    Scaffold(
+                        modifier = Modifier.fillMaxSize()
+                    ) { paddingValues ->
+                        Box(
+                            modifier = Modifier
+                                .fillMaxSize()
+                                .padding(paddingValues)
+                                .consumeWindowInsets(paddingValues)
+                                .windowInsetsPadding(
+                                    WindowInsets.safeDrawing
+                                )
+                        ) {
+                            MainView()
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun initEmojiCompat() {
+        EmojiCompat.init(
+            FontRequestEmojiCompatConfig(
+                this, FontRequest(
+                    "com.google.android.gms.fonts",
+                    "com.google.android.gms",
+                    "Noto Color Emoji Compat",
+                    emptyList()
+                )
+            )
+        )
+    }
+}

+ 123 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/MainView.kt

@@ -0,0 +1,123 @@
+package org.mjdev.desktop
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.tooling.preview.Devices
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.max
+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.desktoppanel.DesktopPanel
+import org.mjdev.desktop.components.sliding.base.VisibilityState.Companion.rememberVisibilityState
+import org.mjdev.desktop.components.tooltip.TooltipState
+import org.mjdev.desktop.components.tooltip.rememberTooltipState
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.extensions.Compose.isDesign
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.LaunchedEffect.runAsync
+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)
+    }
+//    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, 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,
+        )
+        AppsMenu(
+            modifier = Modifier
+                .wrapContentSize()
+                .align(Alignment.BottomStart),
+            appsMenuState = appsMenuState,
+            panelState = panelState,
+            onAppClick = { app ->
+                runAsync {
+                    app.start()
+                    appsMenuState.hide()
+                }
+            },
+            onAppContextMenuClick = {},
+            onCategoryContextMenuClick = {},
+            onUserAvatarClick = {},
+            onActionClick = {},
+            onTooltip = onTooltip,
+        )
+    }
+}
+
+@Suppress("PreviewDeviceShouldUseNewSpec")
+@Preview(device = Devices.TABLET)
+@Composable
+fun MainViewPreview() = preview {
+    MainView()
+}

+ 145 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/DesktopContext.kt

@@ -0,0 +1,145 @@
+package org.mjdev.desktop.context
+
+import android.content.Context
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import coil3.ImageLoader
+import coil3.PlatformContext
+import coil3.compose.LocalPlatformContext
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import org.mjdev.desktop.data.Category
+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.IDesktopContext
+import org.mjdev.desktop.interfaces.ILocale
+import org.mjdev.desktop.interfaces.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.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.AIManager.Companion.AiManager
+import org.mjdev.desktop.managers.ai.plugins.AiPluginGemini
+import org.mjdev.desktop.managers.ai.stt.STTPluginEmpty
+import org.mjdev.desktop.managers.ai.tts.TTSPluginMain
+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.palette.Palette
+import org.mjdev.desktop.managers.theme.ThemeManager
+import kotlin.reflect.full.companionObject
+
+@Suppress("unused")
+class DesktopContext(
+    val context: Context? = null,
+    override val scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
+    override val imageLoader: ImageLoader? = null,
+    val isInDesign: Boolean = false,
+    override val platformContext: PlatformContext? = null
+) : IDesktopContext() {
+
+    val androidContext
+        get() = context
+
+    override var isFirstStart: Boolean = false
+    override var isInstalled: Boolean = false
+    override val machineName: String = ""
+    override val appArgs: List<String> = emptyList()
+    override val currentUser: IUser = allUsers.firstOrNull { u -> u.isLoggedIn } ?: User.Nobody
+    override val currentLocale: ILocale = ILocale.DEFAULT
+    override val theme: ITheme = ITheme.DEFAULT
+    override val appCategories: List<Category> = emptyList()
+    override val allApps: List<IApp> = emptyList()
+    override val favoriteApps: List<IApp> = emptyList()
+    override val isLandscapeMode: Boolean = true
+
+    override val containerSize: DpSize = androidContext?.resources
+        ?.displayMetrics
+        ?.let { dm ->
+            DpSize(dm.widthPixels.dp, dm.heightPixels.dp)
+        } ?: DpSize(1024.dp, 640.dp)
+
+    val allUsers
+        get() = User.allUsers(this)
+
+    override suspend fun logOut() {
+    }
+
+    override suspend fun restart() {
+    }
+
+    override suspend fun suspend() {
+    }
+
+    override suspend fun shutdown() {
+    }
+
+    override suspend fun open(what: Any?) {
+    }
+
+    override suspend fun login(uName: String, uPasswd: String): Boolean {
+        return false
+    }
+
+    override suspend fun logout(): Boolean {
+        return 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(this).apply {
+            // todo user can configure
+            pluginAI = AiPluginGemini(this@DesktopContext)
+            pluginTTS = TTSPluginMain(this@DesktopContext)
+            pluginSTT = STTPluginEmpty(this@DesktopContext)
+        }
+
+        IAppsManager::class -> AppsManager(this)
+        IThemeManager::class -> ThemeManager(this)
+        IProcessManager::class -> ProcessManager(this)
+        else -> cls.companionObject?.members?.first {
+            it.name == "EMPTY"
+        }?.call() as IDelegate
+    }
+
+    companion object {
+        @Composable
+        fun rememberDesktopContext(
+            context: Context,
+            imageLoader: ImageLoader = asyncImageLoader(),
+            platformContext: PlatformContext = LocalPlatformContext.current,
+            scope: CoroutineScope = rememberCoroutineScope(),
+            isInDesign: Boolean = isDesign,
+        ) = remember {
+            Log.i("default initialized desktop provider created")
+            DesktopContext(
+                context = context,
+                scope = scope,
+                imageLoader = imageLoader,
+                isInDesign = isInDesign,
+                platformContext = platformContext
+            )
+        }
+    }
+}

+ 9 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/context/LocalDesktopContext.android.kt

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

+ 63 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/App.kt

@@ -0,0 +1,63 @@
+package org.mjdev.desktop.data
+
+import android.annotation.SuppressLint
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.LauncherActivityInfo
+import android.content.pm.PackageInfo
+import android.os.Build
+import androidx.annotation.RequiresApi
+import org.mjdev.desktop.extensions.JSonExt.toJson
+import org.mjdev.desktop.interfaces.IApp
+
+
+@Suppress("unused")
+class App(
+    private val context: Context?,
+    private val info: LauncherActivityInfo,
+    override val name: String = info.label.toString(),
+    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 = info.toJson(),
+    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)
+            setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
+            setComponent(info.componentName)
+        })
+    }
+
+    override fun toString(): String = name
+
+    private fun PackageInfo.isSystemPackage(): Boolean {
+        return ((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
+            } catch (e: Throwable) {
+                Category.Empty
+            }
+        }
+    }
+}

+ 140 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/Theme.kt

@@ -0,0 +1,140 @@
+package org.mjdev.desktop.data
+
+//import androidx.compose.runtime.mutableStateOf
+//import androidx.compose.ui.graphics.Color
+//import androidx.compose.ui.unit.Dp
+//import androidx.compose.ui.unit.DpSize
+//import androidx.compose.ui.unit.dp
+//import org.mjdev.desktop.extensions.Colors.SuperDarkGray
+//import org.mjdev.desktop.fonts.MaterialSymbolsSharp
+//import org.mjdev.desktop.interfaces.IFont
+//import org.mjdev.desktop.interfaces.ITheme
+//import org.mjdev.desktop.interfaces.IUser
+
+//// todo all components customizable
+//@Suppress("unused", "MemberVisibilityCanBePrivate")
+//class Theme(
+//    backgroundColor: Color,
+//    backgroundRotationDelay: Long,
+//
+//    panelLocation: PanelLocation,
+//    panelDividerWidth: Dp,
+//    panelContentPadding: Dp,
+//    panelHideDelay: Long,
+//
+//    controlCenterLocation: ControlCenterLocation,
+//    controlPanelHideDelay: Long,
+//    controlCenterExpandedWidthPercent: Int,
+//    controlCenterDividerColor: Color = backgroundColor,
+//    controlCenterDividerWidth: Dp,
+//    controlCenterIconColor: Color,
+//    controlCenterIconSize: DpSize,
+//    controlCenterBackgroundAlpha: Float,
+//
+//    iconSet: IFont,
+//
+//    appMenuMinWidthRatio:Float,
+//    appMenuMinHeightRatio:Float,
+//    appMenuOuterPadding: Dp,
+//) : ITheme {
+//
+//    val backgroundColorState = mutableStateOf(backgroundColor)
+//    val backgroundRotationDelayState = mutableStateOf(backgroundRotationDelay)
+//
+//    val panelLocationState = mutableStateOf(panelLocation)
+//    val panelHideDelayState = mutableStateOf(panelHideDelay)
+//    val panelDividerWidthState = mutableStateOf(panelDividerWidth)
+//    val panelContentPaddingState = mutableStateOf(panelContentPadding)
+//
+//    val controlCenterLocationState = mutableStateOf(controlCenterLocation)
+//    val controlPanelHideDelayState = mutableStateOf(controlPanelHideDelay)
+//    var controlCenterExpandedWidthState = mutableStateOf(controlCenterExpandedWidthPercent)
+//    val controlCenterDividerColorState = mutableStateOf(controlCenterDividerColor)
+//    val controlCenterDividerWidthState = mutableStateOf(controlCenterDividerWidth)
+//    val controlCenterIconColorState = mutableStateOf(controlCenterIconColor)
+//    val controlCenterIconSizeState = mutableStateOf(controlCenterIconSize)
+//
+//    val iconSetState = mutableStateOf(iconSet)
+//
+//    val appMenuMinWidthState = mutableStateOf(appMenuMinWidthRatio)
+//    val appMenuMinHeightState = mutableStateOf(appMenuMinHeightRatio)
+//    val appMenuOuterPaddingState = mutableStateOf(appMenuOuterPadding)
+//
+//    val controlCenterBackgroundAlphaState = mutableStateOf(controlCenterBackgroundAlpha)
+//
+//    override var backgroundColor = backgroundColorState.value
+//
+//    override var panelLocation = panelLocationState.value
+//
+//    override var panelDividerWidth = panelDividerWidthState.value
+//
+//    override var panelContentPadding = panelContentPaddingState.value
+//
+//    override var panelHideDelay = panelHideDelayState.value
+//
+//    override var controlCenterLocation = controlCenterLocationState.value
+//
+//    override var controlPanelHideDelay = controlPanelHideDelayState.value
+//
+//    override var controlCenterExpandedWidthPercent = controlCenterExpandedWidthState.value
+//
+//    override var controlCenterDividerColor = controlCenterDividerColorState.value
+//
+//    override var controlCenterDividerWidth = controlCenterDividerWidthState.value
+//
+//    override var controlCenterIconColor = controlCenterIconColorState.value
+//
+//    override var controlCenterIconSize = controlCenterIconSizeState.value
+//
+//    override var controlCenterBackgroundAlpha = controlCenterBackgroundAlphaState.value
+//
+//    override var iconSet : IFont = iconSetState.value
+//
+//    override var backgroundRotationDelay = backgroundRotationDelayState.value
+//
+//    override var appMenuOuterPadding = appMenuOuterPaddingState.value
+//
+//    override var appMenuMinWidthRatio: Float = appMenuMinWidthState.value
+//
+//    override var appMenuMinHeightRatio: Float = appMenuMinHeightState.value
+//
+//    override fun dispose() {
+//        // todo
+//    }
+//
+//    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
+//        )
+//
+//        // todo load from user settings
+//        fun load(
+//            user: IUser
+//        ): Theme = themeCache[user.userName] ?: DEFAULT.apply {
+//            themeCache[user.userName] = this
+//        }
+//    }
+//}

+ 86 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/User.kt

@@ -0,0 +1,86 @@
+package org.mjdev.desktop.data
+
+import android.accounts.Account
+import android.accounts.AccountManager
+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.interfaces.IDesktopContext
+import org.mjdev.desktop.interfaces.ITheme
+import org.mjdev.desktop.interfaces.IUser
+import org.mjdev.desktop.interfaces.IUserDirs
+import java.io.File
+
+class User(
+    val account: Account? = null,
+) : IUser {
+    override val userName: String
+        get() =  account?.name ?: ""
+    override val password: String
+        get() = "" // todo
+    override val uid: String
+        get() = "" // todo
+    override val gid: String
+        get() = "" // todo
+    override val description: String
+        get() = "" // todo
+    override val home: String
+        get() = "/sdcard/" // todo
+    override val shell: String
+        get() = "" // todo
+    override val picture: Any
+        get() =  AccountCircle // todo
+    override val homeDir: Path
+        get() = Environment.getExternalStorageDirectory().path.toPath()
+    override val isLoggedIn
+        get() = isLoggedIn(userName) // todo
+    override val userDirs: IUserDirs
+        get() = UserDirs(homeDir)
+    override val config: DesktopConfig
+        get() = DesktopConfig.load(this) // todo load
+    override val theme: ITheme
+        get() =  ITheme.DEFAULT // Theme.load(this) // todo load
+    override val backgrounds
+        get() = config.desktopBackgrounds
+
+    override fun toString(): String {
+        return userName
+    }
+
+//    fun login(
+//        context: IDesktopContext,
+//        password: String
+//    ): Flow<Boolean> = flow { context.login(userName, password) }
+
+    companion object {
+        val Nobody = User()
+
+        val DEFAULT = Nobody
+
+        fun isLoggedIn(
+            uname: String
+        ): Boolean = false // todo
+
+        fun loadPicture(
+            home: 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
+        }
+
+        fun allUsers(
+            context: IDesktopContext
+        ): List<User> = if (context is DesktopContext) {
+            AccountManager.get(context.context).accounts.map { a ->
+                User(a)
+            }
+        } else emptyList()
+    }
+}

+ 46 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/data/UserDirs.kt

@@ -0,0 +1,46 @@
+package org.mjdev.desktop.data
+
+import okio.Path
+import org.mjdev.desktop.extensions.PathExt.mkdirs
+import org.mjdev.desktop.interfaces.IUserDirs
+
+@Suppress("unused")
+class UserDirs(
+    override val homeDirectory: Path
+) : IUserDirs {
+
+    operator fun get(
+        name: String
+    ): Path = homeDirectory.resolve(name)
+
+    init {
+        runCatching { desktopDirectory.mkdirs() }
+        runCatching { downloadDirectory.mkdirs() }
+        runCatching { templatesDirectory.mkdirs() }
+        runCatching { publicShareDirectory.mkdirs() }
+        runCatching { documentsDirectory.mkdirs() }
+        runCatching { musicDirectory.mkdirs() }
+        runCatching { picturesDirectory.mkdirs() }
+        runCatching { videosDirectory.mkdirs() }
+        runCatching { backgroundsDirectory.mkdirs() }
+    }
+
+    override val desktopDirectory: Path
+        get() = this[IUserDirs.DESKTOP_DIR]
+    override val downloadDirectory: Path
+        get() = this[IUserDirs.DOWNLOAD_DIR]
+    override val templatesDirectory: Path
+        get() = this[IUserDirs.TEMPLATES_DIR]
+    override val publicShareDirectory
+        get() = this[IUserDirs.PUBLICSHARE_DIR]
+    override val documentsDirectory
+        get() = this[IUserDirs.DOCUMENTS_DIR]
+    override val musicDirectory
+        get() = this[IUserDirs.MUSIC_DIR]
+    override val picturesDirectory
+        get() = this[IUserDirs.PICTURES_DIR]
+    override val videosDirectory
+        get() = this[IUserDirs.VIDEOS_DIR]
+    override val backgroundsDirectory: Path
+        get() = this[IUserDirs.BACKGROUNDS_DIR]
+}

+ 37 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/ImageBitmapExt.kt

@@ -0,0 +1,37 @@
+package org.mjdev.desktop.extensions
+
+import android.graphics.Bitmap
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asAndroidBitmap
+import androidx.compose.ui.graphics.asImageBitmap
+import coil3.request.ImageRequest
+import coil3.toBitmap
+import okio.Path
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
+actual object ImageBitmapExt {
+
+    actual fun ImageBitmap.cut(
+        x: Int,
+        y: Int,
+        width: Int,
+        height: Int
+    ): ImageBitmap = Bitmap.createBitmap(asAndroidBitmap(), x, y, width, height).asImageBitmap()
+
+    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()
+            }
+    }.getOrNull()
+
+}

+ 13 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/extensions/JSonExt.kt

@@ -0,0 +1,13 @@
+package org.mjdev.desktop.extensions
+
+import com.google.gson.GsonBuilder
+
+object JSonExt {
+    val gson = GsonBuilder().create()
+
+    fun <T : Any> T.toJson() =
+        gson.toJson(this)
+
+    inline fun <reified T : Any> String.fromJson(): T =
+        gson.fromJson(this, T::class.java)
+}

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

@@ -0,0 +1,11 @@
+package org.mjdev.desktop.helpers.streams
+
+class ResourceStream(
+    private val resName: String,
+    val bytes: ByteArray = ByteArray(0) // to
+//        useResource(resName) { stream ->
+//        stream.readAllBytes()
+//    }
+) {
+    val string get() = String(bytes)
+}

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

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

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

@@ -0,0 +1,25 @@
+package org.mjdev.desktop.helpers.system.meminfo
+
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Context.ACTIVITY_SERVICE
+import org.mjdev.desktop.context.DesktopContext
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+class MemInfoAndroid(
+    context: IDesktopContext
+) : MemInfoStub(context) {
+    val aContext: Context?
+        get() = (context as? DesktopContext)?.context
+    val activityManager: ActivityManager? by lazy {
+        aContext?.getSystemService(ACTIVITY_SERVICE) as ActivityManager
+    }
+    private val meminfo: ActivityManager.MemoryInfo
+        get() = ActivityManager.MemoryInfo().apply {
+            activityManager?.getMemoryInfo(this)
+        }
+
+    override val free: Double = meminfo.availMem.toDouble()
+
+    override val total: Double = meminfo.totalMem.toDouble()
+}

+ 70 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/AIManager.kt

@@ -0,0 +1,70 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.ai
+
+import org.mjdev.desktop.managers.ai.plugins.AiPluginEmpty
+import org.mjdev.desktop.managers.ai.stt.STTPluginEmpty
+import org.mjdev.desktop.managers.ai.stt.base.STTPlugin
+import org.mjdev.desktop.managers.ai.tts.base.TTSPlugin
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.managers.ai.actions.ActionCalculator
+import org.mjdev.desktop.managers.ai.actions.ActionsProvider
+import org.mjdev.desktop.managers.ai.actions.base.ActionException.ActionNone
+import org.mjdev.desktop.managers.ai.plugins.AiPluginGemini
+import org.mjdev.desktop.managers.ai.tts.TTSPluginMain
+
+@Suppress("MemberVisibilityCanBePrivate", "unused", "FunctionName")
+class AIManager(
+    val context: IDesktopContext,
+    var pluginAI: AIPlugin,
+    var pluginSTT: STTPlugin,
+    var pluginTTS: TTSPlugin,
+    var actions: ActionsProvider
+) : IAiManager {
+
+    override val isAvailable: () -> Boolean = { pluginAI !is AiPluginEmpty }
+
+    override fun ask(
+        question: String,
+        block: (question: String, result: String) -> Unit
+    ) {
+        context.scope.launch(Dispatchers.Default) {
+            val actResult = actions.tryAction(question)
+            if (actResult is ActionNone) {
+                pluginAI.ask(question).also { result ->
+                    block.invoke(question, result)
+                }
+            } else {
+                actResult.message?.also { message ->
+                    block.invoke(question, message)
+                }
+            }
+        }
+    }
+
+    override fun talk(
+        text: String,
+        clearQueue: Boolean
+    ) = pluginTTS.talk(text, clearQueue)
+
+    companion object {
+        fun AiManager(
+            context: IDesktopContext,
+            ai: AIPlugin = AiPluginGemini(context),
+            stt: STTPlugin = STTPluginEmpty(context),
+            tts: TTSPlugin = TTSPluginMain(context),
+            actions: ActionsProvider = ActionsProvider(context) {
+                ActionCalculator // todo infix fnc
+            }
+        ) = AIManager(context, ai, stt, tts, actions)
+    }
+}

+ 50 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/plugins/AiPluginGemini.kt

@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.ai.plugins
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.managers.ai.AIPlugin
+
+/*
+* Gemini key : https://makersuite.google.com/app/apikey
+* */
+class AiPluginGemini(
+    private val context: IDesktopContext,
+    private val scope: CoroutineScope = context.scope
+) : AIPlugin {
+//    private val key = loadKey("gemini")
+//    private val generativeModel: GenerativeModel? by lazy {
+//        if (key.isNotEmpty()) {
+//            GenerativeModel(
+//                modelName = "gemini-1.5-pro-latest",
+//                apiKey = key
+//            )
+//        } else null
+//    }
+
+    override suspend fun ask(question: String): String = scope.async {
+        var error: Throwable? = null
+//        runCatching {
+//            if (key.isEmpty()) {
+//                throw (Exception("Error: No gemini api key provided, pleas read manual and provide Your api key."))
+//            } else {
+//                generativeModel?.generateContent(
+//                    content {
+//                        text(question)
+//                    }
+//                )?.text
+//            }
+//        }.onFailure { e ->
+//            error = e
+//        }.getOrNull() ?: error?.message ?: ""
+        ""
+    }.await()
+}

+ 37 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/ai/tts/TTSPluginMain.kt

@@ -0,0 +1,37 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.ai.tts
+
+import org.mjdev.desktop.log.Log
+import org.mjdev.desktop.managers.ai.tts.base.TTSPlugin
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import nl.marc_apps.tts.TextToSpeechEngine
+import org.mjdev.desktop.context.DesktopContext
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+@Suppress("unused")
+class TTSPluginMain(
+    val context: IDesktopContext
+) : TTSPlugin {
+    private val textToSpeech = context.scope.async {
+        nl.marc_apps.tts.TextToSpeechFactory(
+            (context as DesktopContext).androidContext!!,
+            TextToSpeechEngine.SystemDefault
+        ).createOrNull()
+    }
+
+    override fun talk(text: String, clearQueue: Boolean) {
+        Log.i("talking: $text")
+        context.scope.launch(Dispatchers.Default) {
+            textToSpeech.await()?.say(text, clearQueue)
+        }
+    }
+}

+ 92 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/apps/AppsManager.kt

@@ -0,0 +1,92 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.apps
+
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.os.Environment
+import android.os.UserHandle
+import android.os.UserManager
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.runBlocking
+import okio.Path.Companion.toPath
+import org.mjdev.desktop.context.DesktopContext
+import org.mjdev.desktop.data.App
+import org.mjdev.desktop.extensions.MutableStateExt.mutableStateListFlow
+import org.mjdev.desktop.extensions.PathExt.filesOnly
+import org.mjdev.desktop.extensions.PathExt.sortedByName
+import org.mjdev.desktop.interfaces.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
+) : IAppsManager {
+    val TAG = AppsManager::class.simpleName
+
+    private val androidContext by lazy {
+        (context as? DesktopContext)?.context
+    }
+    private val userManger by lazy {
+        androidContext?.getSystemService(Context.USER_SERVICE) as UserManager
+    }
+    private val launcherApps by lazy {
+        androidContext?.getSystemService(LauncherApps::class.java)
+    }
+
+    val userHandles: List<UserHandle>
+        get() = userManger.getUserProfiles()
+
+    override val currentLocale: ILocale
+        get() = Locale.getDefault().toILocale()
+
+    private val allAppsState = mutableStateListFlow {
+        mutableListOf<App>().apply {
+            userHandles.forEach { handle ->
+                launcherApps?.getActivityList(null, handle)?.map { aInfo ->
+                    App(androidContext, aInfo)
+                }?.also { apps ->
+                    addAll(apps)
+                }
+            }
+        }
+    }
+
+    override val allApps: List<App>
+        get() = runBlocking {
+            allAppsState.firstOrNull() ?: emptyList()
+        }
+
+    val backgrounds by lazy {
+        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
+            .absolutePath
+            .toPath()
+            .filesOnly
+            .sortedByName()
+    }
+
+    override val categories
+        get() = allApps.asSequence().flatMap { app ->
+            app.categories
+        }.distinct().toList().sortedBy { c ->
+            c.name
+        }.sortedByDescending { c ->
+            c.priority
+        }.toList()
+
+    override val favoriteApps: List<App>
+        get() = emptyList()
+}
+
+private fun Locale.toILocale(): ILocale = ILocale.from(displayCountry, displayLanguage)
+
+

+ 12 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManager.kt

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

+ 58 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/connectivity/ConnectivityManagerStub.kt

@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.connectivity
+
+import org.mjdev.desktop.data.EthNetwork
+import org.mjdev.desktop.data.WifiNetwork
+import org.mjdev.desktop.data.BthDevice
+import org.mjdev.desktop.data.NetDevice
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+open class ConnectivityManagerStub(
+    private val context: IDesktopContext
+) : IConnectivityManager {
+    override val allDevices: MutableMap<String, NetDevice> = mutableMapOf()
+
+    override val ethDevices: MutableMap<String, NetDevice> = mutableMapOf()
+
+    override val wifiDevices: MutableMap<String, NetDevice> = mutableMapOf()
+
+    override val bthDevices: MutableMap<String, NetDevice> = mutableMapOf()
+
+    override val connectedDevices: MutableMap<String, NetDevice> = mutableMapOf()
+
+    override val ethNetworks: MutableMap<String, EthNetwork> = mutableMapOf()
+
+    override val wifiNetworks: MutableMap<String, WifiNetwork> = mutableMapOf()
+
+    override val bthNetworks: MutableMap<String, BthDevice> = mutableMapOf()
+
+    override val isWifiAdapterAvailable: Boolean
+        get() = wifiDevices.isNotEmpty()
+
+    override val isEthAdapterAvailable
+        get() = ethDevices.isNotEmpty()
+
+    override val isBthAdapterAvailable
+        get() = bthDevices.isNotEmpty()
+
+    override val hasConnectedDevices
+        get() = connectedDevices.isNotEmpty()
+
+    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)
+}

+ 11 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/keys/KeysManager.kt

@@ -0,0 +1,11 @@
+package org.mjdev.desktop.managers.keys
+
+import org.mjdev.desktop.helpers.streams.ResourceStream
+
+class KeysManager {
+
+    fun loadKey(key: String): String = runCatching {
+        ResourceStream("keys/$key.key").string
+    }.getOrNull().orEmpty()
+
+}

+ 22 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/OSManager.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.os
+
+import org.mjdev.desktop.managers.os.base.OSManagerStub
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+@Suppress("FunctionName")
+fun OsManager(
+    context: IDesktopContext
+) = //when (hostOs) {
+// todo other platforms
+//OS.Linux -> OSManagerLinux(context)
+    //else ->
+    OSManagerStub(context)
+//}

+ 31 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/os/base/OSManagerStub.kt

@@ -0,0 +1,31 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.os.base
+
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.managers.os.IOSManager
+
+open class OSManagerStub(
+    private val context: IDesktopContext
+) : IOSManager {
+    open val prettyName: String = ""
+    open val name: String = ""
+    open val versionId: String = ""
+    open val version: String = ""
+    open val versionCodeName: String = ""
+    open val id: String = ""
+    open val idLike: String = ""
+    open val homeUrl: String = ""
+    open val supportUrl: String = ""
+    open val bugReportUrl: String = ""
+    open val privacyPolicyUrl: String = ""
+    open val codename: String = ""
+    open val logo: String = ""
+    override val machineName: String = ""
+}

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

@@ -0,0 +1,39 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.processes
+
+import org.mjdev.desktop.interfaces.IApp
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.interfaces.IProcessListener
+import org.mjdev.desktop.managers.process.IProcessManager
+
+// todo dbus
+@Suppress("unused")
+class ProcessManager(
+    private val context: IDesktopContext,
+    private val checkDelay: Long = 1000L
+) : IProcessManager {
+    private val listeners = mutableListOf<IProcessListener>()
+
+    override val size: Int get() = 0
+
+    override fun addListener(listener: IProcessListener) {
+        listeners.add(listener)
+    }
+
+    override fun removeListener(listener: IProcessListener) {
+        listeners.remove(listener)
+    }
+
+    override fun dispose() {
+    }
+
+    // todo
+    override fun hasAppProcess(app: IApp?): Boolean = false
+}

+ 22 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/ThemeManager.kt

@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.theme
+
+import org.mjdev.desktop.managers.theme.base.ThemeManagerStub
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+// todo other platforms
+@Suppress("FunctionName")
+fun ThemeManager(
+    context: IDesktopContext
+) = //when (hostOs) {
+//    OS.Linux -> ThemeManagerLinux(context)
+//    else ->
+        ThemeManagerStub(context)
+//}

+ 21 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/managers/theme/base/ThemeManagerStub.kt

@@ -0,0 +1,21 @@
+/*
+ * Copyright (c) Milan Jurkulák 2024.
+ *  Contact:
+ *  e: mimoccc@gmail.com
+ *  e: mj@mjdev.org
+ *  w: https://mjdev.org
+ */
+
+package org.mjdev.desktop.managers.theme.base
+
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.managers.theme.IThemeManager
+
+@Suppress("unused")
+open class ThemeManagerStub(
+    val context: IDesktopContext,
+) : IThemeManager {
+    override fun createFromPalette() {}
+
+    override fun clearSystemTheme() {}
+}

+ 58 - 0
composeApp/src/androidMain/kotlin/org/mjdev/desktop/windows/ChromeWindow.kt

@@ -0,0 +1,58 @@
+package org.mjdev.desktop.windows
+
+import android.annotation.SuppressLint
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.layout.LayoutCoordinates
+import androidx.compose.ui.layout.onPlaced
+import androidx.compose.ui.layout.positionOnScreen
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.dp
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.interfaces.IDesktopContext
+
+@SuppressLint("UnusedBoxWithConstraintsScope")
+@Composable
+fun ChromeWindow(
+    modifier: Modifier = Modifier,
+    content: @Composable ChromeWindowState.() -> Unit
+) = withDesktopContext {
+    val windowState = ChromeWindowState(context)
+    Box(
+        modifier = modifier
+            .alpha(0f)
+            .onPlaced { coords ->
+                windowState.onPlaced(coords)
+            },
+        content = { content(windowState) }
+    )
+    ChromeWindowInternal(
+        state = windowState
+    ) {
+        content(windowState)
+    }
+}
+
+@Composable
+fun ChromeWindowInternal(
+    state: ChromeWindowState,
+    content: @Composable () -> Unit
+) {
+    // todo
+    content()
+}
+
+class ChromeWindowState(
+    val context: IDesktopContext,
+) {
+    var size: DpSize = DpSize.Zero
+    var position: DpOffset = DpOffset.Zero
+
+    fun onPlaced(coords: LayoutCoordinates) {
+        position = coords.positionOnScreen().let { pos -> DpOffset(pos.x.dp, pos.y.dp) }
+        size = coords.size.let { size -> DpSize(size.width.dp, size.height.dp) }
+    }
+}

+ 9 - 0
composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_banner.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@color/ic_banner_background" />
+    <foreground>
+        <inset
+            android:drawable="@mipmap/ic_banner_foreground"
+            android:inset="12%" />
+    </foreground>
+</adaptive-icon>

BIN
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp


BIN
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp


BIN
composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp


BIN
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp


BIN
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp


BIN
composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp


BIN
composeApp/src/androidMain/res/mipmap-xhdpi/ic_banner.png


BIN
composeApp/src/androidMain/res/mipmap-xhdpi/ic_banner_foreground.png


BIN
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp


BIN
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp


BIN
composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp


BIN
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp


BIN
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp


BIN
composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp


BIN
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp


BIN
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp


BIN
composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp


+ 4 - 0
composeApp/src/androidMain/res/values/ic_banner_background.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <color name="ic_banner_background">#FFFFFF</color>
+</resources>

+ 11 - 0
composeApp/src/androidMain/res/values/splash.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <style name="Theme.SplashScreen.Splash" parent="Theme.SplashScreen">
+        <item name="android:windowNoTitle">true</item>
+        <item name="android:windowActionBar">false</item>
+        <item name="windowSplashScreenBackground">#00000000</item>
+        <item name="windowSplashScreenAnimatedIcon">@mipmap/ic_launcher</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="postSplashScreenTheme">@style/Theme.SplashScreen.Splash</item>
+    </style>
+</resources>

+ 9 - 0
composeApp/src/androidMain/res/xml/network_security_config.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<network-security-config xmlns:tools="http://schemas.android.com/tools">
+    <base-config cleartextTrafficPermitted="true"
+        tools:ignore="InsecureBaseConfiguration">
+        <trust-anchors>
+            <certificates src="system" />
+        </trust-anchors>
+    </base-config>
+</network-security-config>

BIN
composeApp/src/commonMain/composeResources/drawable/blur4.webp


BIN
composeApp/src/commonMain/composeResources/drawable/icon.png


+ 21 - 0
composeApp/src/commonMain/composeResources/drawable/icon.svg

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    x="0px"
+    y="0px"
+    viewBox="0 0 256 256"
+    enable-background="new 0 0 256 256"
+    xml:space="preserve">
+<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
+    <g><g><g><path fill="#000000" d="M73.8,48.5c-1.6,0.6-3.8,2.1-4.7,3.4c-1.6,2.2-1.8,3.8-2.1,18.7l-0.3,16.2H50.2c-15.4,0-16.7,0.1-18.9,1.8l-2.2,1.7v17.9v17.8l-7.7,0.3C9,127,10,123.5,10,166.3c0,19.8,0.1,36.6,0.1,37.4c0.1,0.9,0.9,2.2,2,3.3c1.8,1.7,2.7,1.7,52.2,1.7c49.6,0,50.4,0,52.6-1.8c2.2-1.7,2.2-1.8,2.2-13.7v-11.9h9.5h9.6l-0.6,3.3c-0.9,6.1-2,7.6-6.4,9.9c-5.1,2.6-6.9,5-6.9,8.8c0,5.3,0.4,5.4,33.2,5.4c28.3,0,29.5-0.1,31.1-1.7c3.9-3.9,1.3-9.2-5.9-12.3c-4.9-2.1-5.2-2.4-6.4-6.7c-0.8-2.5-1.4-5.1-1.4-5.7c0-0.9,6.1-1.1,31.7-1.1c35.3,0,35.5,0,38.1-5.5c1.3-2.7,1.4-8.6,1.2-62.2c-0.3-58.5-0.3-59.2-2.1-61.6c-0.9-1.3-3.1-2.8-4.7-3.4C235,47,78,47,73.8,48.5z M239.1,53.9l1.9,1.3v47.2v47.1l-60.7-0.2l-60.7-0.3l-0.3-29.3l-0.2-29.3l-2.2-1.7c-2.1-1.7-3.3-1.8-23.6-1.8H72V71.4C72,57,72,55.8,73.7,54.2c1.7-1.7,2.8-1.7,82.6-1.7C228.1,52.5,237.4,52.7,239.1,53.9z M114.4,147.7v55.3l-28.1,0.3l-28.1,0.2v-35.9v-36l-2.1-2.4c-2.1-2.3-2.3-2.4-12.4-2.7l-10.4-0.3v-16.5c0-9.1,0.3-16.8,0.6-17.1c0.3-0.3,18.5-0.5,40.5-0.4l40,0.3V147.7z M52.9,167.7l0.2,35.8l-19-0.2l-19.1-0.3l-0.3-34.7c-0.1-19.1,0-35.3,0.3-35.9c0.3-0.9,4.5-1,19-0.9l18.7,0.3L52.9,167.7z M241,163.5c0,7.8-0.3,10.1-1.4,11.2c-1.2,1.2-7.9,1.4-60.9,1.4h-59.5v-11.2v-11.2H180H241L241,163.5L241,163.5z M170.7,186.1c1.6,7.6,2.8,9.4,8.7,12.4c2.9,1.5,5.5,3.3,5.7,3.9c0.3,0.9-5.7,1.1-27.6,1.1c-24.1,0-28-0.2-28-1.3c0-0.7,1.6-2.1,3.7-3.1c5.9-3,7.2-4.7,8.8-11.6l1.5-6.3h13h13.1L170.7,186.1z"/><path
+        fill="#000000"
+        d="M152.3,61.5c-2.6,2.6-0.8,3.9,5,3.7c4.5-0.3,5.2-0.5,5.5-2.1c0.2-1.3-0.3-2-1.7-2.4C157.5,59.9,153.6,60.2,152.3,61.5z" /><path
+        fill="#000000"
+        d="M71.1,95.7c-2.3,2.8,1.5,6.2,4.1,3.6c1.1-1.1,1.2-1.7,0.5-2.9C74.4,94.4,72.5,94.2,71.1,95.7z" /><path
+        fill="#000000"
+        d="M68,189.3c-0.9,2.3,0.9,3.1,6.6,2.8c4.8-0.3,5.5-0.4,5.5-2c0-1.5-0.7-1.7-5.8-2C69.6,188,68.4,188.2,68,189.3z" /><path
+        fill="#000000"
+        d="M27.7,191.9c-0.9,2.2,0.6,3,6.1,3c4.1,0,5.5-0.3,5.9-1.4c0.9-2.3-0.3-2.9-6-2.9C29.7,190.6,28,191,27.7,191.9z" /></g></g></g>
+</svg>

+ 69 - 28
src/main/kotlin/eu/mjdev/desktop/components/appbar/AppBar.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appbar/AppBar.kt

@@ -6,22 +6,36 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.appbar
+package org.mjdev.desktop.components.appbar
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.IntrinsicSize
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.RowScope
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentHeight
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
+import androidx.compose.ui.UiComposable
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Modifier.conditional
-import eu.mjdev.desktop.helpers.compose.Orientation
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.Modifier.conditional
+import org.mjdev.desktop.helpers.compose.Orientation
 
 // todo alignment scopes
 @Composable
+@UiComposable
 fun AppBar(
     modifier: Modifier = Modifier,
     contentPadding: PaddingValues = PaddingValues(4.dp),
@@ -42,11 +56,13 @@ fun AppBar(
     actions: @Composable RowScope.() -> Unit = {},
 ) = when (orientation) {
     Orientation.Horizontal -> Row(
-        modifier = modifier.wrapContentHeight().padding(contentPadding),
+        modifier = modifier.wrapContentHeight()
+            .padding(contentPadding),
         verticalAlignment = contentAlignment,
     ) {
         Row(
-            modifier = Modifier.wrapContentHeight().padding(iconPadding),
+            modifier = Modifier.wrapContentHeight()
+                .padding(iconPadding),
             verticalAlignment = iconVerticalAlignment,
             horizontalArrangement = iconHorizontalArrangement,
             content = icon
@@ -63,7 +79,8 @@ fun AppBar(
             content = title
         )
         Row(
-            modifier = Modifier.wrapContentHeight().padding(actionsPadding),
+            modifier = Modifier.wrapContentHeight()
+                .padding(actionsPadding),
             verticalAlignment = actionsVerticalAlignment,
             horizontalArrangement = actionsHorizontalArrangement,
             content = actions
@@ -76,7 +93,8 @@ fun AppBar(
             .padding(contentPadding),
     ) {
         Row(
-            modifier = Modifier.width(IntrinsicSize.Min).padding(iconPadding),
+            modifier = Modifier.width(IntrinsicSize.Min)
+                .padding(iconPadding),
             verticalAlignment = iconVerticalAlignment,
             content = icon
         )
@@ -99,57 +117,72 @@ fun AppBar(
     }
 }
 
-@Preview
+// todo
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppBarHorizontalPreview() = preview {
     AppBar(
-        modifier = Modifier.height(48.dp).background(Color.Green),
+        modifier = Modifier.height(48.dp)
+            .background(Color.Green),
         orientation = Orientation.Horizontal,
         fillCenter = false,
         icon = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Red)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Red)
             )
         },
         title = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Blue)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Blue)
             )
         },
         actions = {
             Box(
-                modifier = Modifier.size(24.dp, 24.dp).background(Color.Yellow)
+                modifier = Modifier.size(24.dp, 24.dp)
+                    .background(Color.Yellow)
             )
         }
     )
 }
 
-@Preview
+// todo
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppBarHorizontalFillPreview() = preview {
     AppBar(
-        modifier = Modifier.height(48.dp).background(Color.Green),
+        modifier = Modifier.height(48.dp)
+            .background(Color.Green),
         orientation = Orientation.Horizontal,
         fillCenter = true,
         icon = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Red)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Red)
             )
         },
         title = {
             Box(
-                modifier = Modifier.height(24.dp).fillMaxWidth().background(Color.Blue)
+                modifier = Modifier.fillMaxWidth()
+                    .height(24.dp)
+                    .background(Color.Blue)
             )
         },
         actions = {
             Box(
-                modifier = Modifier.size(24.dp, 24.dp).background(Color.Yellow)
+                modifier = Modifier.size(24.dp, 24.dp)
+                    .background(Color.Yellow)
             )
         }
     )
 }
 
-@Preview
+// todo
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppBarVerticalPreview() = preview {
     AppBar(
@@ -158,23 +191,28 @@ fun AppBarVerticalPreview() = preview {
         fillCenter = false,
         icon = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Red)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Red)
             )
         },
         title = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Blue)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Blue)
             )
         },
         actions = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Yellow)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Yellow)
             )
         }
     )
 }
 
-@Preview
+// todo
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppBarVerticalFillPreview() = preview {
     AppBar(
@@ -183,17 +221,20 @@ fun AppBarVerticalFillPreview() = preview {
         fillCenter = true,
         icon = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Red)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Red)
             )
         },
         title = {
             Box(
-                modifier = Modifier.width(24.dp).fillMaxHeight().background(Color.Blue)
+                modifier = Modifier.width(24.dp).fillMaxHeight()
+                    .background(Color.Blue)
             )
         },
         actions = {
             Box(
-                modifier = Modifier.size(24.dp).background(Color.Yellow)
+                modifier = Modifier.size(24.dp)
+                    .background(Color.Yellow)
             )
         }
     )

+ 275 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/AppsMenu.kt

@@ -0,0 +1,275 @@
+package org.mjdev.desktop.components.appsmenu
+
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.wrapContentHeight
+import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Divider
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.RectangleShape
+import androidx.compose.ui.unit.DpOffset
+import androidx.compose.ui.unit.dp
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+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
+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.components.user.UserAvatar
+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.LaunchedEffect.launchedEffect
+import org.mjdev.desktop.extensions.LaunchedEffect.runAsync
+import org.mjdev.desktop.helpers.animation.Animations.AppsMenuEnterAnimation
+import org.mjdev.desktop.helpers.animation.Animations.AppsMenuExitAnimation
+import org.mjdev.desktop.interfaces.IApp
+
+@Suppress("FunctionName")
+@Composable
+fun AppsMenu(
+    modifier: Modifier = Modifier,
+    appsMenuState: AppsMenuState = rememberAppsMenuState(),
+    panelState: VisibilityState = rememberVisibilityState(),
+    enterAnimation: EnterTransition = AppsMenuEnterAnimation,
+    exitAnimation: ExitTransition = AppsMenuExitAnimation,
+    onAppClick: DesktopContextScope.(IApp) -> Unit = { app -> runAsync { app.start() } },
+    onAppContextMenuClick: DesktopContextScope.(IApp) -> Unit = {},
+    onCategoryContextMenuClick: DesktopContextScope.(Category) -> Unit = {},
+    onUserAvatarClick: () -> Unit = {},
+    onActionClick: () -> Unit = {},
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
+    val appCategories by flowBlock(emptyList()) { appsManager.categories }
+    val items by rememberComputed(
+        appsMenuState.searchTextState.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.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
+            )
+//            .widthIn(
+//                min = appMenuMinWidth,
+//                max = containerSize.width
+//            )
+//            .heightIn(
+//                min = appMenuMinHeight,
+//                max = containerSize.height
+//            )
+            .padding(
+                bottom = panelState.height
+            ),
+        enterAnimation = enterAnimation,
+        exitAnimation = exitAnimation,
+        state = appsMenuState
+    ) {
+        BlurPanel(
+            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))
+        ) {
+            Column(
+                modifier = Modifier.fillMaxSize()
+            ) {
+                Box(
+                    modifier = Modifier.clip(RectangleShape),
+                    contentAlignment = Alignment.BottomStart
+                ) {
+                    val shape = BarShape(
+                        offset = 52f,
+                        circleRadius = 24.dp,
+                        cornerRadius = 4.dp,
+                        circleGap = 8.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
+                            )
+                    )
+                    UserAvatar(
+                        avatarSize = 64.dp,
+                        orientation = Orientation.Horizontal,
+                        titleVerticalAlignment = Alignment.Bottom,
+                        iconPadding = PaddingValues(bottom = 16.dp),
+                        titlePadding = PaddingValues(top = 24.dp, start = 20.dp),
+                        onUserAvatarClick = onUserAvatarClick,
+                        onTooltip = onTooltip
+                    )
+                }
+                Divider(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(2.dp)
+                        .padding(
+                            start = 2.dp,
+                            end = 2.dp
+                        ),
+                    color = borderColor,
+                    thickness = 2.dp
+                )
+                AppsList(
+                    modifier = Modifier
+                        .padding(bottom = 62.dp)
+                        .fillMaxSize()
+                        .padding(24.dp),
+                    category = appsMenuState.categoryState.value,
+                    listState = appsMenuState.listState,
+                    onCategoryClick = { c -> appsMenuState.categoryState.value = c },
+                    onAppClick = onAppClick,
+                    onAppContextMenuClick = onAppContextMenuClick,
+                    onCategoryContextMenuClick = onCategoryContextMenuClick,
+                    onTooltip = onTooltip,
+                    items = items,
+                )
+            }
+            Column(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .wrapContentHeight()
+                    .align(Alignment.BottomStart),
+            ) {
+                Divider(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .height(2.dp)
+                        .padding(
+                            start = 2.dp,
+                            end = 2.dp
+                        ),
+                    color = borderColor,
+                    thickness = 2.dp
+                )
+                AppsBottomBar(
+                    modifier = Modifier
+                        .fillMaxWidth()
+                        .wrapContentHeight()
+                        .background(backgroundColor),
+                    onTooltip = onTooltip,
+                    backButtonVisible = items.firstOrNull() is IApp,
+                    searchTextState = appsMenuState.searchTextState,
+                    onActionClick = onActionClick,
+                    onBackClick = {
+                        appsMenuState.categoryState.value = null
+                    },
+                    onContextMenuClick = {
+                        // todo : context menu
+                    }
+                )
+            }
+        }
+    }
+    launchedEffect(appsMenuState.isVisible) { isVisible ->
+        if (!isVisible) {
+            appsMenuState.categoryState.value = null
+        }
+    }
+}
+
+class AppsMenuState(
+    visible: Boolean = false,
+    enabled: Boolean = true,
+    hideDelay: Long = 0L,
+    scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
+    val listState: LazyListState = LazyListState(),
+    val categoryState: MutableState<Category?> = mutableStateOf(null),
+    val searchTextState: MutableState<String> = mutableStateOf(""),
+    val position: DpOffset = DpOffset.Zero
+) : VisibilityState(visible, enabled, hideDelay, scope) {
+    companion object {
+        @Composable
+        fun rememberAppsMenuState(
+            visible: Boolean = false,
+            enabled: Boolean = true,
+            hideDelay: Long = 0L,
+            scope: CoroutineScope = CoroutineScope(Dispatchers.Default),
+            listState: LazyListState = rememberLazyListState(),
+            categoryState: MutableState<Category?> = rememberState(null),
+            searchTextState: MutableState<String> = rememberState(""),
+        ) = AppsMenuState(
+            visible,
+            enabled,
+            hideDelay,
+            scope,
+            listState,
+            categoryState,
+            searchTextState
+        )
+    }
+}
+
+@Suppress("unused")
+//@Preview
+@Composable
+fun AppsMenuPreview() = preview {
+    AppsMenu()
+}

+ 22 - 18
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsBottomBar.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsBottomBar.kt

@@ -1,6 +1,5 @@
-package eu.mjdev.desktop.components.appsmenu
+package org.mjdev.desktop.components.appsmenu.components
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.focusable
 import androidx.compose.foundation.layout.fillMaxWidth
@@ -13,15 +12,15 @@ 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 eu.mjdev.desktop.components.appbar.AppBar
-import eu.mjdev.desktop.components.icon.ShapedIcon
-import eu.mjdev.desktop.components.input.SearchFieldPassive
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
-import eu.mjdev.desktop.extensions.Compose.clear
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.rememberState
-import eu.mjdev.desktop.icons.Icons
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import org.mjdev.desktop.components.appbar.AppBar
+import org.mjdev.desktop.components.icon.ShapedIcon
+import org.mjdev.desktop.components.input.SearchFieldPassive
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.extensions.Colors.SuperDarkGray
+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
 
 @Suppress("FunctionName")
 @Composable
@@ -31,8 +30,9 @@ fun AppsBottomBar(
     searchTextState: MutableState<String> = rememberState(""),
     onContextMenuClick: () -> Unit = {},
     onActionClick: () -> Unit = {},
-    onBackClick: () -> Unit = {}
-) = withDesktopScope {
+    onBackClick: () -> Unit = {},
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
     AppBar(
         modifier = modifier.focusable(false),
         title = {
@@ -48,29 +48,33 @@ fun AppsBottomBar(
                     fontWeight = FontWeight.Bold
                 ),
                 backgroundColor = Color.Transparent,
-                onClearClick = { searchTextState.clear() }
+                onClearClick = { searchTextState.clear() },
+                onTooltip = onTooltip
             )
         },
         icon = {
             ShapedIcon(
                 modifier = Modifier.focusable(false),
                 visible = backButtonVisible && searchTextState.value.isEmpty(),
-                imageVector = Icons.BackArrow,
+                imageVector = ArrowBack,
                 iconColor = borderColor,
                 iconBackgroundColor = iconsTintColor,
                 onRightClick = onContextMenuClick,
-                onClick = onBackClick
+                onClick = onBackClick,
+                onTooltip = onTooltip
             )
         },
         actions = {
             AppsMenuActions(
-                onActionClick = onActionClick
+                onActionClick = onActionClick,
+                onTooltip = onTooltip
             )
         }
     )
 }
 
-@Preview
+@Suppress("unused")
+//@Preview
 @Composable
 fun AppsBottomBarPreview() = preview {
     AppsBottomBar(

+ 32 - 24
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsList.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsList.kt

@@ -6,9 +6,8 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.appsmenu
+package org.mjdev.desktop.components.appsmenu.components
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.lazy.LazyColumn
@@ -20,29 +19,31 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.data.Category
-import eu.mjdev.desktop.extensions.ColorUtils.alpha
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.helpers.compose.rememberForeverLazyListState
-import eu.mjdev.desktop.provider.DesktopScope
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+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.extensions.LaunchedEffect.runAsync
+import org.mjdev.desktop.interfaces.IApp
 
 @Composable
 fun AppsList(
     modifier: Modifier = Modifier,
-    category: String = "",
+    category: Category? = null,
     listState: LazyListState = rememberForeverLazyListState("AppsMenu"),
-    onCategoryClick: DesktopScope.(Category) -> Unit = {},
-    onCategoryContextMenuClick: DesktopScope.(Category) -> Unit = {},
-    onAppClick: DesktopScope.(App) -> Unit = {},
-    onAppContextMenuClick: DesktopScope.(App) -> Unit = {},
-    items: List<Any> = listOf()
-) = withDesktopScope {
+    onCategoryClick: DesktopContextScope.(Category) -> Unit = {},
+    onCategoryContextMenuClick: DesktopContextScope.(Category) -> Unit = {},
+    onAppClick: DesktopContextScope.(IApp) -> Unit = { app -> runAsync { app.start() } },
+    onAppContextMenuClick: DesktopContextScope.(IApp) -> Unit = {},
+    items: List<Any> = listOf(),
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
     LazyColumn(
         modifier = modifier,
-        state = if (category.isEmpty()) listState else rememberLazyListState(),
+        state = if (category == null) listState else rememberLazyListState(),
     ) {
         when (items.firstOrNull()) {
             is Category -> {
@@ -52,15 +53,16 @@ fun AppsList(
                         showDivider = (items.size - 1) > idx,
                         dividerColor = textColor.alpha(0.3f),
                         onClick = { onCategoryClick(item) },
-                        onContextMenuClick = { onCategoryContextMenuClick(item) }
+                        onContextMenuClick = { onCategoryContextMenuClick(item) },
+                        onTooltip = onTooltip
                     )
                 }
             }
 
-            is App -> {
+            is IApp -> {
                 itemsIndexed(items) { idx, item ->
                     AppsMenuApp(
-                        app = item as App,
+                        app = item as IApp,
                         showDivider = (items.size - 1) > idx,
                         dividerColor = textColor.alpha(0.3f),
                         onClick = {
@@ -68,7 +70,8 @@ fun AppsList(
                         },
                         onContextMenuClick = {
                             onAppContextMenuClick(item)
-                        }
+                        },
+                        onTooltip = onTooltip
                     )
                 }
             }
@@ -76,11 +79,16 @@ fun AppsList(
     }
 }
 
-@Preview
+@Suppress("unused")
+//@Preview
 @Composable
 fun AppsListPreview() = preview(320, 640) {
     AppsList(
         modifier = Modifier.background(Color.SuperDarkGray, RoundedCornerShape(16.dp)).padding(8.dp),
-        items = listOf(App.Test, App.Test, App.Test)
+        items = listOf(
+//            App.Test,
+//            App.Test,
+//            App.Test
+        )
     )
 }

+ 27 - 25
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuActions.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuActions.kt

@@ -6,9 +6,8 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.appsmenu
+package org.mjdev.desktop.components.appsmenu.components
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Row
 import androidx.compose.foundation.layout.padding
@@ -19,27 +18,29 @@ 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 eu.mjdev.desktop.components.icon.ShapedIcon
-import eu.mjdev.desktop.extensions.ColorUtils.alpha
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.runAsync
-import eu.mjdev.desktop.extensions.Modifier.circleBorder
-import eu.mjdev.desktop.icons.Icons
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
-import kotlinx.coroutines.Dispatchers
+import org.mjdev.desktop.components.icon.ShapedIcon
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+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.extensions.Modifier.circleBorder
+import org.mjdev.desktop.icons.system.Logout
+import org.mjdev.desktop.icons.system.PowerOff
+import org.mjdev.desktop.icons.system.RestartAlt
 
+@Suppress("UNUSED_PARAMETER")
 @Composable
 fun AppsMenuActions(
     // todo theme
-    iconRestart: ImageVector = Icons.RestartComputer,
+    iconRestart: ImageVector = RestartAlt,
     // todo theme
-    iconPowerOff: ImageVector = Icons.PowerOffComputer,
+    iconPowerOff: ImageVector = PowerOff,
     // todo theme
-    iconLogout: ImageVector = Icons.LogOutUser,
+    iconLogout: ImageVector = Logout,
     modifier: Modifier = Modifier,
     onActionClick: () -> Unit = {},
-) = withDesktopScope {
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
     Row(
         modifier = modifier.wrapContentWidth()
     ) {
@@ -52,9 +53,9 @@ fun AppsMenuActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                runAsync(Dispatchers.IO) {
-                    api.restart()
-                }
+//                runAsync(Dispatchers.Default) {
+//                    context.restart()
+//                }
             }
         )
         ShapedIcon(
@@ -66,9 +67,9 @@ fun AppsMenuActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                runAsync(Dispatchers.IO) {
-                    api.suspend()
-                }
+//                runAsync(Dispatchers.Default) {
+//                    context.suspend()
+//                }
             }
         )
         ShapedIcon(
@@ -80,15 +81,16 @@ fun AppsMenuActions(
             onClick = {
                 onActionClick()
                 // todo : dialog
-                runAsync(Dispatchers.IO) {
-                    api.logOut()
-                }
+//                runAsync(Dispatchers.Default) {
+//                    context.logOut()
+//                }
             }
         )
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppsMenuActionsPreview() = preview {
     AppsMenuActions(

+ 33 - 22
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuApp.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuApp.kt

@@ -1,8 +1,12 @@
-package eu.mjdev.desktop.components.appsmenu
+package org.mjdev.desktop.components.appsmenu.components
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.runtime.Composable
@@ -11,24 +15,29 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.*
-import eu.mjdev.desktop.components.fonticon.FontIcon
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.data.App
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
-import eu.mjdev.desktop.extensions.Compose.onLeftClick
-import eu.mjdev.desktop.extensions.Compose.onMousePress
-import eu.mjdev.desktop.extensions.Compose.onRightClick
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Modifier.circleBorder
-import eu.mjdev.desktop.helpers.shape.DottedShape
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import androidx.compose.ui.unit.Dp
+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.mjdev.desktop.components.fonticon.FontIcon
+import org.mjdev.desktop.components.text.TextAny
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.extensions.Colors.SuperDarkGray
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.LaunchedEffect.runAsync
+import org.mjdev.desktop.extensions.Modifier.circleBorder
+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.mjdev.desktop.interfaces.IApp
 
-@Suppress("FunctionName")
+@Suppress("FunctionName", "UNUSED_PARAMETER")
 @Composable
 fun AppsMenuApp(
     modifier: Modifier = Modifier,
-    app: App? = null,
+    app: IApp? = null,
     icon: String? = null,
     iconSize: DpSize = DpSize(32.dp, 32.dp),
     titleTextSize: TextUnit = 16.sp,
@@ -37,10 +46,11 @@ fun AppsMenuApp(
     dividerPadding: Dp = 48.dp,
     dividerColor: Color = Color.Black,
     onContextMenuClick: () -> Unit = {},
-    onClick: () -> Unit = {},
-) = withDesktopScope {
+    onClick: () -> Unit = { runAsync { app?.start() } },
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
     val appIconName = remember(app, icon) { app?.name ?: icon }
-    val materialIcon = remember(appIconName) { iconSet.iconForName(appIconName) ?: "?".toInt() }
+    val materialIcon = remember(appIconName) { iconSet.iconForName(appIconName) ?: 0 }
     Column(
         modifier = modifier.onMousePress {
             onLeftClick { onClick() }
@@ -93,7 +103,8 @@ fun AppsMenuApp(
     }
 }
 
-@Preview
+@Suppress("unused")
+//@Preview
 @Composable
 fun AppsMenuAppPreview() = preview {
     AppsMenuApp(
@@ -103,6 +114,6 @@ fun AppsMenuAppPreview() = preview {
                 RoundedCornerShape(16.dp)
             )
             .padding(8.dp),
-        app = App.Test
+        app = null // App.Test
     )
 }

+ 30 - 19
src/main/kotlin/eu/mjdev/desktop/components/appsmenu/AppsMenuCategory.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/appsmenu/components/AppsMenuCategory.kt

@@ -1,8 +1,12 @@
-package eu.mjdev.desktop.components.appsmenu
+package org.mjdev.desktop.components.appsmenu.components
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.shape.RoundedCornerShape
 import androidx.compose.material.Divider
 import androidx.compose.runtime.Composable
@@ -11,19 +15,24 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.text.font.FontWeight
-import androidx.compose.ui.unit.*
-import eu.mjdev.desktop.components.fonticon.FontIcon
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.data.Category
-import eu.mjdev.desktop.extensions.Compose.SuperDarkGray
-import eu.mjdev.desktop.extensions.Compose.onLeftClick
-import eu.mjdev.desktop.extensions.Compose.onMousePress
-import eu.mjdev.desktop.extensions.Compose.onRightClick
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Modifier.circleBorder
-import eu.mjdev.desktop.helpers.shape.DottedShape
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import androidx.compose.ui.unit.Dp
+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.mjdev.desktop.components.fonticon.FontIcon
+import org.mjdev.desktop.components.text.TextAny
+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.Compose.preview
+import org.mjdev.desktop.extensions.Modifier.circleBorder
+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
 
+@Suppress("UNUSED_PARAMETER")
 @Composable
 fun AppsMenuCategory(
     modifier: Modifier = Modifier,
@@ -34,10 +43,11 @@ fun AppsMenuCategory(
     dividerPadding: Dp = 48.dp,
     dividerColor: Color = Color.Black,
     onClick: (category: Category) -> Unit = {},
-    onContextMenuClick: (category: Category) -> Unit = {}
-) = withDesktopScope {
+    onContextMenuClick: (category: Category) -> Unit = {},
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
     val categoryName = remember(category) { category.name }
-    val materialIcon = remember(categoryName) { iconSet.iconForName(categoryName) ?: "?".toInt() }
+    val materialIcon = remember(categoryName) { iconSet.iconForName(categoryName) ?: 0 }
     Column(
         modifier = modifier.padding(vertical = 2.dp)
             .onMousePress {
@@ -80,7 +90,8 @@ fun AppsMenuCategory(
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun AppsMenuCategoryPreview() = preview {
     AppsMenuCategory(

+ 105 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/background/BackgroundImage.kt

@@ -0,0 +1,105 @@
+package org.mjdev.desktop.components.background
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.Color.Companion.Transparent
+import androidx.compose.ui.layout.ContentScale
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.delay
+import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
+import org.mjdev.desktop.extensions.Colors.ContrastColorFilter
+import org.mjdev.desktop.extensions.Colors.SuperDarkGray
+import org.mjdev.desktop.extensions.Colors.alpha
+import org.mjdev.desktop.extensions.Compose.Crossfade
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.LaunchedEffect.launch
+import org.mjdev.desktop.extensions.LaunchedEffect.launchedEffect
+import org.mjdev.desktop.extensions.MutableStateExt.rememberComputed
+import org.mjdev.desktop.helpers.generic.Queue
+import kotlin.math.min
+
+@Suppress("FunctionName")
+@Composable
+fun BackgroundImage(
+    modifier: Modifier = Modifier,
+    switchDelay: Long = 60000L,
+    backgroundColor: Color = Color.SuperDarkGray,
+    images: MutableList<Any> = mutableListOf(),
+    onError: (Throwable) -> Unit = { e -> e.printStackTrace() },
+    onChange: (bck: Any?) -> Unit = {}
+) = withDesktopContext {
+    Box(
+        modifier = modifier.background(backgroundColor)
+    ) {
+        val backgroundQueue by rememberComputed(images, images.size) { Queue(images) }
+        val fadeInDuration by rememberComputed(switchDelay) { switchDelay / 8 }
+        val fadeOutDuration by rememberComputed(switchDelay) { switchDelay / 4 }
+        var currentBackground by remember { mutableStateOf(backgroundQueue.nextOrNull()) }
+        Crossfade(
+            targetState = currentBackground ?: Color.SuperDarkGray,
+            fadeInDuration = fadeInDuration,
+            fadeOutDuration = fadeOutDuration
+        ) { imageValue ->
+            launch(Dispatchers.Default) {
+                onChange(currentBackground)
+            }
+            ImageAny(
+                modifier = modifier,
+                contentScale = ContentScale.Crop,
+                imageLoader = imageLoader,
+                src = imageValue,
+                onFail = onError,
+                colorFilter = ContrastColorFilter(1.5f),
+                onAnimationFinish = {
+                    currentBackground = backgroundQueue.nextOrNull()
+                }
+            )
+            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
+                        ),
+                        radius = min(
+                            containerSize.width.value,
+                            containerSize.height.value
+                        ) * 1.2f
+                    )
+                )
+            )
+        }
+        launchedEffect(currentBackground) {
+            if (switchDelay > 0) {
+                delay(switchDelay)
+                currentBackground = backgroundQueue.nextOrNull()
+            }
+        }
+    }
+}
+
+@Suppress("unused")
+//@Preview
+@Composable
+fun BackgroundImagePreview() = preview {
+    BackgroundImage()
+}

+ 14 - 11
src/main/kotlin/eu/mjdev/desktop/components/blur/BlurPanel.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/blur/BlurPanel.kt

@@ -6,9 +6,8 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.blur
+package org.mjdev.desktop.components.blur
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.BoxScope
@@ -23,25 +22,28 @@ import androidx.compose.ui.draw.blur
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.extensions.ColorUtils.alpha
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import org.jetbrains.compose.resources.DrawableResource
+import org.jetbrains.compose.resources.painterResource
+import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.composeapp.generated.resources.Res
+import org.mjdev.desktop.composeapp.generated.resources.blur4
+import org.mjdev.desktop.extensions.Colors.alpha
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 
 // todo real blur
 @Composable
 fun BlurPanel(
     modifier: Modifier = Modifier,
-    resourceId: String = "blur4.png",
+    resourceId: DrawableResource = Res.drawable.blur4,
     contentAlignment: Alignment = Alignment.TopStart,
     propagateMinConstraints: Boolean = false,
     blurRadius: Dp = 12.dp,
     alpha: Float = 0.3f,
     content: @Composable BoxScope.() -> Unit = {}
-) = withDesktopScope {
+) = withDesktopContext {
     val transparentGradientBrush = Brush.linearGradient(
         colors = listOf(
             Color(0x66FFFFFF),
@@ -58,7 +60,7 @@ fun BlurPanel(
                 .alpha(alpha)
                 .background(transparentGradientBrush)
                 .blur(blurRadius, edgeTreatment = BlurredEdgeTreatment.Unbounded),
-            src = painterResource("images/$resourceId"),
+            src = painterResource(resourceId),
             contentScale = if (maxHeight > maxWidth) ContentScale.FillHeight else ContentScale.FillWidth
         )
         Box(
@@ -76,7 +78,8 @@ fun BlurPanel(
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun BlurPanelPreview() = preview(480, 800) {
     BlurPanel()

+ 7 - 7
src/main/kotlin/eu/mjdev/desktop/components/button/ButtonPrimary.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonPrimary.kt

@@ -1,12 +1,11 @@
-package eu.mjdev.desktop.components.button
+package org.mjdev.desktop.components.button
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.Composable
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import org.mjdev.desktop.components.text.TextAny
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 
 @Composable
 fun ButtonPrimary(
@@ -14,7 +13,7 @@ fun ButtonPrimary(
     enabled: Boolean = true,
     visible: Boolean = true,
     onClick: () -> Unit = {}
-) = withDesktopScope {
+) = withDesktopContext {
     if (visible) {
         Button(
             onClick = onClick,
@@ -32,7 +31,8 @@ fun ButtonPrimary(
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun ButtonPrimaryPreview() = preview {
     ButtonPrimary()

+ 7 - 7
src/main/kotlin/eu/mjdev/desktop/components/button/ButtonSecondary.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/ButtonSecondary.kt

@@ -1,12 +1,11 @@
-package eu.mjdev.desktop.components.button
+package org.mjdev.desktop.components.button
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.material.Button
 import androidx.compose.material.ButtonDefaults
 import androidx.compose.runtime.Composable
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.provider.DesktopScope.Companion.withDesktopScope
+import org.mjdev.desktop.components.text.TextAny
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.context.DesktopContextScope.Companion.withDesktopContext
 
 // todo
 @Composable
@@ -15,7 +14,7 @@ fun ButtonSecondary(
     enabled: Boolean = true,
     visible: Boolean = true,
     onClick: () -> Unit = {}
-) = withDesktopScope {
+) = withDesktopContext {
     if (visible) {
         Button(
             onClick = onClick,
@@ -33,7 +32,8 @@ fun ButtonSecondary(
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun ButtonSecondaryPreview() = preview {
     ButtonSecondary()

+ 8 - 8
src/main/kotlin/eu/mjdev/desktop/components/button/TransparentButton.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/button/TransparentButton.kt

@@ -6,9 +6,8 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.button
+package org.mjdev.desktop.components.button
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.RowScope
 import androidx.compose.material.Button
@@ -18,10 +17,10 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.extensions.Compose.noElevation
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.transparent
-import eu.mjdev.desktop.icons.Icons
+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
 
 @Composable
 fun TransparentButton(
@@ -37,12 +36,13 @@ fun TransparentButton(
     content = content
 )
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun TransparentButtonPreview() = preview {
     TransparentButton {
         Icon(
-            imageVector = Icons.User,
+            imageVector = AccountCircle,
             contentDescription = "",
             tint = Color.White
         )

+ 17 - 12
src/main/kotlin/eu/mjdev/desktop/components/card/Card.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/Card.kt

@@ -6,9 +6,8 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.ColumnScope
@@ -17,15 +16,20 @@ import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceBorder
-import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceColors
-import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceGlow
-import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceScale
-import eu.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceShape
-import eu.mjdev.desktop.components.surface.Surface
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.extensions.Compose.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
+import org.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceBorder
+import org.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceColors
+import org.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceGlow
+import org.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceScale
+import org.mjdev.desktop.components.card.base.CardDefaults.toClickableSurfaceShape
+import org.mjdev.desktop.components.card.base.CardGlow
+import org.mjdev.desktop.components.card.base.CardScale
+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
 
 // todo, card like in android tv
 @Composable
@@ -54,7 +58,8 @@ fun Card(
     Column(content = content)
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun CardPreview() = preview {
     Card(

+ 40 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CardContent.kt

@@ -0,0 +1,40 @@
+package org.mjdev.desktop.components.card
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material.MaterialTheme
+import androidx.compose.material.ProvideTextStyle
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.graphicsLayer
+import org.mjdev.desktop.components.card.base.CardDefaults.DescriptionAlpha
+import org.mjdev.desktop.components.card.base.CardDefaults.SubtitleAlpha
+
+@Composable
+fun CardContent(
+    title: @Composable () -> Unit = {},
+    subtitle: @Composable () -> Unit = {},
+    description: @Composable () -> Unit = {}
+) = Column {
+    ProvideTextStyle(MaterialTheme.typography.subtitle2) {
+        title()
+    }
+    ProvideTextStyle(MaterialTheme.typography.body2) {
+        Box(
+            modifier = Modifier.graphicsLayer {
+                alpha = SubtitleAlpha
+            }
+        ) {
+            subtitle()
+        }
+    }
+    ProvideTextStyle(MaterialTheme.typography.body2) {
+        Box(
+            modifier = Modifier.graphicsLayer {
+                alpha = DescriptionAlpha
+            }
+        ) {
+            description()
+        }
+    }
+}

+ 10 - 10
src/main/kotlin/eu/mjdev/desktop/components/card/ClassicCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ClassicCard.kt

@@ -6,21 +6,20 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.*
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.icons.Icons
+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
 
 // todo
 @Composable
@@ -67,7 +66,8 @@ fun ClassicCard(
     }
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun ClassicCardPreview() = preview {
     ClassicCard(
@@ -80,7 +80,7 @@ fun ClassicCardPreview() = preview {
         scale = CardDefaults.scale(1f),
         image = {
             ImageAny(
-                src = Icons.User,
+                src = AccountCircle,
                 contentDescription = ""
             )
         },

+ 22 - 46
src/main/kotlin/eu/mjdev/desktop/components/card/CompactCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/CompactCard.kt

@@ -6,29 +6,32 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.ProvideTextStyle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.BoxScope
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.draw.drawWithCache
 import androidx.compose.ui.graphics.Brush
 import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.graphicsLayer
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.card.base.CardDefaults.DescriptionAlpha
-import eu.mjdev.desktop.components.card.base.CardDefaults.SubtitleAlpha
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.icons.Icons
+import org.mjdev.desktop.components.card.base.CardBorder
+import org.mjdev.desktop.components.card.base.CardColors
+import org.mjdev.desktop.components.card.base.CardDefaults
+import org.mjdev.desktop.components.card.base.CardGlow
+import org.mjdev.desktop.components.card.base.CardScale
+import org.mjdev.desktop.components.card.base.CardShape
+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
 
 // todo
 @Composable
@@ -81,36 +84,8 @@ fun CompactCard(
     }
 }
 
-@Composable
-fun CardContent(
-    title: @Composable () -> Unit = {},
-    subtitle: @Composable () -> Unit = {},
-    description: @Composable () -> Unit = {}
-) = Column {
-    ProvideTextStyle(MaterialTheme.typography.subtitle2) {
-        title()
-    }
-    ProvideTextStyle(MaterialTheme.typography.body2) {
-        Box(
-            modifier = Modifier.graphicsLayer {
-                alpha = SubtitleAlpha
-            }
-        ) {
-            subtitle()
-        }
-    }
-    ProvideTextStyle(MaterialTheme.typography.body2) {
-        Box(
-            modifier = Modifier.graphicsLayer {
-                alpha = DescriptionAlpha
-            }
-        ) {
-            description()
-        }
-    }
-}
-
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun CardContentPreview() = preview {
     CardContent(
@@ -126,7 +101,8 @@ fun CardContentPreview() = preview {
     )
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun CompactCardPreview() = preview {
     CompactCard(
@@ -141,7 +117,7 @@ fun CompactCardPreview() = preview {
         image = {
             ImageAny(
                 modifier = Modifier.fillMaxSize(),
-                src = Icons.User,
+                src = AccountCircle,
                 contentDescription = ""
             )
         },

+ 33 - 24
src/main/kotlin/eu/mjdev/desktop/components/card/FocusableCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/FocusableCard.kt

@@ -6,15 +6,17 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
 import androidx.compose.material.MaterialTheme
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.BrokenImage
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
@@ -26,23 +28,29 @@ 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 eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
-import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
-import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.components.text.AutoHideEmptyText
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.focusState
-import eu.mjdev.desktop.extensions.Compose.isFocused
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
-import eu.mjdev.desktop.extensions.Compose.rememberFocusState
-import eu.mjdev.desktop.extensions.Compose.requestFocusOnTouch
-import eu.mjdev.desktop.helpers.compose.FocusHelper
+import org.mjdev.desktop.components.card.base.CardBorder
+import org.mjdev.desktop.components.card.base.CardColors
+import org.mjdev.desktop.components.card.base.CardDefaults
+import org.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
+import org.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
+import org.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
+import org.mjdev.desktop.components.card.base.CardGlow
+import org.mjdev.desktop.components.card.base.CardScale
+import org.mjdev.desktop.components.card.base.CardShape
+import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.components.surface.base.Glow
+import org.mjdev.desktop.components.text.AutoHideEmptyText
+import org.mjdev.desktop.components.text.TextAny
+import org.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.FocusState.focusState
+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.extensions.FocusState.requestFocusOnTouch
+import org.mjdev.desktop.helpers.compose.FocusHelper
+import org.mjdev.desktop.icons.image.BrokenImage
 
-// todo
+// todo calculate
 @Composable
 fun FocusableCard(
     item: Any? = null,
@@ -59,7 +67,7 @@ fun FocusableCard(
     showTitle: Boolean = true,
     placeholder: @Composable () -> Unit = {
         Image(
-            imageVector = Icons.Filled.BrokenImage,
+            imageVector = BrokenImage,
             contentDescription = "",
             contentScale = contentScale
         )
@@ -87,7 +95,7 @@ fun FocusableCard(
         }
     },
     titlePadding: PaddingValues = PaddingValues(8.dp),
-    cardWidth: Dp = computeCardWidth(),
+    cardWidth: Dp = 0.dp, // computeCardWidth(),
     aspectRatio: Float = 16f / 9f,
     onClick: ((item: Any?) -> Unit)? = null,
 ) = CompactCard(
@@ -159,7 +167,8 @@ fun FocusableCard(
     },
 )
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun FocusableCardPreview() = preview {
     FocusableCard(

+ 22 - 17
src/main/kotlin/eu/mjdev/desktop/components/card/ItemCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/ItemCard.kt

@@ -6,15 +6,12 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.Image
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.size
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.BrokenImage
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.MutableState
 import androidx.compose.ui.Modifier
@@ -24,17 +21,24 @@ 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 eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
-import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
-import eu.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
-import eu.mjdev.desktop.extensions.Compose.rememberFocusState
-import eu.mjdev.desktop.helpers.compose.FocusHelper
+import org.mjdev.desktop.components.card.base.CardBorder
+import org.mjdev.desktop.components.card.base.CardColors
+import org.mjdev.desktop.components.card.base.CardDefaults
+import org.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
+import org.mjdev.desktop.components.card.base.CardDefaults.colorFocusBorder
+import org.mjdev.desktop.components.card.base.CardDefaults.colorFocusGlow
+import org.mjdev.desktop.components.card.base.CardGlow
+import org.mjdev.desktop.components.card.base.CardScale
+import org.mjdev.desktop.components.card.base.CardShape
+import org.mjdev.desktop.components.image.ImageAny
+import org.mjdev.desktop.components.surface.base.Glow
+import org.mjdev.desktop.extensions.Compose.preview
+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
 
+// todo calculate
 @Composable
 fun ItemCard(
     item: Any? = null,
@@ -49,7 +53,7 @@ fun ItemCard(
     aspectRatio: Float = 16f / 9f,
     placeholder: @Composable () -> Unit = {
         Image(
-            imageVector = Icons.Filled.BrokenImage,
+            imageVector = BrokenImage,
             "",
             modifier
         )
@@ -63,7 +67,7 @@ fun ItemCard(
 //            placeholder = placeholder
         )
     },
-    cardWidth: Dp = computeCardWidth(),
+    cardWidth: Dp = 0.dp, // computeCardWidth(),
     focused: Boolean = false,
     focusState: MutableState<FocusState> = rememberFocusState(
         item,
@@ -97,7 +101,8 @@ fun ItemCard(
     placeholder = placeholder
 )
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun ItemCardPreview() = preview {
     ItemCard(

+ 16 - 16
src/main/kotlin/eu/mjdev/desktop/components/card/PhotoCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/PhotoCard.kt

@@ -1,6 +1,5 @@
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.layout.PaddingValues
 import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.padding
@@ -14,18 +13,18 @@ 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 eu.mjdev.desktop.components.card.base.CardDefaults
-import eu.mjdev.desktop.components.card.base.CardDefaults.HorizontalImageAspectRatio
-import eu.mjdev.desktop.components.card.base.CardScale
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.image.PhotoImage
-import eu.mjdev.desktop.extensions.Compose.isFocused
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.extensions.Compose.rememberFocusRequester
-import eu.mjdev.desktop.extensions.Compose.rememberFocusState
-import eu.mjdev.desktop.helpers.compose.FocusHelper
-import eu.mjdev.desktop.provider.DesktopProvider
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
+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.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.interfaces.IDesktopContext
 
 @Composable
 fun PhotoCard(
@@ -87,14 +86,15 @@ fun PhotoCard(
 
 @Composable
 fun computeCardWidth(
-    api: DesktopProvider = LocalDesktop.current,
+    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
 }
 
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun PhotoCardPreview() = preview {
     PhotoCard(

+ 9 - 10
src/main/kotlin/eu/mjdev/desktop/components/card/WideClassicCard.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/WideClassicCard.kt

@@ -6,21 +6,20 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card
+package org.mjdev.desktop.components.card
 
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.foundation.interaction.MutableInteractionSource
 import androidx.compose.foundation.layout.*
 import androidx.compose.runtime.Composable
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.card.base.*
-import eu.mjdev.desktop.components.image.ImageAny
-import eu.mjdev.desktop.components.surface.base.Glow
-import eu.mjdev.desktop.components.text.TextAny
-import eu.mjdev.desktop.extensions.Compose.preview
-import eu.mjdev.desktop.icons.Icons
+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
 
 @Suppress("unused")
 @Composable
@@ -56,7 +55,7 @@ fun WideClassicCard(
     }
 }
 
-@Preview
+//@Preview
 @Composable
 fun WideClassicCardPreview() = preview {
     WideClassicCard(
@@ -69,7 +68,7 @@ fun WideClassicCardPreview() = preview {
         scale = CardDefaults.scale(1f),
         image = {
             ImageAny(
-                src = Icons.User,
+                src = AccountCircle,
                 contentDescription = ""
             )
         },

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardBorder.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardBorder.kt

@@ -6,10 +6,10 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.runtime.Immutable
-import eu.mjdev.desktop.components.surface.base.Border
+import org.mjdev.desktop.components.surface.base.Border
 
 @Immutable
 class CardBorder(

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardColors.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardColors.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.ui.graphics.Color

+ 8 - 2
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardDefaults.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardDefaults.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.foundation.BorderStroke
 import androidx.compose.foundation.shape.RoundedCornerShape
@@ -20,7 +20,13 @@ import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Shape
 import androidx.compose.ui.unit.Dp
 import androidx.compose.ui.unit.dp
-import eu.mjdev.desktop.components.surface.base.*
+import org.mjdev.desktop.components.surface.base.Border
+import org.mjdev.desktop.components.surface.base.ClickableSurfaceBorder
+import org.mjdev.desktop.components.surface.base.ClickableSurfaceColors
+import org.mjdev.desktop.components.surface.base.ClickableSurfaceGlow
+import org.mjdev.desktop.components.surface.base.ClickableSurfaceScale
+import org.mjdev.desktop.components.surface.base.ClickableSurfaceShape
+import org.mjdev.desktop.components.surface.base.Glow
 
 @Suppress("ConstPropertyName", "unused", "MemberVisibilityCanBePrivate")
 object CardDefaults {

+ 2 - 2
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardGlow.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardGlow.kt

@@ -6,10 +6,10 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.runtime.Immutable
-import eu.mjdev.desktop.components.surface.base.Glow
+import org.mjdev.desktop.components.surface.base.Glow
 
 @Immutable
 class CardGlow(

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardScale.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardScale.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.runtime.Immutable
 

+ 1 - 1
src/main/kotlin/eu/mjdev/desktop/components/card/base/CardShape.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/card/base/CardShape.kt

@@ -6,7 +6,7 @@
  *  w: https://mjdev.org
  */
 
-package eu.mjdev.desktop.components.card.base
+package org.mjdev.desktop.components.card.base
 
 import androidx.compose.runtime.Immutable
 import androidx.compose.ui.graphics.Shape

+ 7 - 6
src/main/kotlin/eu/mjdev/desktop/components/charts/DonutChart.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chart/DonutChart.kt

@@ -1,8 +1,7 @@
-package eu.mjdev.desktop.components.charts
+package org.mjdev.desktop.components.chart
 
 import androidx.compose.animation.core.AnimationSpec
 import androidx.compose.animation.core.TweenSpec
-import androidx.compose.desktop.ui.tooling.preview.Preview
 import androidx.compose.runtime.*
 import androidx.compose.ui.Modifier
 import androidx.compose.ui.graphics.Color
@@ -10,9 +9,10 @@ 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 eu.mjdev.desktop.extensions.Compose.preview
+import org.mjdev.desktop.extensions.Compose.preview
 import kotlinx.coroutines.delay
 import kotlinx.coroutines.isActive
+import org.mjdev.desktop.extensions.System.currentTimeMillis
 
 @Suppress("FunctionName")
 @Composable
@@ -44,7 +44,7 @@ fun DonutChart(
         dataHandler,
         needRefresh
     ) { mutableStateOf(dataHandler()) }
-    DonutChart(
+   DonutChart(
         modifier = modifier,
         pieChartData = data,
         centerTitle = title,
@@ -60,13 +60,14 @@ fun DonutChart(
     LaunchedEffect(Unit) {
         while (isActive) {
             delay(refreshTimeout)
-            needRefresh = System.currentTimeMillis()
+            needRefresh = currentTimeMillis()
         }
     }
 }
 
 // todo
-@Preview
+//@Preview
+@Suppress("unused")
 @Composable
 fun DonutChartPreview() = preview(480) {
     DonutChart(

+ 23 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/chromecast/ChromecastView.kt

@@ -0,0 +1,23 @@
+package org.mjdev.desktop.components.chromecast
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.material.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+//import androidx.compose.ui.tooling.preview.Preview
+
+// todo
+@Composable
+fun ChromecastView(
+    modifier: Modifier = Modifier
+) {
+    Box(modifier) {
+        Text(text = "ChromecastView")
+    }
+}
+
+//@Preview(name = "ChromecastView")
+@Composable
+private fun PreviewChromecastView() {
+    ChromecastView()
+}

+ 191 - 0
composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/ControlCenter.kt

@@ -0,0 +1,191 @@
+package org.mjdev.desktop.components.controlcenter
+
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.foundation.background
+import androidx.compose.foundation.gestures.Orientation.Horizontal
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.layout.wrapContentSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.Divider
+import androidx.compose.material.Icon
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.MutableState
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+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.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.LaunchedEffect.runAsync
+import org.mjdev.desktop.extensions.Modifier.onLeftClick
+import org.mjdev.desktop.extensions.Modifier.onMouseEnter
+import org.mjdev.desktop.extensions.Modifier.onMousePress
+import org.mjdev.desktop.extensions.Modifier.onRightClick
+import org.mjdev.desktop.extensions.MutableStateExt.rememberCalculated
+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
+
+@Composable
+fun ControlCenter(
+    modifier: Modifier = Modifier,
+    controlCenterState: VisibilityState = rememberVisibilityState(),
+    pagerState: MutableState<Int> = rememberState(0),
+    enterAnimation: EnterTransition = ControlCenterEnterAnimation,
+    exitAnimation: ExitTransition = ControlCenterExitAnimation,
+    onContextMenuClick: () -> Unit = {},
+    onFocusChange: (focused: Boolean) -> Unit = {},
+    onTooltip: (item: Any?) -> Unit = {}
+) = withDesktopContext {
+    val controlCenterExpandedWidth: Dp by rememberComputed(
+        theme.controlCenterExpandedWidthPercent,
+        containerSize.width
+    ) {
+        (containerSize.width / 100f) * theme.controlCenterExpandedWidthPercent.orElse { 0 }
+    }
+    val pagesFiltered by rememberCalculated(controlCenterPages) {
+        controlCenterPages.filter { p -> p.condition(context) }
+    }
+    SlidingPanel(
+        modifier = modifier.fillMaxHeight(),
+        orientation = Horizontal,
+        state = controlCenterState,
+        enterAnimation = enterAnimation,
+        exitAnimation = exitAnimation,
+        onPointerEnter = {
+            onFocusChange(true)
+        }
+    ) { isVisible ->
+        if (!isVisible) {
+            Divider(
+                modifier = Modifier.fillMaxHeight()
+                    .width(controlCenterDividerWidth),
+                color = if (isDebug) Color.Red else Color.Transparent,
+                thickness = controlCenterDividerWidth
+            )
+        } else {
+            BlurPanel(
+                modifier = Modifier.fillMaxHeight()
+                    .matchParentSize()
+            )
+            Box(
+                modifier = Modifier.fillMaxHeight()
+                    .wrapContentSize()
+                    .background(
+                        backgroundColor.copy(alpha = controlCenterBackgroundAlpha)
+                    ),
+            ) {
+                Row(
+                    modifier = Modifier.fillMaxHeight(),
+                ) {
+                    Box(
+                        contentAlignment = Alignment.TopEnd
+                    ) {
+                        LazyColumn(
+                            modifier = Modifier
+                                .fillMaxHeight()
+                                .background(backgroundColor)
+                                .padding(8.dp),
+                            verticalArrangement = Arrangement.Top,
+                            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
+                                                }
+                                            }
+                                            onRightClick {
+                                                runAsync {
+                                                    pagerState.value = idx
+                                                }
+                                                onContextMenuClick()
+                                            }
+                                        },
+                                    imageVector = page.icon,
+                                    contentDescription = "",
+                                    tint = if (pagerState.value == idx) backgroundColor else textColor
+                                )
+                            }
+                        }
+                        Box(
+                            modifier = Modifier
+                                .padding(
+                                    end = controlCenterIconSize.width + 16.dp
+                                )
+                                .fillMaxHeight(),
+                        ) {
+                            Row(
+                                Modifier.fillMaxHeight()
+                            ) {
+                                Divider(
+                                    modifier = Modifier.fillMaxHeight()
+                                        .width(2.dp),
+                                    color = borderColor,
+                                    thickness = 2.dp
+                                )
+                                Column(
+                                    modifier = Modifier.width(controlCenterExpandedWidth),
+                                ) {
+                                    val page = pagesFiltered[pagerState.value]
+                                    Box(modifier = Modifier.weight(1f)) {
+                                        page.render()
+                                    }
+                                }
+                                Divider(
+                                    modifier = Modifier.fillMaxHeight()
+                                        .width(2.dp),
+                                    color = borderColor,
+                                    thickness = 2.dp
+                                )
+                            }
+                        }
+                        Divider(
+                            modifier = Modifier.fillMaxHeight().width(2.dp),
+                            color = Color.White.copy(0.1f),
+                            thickness = 2.dp
+                        )
+                    }
+                }
+            }
+        }
+    }
+}

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

@@ -0,0 +1,54 @@
+package org.mjdev.desktop.components.controlcenter.base
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Settings
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
+import org.mjdev.desktop.components.controlcenter.base.ControlCenterPageScope.Companion.withControlCenterPageScope
+import org.mjdev.desktop.interfaces.IControlCenterPageDataSaver
+import org.mjdev.desktop.interfaces.IDesktopContext
+import org.mjdev.desktop.interfaces.IPage
+
+@Suppress("MemberVisibilityCanBePrivate")
+class ControlCenterPage(
+    override val icon: ImageVector = Icons.Filled.Settings,
+    override val name: String = "",
+    override val condition: IDesktopContext.() -> Boolean = { true },
+    override val saver: IControlCenterPageDataSaver? = null,
+    val cache: PageCache = PageCache(saver),
+    val showHeader: Boolean = true,
+//    val scrollState: ScrollState = ScrollState(0),
+    val content: @Composable ControlCenterPageScope.() -> Unit = {},
+) : IPage {
+    @Composable
+    override fun render() {
+        withControlCenterPageScope(
+            cache = cache
+        ) {
+            Column(
+                modifier = Modifier.fillMaxSize()
+            ) {
+                PageHeader(
+                    page = this@ControlCenterPage
+                )
+                Box(
+                    modifier = Modifier.weight(1f),
+                ) {
+                    content()
+                }
+//            VerticalScrollbar(
+//                modifier = Modifier.fillMaxHeight(),
+//                adapter = rememberScrollbarAdapter(scrollState)
+//            )
+            }
+        }
+    }
+
+    override fun dispose() {
+        // todo
+    }
+}

+ 8 - 8
src/main/kotlin/eu/mjdev/desktop/components/controlcenter/base/ControlCenterPageScope.kt → composeApp/src/commonMain/kotlin/org/mjdev/desktop/components/controlcenter/base/ControlCenterPageScope.kt

@@ -1,23 +1,23 @@
-package eu.mjdev.desktop.components.controlcenter.base
+package org.mjdev.desktop.components.controlcenter.base
 
 import androidx.compose.runtime.Composable
 import androidx.compose.runtime.DisallowComposableCalls
-import eu.mjdev.desktop.provider.DesktopProvider
-import eu.mjdev.desktop.provider.DesktopProvider.Companion.LocalDesktop
-import eu.mjdev.desktop.provider.DesktopScope
+import org.mjdev.desktop.context.DesktopContextScope
+import org.mjdev.desktop.context.LocalDesktopContext
+import org.mjdev.desktop.interfaces.IDesktopContext
 
 class ControlCenterPageScope(
-    api: DesktopProvider,
+    context: IDesktopContext,
     val cache: PageCache
-) : DesktopScope(api) {
+) : DesktopContextScope(context) {
     companion object {
         @Suppress("UnusedReceiverParameter")
         @Composable
         fun ControlCenterPage.withControlCenterPageScope(
-            api: DesktopProvider = LocalDesktop.current,
+            context: IDesktopContext = LocalDesktopContext.current,
             cache: PageCache,
             block: @Composable ControlCenterPageScope.() -> Unit
-        ) = ControlCenterPageScope(api, cache).apply {
+        ) = ControlCenterPageScope(context, cache).apply {
             block()
         }
 

Daži faili netika attēloti, jo izmaiņu fails ir pārāk liels