├── .fleet └── receipt.json ├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── build.gradle.kts ├── example ├── build.gradle.kts └── src │ ├── commonMain │ └── composeResources │ │ ├── drawable │ │ └── compose-multiplatform.xml │ │ └── values │ │ └── strings.xml │ └── desktopMain │ ├── kotlin │ ├── io │ │ └── github │ │ │ └── lumkit │ │ │ └── desktop │ │ │ └── example │ │ │ ├── App.kt │ │ │ ├── MyViewModel.kt │ │ │ ├── navigation │ │ │ └── navigation.kt │ │ │ └── screen │ │ │ ├── AlertExampleScreen.kt │ │ │ ├── ButtonExampleScreen.kt │ │ │ ├── ChartExampleScreen.kt │ │ │ ├── LayerWindowExampleScreen.kt │ │ │ ├── ProgressExampleScreen.kt │ │ │ ├── SettingsScreen.kt │ │ │ ├── TextFieldExampleScreen.kt │ │ │ ├── ToastExampleScreen.kt │ │ │ └── ViewModelExampleScreen.kt │ └── main.kt │ └── resources │ └── material-theme.json ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── lint-compose-ui ├── build.gradle.kts └── src │ └── desktopMain │ ├── kotlin │ └── io │ │ └── github │ │ └── lumkit │ │ └── desktop │ │ ├── Const.kt │ │ ├── LintApplication.kt │ │ ├── annotates │ │ ├── FileMode.kt │ │ ├── IntDef.kt │ │ ├── PreferencesMode.kt │ │ └── StringDef.kt │ │ ├── common │ │ └── Colors.kt │ │ ├── context │ │ ├── Context.kt │ │ ├── ContextWrapper.kt │ │ ├── Toast.kt │ │ └── ToastQueue.kt │ │ ├── data │ │ ├── DarkThemeMode.kt │ │ ├── SharedPreferenceBean.kt │ │ ├── ThemeBean.kt │ │ └── WindowSize.kt │ │ ├── lifecycle │ │ ├── Lifecycle.kt │ │ └── ViewModel.kt │ │ ├── preferences │ │ └── SharedPreferences.kt │ │ ├── shell │ │ └── KeepShell.kt │ │ ├── ui │ │ ├── LintWindow.kt │ │ ├── components │ │ │ ├── LintButton.kt │ │ │ ├── LintCard.kt │ │ │ ├── LintChart.kt │ │ │ ├── LintDivider.kt │ │ │ ├── LintFolder.kt │ │ │ ├── LintProgress.kt │ │ │ ├── LintScrollBar.kt │ │ │ ├── LintSide.kt │ │ │ └── LintTextField.kt │ │ ├── dialog │ │ │ └── LintAlert.kt │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Design.kt │ │ │ ├── LintTheme.kt │ │ │ └── Theme.kt │ │ └── util │ │ └── Jsons.kt │ └── resources │ └── default-theme.json ├── settings.gradle.kts └── static └── img └── screen-shoot.png /.fleet/receipt.json: -------------------------------------------------------------------------------- 1 | // Project generated by Kotlin Multiplatform Wizard 2 | { 3 | "spec": { 4 | "template_id": "kmt", 5 | "targets": { 6 | "desktop": { 7 | "ui": [ 8 | "compose" 9 | ] 10 | } 11 | } 12 | }, 13 | "timestamp": "2024-04-07T12:37:34.235806975Z" 14 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle files 2 | .gradle/ 3 | build/ 4 | 5 | # Local configuration file (sdk path, etc) 6 | local.properties 7 | 8 | # Log/OS Files 9 | *.log 10 | 11 | # Android Studio generated files and folders 12 | captures/ 13 | .externalNativeBuild/ 14 | .cxx/ 15 | *.apk 16 | output.json 17 | 18 | # IntelliJ 19 | *.iml 20 | .idea/ 21 | 22 | # Keystore files 23 | *.jks 24 | *.keystore 25 | 26 | # Google Services (e.g. APIs or Firebase) 27 | google-services.json 28 | 29 | # Android Profiling 30 | *.hprof -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Lint UI for Compose Desktop 2 | 3 | [![License](https://img.shields.io/github/license/lumkit/lint-ui)](LICENSE) 4 | [![Version](https://img.shields.io/github/v/release/lumkit/lint-ui?include_prereleases)](https://github.com/lumkit/lint-ui/releases) 5 | [![Maven Central](https://img.shields.io/maven-central/v/io.github.lumkit/lint-compose-ui)](https://central.sonatype.com/artifact/io.github.lumkit/lint-compose-ui/) 6 | 7 | A Compose Desktop UI framework supporting global theme control. (aka LintUI) 8 | 9 | ## Introduce 10 | 11 | This is a UI framework developed for Compose Desktop (on Material Design 3), 12 | and it integrates many features that normal applications 13 | should have (such as data persistence). 14 | This UI framework will remain open source and free. 15 | 16 | ## Features 17 | 18 | - [x] Support the Root Panel covering the whole window (Lint Window) 19 | - [x] Material Design3 color matching style (Lint Theme Scope) 20 | - [x] Commonly used widgets 21 | - [x] Buttons (Lint Button) 22 | - [x] Cards (Lint Card) 23 | - [x] Dividers (Lint Dividers) 24 | - [x] Folded container (Lint Folder) 25 | - [x] Minimizable, collapsible and nestable side navigation bar (Lint Side) 26 | - [x] Flexible window without native decoration (Lint Layer Window) 27 | - [x] Dialog of basic style (Lint Dialog) 28 | - [x] Toast (Lint Window Toast) 29 | - [x] Circle Indicator & Linear Indicator (Lint Progress/Lint Indicator) 30 | - [x] Stack Chart (Lint Stack Chart) 31 | - [ ] More beautiful and practical UI components will continue to be developed in the future 32 | - [x] A simple context provider 33 | - [x] Shared Preferences based on SQLite (SharedPreferences) 34 | - [x] Out of the box theme management store (Lint Theme Store) 35 | - [x] Out-of-box theme installation framework 36 | - [x] Unify the global theme 37 | - [x] Dynamic perception system dark mode 38 | - [x] View Model Store 39 | - [ ] More features will be continuously updated in the future 40 | 41 | ## Screen shoot 42 | 43 | ![screen-shoot.png](static/img/screen-shoot.png) 44 | 45 | ## Use this library in your project 46 | 47 | Before that, you can run the example we provided for you to see the concrete effect. 48 | 49 | ```shell 50 | ./gradlew desktopRun -DmainClass=MainKt --quiet 51 | ``` 52 | 53 | ### LintUI framework version requirements 54 | 55 | | Lint UI | Kotlin | Compose Framework | Compose Plugin | Java | 56 | |:-------:|:---------:|:-----------------:|:--------------:|:--------:| 57 | | 1.0.1 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 58 | | 1.0.2 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 59 | | 1.0.3 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 60 | | 1.0.4 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 61 | | 1.0.5 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 62 | | 1.0.6 | 1.9.22 | 1.6.2 | 1.6.0 | JDK 17++ | 63 | | 1.0.7 | 2.0.0-RC2 | 1.6.11 | 1.6.11 | JDK 17++ | 64 | 65 | ### 1. Configure the Maven central warehouse for the project. 66 | 67 | ```kotlin 68 | dependencyResolutionManagement { 69 | repositories { 70 | google() 71 | mavenCentral() 72 | maven(url = "https://jitpack.io") 73 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 74 | } 75 | } 76 | ``` 77 | 78 | ### 2. Import the [lint-compose-ui] dependency. 79 | 80 | ```kotlin 81 | dependencies { 82 | // You just import dependencies such as jb-compose-desktop-currentOs and jb-compose-components-resources. 83 | 84 | // Base on KMP 85 | implementation("io.github.lumkit:lint-compose-ui:1.0.7") 86 | // Only Desktop 87 | implementation("io.github.lumkit:lint-compose-ui-desktop:1.0.7") 88 | } 89 | ``` 90 | 91 | ### 3. Start writing your first desktop application 92 | 93 | ```kotlin 94 | import androidx.compose.foundation.layout.fillMaxSize 95 | import androidx.compose.ui.Modifier 96 | import androidx.compose.ui.window.MenuBar 97 | import io.github.lumkit.desktop.context.LocalContext 98 | import io.github.lumkit.desktop.data.DarkThemeMode 99 | import io.github.lumkit.desktop.example.App 100 | import io.github.lumkit.desktop.lintApplication 101 | import io.github.lumkit.desktop.ui.LintWindow 102 | import io.github.lumkit.desktop.ui.theme.AnimatedLintTheme 103 | import io.github.lumkit.desktop.ui.theme.LocalThemeStore 104 | import lint_ui.example.generated.resources.* 105 | import org.jetbrains.compose.resources.ExperimentalResourceApi 106 | import org.jetbrains.compose.resources.painterResource 107 | import org.jetbrains.compose.resources.stringResource 108 | import java.awt.Dimension 109 | 110 | @OptIn(ExperimentalResourceApi::class) 111 | fun main() = lintApplication( 112 | packageName = "LintUIExample" 113 | ) { 114 | val context = LocalContext.current 115 | 116 | // Toggles the global dark theme mode. 117 | LintWindow( 118 | onCloseRequest = ::exitApplication, 119 | rememberSize = true, 120 | title = context.getPackageName(), 121 | icon = painterResource(Res.drawable.compose_multiplatform), 122 | ) { 123 | AnimatedLintTheme( 124 | modifier = Modifier.fillMaxSize(), 125 | ) { 126 | Text("Hello Lint UI!") 127 | } 128 | } 129 | } 130 | ``` 131 | 132 | ## How to use some built-in APIs. 133 | 134 | * Context 135 | ```kotlin 136 | // Get global context 137 | val context = LocalContext.current 138 | 139 | // App work path 140 | val filesDir = context.getFilesDir() 141 | 142 | // App package name 143 | val packageName = context.getPackageName() 144 | 145 | // Persistent file name of App (based on sqlite implementation) 146 | val sharedPreferences = context.getSharedPreferences("shared preference file name") 147 | 148 | // Gets the theme instance in the specified persistence file. 149 | val theme = context.getTheme(sharedPreferences = sharedPreferences) 150 | ``` 151 | 152 | * Shared Preferences 153 | ```kotlin 154 | // Gets the global SharedPreferences instance. 155 | val sharedPreferences = LocalSharedPreferences.current 156 | 157 | // Get the data in the hardware (is-dark is virtual). 158 | val isDark = sharedPreferences.get("is-dark") 159 | 160 | // Put string 161 | sharedPreferences.putString("is-dark", isDark) 162 | // Put type data 163 | sharedPreferences.put("type-data", arrayListOf("Dark", "Light")) 164 | ``` 165 | 166 | * ViewModel 167 | ```kotlin 168 | // Create a new entity class that inherits ViewModel[io.github.lumkit.desktop.lifecycle.ViewModel], such as "MyViewModel". 169 | class MyViewModel : ViewModel() { 170 | 171 | private val _text = MutableStateFlow("") 172 | val text: StateFlow = _text.asStateFlow() 173 | 174 | fun setText(text: String) { 175 | _text.value = text 176 | } 177 | 178 | } 179 | 180 | // use view model and state 181 | val viewModel = viewModel() 182 | val text by viewModel.text.collectAsState() 183 | 184 | LintTextField( 185 | value = text, 186 | onValueChange = viewModel::setText, 187 | label = { 188 | Text("输入内容") 189 | } 190 | ) 191 | ``` 192 | 193 | * More built-in APIs will be gradually opened, so stay tuned 194 | 195 | ## Excellent cited framework or repositories (Rank insensitive) 196 | 197 | * [FlatLat](https://github.com/JFormDesigner/FlatLaf) 198 | * [Exposed](https://github.com/JetBrains/Exposed) 199 | * [Gson](https://github.com/google/gson) 200 | * [JSystemThemeDetector](https://github.com/Dansoftowner/jSystemThemeDetector) 201 | 202 | ## End -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | // this is necessary to avoid the plugins to be loaded multiple times 3 | // in each subproject's classloader 4 | alias(libs.plugins.jetbrainsCompose) apply false 5 | alias(libs.plugins.kotlinMultiplatform) apply false 6 | alias(libs.plugins.compose.compiler) apply false 7 | // id("io.lumkit.build") 8 | } -------------------------------------------------------------------------------- /example/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.jetbrainsCompose) 6 | alias(libs.plugins.compose.compiler) 7 | } 8 | 9 | kotlin { 10 | jvm("desktop") 11 | 12 | sourceSets { 13 | val desktopMain by getting 14 | 15 | commonMain.dependencies { 16 | implementation(compose.components.resources) 17 | } 18 | desktopMain.dependencies { 19 | implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) 20 | implementation(compose.desktop.currentOs) 21 | implementation(project(":lint-compose-ui")) 22 | // implementation(libs.lint.compose.ui) 23 | } 24 | } 25 | } 26 | 27 | 28 | compose.desktop { 29 | application { 30 | mainClass = "MainKt" 31 | 32 | nativeDistributions { 33 | modules("java.sql") 34 | 35 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb, TargetFormat.Exe) 36 | packageName = "io.github.lumkit.lint" 37 | packageVersion = libs.versions.lint.compose.ui.get() 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /example/src/commonMain/composeResources/drawable/compose-multiplatform.xml: -------------------------------------------------------------------------------- 1 | 6 | 10 | 14 | 18 | 24 | 30 | 36 | 37 | -------------------------------------------------------------------------------- /example/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 设置 4 | 深色主题 5 | 跟随系统 6 | 浅色 7 | 深色 8 | 主题设置 9 | 主题颜色 10 | 获取主题 11 | 卸载主题 12 | -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/App.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.FastOutSlowInEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.rememberScrollbarAdapter 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.foundation.verticalScroll 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.filled.Menu 13 | import androidx.compose.material.icons.filled.Search 14 | import androidx.compose.material.icons.filled.Settings 15 | import androidx.compose.material3.DropdownMenu 16 | import androidx.compose.material3.DropdownMenuItem 17 | import androidx.compose.material3.Icon 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.* 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.Dp 23 | import androidx.compose.ui.unit.dp 24 | import androidx.compose.ui.window.PopupProperties 25 | import io.github.lumkit.desktop.example.navigation.NavItem 26 | import io.github.lumkit.desktop.example.navigation.screens 27 | import io.github.lumkit.desktop.example.navigation.settingsNavigation 28 | import io.github.lumkit.desktop.ui.components.* 29 | 30 | @Composable 31 | fun App() { 32 | val navItem = remember { mutableStateOf(screens.first()) } 33 | 34 | Row( 35 | modifier = Modifier.fillMaxSize(), 36 | horizontalArrangement = Arrangement.spacedBy(8.dp), 37 | ) { 38 | SideBar(navItem) 39 | LintCard( 40 | modifier = Modifier.fillMaxSize(), 41 | shape = RoundedCornerShape(topStart = 6.dp, topEnd = 0.dp, bottomStart = 0.dp, bottomEnd = 0.dp), 42 | ) { 43 | AnimatedContent( 44 | targetState = navItem.value.screen, 45 | transitionSpec = { 46 | (fadeIn(tween(easing = FastOutSlowInEasing)) + expandVertically(tween(easing = FastOutSlowInEasing)) + scaleIn( 47 | tween() 48 | )).togetherWith( 49 | shrinkVertically(tween(easing = FastOutSlowInEasing)) + fadeOut( 50 | tween(easing = FastOutSlowInEasing) 51 | ) + scaleOut(tween()) 52 | ) 53 | } 54 | ) { 55 | it() 56 | } 57 | } 58 | } 59 | } 60 | 61 | @Composable 62 | private fun SideBar(navItem: MutableState) { 63 | Column( 64 | modifier = Modifier.fillMaxHeight() 65 | .padding(start = 8.dp), 66 | verticalArrangement = Arrangement.spacedBy(8.dp) 67 | ) { 68 | val minimize = remember { mutableStateOf(false) } 69 | val width by remember { mutableStateOf(320.dp) } 70 | var searchValue by remember { mutableStateOf("") } 71 | 72 | LintNavigationIconButton( 73 | width = width, 74 | minimize = minimize.value, 75 | title = { 76 | Text("控制栏", softWrap = false) 77 | }, 78 | onClick = { 79 | minimize.value = !minimize.value 80 | } 81 | ) { 82 | Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu") 83 | } 84 | 85 | Column( 86 | horizontalAlignment = Alignment.End 87 | ) { 88 | LintSearchSide( 89 | width = width, 90 | minimize = minimize.value, 91 | icon = { 92 | Icon(imageVector = Icons.Default.Search, contentDescription = "Search") 93 | }, 94 | value = searchValue, 95 | onValueChange = { 96 | searchValue = it 97 | }, 98 | onClick = { 99 | minimize.value = false 100 | }, 101 | onClean = { 102 | searchValue = "" 103 | }, 104 | ) 105 | DropdownMenu( 106 | searchValue.trim().isNotEmpty(), 107 | onDismissRequest = {}, 108 | properties = PopupProperties(focusable = false) 109 | ) { 110 | for (i in 0 until 5) { 111 | DropdownMenuItem( 112 | text = { 113 | Text("搜索项 ${i + 1}") 114 | }, 115 | onClick = { 116 | searchValue = "" 117 | } 118 | ) 119 | } 120 | } 121 | } 122 | 123 | Row( 124 | modifier = Modifier.fillMaxHeight().weight(1f), 125 | ) { 126 | val scrollState = rememberScrollState() 127 | val adapter = rememberScrollbarAdapter(scrollState) 128 | Column( 129 | modifier = Modifier.fillMaxHeight().verticalScroll(scrollState), 130 | verticalArrangement = Arrangement.spacedBy(4.dp) 131 | ) { 132 | screens.forEach { screen -> 133 | NavigationSide(width, minimize, navItem, screen) 134 | } 135 | } 136 | AnimatedVisibility( 137 | visible = !minimize.value, 138 | ) { 139 | LintVerticalScrollBar( 140 | modifier = Modifier.padding(start = 4.dp), 141 | adapter = adapter 142 | ) 143 | } 144 | } 145 | 146 | LintSideNavigationBar( 147 | minimize = minimize.value, 148 | width = width, 149 | selected = navItem.value == settingsNavigation, 150 | onClick = { 151 | navItem.value = settingsNavigation 152 | }, 153 | icon = { 154 | Icon(imageVector = Icons.Default.Settings, contentDescription = null) 155 | }, 156 | title = { 157 | Text(settingsNavigation.title, softWrap = false) 158 | } 159 | ) 160 | Spacer(Modifier) 161 | } 162 | } 163 | 164 | @Composable 165 | private fun NavigationSide( 166 | width: Dp, 167 | minimize: MutableState, 168 | navItemState: MutableState, 169 | navItem: NavItem 170 | ) { 171 | var isExpanded by remember { mutableStateOf(true) } 172 | val navItems = navItem.items 173 | LintSideNavigationBar( 174 | width = width, 175 | minimize = minimize.value, 176 | selected = navItemState.value == navItem, 177 | expanded = isExpanded, 178 | icon = { navItem.icon?.invoke() }, 179 | title = { 180 | Text(navItem.title, softWrap = false) 181 | }, 182 | subtitle = if (navItem.subtitle == null) { 183 | null 184 | } else { 185 | { 186 | Text(navItem.subtitle, softWrap = false) 187 | } 188 | }, 189 | onClick = { 190 | navItemState.value = navItem 191 | }, 192 | onExpandedClick = { 193 | isExpanded = !isExpanded 194 | }, 195 | child = if (navItems == null) { 196 | null 197 | } else { 198 | { 199 | navItems.forEach { 200 | NavigationSide(width, minimize, navItemState, it) 201 | } 202 | } 203 | } 204 | ) 205 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/MyViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example 2 | 3 | import io.github.lumkit.desktop.lifecycle.ViewModel 4 | import kotlinx.coroutines.flow.MutableStateFlow 5 | import kotlinx.coroutines.flow.StateFlow 6 | import kotlinx.coroutines.flow.asStateFlow 7 | 8 | class MyViewModel : ViewModel() { 9 | 10 | private val _text = MutableStateFlow("") 11 | val text: StateFlow = _text.asStateFlow() 12 | 13 | fun setText(text: String) { 14 | _text.value = text 15 | } 16 | 17 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/navigation/navigation.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.navigation 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.material.icons.Icons 7 | import androidx.compose.material.icons.automirrored.filled.Chat 8 | import androidx.compose.material.icons.filled.* 9 | import androidx.compose.material3.Icon 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import io.github.lumkit.desktop.example.screen.* 16 | 17 | data class NavItem( 18 | val title: String, 19 | val subtitle: String? = null, 20 | val icon: @Composable (() -> Unit)? = null, 21 | val screen: @Composable () -> Unit, 22 | val items: Array? = null, 23 | ) { 24 | override fun equals(other: Any?): Boolean { 25 | if (this === other) return true 26 | if (javaClass != other?.javaClass) return false 27 | 28 | other as NavItem 29 | 30 | if (title != other.title) return false 31 | if (subtitle != other.subtitle) return false 32 | if (icon != other.icon) return false 33 | if (screen != other.screen) return false 34 | if (items != null) { 35 | if (other.items == null) return false 36 | if (!items.contentEquals(other.items)) return false 37 | } else if (other.items != null) return false 38 | 39 | return true 40 | } 41 | 42 | override fun hashCode(): Int { 43 | var result = title.hashCode() 44 | result = 31 * result + (subtitle?.hashCode() ?: 0) 45 | result = 31 * result + (icon?.hashCode() ?: 0) 46 | result = 31 * result + screen.hashCode() 47 | result = 31 * result + (items?.contentHashCode() ?: 0) 48 | return result 49 | } 50 | } 51 | 52 | val settingsNavigation = NavItem( 53 | title = "设置", 54 | screen = @Composable { 55 | SettingsScreen() 56 | }, 57 | ) 58 | 59 | val screens = arrayOf( 60 | NavItem( 61 | title = "小组件", 62 | subtitle = "一些常用的小组件", 63 | icon = { 64 | Icon(Icons.Default.Widgets, contentDescription = null) 65 | }, 66 | screen = @Composable { 67 | Column( 68 | Modifier.fillMaxSize(), 69 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically), 70 | horizontalAlignment = Alignment.CenterHorizontally 71 | ) { 72 | Text("欢迎使用Lint UI Compose Framework") 73 | } 74 | }, 75 | items = arrayOf( 76 | NavItem( 77 | title = "Lint Buttons", 78 | subtitle = "基于MD3色彩风格设计的各式按钮", 79 | icon = { 80 | Icon(imageVector = Icons.Default.SmartButton, contentDescription = null) 81 | }, 82 | screen = @Composable { 83 | ButtonExampleScreen() 84 | }, 85 | ), 86 | NavItem( 87 | title = "Lint TextField", 88 | subtitle = "基于MD3色彩风格设计的文本输入框", 89 | icon = { 90 | Icon(imageVector = Icons.Default.TextFields, contentDescription = null) 91 | }, 92 | screen = @Composable { 93 | TextFieldExampleScreen() 94 | }, 95 | ), 96 | NavItem( 97 | title = "Lint Progress", 98 | subtitle = "基于MD3色彩风格设计的进度条", 99 | icon = { 100 | Icon(imageVector = Icons.Filled.Circle, contentDescription = null) 101 | }, 102 | screen = @Composable { 103 | ProgressExampleScreen() 104 | }, 105 | ), 106 | NavItem( 107 | title = "Lint Chats", 108 | subtitle = "一些简单的图表组件", 109 | icon = { 110 | Icon(imageVector = Icons.Default.BarChart, contentDescription = null) 111 | }, 112 | screen = @Composable { 113 | ChartExampleScreen() 114 | }, 115 | ), 116 | ), 117 | ), 118 | NavItem( 119 | title = "模态框", 120 | subtitle = "一些内置的弹窗样式", 121 | icon = { 122 | Icon(Icons.AutoMirrored.Filled.Chat, contentDescription = null) 123 | }, 124 | screen = @Composable { 125 | AlertExampleScreen() 126 | }, 127 | ), 128 | NavItem( 129 | title = "自定义无边框窗口", 130 | subtitle = "基于Compose组件绘制的无原生装饰的窗口", 131 | icon = { 132 | Icon(Icons.Default.Window, contentDescription = null) 133 | }, 134 | screen = @Composable { 135 | LayerWindowExampleScreen() 136 | }, 137 | ), 138 | NavItem( 139 | title = "Toast", 140 | subtitle = "简单的弹出提示示例", 141 | icon = { 142 | Icon(Icons.Default.PrivacyTip, contentDescription = null) 143 | }, 144 | screen = @Composable { 145 | ToastExampleScreen() 146 | }, 147 | ), 148 | NavItem( 149 | title = "Simple ViewModel", 150 | subtitle = "简单的单例ViewModel", 151 | icon = { 152 | Icon(Icons.Default.ModelTraining, contentDescription = null) 153 | }, 154 | screen = @Composable { 155 | ViewModelExampleScreen() 156 | }, 157 | ), 158 | ) -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/AlertExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.unit.dp 12 | import io.github.lumkit.desktop.ui.components.LintButton 13 | import io.github.lumkit.desktop.ui.dialog.LintAlert 14 | 15 | @OptIn(ExperimentalLayoutApi::class) 16 | @Composable 17 | fun AlertExampleScreen() { 18 | FlowRow( 19 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 20 | horizontalArrangement = Arrangement.spacedBy(8.dp), 21 | verticalArrangement = Arrangement.spacedBy(8.dp) 22 | ) { 23 | Column( 24 | verticalArrangement = Arrangement.spacedBy(8.dp), 25 | ) { 26 | val isShow = remember { mutableStateOf(false) } 27 | LintButton( 28 | onClick = { 29 | isShow.value = true 30 | } 31 | ) { 32 | Text("无按钮弹窗") 33 | } 34 | 35 | LintAlert( 36 | visible = isShow, 37 | title = "提示", 38 | ) { 39 | Text("这是一个基本的无按钮弹窗") 40 | } 41 | } 42 | 43 | Column( 44 | verticalArrangement = Arrangement.spacedBy(8.dp), 45 | ) { 46 | val isShow = remember { mutableStateOf(false) } 47 | LintButton( 48 | onClick = { 49 | isShow.value = true 50 | } 51 | ) { 52 | Text("单按钮弹窗") 53 | } 54 | 55 | LintAlert( 56 | visible = isShow, 57 | title = "提示", 58 | confirmButtonText = "确定", 59 | onConfirm = { 60 | isShow.value = false 61 | } 62 | ) { 63 | Text("这是一个基本的单按钮弹窗") 64 | } 65 | } 66 | 67 | Column( 68 | verticalArrangement = Arrangement.spacedBy(8.dp), 69 | ) { 70 | val isShow = remember { mutableStateOf(false) } 71 | LintButton( 72 | onClick = { 73 | isShow.value = true 74 | } 75 | ) { 76 | Text("双按钮弹窗") 77 | } 78 | 79 | LintAlert( 80 | visible = isShow, 81 | title = "提示", 82 | confirmButtonText = "确定", 83 | onConfirm = { 84 | isShow.value = false 85 | }, 86 | cancelButtonText = "取消", 87 | onCancel = { 88 | isShow.value = false 89 | } 90 | ) { 91 | Text("这是一个基本的双按钮弹窗") 92 | } 93 | } 94 | 95 | Column( 96 | verticalArrangement = Arrangement.spacedBy(8.dp), 97 | ) { 98 | val isShow = remember { mutableStateOf(false) } 99 | LintButton( 100 | onClick = { 101 | isShow.value = true 102 | } 103 | ) { 104 | Text("复合布局弹窗") 105 | } 106 | 107 | LintAlert( 108 | visible = isShow, 109 | title = "提示", 110 | confirmButtonText = "确定", 111 | onConfirm = { 112 | isShow.value = false 113 | }, 114 | cancelButtonText = "取消", 115 | onCancel = { 116 | isShow.value = false 117 | }, 118 | scrollable = false 119 | ) { 120 | Text("这是一个复合布局弹窗(偷个懒)") 121 | ProgressExampleScreen() 122 | } 123 | } 124 | 125 | Column( 126 | verticalArrangement = Arrangement.spacedBy(8.dp), 127 | ) { 128 | val isShow = remember { mutableStateOf(false) } 129 | LintButton( 130 | onClick = { 131 | isShow.value = true 132 | } 133 | ) { 134 | Text("不可取消双按钮弹窗") 135 | } 136 | 137 | LintAlert( 138 | visible = isShow, 139 | isCancel = false, 140 | title = "提示", 141 | confirmButtonText = "确定", 142 | onConfirm = { 143 | isShow.value = false 144 | }, 145 | cancelButtonText = "取消", 146 | onCancel = { 147 | isShow.value = false 148 | } 149 | ) { 150 | Text("这是一个不可取消双按钮弹窗") 151 | } 152 | } 153 | } 154 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/ButtonExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.foundation.Image 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.foundation.rememberScrollState 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.draw.alpha 13 | import androidx.compose.ui.draw.rotate 14 | import androidx.compose.ui.unit.dp 15 | import io.github.lumkit.desktop.ui.components.* 16 | import lint_ui.example.generated.resources.Res 17 | import lint_ui.example.generated.resources.compose_multiplatform 18 | import org.jetbrains.compose.resources.ExperimentalResourceApi 19 | import org.jetbrains.compose.resources.painterResource 20 | 21 | @OptIn(ExperimentalResourceApi::class, ExperimentalLayoutApi::class) 22 | @Composable 23 | fun ButtonExampleScreen() { 24 | FlowRow( 25 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 26 | horizontalArrangement = Arrangement.spacedBy(8.dp), 27 | verticalArrangement = Arrangement.spacedBy(8.dp) 28 | ) { 29 | Column( 30 | verticalArrangement = Arrangement.spacedBy(8.dp), 31 | ) { 32 | var count by remember { mutableStateOf(0) } 33 | Text("主题色按钮") 34 | LintButton( 35 | onClick = { 36 | count++ 37 | } 38 | ) { 39 | Text("点我!($count 次了)") 40 | } 41 | } 42 | Column( 43 | verticalArrangement = Arrangement.spacedBy(8.dp), 44 | ) { 45 | var count by remember { mutableStateOf(0) } 46 | Text("文本按钮") 47 | LintTextButton( 48 | onClick = { 49 | count++ 50 | } 51 | ) { 52 | Text("点我!($count 次了)") 53 | } 54 | } 55 | Column( 56 | verticalArrangement = Arrangement.spacedBy(8.dp), 57 | ) { 58 | var isShow by remember { mutableStateOf(false) } 59 | Text("描边按钮") 60 | LintOutlinedButton( 61 | onClick = { 62 | isShow = !isShow 63 | } 64 | ) { 65 | Text("点我一下") 66 | } 67 | 68 | AnimatedVisibility(isShow) { 69 | Image( 70 | modifier = Modifier.size(120.dp), 71 | painter = painterResource(Res.drawable.compose_multiplatform), 72 | contentDescription = null 73 | ) 74 | } 75 | } 76 | Column( 77 | verticalArrangement = Arrangement.spacedBy(8.dp), 78 | ) { 79 | var isShow by remember { mutableStateOf(false) } 80 | Text("阴影按钮") 81 | LintElevatedButton( 82 | onClick = { 83 | isShow = !isShow 84 | } 85 | ) { 86 | Text("点我一下") 87 | } 88 | 89 | val infiniteTransition = rememberInfiniteTransition() 90 | val rotation by infiniteTransition.animateFloat( 91 | initialValue = 0f, 92 | targetValue = 360f, 93 | animationSpec = infiniteRepeatable( 94 | animation = tween(durationMillis = 2000, easing = LinearEasing), 95 | repeatMode = RepeatMode.Restart 96 | ) 97 | ) 98 | AnimatedVisibility(isShow) { 99 | Image( 100 | modifier = Modifier.size(120.dp).rotate(rotation), 101 | painter = painterResource(Res.drawable.compose_multiplatform), 102 | contentDescription = null 103 | ) 104 | } 105 | } 106 | Column( 107 | verticalArrangement = Arrangement.spacedBy(8.dp), 108 | ) { 109 | var isShow by remember { mutableStateOf(false) } 110 | Text("填充色调按钮") 111 | LintFilledTonalButton( 112 | onClick = { 113 | isShow = !isShow 114 | } 115 | ) { 116 | Text("点我一下") 117 | } 118 | 119 | val infiniteTransition = rememberInfiniteTransition() 120 | val alpha by infiniteTransition.animateFloat( 121 | initialValue = 0f, 122 | targetValue = 1f, 123 | animationSpec = infiniteRepeatable( 124 | animation = tween(durationMillis = 2000, easing = LinearEasing), 125 | repeatMode = RepeatMode.Reverse 126 | ) 127 | ) 128 | AnimatedVisibility(isShow) { 129 | Image( 130 | modifier = Modifier.size(120.dp).alpha(alpha), 131 | painter = painterResource(Res.drawable.compose_multiplatform), 132 | contentDescription = null 133 | ) 134 | } 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/ChartExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import io.github.lumkit.desktop.ui.components.LintStackChart 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.isActive 14 | import kotlinx.coroutines.withContext 15 | import kotlin.random.Random 16 | 17 | @OptIn(ExperimentalLayoutApi::class) 18 | @Composable 19 | fun ChartExampleScreen() { 20 | FlowRow( 21 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 22 | horizontalArrangement = Arrangement.spacedBy(16.dp), 23 | verticalArrangement = Arrangement.spacedBy(28.dp) 24 | ) { 25 | Column( 26 | verticalArrangement = Arrangement.spacedBy(8.dp), 27 | ) { 28 | var progress by remember { mutableStateOf(0f) } 29 | Text(text = "栈条形图") 30 | LintStackChart( 31 | modifier = Modifier.fillMaxWidth() 32 | .height(72.dp), 33 | progress = progress 34 | ) 35 | 36 | LaunchedEffect(Unit) { 37 | withContext(Dispatchers.IO) { 38 | while (isActive) { 39 | progress = Random.nextFloat() 40 | delay(1000) 41 | } 42 | } 43 | } 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/LayerWindowExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.rememberScrollState 6 | import androidx.compose.foundation.verticalScroll 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.* 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.window.WindowPlacement 15 | import androidx.compose.ui.window.WindowPosition 16 | import androidx.compose.ui.window.rememberWindowState 17 | import io.github.lumkit.desktop.ui.LintLayerWindow 18 | import io.github.lumkit.desktop.ui.components.LintButton 19 | import lint_ui.example.generated.resources.Res 20 | import lint_ui.example.generated.resources.compose_multiplatform 21 | import org.jetbrains.compose.resources.ExperimentalResourceApi 22 | import org.jetbrains.compose.resources.painterResource 23 | import java.awt.Dimension 24 | 25 | @OptIn(ExperimentalLayoutApi::class, ExperimentalResourceApi::class, ExperimentalMaterial3Api::class) 26 | @Composable 27 | fun LayerWindowExampleScreen() { 28 | FlowRow( 29 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 30 | horizontalArrangement = Arrangement.spacedBy(8.dp), 31 | verticalArrangement = Arrangement.spacedBy(8.dp) 32 | ) { 33 | Column( 34 | verticalArrangement = Arrangement.spacedBy(8.dp), 35 | ) { 36 | val isShow = remember { mutableStateOf(false) } 37 | LintButton( 38 | onClick = { 39 | isShow.value = true 40 | } 41 | ) { 42 | Text("启动新窗口") 43 | } 44 | 45 | val windowState = rememberWindowState( 46 | position = WindowPosition(Alignment.Center), 47 | ) 48 | 49 | LintLayerWindow( 50 | showState = isShow, 51 | state = windowState, 52 | title = "new window", 53 | icon = painterResource(Res.drawable.compose_multiplatform), 54 | ) { 55 | window.minimumSize = Dimension(400, 300) 56 | 57 | Scaffold( 58 | modifier = Modifier.fillMaxSize(), 59 | topBar = { 60 | TopAppBar( 61 | title = { 62 | Text("自定义窗口") 63 | }, 64 | navigationIcon = { 65 | Image( 66 | modifier = Modifier.padding(16.dp).size(28.dp), 67 | painter = painterResource(Res.drawable.compose_multiplatform), 68 | contentDescription = null 69 | ) 70 | }, 71 | actions = { 72 | Row( 73 | horizontalArrangement = Arrangement.spacedBy(8.dp), 74 | ) { 75 | IconButton( 76 | onClick = { 77 | windowState.isMinimized = true 78 | } 79 | ) { 80 | Icon(Icons.Filled.Minimize, null) 81 | } 82 | IconButton( 83 | onClick = { 84 | windowState.placement = if (windowState.placement == WindowPlacement.Floating) WindowPlacement.Maximized else WindowPlacement.Floating 85 | } 86 | ) { 87 | Icon(if (windowState.placement == WindowPlacement.Floating) Icons.Default.Maximize else Icons.Default.ContentCopy, null) 88 | } 89 | IconButton( 90 | onClick = { 91 | isShow.value = false 92 | } 93 | ) { 94 | Icon(Icons.Filled.Close, null) 95 | } 96 | } 97 | } 98 | ) 99 | } 100 | ) { 101 | Column( 102 | modifier = Modifier.padding(it).fillMaxSize() 103 | ) { 104 | ProgressExampleScreen() 105 | } 106 | } 107 | } 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/ProgressExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import io.github.lumkit.desktop.ui.components.LintCircleIndicator 11 | import io.github.lumkit.desktop.ui.components.LintHorizontalLinearIndicator 12 | import io.github.lumkit.desktop.ui.components.LintHorizontalLinearProgress 13 | 14 | @OptIn(ExperimentalLayoutApi::class) 15 | @Composable 16 | fun ProgressExampleScreen() { 17 | var progress by remember { mutableStateOf(.5f) } 18 | 19 | FlowRow( 20 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 21 | horizontalArrangement = Arrangement.spacedBy(16.dp), 22 | verticalArrangement = Arrangement.spacedBy(28.dp) 23 | ) { 24 | Column( 25 | verticalArrangement = Arrangement.spacedBy(8.dp), 26 | ) { 27 | Text("水平线性拖动条(${String.format("%.2f%s", progress * 100f, "%")})") 28 | LintHorizontalLinearProgress( 29 | modifier = Modifier.fillMaxWidth(), 30 | progress = progress, 31 | onProgressChanged = { 32 | progress = it 33 | } 34 | ) 35 | } 36 | Column( 37 | verticalArrangement = Arrangement.spacedBy(8.dp), 38 | ) { 39 | Text("水平线性进度条(${String.format("%.2f%s", progress * 100f, "%")})") 40 | LintHorizontalLinearIndicator( 41 | modifier = Modifier.fillMaxWidth(), 42 | progress = progress 43 | ) 44 | } 45 | Column( 46 | verticalArrangement = Arrangement.spacedBy(8.dp), 47 | ) { 48 | Text("圆形进度条(${String.format("%.2f%s", progress * 100f, "%")})") 49 | LintCircleIndicator( 50 | modifier = Modifier.size(65.dp), 51 | thickness = 8.dp, 52 | progress = progress, 53 | ) 54 | } 55 | Column( 56 | verticalArrangement = Arrangement.spacedBy(8.dp), 57 | ) { 58 | Text("水平线性加载条") 59 | LintHorizontalLinearIndicator( 60 | modifier = Modifier.fillMaxWidth().height(8.dp), 61 | ) 62 | } 63 | Column( 64 | verticalArrangement = Arrangement.spacedBy(8.dp), 65 | ) { 66 | Text("圆形加载条") 67 | LintCircleIndicator( 68 | modifier = Modifier.size(65.dp), 69 | strokeWidth = 8.dp 70 | ) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/SettingsScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Add 9 | import androidx.compose.material.icons.filled.BorderColor 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.* 12 | import androidx.compose.runtime.snapshots.SnapshotStateList 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.ExperimentalComposeUiApi 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.draw.clip 17 | import androidx.compose.ui.graphics.Color 18 | import androidx.compose.ui.input.pointer.PointerEventType 19 | import androidx.compose.ui.input.pointer.isSecondaryPressed 20 | import androidx.compose.ui.input.pointer.onPointerEvent 21 | import androidx.compose.ui.unit.dp 22 | import io.github.lumkit.desktop.ui.components.LintFolder 23 | import io.github.lumkit.desktop.ui.components.LintOutlineCard 24 | import io.github.lumkit.desktop.ui.components.LintTextButton 25 | import io.github.lumkit.desktop.ui.theme.* 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.launch 28 | import kotlinx.coroutines.withContext 29 | import lint_ui.example.generated.resources.* 30 | import lint_ui.example.generated.resources.Res 31 | import lint_ui.example.generated.resources.text_color_scheme 32 | import lint_ui.example.generated.resources.text_color_scheme_settings 33 | import lint_ui.example.generated.resources.text_settings 34 | import org.jetbrains.compose.resources.ExperimentalResourceApi 35 | import org.jetbrains.compose.resources.stringResource 36 | import java.awt.Desktop 37 | import java.net.URI 38 | import javax.swing.JFileChooser 39 | import javax.swing.filechooser.FileNameExtensionFilter 40 | 41 | @OptIn(ExperimentalMaterial3Api::class, ExperimentalResourceApi::class, ExperimentalLayoutApi::class) 42 | @Composable 43 | fun SettingsScreen() { 44 | Column( 45 | modifier = Modifier.fillMaxSize(), 46 | verticalArrangement = Arrangement.spacedBy(8.dp) 47 | ) { 48 | TopAppBar( 49 | colors = TopAppBarDefaults.topAppBarColors().copy(containerColor = Color.Transparent), 50 | title = { 51 | Text(stringResource(Res.string.text_settings)) 52 | } 53 | ) 54 | Column( 55 | modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), 56 | verticalArrangement = Arrangement.spacedBy(8.dp) 57 | ) { 58 | val themeStore = LocalThemeStore.current 59 | var expanded by remember { mutableStateOf(true) } 60 | Spacer(Modifier) 61 | Text(stringResource(Res.string.text_color_scheme_settings), style = MaterialTheme.typography.bodyMedium) 62 | LintFolder( 63 | modifier = Modifier.fillMaxWidth(), 64 | expanded = expanded, 65 | icon = { 66 | Icon(imageVector = Icons.Default.BorderColor, contentDescription = null) 67 | }, 68 | label = { 69 | Text(text = stringResource(Res.string.text_color_scheme)) 70 | }, 71 | trailingIcon = { 72 | LintTextButton( 73 | onClick = { 74 | if (Desktop.isDesktopSupported() && Desktop.getDesktop().isSupported(Desktop.Action.BROWSE)) { 75 | try { 76 | Desktop.getDesktop().browse(URI("https://m3.material.io/theme-builder")) 77 | } catch (e: Exception) { 78 | e.printStackTrace() 79 | } 80 | } 81 | } 82 | ) { 83 | Text(stringResource(Res.string.text_set_color_theme)) 84 | } 85 | }, 86 | onClick = { 87 | expanded = !expanded 88 | } 89 | ) { 90 | val lintTheme = LocalLintTheme.current 91 | val themes = remember { mutableStateListOf() } 92 | 93 | LaunchedEffect(Unit) { 94 | withContext(Dispatchers.IO) { 95 | if (themes.isEmpty()) { 96 | themes.addAll( 97 | lintTheme.loadThemesList().map { 98 | LintTheme.LintThemeColorSchemes( 99 | label = it.label, 100 | light = lintTheme lightColorScheme it.schemes.light, 101 | dark = lintTheme lightColorScheme it.schemes.dark, 102 | ) 103 | } 104 | ) 105 | } 106 | } 107 | } 108 | 109 | FlowRow( 110 | modifier = Modifier.fillMaxWidth().padding(8.dp), 111 | verticalArrangement = Arrangement.spacedBy(8.dp), 112 | horizontalArrangement = Arrangement.spacedBy(8.dp) 113 | ) { 114 | ThemeItem( 115 | lintTheme, 116 | themeStore, 117 | theme = DefaultLintTheme 118 | ) 119 | themes.toList().forEachIndexed { index, lintThemeColorSchemes -> 120 | ThemeItem( 121 | lintTheme, 122 | themeStore, 123 | lintThemeColorSchemes 124 | ) { 125 | themes.removeAt(index) 126 | } 127 | } 128 | InstallThemeItem( 129 | lintTheme, 130 | themes, 131 | themeStore, 132 | ) 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | @OptIn(ExperimentalComposeUiApi::class, ExperimentalResourceApi::class) 140 | @Composable 141 | private fun ThemeItem( 142 | lintTheme: LintTheme, 143 | themeStore: ThemeStore, 144 | theme: LintTheme.LintThemeColorSchemes, 145 | onUninstall: (() -> Unit)? = null 146 | ) { 147 | val isDarkTheme = themeStore.isDarkTheme 148 | var showMenu by remember { mutableStateOf(false) } 149 | Column { 150 | Box( 151 | modifier = Modifier.size(65.dp) 152 | .background( 153 | color = if (isDarkTheme) { 154 | theme.dark.primaryContainer 155 | } else { 156 | theme.light.primaryContainer 157 | }, 158 | RoundedCornerShape(4.dp) 159 | ).clip( 160 | RoundedCornerShape(4.dp) 161 | ).clickable { 162 | themeStore.colorSchemes = theme 163 | }.onPointerEvent( 164 | PointerEventType.Press 165 | ) { 166 | if (it.buttons.isSecondaryPressed) { 167 | showMenu = true 168 | } 169 | }, 170 | contentAlignment = Alignment.Center 171 | ) { 172 | Checkbox( 173 | modifier = Modifier.align(Alignment.TopEnd).padding(8.dp), 174 | checked = themeStore.colorSchemes.label == theme.label, 175 | onCheckedChange = null 176 | ) 177 | } 178 | 179 | DropdownMenu( 180 | expanded = showMenu, 181 | onDismissRequest = { showMenu = false }, 182 | ) { 183 | DropdownMenuItem( 184 | text = { 185 | Text(text = stringResource(Res.string.text_uninstall_theme)) 186 | }, 187 | onClick = { 188 | try { 189 | lintTheme.uninstallThemeByName(theme.label ?: "") { 190 | showMenu = false 191 | onUninstall?.invoke() 192 | } 193 | }catch (e: Exception){ 194 | e.printStackTrace() 195 | } 196 | } 197 | ) 198 | } 199 | } 200 | } 201 | 202 | @Composable 203 | private fun InstallThemeItem( 204 | lintTheme: LintTheme, 205 | themes: SnapshotStateList, 206 | themeStore: ThemeStore, ) { 207 | val coroutineScope = rememberCoroutineScope { Dispatchers.IO } 208 | LintOutlineCard( 209 | onClick = { 210 | val fileChooser = JFileChooser().apply { 211 | fileFilter = FileNameExtensionFilter("选择主题配置文件(.json)", "json") 212 | isAcceptAllFileFilterUsed = false 213 | } 214 | val result = fileChooser.showOpenDialog(null) 215 | if (result == JFileChooser.APPROVE_OPTION) { 216 | coroutineScope.launch { 217 | try { 218 | val selectedFile = fileChooser.selectedFile 219 | lintTheme.installTheme(selectedFile) 220 | val themeBean = lintTheme.read(selectedFile.name) 221 | lintTheme.useTheme(themeBean) { 222 | themeStore.colorSchemes = it 223 | themes.add(it) 224 | } 225 | }catch (e: Exception) { 226 | e.printStackTrace() 227 | } 228 | } 229 | } 230 | } 231 | ) { 232 | Box( 233 | modifier = Modifier.size(65.dp), 234 | contentAlignment = Alignment.Center 235 | ) { 236 | Icon(Icons.Default.Add, contentDescription = null, modifier = Modifier.size(18.dp)) 237 | } 238 | } 239 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/TextFieldExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.verticalScroll 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.unit.dp 10 | import io.github.lumkit.desktop.ui.components.LintOutlinedTextField 11 | import io.github.lumkit.desktop.ui.components.LintTextField 12 | 13 | @OptIn(ExperimentalLayoutApi::class) 14 | @Composable 15 | fun TextFieldExampleScreen() { 16 | FlowRow( 17 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 18 | horizontalArrangement = Arrangement.spacedBy(8.dp), 19 | verticalArrangement = Arrangement.spacedBy(8.dp) 20 | ) { 21 | Column( 22 | verticalArrangement = Arrangement.spacedBy(8.dp), 23 | ) { 24 | var text by remember { mutableStateOf("") } 25 | Text("基础输入框") 26 | LintTextField( 27 | modifier = Modifier.fillMaxWidth(), 28 | value = text, 29 | onValueChange = { text = it }, 30 | ) 31 | } 32 | Column( 33 | verticalArrangement = Arrangement.spacedBy(8.dp), 34 | ) { 35 | var text by remember { mutableStateOf("") } 36 | Text("描边输入框") 37 | LintOutlinedTextField( 38 | modifier = Modifier.fillMaxWidth(), 39 | label = { 40 | Text("描边输入框") 41 | }, 42 | value = text, 43 | onValueChange = { text = it }, 44 | ) 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/ToastExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.shape.CircleShape 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.Android 11 | import androidx.compose.material.icons.filled.Check 12 | import androidx.compose.material.icons.outlined.Android 13 | import androidx.compose.material3.* 14 | import androidx.compose.runtime.* 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.draw.scale 19 | import androidx.compose.ui.draw.shadow 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.unit.dp 22 | import io.github.lumkit.desktop.context.LocalContextWrapper 23 | import io.github.lumkit.desktop.context.Toast 24 | import io.github.lumkit.desktop.context.Toast.showToast 25 | import io.github.lumkit.desktop.ui.components.* 26 | import io.github.lumkit.desktop.ui.theme.AnimatedLintTheme 27 | import io.github.lumkit.desktop.ui.theme.LintTheme 28 | import lint_ui.example.generated.resources.Res 29 | import lint_ui.example.generated.resources.compose_multiplatform 30 | import org.jetbrains.compose.resources.ExperimentalResourceApi 31 | import org.jetbrains.compose.resources.painterResource 32 | 33 | @OptIn(ExperimentalLayoutApi::class, ExperimentalResourceApi::class) 34 | @Composable 35 | fun ToastExampleScreen() { 36 | FlowRow( 37 | modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(16.dp), 38 | horizontalArrangement = Arrangement.spacedBy(8.dp), 39 | verticalArrangement = Arrangement.spacedBy(8.dp) 40 | ) { 41 | Column( 42 | verticalArrangement = Arrangement.spacedBy(8.dp), 43 | ) { 44 | val context = LocalContextWrapper.current 45 | var message by remember { mutableStateOf("This is a toast!") } 46 | var time by remember { mutableStateOf(Toast.LENGTH_SHORT) } 47 | var alignment by remember { mutableStateOf(Alignment.BottomEnd) } 48 | 49 | LintTextField( 50 | value = message, 51 | onValueChange = { message = it }, 52 | label = { 53 | Text("消息内容") 54 | }, 55 | ) 56 | Spacer(modifier = Modifier) 57 | Text("显示时长") 58 | Row( 59 | horizontalArrangement = Arrangement.spacedBy(16.dp), 60 | ) { 61 | Row( 62 | modifier = Modifier.clickable { time = Toast.LENGTH_SHORT }, 63 | verticalAlignment = Alignment.CenterVertically, 64 | horizontalArrangement = Arrangement.spacedBy(4.dp) 65 | ) { 66 | Checkbox(checked = time == Toast.LENGTH_SHORT, onCheckedChange = null) 67 | Text("短时间") 68 | } 69 | Row( 70 | modifier = Modifier.clickable { time = Toast.LENGTH_LONG }, 71 | verticalAlignment = Alignment.CenterVertically, 72 | horizontalArrangement = Arrangement.spacedBy(4.dp) 73 | ) { 74 | Checkbox(checked = time == Toast.LENGTH_LONG, onCheckedChange = null) 75 | Text("较长时间") 76 | } 77 | } 78 | Spacer(modifier = Modifier) 79 | Text("位置") 80 | FlowRow( 81 | horizontalArrangement = Arrangement.spacedBy(12.dp), 82 | verticalArrangement = Arrangement.spacedBy(16.dp) 83 | ) { 84 | Row( 85 | modifier = Modifier.clickable { alignment = Alignment.TopStart }, 86 | verticalAlignment = Alignment.CenterVertically, 87 | horizontalArrangement = Arrangement.spacedBy(4.dp) 88 | ) { 89 | Checkbox(checked = alignment == Alignment.TopStart, onCheckedChange = null) 90 | Text("TopStart") 91 | } 92 | Row( 93 | modifier = Modifier.clickable { alignment = Alignment.TopCenter }, 94 | verticalAlignment = Alignment.CenterVertically, 95 | horizontalArrangement = Arrangement.spacedBy(4.dp) 96 | ) { 97 | Checkbox(checked = alignment == Alignment.TopCenter, onCheckedChange = null) 98 | Text("TopCenter") 99 | } 100 | Row( 101 | modifier = Modifier.clickable { alignment = Alignment.TopEnd }, 102 | verticalAlignment = Alignment.CenterVertically, 103 | horizontalArrangement = Arrangement.spacedBy(4.dp) 104 | ) { 105 | Checkbox(checked = alignment == Alignment.TopEnd, onCheckedChange = null) 106 | Text("TopEnd") 107 | } 108 | Row( 109 | modifier = Modifier.clickable { alignment = Alignment.CenterStart }, 110 | verticalAlignment = Alignment.CenterVertically, 111 | horizontalArrangement = Arrangement.spacedBy(4.dp) 112 | ) { 113 | Checkbox(checked = alignment == Alignment.CenterStart, onCheckedChange = null) 114 | Text("CenterStart") 115 | } 116 | Row( 117 | modifier = Modifier.clickable { alignment = Alignment.Center }, 118 | verticalAlignment = Alignment.CenterVertically, 119 | horizontalArrangement = Arrangement.spacedBy(4.dp) 120 | ) { 121 | Checkbox(checked = alignment == Alignment.Center, onCheckedChange = null) 122 | Text("Center") 123 | } 124 | Row( 125 | modifier = Modifier.clickable { alignment = Alignment.CenterEnd }, 126 | verticalAlignment = Alignment.CenterVertically, 127 | horizontalArrangement = Arrangement.spacedBy(4.dp) 128 | ) { 129 | Checkbox(checked = alignment == Alignment.CenterEnd, onCheckedChange = null) 130 | Text("CenterEnd") 131 | } 132 | Row( 133 | modifier = Modifier.clickable { alignment = Alignment.BottomStart }, 134 | verticalAlignment = Alignment.CenterVertically, 135 | horizontalArrangement = Arrangement.spacedBy(4.dp) 136 | ) { 137 | Checkbox(checked = alignment == Alignment.BottomStart, onCheckedChange = null) 138 | Text("BottomStart") 139 | } 140 | Row( 141 | modifier = Modifier.clickable { alignment = Alignment.BottomCenter }, 142 | verticalAlignment = Alignment.CenterVertically, 143 | horizontalArrangement = Arrangement.spacedBy(4.dp) 144 | ) { 145 | Checkbox(checked = alignment == Alignment.BottomCenter, onCheckedChange = null) 146 | Text("BottomCenter") 147 | } 148 | Row( 149 | modifier = Modifier.clickable { alignment = Alignment.BottomEnd }, 150 | verticalAlignment = Alignment.CenterVertically, 151 | horizontalArrangement = Arrangement.spacedBy(4.dp) 152 | ) { 153 | Checkbox(checked = alignment == Alignment.BottomEnd, onCheckedChange = null) 154 | Text("BottomEnd") 155 | } 156 | } 157 | Spacer(modifier = Modifier) 158 | LintButton( 159 | onClick = { 160 | // Show your toast 161 | context.showToast( 162 | message, 163 | time, 164 | alignment, 165 | ) 166 | } 167 | ) { 168 | Text("弹出默认提示") 169 | } 170 | LintButton( 171 | onClick = { 172 | // Show your toast 173 | context.showToast( 174 | message, 175 | time, 176 | alignment, 177 | leadingIcon = { 178 | Image(painterResource(Res.drawable.compose_multiplatform), null) 179 | }, 180 | trailingIcon = { 181 | Icon(Icons.Default.Check, contentDescription = null, tint = Color.Green) 182 | } 183 | ) 184 | } 185 | ) { 186 | Text("弹出有图标的提示") 187 | } 188 | LintButton( 189 | onClick = { 190 | // Show your toast 191 | context.showToast( 192 | time, 193 | alignment, 194 | ) { 195 | // custom toast content 196 | LintTheme { 197 | Surface( 198 | modifier = Modifier.padding(16.dp).clip(CircleShape) 199 | .shadow(5.dp), 200 | color = Color(0xFF85C077), 201 | ) { 202 | Row( 203 | modifier = Modifier.padding(horizontal = 24.dp, vertical = 8.dp), 204 | verticalAlignment = Alignment.CenterVertically, 205 | horizontalArrangement = Arrangement.spacedBy(16.dp) 206 | ) { 207 | Text(message, color = Color.White) 208 | Icon(Icons.Default.Check, contentDescription = null, tint = Color.White) 209 | } 210 | } 211 | } 212 | } 213 | } 214 | ) { 215 | Text("弹出自定义内容提示") 216 | } 217 | } 218 | } 219 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/io/github/lumkit/desktop/example/screen/ViewModelExampleScreen.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.example.screen 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.pager.HorizontalPager 6 | import androidx.compose.foundation.pager.VerticalPager 7 | import androidx.compose.foundation.pager.rememberPagerState 8 | import androidx.compose.material3.Text 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.collectAsState 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.runtime.rememberCoroutineScope 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import io.github.lumkit.desktop.example.MyViewModel 17 | import io.github.lumkit.desktop.lifecycle.viewModel 18 | import io.github.lumkit.desktop.ui.components.LintButton 19 | import io.github.lumkit.desktop.ui.components.LintTextField 20 | import kotlinx.coroutines.launch 21 | 22 | @OptIn(ExperimentalLayoutApi::class, ExperimentalFoundationApi::class) 23 | @Composable 24 | fun ViewModelExampleScreen() { 25 | val pageState = rememberPagerState { 2 } 26 | val scope = rememberCoroutineScope() 27 | Column( 28 | modifier = Modifier.fillMaxSize(), 29 | ) { 30 | Row( 31 | horizontalArrangement = Arrangement.spacedBy(8.dp) 32 | ) { 33 | LintButton( 34 | onClick = { 35 | scope.launch { pageState.animateScrollToPage(0) } 36 | } 37 | ) { 38 | Text("跳转屏幕A") 39 | } 40 | LintButton( 41 | onClick = { 42 | scope.launch { pageState.animateScrollToPage(1) } 43 | } 44 | ) { 45 | Text("跳转屏幕B") 46 | } 47 | } 48 | 49 | HorizontalPager( 50 | modifier = Modifier.fillMaxSize(), 51 | state = pageState, 52 | ) { 53 | when (it) { 54 | 0 -> ScreenA() 55 | 1 -> ScreenB() 56 | } 57 | } 58 | } 59 | } 60 | 61 | @Composable 62 | private fun ScreenA() { 63 | Box( 64 | modifier = Modifier.fillMaxSize(), 65 | contentAlignment = Alignment.Center 66 | ) { 67 | val viewModel = viewModel() 68 | 69 | val text by viewModel.text.collectAsState() 70 | 71 | LintTextField( 72 | value = text, 73 | onValueChange = viewModel::setText, 74 | label = { 75 | Text("输入内容") 76 | } 77 | ) 78 | } 79 | } 80 | 81 | @Composable 82 | private fun ScreenB() { 83 | val viewModel = viewModel() 84 | val text by viewModel.text.collectAsState() 85 | Column( 86 | modifier = Modifier.fillMaxSize(), 87 | verticalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterVertically), 88 | horizontalAlignment = Alignment.CenterHorizontally 89 | ) { 90 | Text(text, Modifier.padding(16.dp)) 91 | LintButton( 92 | onClick = { 93 | viewModel.setText("") 94 | } 95 | ) { 96 | Text("清空内容") 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /example/src/desktopMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.fillMaxSize 2 | import androidx.compose.ui.Modifier 3 | import androidx.compose.ui.window.MenuBar 4 | import io.github.lumkit.desktop.context.LocalContext 5 | import io.github.lumkit.desktop.data.DarkThemeMode 6 | import io.github.lumkit.desktop.example.App 7 | import io.github.lumkit.desktop.lintApplication 8 | import io.github.lumkit.desktop.ui.LintWindow 9 | import io.github.lumkit.desktop.ui.theme.AnimatedLintTheme 10 | import io.github.lumkit.desktop.ui.theme.LocalThemeStore 11 | import lint_ui.example.generated.resources.* 12 | import org.jetbrains.compose.resources.ExperimentalResourceApi 13 | import org.jetbrains.compose.resources.painterResource 14 | import org.jetbrains.compose.resources.stringResource 15 | import java.awt.Dimension 16 | 17 | @OptIn(ExperimentalResourceApi::class) 18 | fun main() = lintApplication( 19 | packageName = "LintUIExample" 20 | ) { 21 | val context = LocalContext.current 22 | 23 | // Toggles the global dark theme mode. 24 | LintWindow( 25 | onCloseRequest = ::exitApplication, 26 | rememberSize = true, 27 | title = context.getPackageName(), 28 | icon = painterResource(Res.drawable.compose_multiplatform), 29 | ) { 30 | window.minimumSize = Dimension(800, 600) 31 | AnimatedLintTheme( 32 | modifier = Modifier.fillMaxSize(), 33 | ) { 34 | App() 35 | } 36 | 37 | // Toggles the global dark theme mode. 38 | val themeMode = LocalThemeStore.current 39 | MenuBar { 40 | Menu(stringResource(Res.string.text_dark_theme)) { 41 | DarkThemeMode.entries.forEach { entry -> 42 | CheckboxItem( 43 | when (entry) { 44 | DarkThemeMode.SYSTEM -> stringResource(Res.string.text_theme_system) 45 | DarkThemeMode.LIGHT -> stringResource(Res.string.text_theme_light) 46 | DarkThemeMode.DARK -> stringResource(Res.string.text_theme_dark) 47 | }, 48 | checked = themeMode.darkTheme == entry 49 | ) { 50 | themeMode.darkTheme = entry 51 | } 52 | } 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | 3 | #Gradle 4 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 5 | 6 | #Development 7 | development=true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | compose = "1.6.11" 3 | compose-plugin = "1.6.11" 4 | junit = "4.13.2" 5 | kotlin = "2.0.0-RC2" 6 | exposed = "0.49.0" 7 | sqlite-jdbc = "3.44.1.0" 8 | gson = "2.10.1" 9 | j-system-theme-detector = "3.8" 10 | flat-lat = "3.4.1" 11 | lint-compose-ui = "1.0.7" 12 | vanniktech = "0.28.0" 13 | kotlinx-serialization = "1.6.3" 14 | 15 | [libraries] 16 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 17 | kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 18 | junit = { group = "junit", name = "junit", version.ref = "junit" } 19 | compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose" } 20 | compose-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose" } 21 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 22 | 23 | exposed-core = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } 24 | exposed-crypt = { module = "org.jetbrains.exposed:exposed-crypt", version.ref = "exposed" } 25 | exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } 26 | exposed-java-time = { module = "org.jetbrains.exposed:exposed-java-time", version.ref = "exposed" } 27 | exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } 28 | exposed-json = { module = "org.jetbrains.exposed:exposed-json", version.ref = "exposed" } 29 | exposed-kotlin-datetime = { module = "org.jetbrains.exposed:exposed-kotlin-datetime", version.ref = "exposed" } 30 | sqlite-jdbc = { module = "org.xerial:sqlite-jdbc", version.ref = "sqlite-jdbc" } 31 | gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } 32 | j-system-theme-detector = { module = "com.github.Dansoftowner:jSystemThemeDetector", version.ref = "j-system-theme-detector" } 33 | flat-lat = { module = "com.formdev:flatlaf", version.ref = "flat-lat" } 34 | lint-compose-ui = { module = "io.github.lumkit:lint-compose-ui", version.ref = "lint-compose-ui" } 35 | 36 | [plugins] 37 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 38 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 39 | vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktech" } 40 | kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 41 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 42 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumkit/lint-ui/1759cae755d6e22d1bd77c0853f90045c77abca6/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip 4 | networkTimeout=30000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Use the maximum available, or set MAX_FD != -1 to use that value. 89 | MAX_FD=maximum 90 | 91 | warn () { 92 | echo "$*" 93 | } >&2 94 | 95 | die () { 96 | echo 97 | echo "$*" 98 | echo 99 | exit 1 100 | } >&2 101 | 102 | # OS specific support (must be 'true' or 'false'). 103 | cygwin=false 104 | msys=false 105 | darwin=false 106 | nonstop=false 107 | case "$( uname )" in #( 108 | CYGWIN* ) cygwin=true ;; #( 109 | Darwin* ) darwin=true ;; #( 110 | MSYS* | MINGW* ) msys=true ;; #( 111 | NONSTOP* ) nonstop=true ;; 112 | esac 113 | 114 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 115 | 116 | 117 | # Determine the Java command to use to start the JVM. 118 | if [ -n "$JAVA_HOME" ] ; then 119 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 120 | # IBM's JDK on AIX uses strange locations for the executables 121 | JAVACMD=$JAVA_HOME/jre/sh/java 122 | else 123 | JAVACMD=$JAVA_HOME/bin/java 124 | fi 125 | if [ ! -x "$JAVACMD" ] ; then 126 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 127 | 128 | Please set the JAVA_HOME variable in your environment to match the 129 | location of your Java installation." 130 | fi 131 | else 132 | JAVACMD=java 133 | if ! command -v java >/dev/null 2>&1 134 | then 135 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 136 | 137 | Please set the JAVA_HOME variable in your environment to match the 138 | location of your Java installation." 139 | fi 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | 201 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 202 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 203 | 204 | # Collect all arguments for the java command; 205 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 206 | # shell script including quotes and variable substitutions, so put them in 207 | # double quotes to make sure that they get re-expanded; and 208 | # * put everything else in single quotes, so that it's not re-expanded. 209 | 210 | set -- \ 211 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 212 | -classpath "$CLASSPATH" \ 213 | org.gradle.wrapper.GradleWrapperMain \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /lint-compose-ui/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.SonatypeHost 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.jetbrainsCompose) 6 | alias(libs.plugins.compose.compiler) 7 | alias(libs.plugins.vanniktech.maven.publish) 8 | alias(libs.plugins.kotlinxSerialization) 9 | } 10 | 11 | group = "io.github.lumkit" 12 | version = libs.versions.lint.compose.ui.get() 13 | 14 | kotlin { 15 | jvm("desktop") 16 | sourceSets { 17 | val desktopMain by getting 18 | 19 | commonMain.dependencies { 20 | api(compose.runtime) 21 | api(compose.foundation) 22 | api(compose.ui) 23 | api(compose.components.resources) 24 | api(compose.components.uiToolingPreview) 25 | api(compose.animation) 26 | api(compose.animationGraphics) 27 | api(compose.material3) 28 | api(compose.runtimeSaveable) 29 | api(compose.preview) 30 | api(compose.materialIconsExtended) 31 | api(compose.uiTooling) 32 | api(compose.uiUtil) 33 | api(libs.kotlinx.serialization.json) 34 | 35 | } 36 | desktopMain.dependencies { 37 | api(compose.desktop.common) 38 | 39 | //sql 40 | api(libs.exposed.core) 41 | api(libs.exposed.crypt) 42 | api(libs.exposed.dao) 43 | api(libs.exposed.java.time) 44 | api(libs.exposed.jdbc) 45 | api(libs.exposed.json) 46 | api(libs.exposed.kotlin.datetime) 47 | api(libs.sqlite.jdbc) 48 | 49 | api(libs.j.system.theme.detector) 50 | api(libs.flat.lat) 51 | } 52 | } 53 | } 54 | 55 | mavenPublishing { 56 | publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) 57 | signAllPublications() 58 | 59 | allprojects.forEach { project -> 60 | project.afterEvaluate { 61 | project.extensions.findByType(PublishingExtension::class.java)?.apply { 62 | project.extensions.findByType(SigningExtension::class.java)?.apply { 63 | useGpgCmd() 64 | publishing.publications.withType(MavenPublication::class.java).forEach { publication -> 65 | sign(publication) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | coordinates( 73 | groupId = "io.github.lumkit", 74 | artifactId = "lint-compose-ui", 75 | version = libs.versions.lint.compose.ui.get() 76 | ) 77 | 78 | pom { 79 | name.set("lint-compose-ui") 80 | description.set("A Compose Desktop UI framework supporting global theme control. (aka LintUI)") 81 | url.set("https://github.com/lumkit/lint-ui") 82 | 83 | licenses { 84 | license { 85 | name.set("GNU LESSER GENERAL PUBLIC LICENSE, Version 2.1") 86 | url.set("https://www.gnu.org/licenses/old-licenses/lgpl-2.1.txt") 87 | } 88 | } 89 | 90 | developers { 91 | developer { 92 | name.set("lumkit") 93 | email.set("2205903933@qq.com") 94 | url.set("https://github.com/lumkit") 95 | } 96 | } 97 | 98 | scm { 99 | url.set("https://github.com/lumkit/lint-ui") 100 | connection.set("scm:git:git://github.com/lumkit/lint-ui.git") 101 | developerConnection.set("scm:git:ssh://github.com/lumkit/lint-ui.git") 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/Const.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop 2 | 3 | object Const { 4 | const val WINDOW_SIZE = "window_size" 5 | const val THEME_INSTALL_DIRECTORY = ".themes" 6 | const val USED_THEME_FILE_NAME = "used_theme_file" 7 | const val APP_CONFIGURATION = "configuration" 8 | const val DARK_THEME_MODE = "dark_theme_mode" 9 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/LintApplication.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop 2 | 3 | import androidx.compose.runtime.* 4 | import androidx.compose.ui.window.ApplicationScope 5 | import androidx.compose.ui.window.application 6 | import com.jthemedetecor.OsThemeDetector 7 | import io.github.lumkit.desktop.annotates.MODE_FILES 8 | import io.github.lumkit.desktop.context.Context 9 | import io.github.lumkit.desktop.context.LocalContext 10 | import io.github.lumkit.desktop.data.DarkThemeMode 11 | import io.github.lumkit.desktop.preferences.LocalSharedPreferences 12 | import io.github.lumkit.desktop.preferences.SharedPreferences 13 | import io.github.lumkit.desktop.ui.theme.LintTheme 14 | import io.github.lumkit.desktop.ui.theme.LocalLintTheme 15 | import io.github.lumkit.desktop.ui.theme.LocalThemeStore 16 | import io.github.lumkit.desktop.ui.theme.ThemeStore 17 | import kotlinx.coroutines.Dispatchers 18 | import kotlinx.coroutines.flow.flowOn 19 | import kotlinx.coroutines.flow.launchIn 20 | import kotlinx.coroutines.flow.onEach 21 | import java.io.File 22 | 23 | /** 24 | * An entry point for the Compose application. 25 | * @see androidx.compose.ui.window.application 26 | */ 27 | fun lintApplication( 28 | packageName: String = "Lint UI", 29 | exitProcessOnExit: Boolean = true, 30 | content: @Composable ApplicationScope.() -> Unit 31 | ) = application(exitProcessOnExit = exitProcessOnExit) { 32 | 33 | val context = object : Context() { 34 | override fun getPackageName(): String = packageName 35 | 36 | override fun getDir(name: String): File = File(appDir, name).apply { 37 | if (!exists()) mkdirs() 38 | } 39 | 40 | override fun getFilesDir(): File = getDir(MODE_FILES).apply { 41 | if (!exists()) mkdirs() 42 | } 43 | 44 | override fun getSharedPreferences(name: String): SharedPreferences = SharedPreferences(this, name) 45 | 46 | override fun getTheme(sharedPreferences: SharedPreferences): LintTheme = LintTheme( 47 | context = this, 48 | sharedPreferences = sharedPreferences, 49 | ) 50 | } 51 | 52 | val sharedPreferences = context.getSharedPreferences(Const.APP_CONFIGURATION) 53 | val lintTheme = context.getTheme() 54 | 55 | val themeStore = remember { ThemeStore() } 56 | 57 | // watch system dark theme change 58 | themeStore.isSystemDarkTheme = OsThemeDetector.getDetector().isDark 59 | OsThemeDetector.getDetector().registerListener { isDarkThemeEnabled -> 60 | if (themeStore.darkTheme == DarkThemeMode.SYSTEM) { 61 | themeStore.isSystemDarkTheme = isDarkThemeEnabled 62 | } 63 | } 64 | 65 | // Initialize dark theme mode 66 | try { 67 | themeStore.darkTheme = DarkThemeMode.valueOf(sharedPreferences.getString(Const.DARK_THEME_MODE).toString()) 68 | }catch (_: Exception) {} 69 | 70 | // Initialize color scheme 71 | try { 72 | themeStore.colorSchemes = lintTheme.getUsedTheme() 73 | }catch (_: Exception) {} 74 | 75 | LaunchedEffect(Unit) { 76 | // Persistent theme color configuration 77 | snapshotFlow { themeStore.darkTheme } 78 | .onEach { 79 | sharedPreferences.putString(Const.DARK_THEME_MODE, it.name) 80 | }.flowOn(Dispatchers.IO) 81 | .launchIn(this) 82 | 83 | snapshotFlow { themeStore.colorSchemes } 84 | .onEach { 85 | sharedPreferences.putString(Const.USED_THEME_FILE_NAME, it.label) 86 | }.flowOn(Dispatchers.IO) 87 | .launchIn(this) 88 | } 89 | 90 | CompositionLocalProvider( 91 | LocalContext provides context, 92 | LocalSharedPreferences provides sharedPreferences, 93 | LocalLintTheme provides lintTheme, 94 | LocalThemeStore provides themeStore, 95 | ) { 96 | content() 97 | } 98 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/annotates/FileMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.annotates 2 | 3 | const val MODE_ROOT = "" 4 | const val MODE_FILES = "files" 5 | const val MODE_CACHES = "caches" 6 | const val MODE_DATABASE = "database" 7 | 8 | @StringDef( 9 | MODE_ROOT, 10 | MODE_FILES, 11 | MODE_CACHES, 12 | MODE_DATABASE, 13 | ) 14 | @Retention(AnnotationRetention.SOURCE) 15 | annotation class FileMode 16 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/annotates/IntDef.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.annotates 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) 5 | annotation class IntDef(vararg val value: Int) -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/annotates/PreferencesMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.annotates 2 | 3 | const val MODE_PUBLIC: Int = 0x0000 4 | 5 | @IntDef(MODE_PUBLIC) 6 | @Retention(AnnotationRetention.SOURCE) 7 | annotation class PreferencesMode -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/annotates/StringDef.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.annotates 2 | 3 | @Retention(AnnotationRetention.SOURCE) 4 | @Target(AnnotationTarget.VALUE_PARAMETER, AnnotationTarget.FIELD, AnnotationTarget.FUNCTION, AnnotationTarget.ANNOTATION_CLASS) 5 | annotation class StringDef(vararg val value: String) -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/common/Colors.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.common 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | fun String.toColor() : Color { 6 | val colorString = if (startsWith("#")) substring(1) else this 7 | val colorLong = colorString.toLong(16) 8 | return when (colorString.length) { 9 | // RGB格式,没有透明度,假定为不透明 10 | 6 -> Color((0xFF shl 24) or (colorLong.toInt() and 0xFFFFFF)) 11 | // ARGB格式 12 | 8 -> Color(colorLong) 13 | else -> throw IllegalArgumentException("Unsupported color format: $this") 14 | } 15 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/context/Context.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.context 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.ui.awt.ComposeWindow 5 | import io.github.lumkit.desktop.Const 6 | import io.github.lumkit.desktop.annotates.FileMode 7 | import io.github.lumkit.desktop.annotates.PreferencesMode 8 | import io.github.lumkit.desktop.preferences.SharedPreferences 9 | import io.github.lumkit.desktop.ui.theme.LintTheme 10 | import org.jetbrains.skiko.hostOs 11 | import java.io.File 12 | import java.nio.file.Paths 13 | 14 | val LocalContext = compositionLocalOf { error("Not provided.") } 15 | 16 | /** 17 | * Interface to global information about an application environment. 18 | */ 19 | abstract class Context internal constructor() { 20 | 21 | protected val appDir by lazy { 22 | val path = when { 23 | hostOs.isWindows -> System.getenv("LOCALAPPDATA") ?: Paths.get( 24 | System.getProperty("user.home"), 25 | "AppData", 26 | "Local" 27 | ).toString() 28 | 29 | hostOs.isMacOS -> Paths.get(System.getProperty("user.home"), "Library", "Caches").toString() 30 | else -> Paths.get(System.getProperty("user.home"), ".cache").toString() 31 | } 32 | val file = File(path, getPackageName()) 33 | if (!file.exists()) file.mkdirs() 34 | file 35 | } 36 | 37 | /** 38 | * Return the name of this application's package. 39 | */ 40 | abstract fun getPackageName(): String 41 | 42 | abstract fun getDir(@FileMode name: String): File 43 | 44 | abstract fun getFilesDir(): File 45 | 46 | abstract fun getSharedPreferences(name: String): SharedPreferences 47 | 48 | // abstract fun getVersion(): String 49 | // 50 | // abstract fun getVersionName(): String 51 | abstract fun getTheme(sharedPreferences: SharedPreferences = getSharedPreferences(Const.APP_CONFIGURATION)): LintTheme 52 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/context/ContextWrapper.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.context 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import androidx.compose.runtime.mutableStateListOf 5 | import androidx.compose.runtime.snapshots.SnapshotStateMap 6 | import androidx.compose.ui.awt.ComposeWindow 7 | import io.github.lumkit.desktop.lifecycle.ViewModel 8 | 9 | val LocalContextWrapper = compositionLocalOf { error("context_wrapper not initialized") } 10 | 11 | abstract class ContextWrapper internal constructor(): Context() { 12 | 13 | internal val toastQueues = mutableStateListOf() 14 | abstract fun getWindow(): ComposeWindow 15 | abstract val viewModelPool: SnapshotStateMap 16 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/context/Toast.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.context 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.border 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.ui.Alignment 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.draw.clip 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.unit.dp 14 | import io.github.lumkit.desktop.ui.theme.AnimatedLintTheme 15 | 16 | object Toast { 17 | 18 | const val LENGTH_SHORT: Long = 1500 19 | const val LENGTH_LONG: Long = 3000 20 | 21 | fun ContextWrapper.showToast( 22 | time: Long, 23 | alignment: Alignment = Alignment.BottomEnd, 24 | content: @Composable () -> Unit 25 | ) { 26 | toastQueues.add( 27 | ToastQueue(time, alignment, content) 28 | ) 29 | } 30 | 31 | fun ContextWrapper.showToast( 32 | message: String, 33 | time: Long = LENGTH_SHORT, 34 | alignment: Alignment = Alignment.BottomEnd, 35 | leadingIcon: @Composable (() -> Unit)? = null, 36 | trailingIcon: @Composable (() -> Unit)? = null, 37 | ) { 38 | showToast( 39 | time, 40 | alignment, 41 | ) { 42 | AnimatedLintTheme( 43 | modifier = Modifier 44 | .padding(16.dp) 45 | .clip(RoundedCornerShape(8.dp)) 46 | .border( 47 | border = BorderStroke(1.dp, DividerDefaults.color), 48 | shape = RoundedCornerShape(8.dp) 49 | ), 50 | ) { 51 | ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { 52 | Row( 53 | modifier = Modifier.padding(vertical = 8.dp, horizontal = 16.dp), 54 | verticalAlignment = Alignment.CenterVertically, 55 | horizontalArrangement = Arrangement.spacedBy(12.dp) 56 | ) { 57 | leadingIcon?.let { 58 | Surface( 59 | modifier = Modifier.size(24.dp), 60 | color = Color.Transparent 61 | ) { 62 | it() 63 | } 64 | } 65 | Text(message) 66 | trailingIcon?.let { 67 | Surface( 68 | modifier = Modifier.size(24.dp), 69 | color = Color.Transparent 70 | ) { 71 | it() 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/context/ToastQueue.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.context 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.Alignment 5 | 6 | internal data class ToastQueue( 7 | val time: Long, 8 | val alignment: Alignment = Alignment.BottomEnd, 9 | val content: @Composable () -> Unit, 10 | ) 11 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/data/DarkThemeMode.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.data 2 | 3 | /** 4 | * App dark theme mode 5 | */ 6 | enum class DarkThemeMode { 7 | SYSTEM, LIGHT, DARK 8 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/data/SharedPreferenceBean.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.data 2 | 3 | import org.jetbrains.exposed.dao.id.IntIdTable 4 | 5 | data class SharedPreference( 6 | val name: String, 7 | ): IntIdTable(name) { 8 | val key = varchar("name", 255).uniqueIndex() 9 | val value = text("value").nullable() 10 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/data/ThemeBean.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | /** 6 | * Theme file export entity. 7 | */ 8 | @Serializable 9 | data class ThemeBean( 10 | val label: String? = null, 11 | val seed: String, 12 | val schemes: Schemes 13 | ) 14 | 15 | @Serializable 16 | data class Schemes( 17 | val light: SchemesBasic, 18 | val dark: SchemesBasic, 19 | ) 20 | 21 | @Serializable 22 | data class SchemesBasic( 23 | val primary: String, 24 | val surfaceTint: String, 25 | val onPrimary: String, 26 | val primaryContainer: String, 27 | val onPrimaryContainer: String, 28 | val secondary: String, 29 | val onSecondary: String, 30 | val secondaryContainer: String, 31 | val onSecondaryContainer: String, 32 | val tertiary: String, 33 | val onTertiary: String, 34 | val tertiaryContainer: String, 35 | val onTertiaryContainer: String, 36 | val error: String, 37 | val onError: String, 38 | val errorContainer: String, 39 | val onErrorContainer: String, 40 | val background: String, 41 | val onBackground: String, 42 | val surface: String, 43 | val onSurface: String, 44 | val surfaceVariant: String, 45 | val onSurfaceVariant: String, 46 | val outline: String, 47 | val outlineVariant: String, 48 | val shadow: String, 49 | val scrim: String, 50 | val inverseSurface: String, 51 | val inverseOnSurface: String, 52 | val inversePrimary: String, 53 | val primaryFixed: String, 54 | val onPrimaryFixed: String, 55 | val primaryFixedDim: String, 56 | val onPrimaryFixedVariant: String, 57 | val secondaryFixed: String, 58 | val onSecondaryFixed: String, 59 | val secondaryFixedDim: String, 60 | val onSecondaryFixedVariant: String, 61 | val tertiaryFixed: String, 62 | val onTertiaryFixed: String, 63 | val tertiaryFixedDim: String, 64 | val onTertiaryFixedVariant: String, 65 | val surfaceDim: String, 66 | val surfaceBright: String, 67 | val surfaceContainerLowest: String, 68 | val surfaceContainerLow: String, 69 | val surfaceContainer: String, 70 | val surfaceContainerHigh: String, 71 | val surfaceContainerHighest: String, 72 | ) -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/data/WindowSize.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class WindowSize( 7 | val width: Float, 8 | val height: Float, 9 | ) 10 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/lifecycle/Lifecycle.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.lifecycle 2 | 3 | interface Lifecycle { 4 | fun onCreate() 5 | fun onDestroy() 6 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/lifecycle/ViewModel.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.lifecycle 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.Stable 5 | import androidx.compose.runtime.remember 6 | import io.github.lumkit.desktop.context.LocalContextWrapper 7 | import kotlin.reflect.full.createInstance 8 | 9 | @Stable 10 | abstract class ViewModel 11 | 12 | @Composable 13 | inline fun viewModel(): T { 14 | val contextWrapper = LocalContextWrapper.current 15 | val viewModelPool = contextWrapper.viewModelPool 16 | val viewModelMap = viewModelPool.filter { it.key == T::class } 17 | val viewModel: T = if (viewModelMap.isNotEmpty()) { 18 | viewModelMap.map { it.value }.first() as T 19 | } else { 20 | val instance = T::class.createInstance() 21 | viewModelPool[instance::class] = instance 22 | instance 23 | } 24 | return remember { viewModel } 25 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/preferences/SharedPreferences.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.preferences 2 | 3 | import androidx.compose.runtime.compositionLocalOf 4 | import io.github.lumkit.desktop.annotates.MODE_DATABASE 5 | import io.github.lumkit.desktop.context.Context 6 | import io.github.lumkit.desktop.data.SharedPreference 7 | import io.github.lumkit.desktop.util.json 8 | import kotlinx.serialization.encodeToString 9 | import org.jetbrains.exposed.sql.* 10 | import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq 11 | import org.jetbrains.exposed.sql.transactions.transaction 12 | import java.io.File 13 | 14 | val LocalSharedPreferences = compositionLocalOf { error("Not provided.") } 15 | 16 | class SharedPreferences internal constructor( 17 | private val context: Context, 18 | private val name: String, 19 | ) { 20 | private val db by lazy { 21 | Database.connect("jdbc:sqlite:${File(context.getDir(MODE_DATABASE), "shared").absolutePath}.db", "org.sqlite.JDBC") 22 | } 23 | 24 | private val sharedPreferenceBean by lazy { SharedPreference(name) } 25 | 26 | init { 27 | transaction(db) { 28 | addLogger(StdOutSqlLogger) 29 | SchemaUtils.create(sharedPreferenceBean) 30 | } 31 | } 32 | 33 | fun getString(key: String): String? = transaction(db) { 34 | sharedPreferenceBean.selectAll() 35 | .where { sharedPreferenceBean.key eq key } 36 | .firstOrNull() 37 | ?.get(sharedPreferenceBean.value) 38 | } 39 | 40 | fun putString(key: String, value: String?): Unit = transaction(db) { 41 | if (value == null) { 42 | sharedPreferenceBean.deleteWhere { sharedPreferenceBean.key eq key } 43 | return@transaction 44 | } 45 | if (sharedPreferenceBean.selectAll().where { sharedPreferenceBean.key eq key }.firstOrNull() == null) { 46 | sharedPreferenceBean.insert { 47 | it[this.key] = key 48 | it[this.value] = value 49 | } 50 | } else { 51 | sharedPreferenceBean.update({ sharedPreferenceBean.key eq key }) { 52 | it[this.value] = value 53 | } 54 | } 55 | } 56 | 57 | fun toMap(): Map = transaction(db) { 58 | val map = HashMap() 59 | sharedPreferenceBean.selectAll().forEach { 60 | map[it[sharedPreferenceBean.key]] = it[sharedPreferenceBean.value] 61 | } 62 | map 63 | } 64 | 65 | inline fun get(key: String): T = json.decodeFromString(getString(key) ?: "") 66 | fun getList(key: String): List = json.decodeFromString(getString(key) ?: "") 67 | fun getMap(key: String): Map = json.decodeFromString(getString(key) ?: "") 68 | 69 | inline fun put(key: String, value: T) { 70 | putString(key, json.encodeToString(value)) 71 | } 72 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/shell/KeepShell.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.shell 2 | 3 | import org.jetbrains.skiko.hostOs 4 | import java.io.BufferedReader 5 | import java.io.BufferedWriter 6 | import java.io.File 7 | import java.util.concurrent.locks.ReentrantLock 8 | 9 | /** 10 | * Created by Hello on 2018/01/23. 11 | */ 12 | class KeepShell(private val workPath: String) { 13 | private var p: Process? = null 14 | private var out: BufferedWriter? = null 15 | private var reader: BufferedReader? = null 16 | private var currentIsIdle = true 17 | val isIdle: Boolean 18 | get() { 19 | return currentIsIdle 20 | } 21 | 22 | fun tryExit() { 23 | try { 24 | if (out != null) 25 | out?.close() 26 | if (reader != null) 27 | reader?.close() 28 | } catch (ex: Exception) { 29 | ex.printStackTrace() 30 | } 31 | try { 32 | p?.destroy() 33 | } catch (ex: Exception) { 34 | ex.printStackTrace() 35 | } 36 | enterLockTime = 0L 37 | out = null 38 | reader = null 39 | p = null 40 | currentIsIdle = true 41 | } 42 | 43 | private val GET_ROOT_TIMEOUT = 20000L 44 | private val mLock = ReentrantLock() 45 | private val LOCK_TIMEOUT = 10000L 46 | private var enterLockTime = 0L 47 | 48 | private fun getRuntimeShell() { 49 | if (p != null) return 50 | val getSu = Thread { 51 | try { 52 | mLock.lockInterruptibly() 53 | enterLockTime = System.currentTimeMillis() 54 | p = run { 55 | val processBuilder = when { 56 | hostOs.isWindows -> ProcessBuilder("cmd", "/k") 57 | else -> ProcessBuilder("sh") 58 | } 59 | processBuilder.directory(File(workPath)) 60 | processBuilder.redirectErrorStream(true) 61 | processBuilder.start() 62 | } 63 | out = p?.outputWriter() 64 | reader = p?.inputReader(Charsets.UTF_8) 65 | Thread { 66 | try { 67 | var line: String? 68 | val errorReader = p?.errorReader() 69 | while (errorReader?.readLine().also { line = it } != null) { 70 | println("KeepShellPublic-ERROR: $line") 71 | } 72 | } catch (ex: Exception) { 73 | println("c: " + ex.message) 74 | } 75 | }.start() 76 | } catch (ex: Exception) { 77 | println("getRuntime: " + ex.message) 78 | } finally { 79 | enterLockTime = 0L 80 | mLock.unlock() 81 | } 82 | } 83 | getSu.start() 84 | getSu.join(10000) 85 | if (p == null && getSu.state != Thread.State.TERMINATED) { 86 | enterLockTime = 0L 87 | getSu.interrupt() 88 | } 89 | } 90 | 91 | private var br = "\n\n" 92 | 93 | private val shellOutputCache = StringBuilder() 94 | private val startTag = "SH_START" 95 | private val endTag = "SH_END" 96 | private val startTagBytes = "ECHO $startTag" 97 | private val endTagBytes = "ECHO $endTag" 98 | 99 | fun doCmdSync(cmd: String): String { 100 | if (mLock.isLocked && enterLockTime > 0 && System.currentTimeMillis() - enterLockTime > LOCK_TIMEOUT) { 101 | tryExit() 102 | println("doCmdSync-Lock: " + "${System.currentTimeMillis()} - $enterLockTime > $LOCK_TIMEOUT") 103 | } 104 | getRuntimeShell() 105 | try { 106 | mLock.lockInterruptibly() 107 | currentIsIdle = false 108 | 109 | out?.run { 110 | when { 111 | hostOs.isWindows -> write("$startTagBytes & $cmd & $endTagBytes\n") 112 | else -> { 113 | write("$startTagBytes\n") 114 | write("$cmd\n") 115 | write("$endTagBytes\n") 116 | } 117 | } 118 | flush() 119 | } 120 | 121 | var line: String? 122 | while (reader?.readLine().also { line = it } != null) { 123 | // println(line) 124 | if (line?.contains(workPath) == true) 125 | continue 126 | if (line?.contains(startTag) == true) { 127 | shellOutputCache.clear() 128 | } else if (line?.contains(endTag) == true) { 129 | shellOutputCache.append(line?.substring(0, line?.indexOf(endTag) ?: 0)) 130 | break 131 | } else { 132 | if (line?.contains(startTag) == false && line?.contains(endTag) == false) { 133 | shellOutputCache.append(line).append("\n") 134 | } 135 | } 136 | } 137 | return shellOutputCache.toString().replace("\t", "\t") 138 | } catch (e: Exception) { 139 | tryExit() 140 | println("KeepShellAsync: " + e.message) 141 | return "error" 142 | } finally { 143 | enterLockTime = 0L 144 | mLock.unlock() 145 | currentIsIdle = true 146 | } 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintButton.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.PaddingValues 6 | import androidx.compose.foundation.layout.RowScope 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material3.* 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.runtime.remember 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Shape 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun LintButton( 17 | onClick: () -> Unit, 18 | modifier: Modifier = Modifier, 19 | enabled: Boolean = true, 20 | shape: Shape = RoundedCornerShape(6.dp), 21 | colors: ButtonColors = ButtonDefaults.buttonColors(), 22 | elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), 23 | border: BorderStroke? = null, 24 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 25 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 26 | content: @Composable RowScope.() -> Unit 27 | ) { 28 | Button( 29 | onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, content 30 | ) 31 | } 32 | 33 | @Composable 34 | fun LintTextButton( 35 | onClick: () -> Unit, 36 | modifier: Modifier = Modifier, 37 | enabled: Boolean = true, 38 | shape: Shape = RoundedCornerShape(6.dp), 39 | colors: ButtonColors = ButtonDefaults.textButtonColors(), 40 | elevation: ButtonElevation? = null, 41 | border: BorderStroke? = null, 42 | contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, 43 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 44 | content: @Composable RowScope.() -> Unit 45 | ) { 46 | TextButton( 47 | onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, content 48 | ) 49 | } 50 | 51 | @Composable 52 | fun LintOutlinedButton( 53 | onClick: () -> Unit, 54 | modifier: Modifier = Modifier, 55 | enabled: Boolean = true, 56 | shape: Shape = RoundedCornerShape(6.dp), 57 | colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), 58 | elevation: ButtonElevation? = null, 59 | border: BorderStroke? = BorderStroke(.5.dp, DividerDefaults.color), 60 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 61 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 62 | content: @Composable RowScope.() -> Unit 63 | ) { 64 | OutlinedButton( 65 | onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, content 66 | ) 67 | } 68 | 69 | @Composable 70 | fun LintElevatedButton( 71 | onClick: () -> Unit, 72 | modifier: Modifier = Modifier, 73 | enabled: Boolean = true, 74 | shape: Shape = RoundedCornerShape(6.dp), 75 | colors: ButtonColors = ButtonDefaults.elevatedButtonColors(), 76 | elevation: ButtonElevation? = ButtonDefaults.elevatedButtonElevation(), 77 | border: BorderStroke? = null, 78 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 79 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 80 | content: @Composable RowScope.() -> Unit 81 | ) { 82 | ElevatedButton(onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, content) 83 | } 84 | 85 | @Composable 86 | fun LintFilledTonalButton( 87 | onClick: () -> Unit, 88 | modifier: Modifier = Modifier, 89 | enabled: Boolean = true, 90 | shape: Shape = RoundedCornerShape(6.dp), 91 | colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(), 92 | elevation: ButtonElevation? = ButtonDefaults.filledTonalButtonElevation(), 93 | border: BorderStroke? = null, 94 | contentPadding: PaddingValues = ButtonDefaults.ContentPadding, 95 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 96 | content: @Composable RowScope.() -> Unit 97 | ) { 98 | FilledTonalButton( 99 | onClick, modifier, enabled, shape, colors, elevation, border, contentPadding, interactionSource, content 100 | ) 101 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintCard.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.foundation.BorderStroke 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.ColumnScope 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun LintCard( 16 | modifier: Modifier = Modifier, 17 | shape: Shape = RoundedCornerShape(6.dp), 18 | colors: CardColors = CardDefaults.cardColors(), 19 | elevation: CardElevation = CardDefaults.cardElevation(), 20 | border: BorderStroke? = null, 21 | content: @Composable ColumnScope.() -> Unit 22 | ) { 23 | Card( 24 | modifier, shape, colors, elevation, border, content 25 | ) 26 | } 27 | 28 | @Composable 29 | fun LintCard( 30 | onClick: () -> Unit, 31 | modifier: Modifier = Modifier, 32 | enabled: Boolean = true, 33 | shape: Shape = RoundedCornerShape(6.dp), 34 | colors: CardColors = CardDefaults.cardColors(), 35 | elevation: CardElevation = CardDefaults.cardElevation(), 36 | border: BorderStroke? = null, 37 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 38 | content: @Composable ColumnScope.() -> Unit 39 | ) { 40 | Card( 41 | onClick, modifier, enabled, shape, colors, elevation, border, interactionSource, content 42 | ) 43 | } 44 | 45 | @Composable 46 | fun LintOutlineCard( 47 | modifier: Modifier = Modifier, 48 | shape: Shape = RoundedCornerShape(6.dp), 49 | colors: CardColors = CardDefaults.outlinedCardColors(), 50 | elevation: CardElevation = CardDefaults.outlinedCardElevation(), 51 | border: BorderStroke = BorderStroke(.5.dp, DividerDefaults.color), 52 | content: @Composable ColumnScope.() -> Unit 53 | ) { 54 | OutlinedCard( 55 | modifier, shape, colors, elevation, border, content 56 | ) 57 | } 58 | 59 | @Composable 60 | fun LintOutlineCard( 61 | onClick: () -> Unit, 62 | modifier: Modifier = Modifier, 63 | enabled: Boolean = true, 64 | shape: Shape = RoundedCornerShape(6.dp), 65 | colors: CardColors = CardDefaults.outlinedCardColors(), 66 | elevation: CardElevation = CardDefaults.outlinedCardElevation(), 67 | border: BorderStroke = BorderStroke(.5.dp, DividerDefaults.color), 68 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 69 | content: @Composable ColumnScope.() -> Unit 70 | ) { 71 | OutlinedCard( 72 | onClick, modifier, enabled, shape, colors, elevation, border, interactionSource, content 73 | ) 74 | } 75 | 76 | @Composable 77 | fun LintElevatedCard( 78 | modifier: Modifier = Modifier, 79 | shape: Shape = RoundedCornerShape(6.dp), 80 | colors: CardColors = CardDefaults.elevatedCardColors(), 81 | elevation: CardElevation = CardDefaults.elevatedCardElevation(), 82 | content: @Composable ColumnScope.() -> Unit 83 | ) { 84 | ElevatedCard( 85 | modifier, shape, colors, elevation, content 86 | ) 87 | } 88 | 89 | @Composable 90 | fun LintElevatedCard( 91 | onClick: () -> Unit, 92 | modifier: Modifier = Modifier, 93 | enabled: Boolean = true, 94 | shape: Shape = RoundedCornerShape(6.dp), 95 | colors: CardColors = CardDefaults.elevatedCardColors(), 96 | elevation: CardElevation = CardDefaults.elevatedCardElevation(), 97 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 98 | content: @Composable ColumnScope.() -> Unit 99 | ) { 100 | ElevatedCard( 101 | onClick, modifier, enabled, shape, colors, elevation, interactionSource, content 102 | ) 103 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintChart.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.foundation.Canvas 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.geometry.CornerRadius 9 | import androidx.compose.ui.geometry.Offset 10 | import androidx.compose.ui.geometry.Size 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.unit.Dp 13 | import androidx.compose.ui.unit.dp 14 | import java.util.concurrent.LinkedBlockingQueue 15 | 16 | @Composable 17 | fun LintStackChart( 18 | modifier: Modifier = Modifier, 19 | progress: Float, 20 | chartWith: Dp = 10.dp, 21 | defaultColor: Color = MaterialTheme.colorScheme.primary.copy(alpha = .125f), 22 | midColor: Color = Color(0xFFFC8A1B), 23 | highColor: Color = Color(0xFFF9592F), 24 | ) { 25 | val loadHistory = remember { LinkedBlockingQueue() } 26 | 27 | Canvas( 28 | modifier = modifier 29 | ) { 30 | val width = this.drawContext.size.width 31 | val height = this.drawContext.size.height 32 | val chartWithPx = chartWith.toPx() 33 | val chartCount = (width / chartWithPx).toInt() 34 | 35 | val space = (width - chartCount * chartWithPx) / 2f 36 | 37 | if (loadHistory.size <= 0) { 38 | for (i in 0 until chartCount - 1) { 39 | loadHistory.add(0f) 40 | } 41 | } 42 | 43 | loadHistory.put((progress * 100f).bounds(0f, 100f)) 44 | 45 | if (loadHistory.size > chartCount) { 46 | for (i in chartCount until loadHistory.size) { 47 | loadHistory.poll() 48 | } 49 | } 50 | 51 | var index = 0 52 | loadHistory.forEach { ratio -> 53 | var chartColor = if (ratio > 85f) { 54 | highColor 55 | } else if (ratio > 65f) { 56 | midColor 57 | } else { 58 | defaultColor 59 | } 60 | chartColor = if (ratio > 50f) { 61 | chartColor.copy(alpha = 1f) 62 | } else { 63 | val fl = 0.5f + (ratio / 100.0f) 64 | chartColor.copy(alpha = if (fl <= 0f) 0f else if (fl >= 1f) 1f else fl) 65 | } 66 | val top = if (ratio <= 2f) { 67 | height - 10f 68 | } else if (ratio >= 98f) { 69 | 0f 70 | } else { 71 | (100f - ratio) * height / 100f 72 | } 73 | drawRoundRect( 74 | color = chartColor, 75 | topLeft = Offset(x = chartWithPx * index + space, y = top), 76 | size = Size(chartWithPx * .9f, height - top), 77 | cornerRadius = CornerRadius(3f, 3f) 78 | ) 79 | index++ 80 | } 81 | } 82 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintDivider.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.material3.DividerDefaults 4 | import androidx.compose.material3.HorizontalDivider 5 | import androidx.compose.material3.VerticalDivider 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Modifier 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.unit.Dp 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | fun LintVerticalDivider( 14 | modifier: Modifier = Modifier, 15 | thickness: Dp = .5.dp, 16 | color: Color = DividerDefaults.color, 17 | ) { 18 | VerticalDivider( 19 | modifier, thickness, color 20 | ) 21 | } 22 | 23 | @Composable 24 | fun LintHorizontalDivider( 25 | modifier: Modifier = Modifier, 26 | thickness: Dp = .5.dp, 27 | color: Color = DividerDefaults.color, 28 | ) { 29 | HorizontalDivider( 30 | modifier, thickness, color 31 | ) 32 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintFolder.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.ProvideTextStyle 9 | import androidx.compose.material3.Surface 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun LintFolder( 19 | modifier: Modifier, 20 | expanded: Boolean = true, 21 | icon: @Composable (() -> Unit)? = null, 22 | label: @Composable () -> Unit, 23 | tooltipText: @Composable (() -> Unit)? = null, 24 | trailingIcon: @Composable (RowScope.() -> Unit)? = null, 25 | onClick: (() -> Unit)? = null, 26 | children: @Composable ColumnScope.() -> Unit = {} 27 | ) { 28 | LintOutlineCard( 29 | modifier = modifier, 30 | ) { 31 | Row( 32 | modifier = Modifier.fillMaxWidth() 33 | .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), 34 | verticalAlignment = Alignment.CenterVertically, 35 | ) { 36 | icon?.also { 37 | Spacer(Modifier.width(16.dp)) 38 | Surface( 39 | modifier = Modifier.size(16.dp), 40 | color = Color.Transparent, 41 | ) { 42 | it() 43 | } 44 | } 45 | Spacer(Modifier.width(16.dp)) 46 | Box( 47 | modifier = Modifier.fillMaxWidth().weight(1f), 48 | contentAlignment = Alignment.CenterStart 49 | ) { 50 | Column( 51 | modifier = Modifier.padding(vertical = 8.dp) 52 | ) { 53 | Text("", style = MaterialTheme.typography.titleMedium) 54 | Text("", style = MaterialTheme.typography.labelMedium) 55 | } 56 | Column { 57 | ProvideTextStyle(MaterialTheme.typography.titleMedium) { 58 | label() 59 | } 60 | tooltipText?.also { 61 | ProvideTextStyle(MaterialTheme.typography.labelMedium) { 62 | it() 63 | } 64 | } 65 | } 66 | } 67 | trailingIcon?.also { 68 | Spacer(Modifier.width(16.dp)) 69 | Row( 70 | modifier = Modifier.padding(vertical = 8.dp), 71 | ) { 72 | it() 73 | } 74 | } 75 | Spacer(Modifier.width(16.dp)) 76 | TrailingIconButton(expanded = expanded) 77 | Spacer(Modifier.width(16.dp)) 78 | } 79 | AnimatedVisibility( 80 | visible = expanded, 81 | enter = fadeIn(tween()) + expandVertically(tween()), 82 | exit = fadeOut(tween()) + shrinkVertically(tween()), 83 | ) { 84 | Column { 85 | LintHorizontalDivider() 86 | children() 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | fun LintItem( 94 | modifier: Modifier, 95 | icon: @Composable (() -> Unit)? = null, 96 | label: @Composable () -> Unit, 97 | tooltipText: @Composable (() -> Unit)? = null, 98 | trailingIcon: @Composable (RowScope.() -> Unit)? = null, 99 | onClick: (() -> Unit)? = null, 100 | ) { 101 | LintOutlineCard( 102 | modifier = modifier, 103 | ) { 104 | Row( 105 | modifier = Modifier.fillMaxWidth() 106 | .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), 107 | verticalAlignment = Alignment.CenterVertically, 108 | ) { 109 | icon?.also { 110 | Spacer(Modifier.width(16.dp)) 111 | Surface( 112 | modifier = Modifier.size(16.dp), 113 | color = Color.Transparent, 114 | ) { 115 | it() 116 | } 117 | } 118 | Spacer(Modifier.width(16.dp)) 119 | Box( 120 | modifier = Modifier.fillMaxWidth().weight(1f), 121 | contentAlignment = Alignment.CenterStart 122 | ) { 123 | Column( 124 | modifier = Modifier.padding(vertical = 8.dp) 125 | ) { 126 | Text("", style = MaterialTheme.typography.titleMedium) 127 | Text("", style = MaterialTheme.typography.labelMedium) 128 | } 129 | Column { 130 | ProvideTextStyle(MaterialTheme.typography.titleMedium) { 131 | label() 132 | } 133 | tooltipText?.also { 134 | ProvideTextStyle(MaterialTheme.typography.labelMedium) { 135 | it() 136 | } 137 | } 138 | } 139 | } 140 | trailingIcon?.also { 141 | Spacer(Modifier.width(16.dp)) 142 | Row( 143 | modifier = Modifier.padding(vertical = 8.dp), 144 | ) { 145 | it() 146 | } 147 | } 148 | Spacer(Modifier.width(16.dp)) 149 | } 150 | } 151 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintProgress.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.animation.core.Easing 4 | import androidx.compose.animation.core.FastOutSlowInEasing 5 | import androidx.compose.animation.core.animateFloatAsState 6 | import androidx.compose.animation.core.tween 7 | import androidx.compose.foundation.Canvas 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material3.CircularProgressIndicator 10 | import androidx.compose.material3.LinearProgressIndicator 11 | import androidx.compose.material3.MaterialTheme 12 | import androidx.compose.material3.ProgressIndicatorDefaults 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.ExperimentalComposeUiApi 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.geometry.CornerRadius 17 | import androidx.compose.ui.geometry.Offset 18 | import androidx.compose.ui.geometry.Size 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.graphics.StrokeCap 21 | import androidx.compose.ui.graphics.drawscope.Stroke 22 | import androidx.compose.ui.input.pointer.PointerEventType 23 | import androidx.compose.ui.input.pointer.onPointerEvent 24 | import androidx.compose.ui.platform.LocalDensity 25 | import androidx.compose.ui.unit.Dp 26 | import androidx.compose.ui.unit.dp 27 | 28 | /** 29 | * A horizontal, rounded, draggable progress bar. 30 | */ 31 | @OptIn(ExperimentalComposeUiApi::class) 32 | @Composable 33 | fun LintHorizontalLinearProgress( 34 | modifier: Modifier, 35 | progress: Float, 36 | onProgressChanged: (Float) -> Unit = {}, 37 | thickness: Dp = 16.dp, 38 | radius: CornerRadius = CornerRadius( 39 | with(LocalDensity.current) { (thickness / 2f).toPx() }, 40 | with(LocalDensity.current) { (thickness / 2f).toPx() } 41 | ), 42 | easing: Easing = FastOutSlowInEasing, 43 | color: Color = ProgressIndicatorDefaults.linearColor, 44 | trackColor: Color = MaterialTheme.colorScheme.surface, 45 | ) { 46 | val density = LocalDensity.current 47 | var viewWidth = 0f 48 | val circleRadius = with(density) { thickness.toPx() * .5f } 49 | 50 | var release by remember { mutableStateOf(true) } 51 | var moving by remember { mutableStateOf(false) } 52 | 53 | val realProgress = if (!release && moving) { 54 | progress 55 | } else { 56 | val animateProgress by animateFloatAsState( 57 | progress, 58 | animationSpec = tween(easing = easing) 59 | ) 60 | animateProgress 61 | } 62 | 63 | val thumbSize by animateFloatAsState( 64 | targetValue = if (release) with(density) { thickness.toPx() * .6f } 65 | else with(density) { thickness.toPx() * .8f }, 66 | ) 67 | 68 | Canvas( 69 | modifier = modifier.height(thickness) 70 | .onPointerEvent(PointerEventType.Release) { 71 | release = true 72 | moving = false 73 | it.changes.first().consume() 74 | }.onPointerEvent(PointerEventType.Press) { 75 | release = false 76 | moving = false 77 | onProgressChanged(((it.changes.first().position.x - circleRadius) / viewWidth).bounds(0f, 1f)) 78 | it.changes.first().consume() 79 | }.onPointerEvent(PointerEventType.Move) { 80 | moving = true 81 | if (!release) { 82 | onProgressChanged(((it.changes.first().position.x - circleRadius) / viewWidth).bounds(0f, 1f)) 83 | } 84 | it.changes.first().consume() 85 | }, 86 | ) { 87 | val thicknessPx = thickness.toPx() 88 | val width = size.width - thicknessPx 89 | viewWidth = width 90 | val offsetX = ((width * realProgress) + thicknessPx).bounds(thicknessPx, size.width) 91 | drawRoundRect( 92 | color = trackColor, 93 | size = size, 94 | cornerRadius = radius 95 | ) 96 | drawRoundRect( 97 | color = color, 98 | size = size.copy(width = offsetX), 99 | cornerRadius = radius 100 | ) 101 | val padding = (size.height - thumbSize) * .5f 102 | drawRoundRect( 103 | topLeft = Offset(offsetX - thumbSize - padding, padding), 104 | color = trackColor, 105 | size = Size(thumbSize, thumbSize), 106 | cornerRadius = radius 107 | ) 108 | } 109 | } 110 | 111 | @Composable 112 | fun LintHorizontalLinearIndicator( 113 | modifier: Modifier, 114 | progress: Float, 115 | thickness: Dp = 16.dp, 116 | radius: CornerRadius = CornerRadius( 117 | with(LocalDensity.current) { (thickness / 2f).toPx() }, 118 | with(LocalDensity.current) { (thickness / 2f).toPx() } 119 | ), 120 | easing: Easing = FastOutSlowInEasing, 121 | color: Color = ProgressIndicatorDefaults.linearColor, 122 | trackColor: Color = MaterialTheme.colorScheme.surface, 123 | ) { 124 | val realProgress by animateFloatAsState( 125 | progress, 126 | animationSpec = tween(easing = easing) 127 | ) 128 | Canvas( 129 | modifier = modifier.height(thickness) 130 | ) { 131 | val thicknessPx = thickness.toPx() 132 | val width = size.width - thicknessPx 133 | val offsetX = ((width * realProgress) + thicknessPx).bounds(thicknessPx, size.width) 134 | drawRoundRect( 135 | color = trackColor, 136 | size = size, 137 | cornerRadius = radius 138 | ) 139 | drawRoundRect( 140 | color = color, 141 | size = size.copy(width = offsetX), 142 | cornerRadius = radius 143 | ) 144 | } 145 | } 146 | 147 | @Composable 148 | fun LintCircleIndicator( 149 | modifier: Modifier, 150 | progress: Float, 151 | thickness: Dp = 16.dp, 152 | easing: Easing = FastOutSlowInEasing, 153 | color: Color = ProgressIndicatorDefaults.linearColor, 154 | trackColor: Color = MaterialTheme.colorScheme.surface, 155 | ) { 156 | val realProgress by animateFloatAsState( 157 | progress, 158 | animationSpec = tween(easing = easing) 159 | ) 160 | Canvas( 161 | modifier = modifier 162 | ) { 163 | val thicknessPx = thickness.toPx() 164 | drawCircle( 165 | color = trackColor, 166 | style = Stroke(width = thicknessPx) 167 | ) 168 | drawArc( 169 | color = color, 170 | startAngle = -90f, 171 | sweepAngle = 360 * realProgress, 172 | useCenter = false, 173 | style = Stroke(width = thicknessPx, cap = StrokeCap.Round) 174 | ) 175 | } 176 | } 177 | 178 | @Composable 179 | fun LintCircleIndicator( 180 | modifier: Modifier = Modifier, 181 | color: Color = ProgressIndicatorDefaults.circularColor, 182 | strokeWidth: Dp = ProgressIndicatorDefaults.CircularStrokeWidth, 183 | trackColor: Color = ProgressIndicatorDefaults.circularTrackColor, 184 | strokeCap: StrokeCap = StrokeCap.Round, 185 | ) { 186 | CircularProgressIndicator( 187 | modifier, color, strokeWidth, trackColor, strokeCap 188 | ) 189 | } 190 | 191 | @Composable 192 | fun LintHorizontalLinearIndicator( 193 | modifier: Modifier = Modifier, 194 | color: Color = ProgressIndicatorDefaults.linearColor, 195 | trackColor: Color = ProgressIndicatorDefaults.linearTrackColor, 196 | strokeCap: StrokeCap = StrokeCap.Round, 197 | ) { 198 | LinearProgressIndicator( 199 | modifier, color, trackColor, strokeCap 200 | ) 201 | } 202 | 203 | internal infix fun Float.min(min: Float) = if (this <= min) min else this 204 | internal infix fun Float.max(max: Float) = if (this >= max) max else this 205 | internal fun Float.bounds(min: Float, max: Float) = if (this >= max) max else if (this <= min) min else this -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintScrollBar.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.foundation.HorizontalScrollbar 4 | import androidx.compose.foundation.LocalScrollbarStyle 5 | import androidx.compose.foundation.ScrollbarStyle 6 | import androidx.compose.foundation.VerticalScrollbar 7 | import androidx.compose.foundation.interaction.MutableInteractionSource 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.foundation.layout.width 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.remember 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | 15 | @Composable 16 | fun LintVerticalScrollBar( 17 | adapter: androidx.compose.foundation.v2.ScrollbarAdapter, 18 | modifier: Modifier = Modifier, 19 | reverseLayout: Boolean = false, 20 | style: ScrollbarStyle = LocalScrollbarStyle.current, 21 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } 22 | ) { 23 | VerticalScrollbar( 24 | adapter, modifier.width(4.dp), reverseLayout, style, interactionSource 25 | ) 26 | } 27 | 28 | @Composable 29 | fun LintHorizontalScrollbar( 30 | adapter: androidx.compose.foundation.v2.ScrollbarAdapter, 31 | modifier: Modifier = Modifier, 32 | reverseLayout: Boolean = false, 33 | style: ScrollbarStyle = LocalScrollbarStyle.current, 34 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } 35 | ) { 36 | HorizontalScrollbar( 37 | adapter, modifier.height(4.dp), reverseLayout, style, interactionSource 38 | ) 39 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintSide.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.animation.* 4 | import androidx.compose.animation.core.* 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.clickable 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.foundation.shape.RoundedCornerShape 10 | import androidx.compose.material.icons.Icons 11 | import androidx.compose.material.icons.filled.Close 12 | import androidx.compose.material.icons.filled.KeyboardArrowDown 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.MaterialTheme 15 | import androidx.compose.material3.ProvideTextStyle 16 | import androidx.compose.material3.Surface 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.runtime.CompositionLocalProvider 19 | import androidx.compose.runtime.compositionLocalOf 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.draw.clip 24 | import androidx.compose.ui.draw.rotate 25 | import androidx.compose.ui.graphics.Color 26 | import androidx.compose.ui.unit.Dp 27 | import androidx.compose.ui.unit.dp 28 | import androidx.compose.ui.unit.times 29 | 30 | private val LocalNavigationItemLevel = compositionLocalOf { 0 } 31 | private val navHeight = 52.dp 32 | 33 | @Composable 34 | fun LintSideNavigationBar( 35 | modifier: Modifier = Modifier, 36 | minimize: Boolean = false, 37 | expanded: Boolean = true, 38 | selected: Boolean = false, 39 | width: Dp = 320.dp, 40 | icon: @Composable () -> Unit = {}, 41 | title: @Composable () -> Unit, 42 | subtitle: @Composable (() -> Unit)? = null, 43 | onClick: (() -> Unit)? = null, 44 | onExpandedClick: (() -> Unit)? = null, 45 | child: @Composable (ColumnScope.() -> Unit)? = null 46 | ) { 47 | val animatedWidth by animateDpAsState( 48 | targetValue = if (minimize) navHeight else width, 49 | animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) 50 | ) 51 | Column( 52 | modifier = modifier.width(animatedWidth).clip(RoundedCornerShape(6.dp)), 53 | ) { 54 | NavItem(expanded, selected, icon, title, subtitle, onClick, onExpandedClick, child) 55 | AnimatedVisibility( 56 | visible = expanded && !minimize && animatedWidth == width, 57 | ) { 58 | Column( 59 | modifier = Modifier.fillMaxWidth(), 60 | verticalArrangement = Arrangement.spacedBy(4.dp) 61 | ) { 62 | Spacer(modifier = Modifier) 63 | CompositionLocalProvider( 64 | LocalNavigationItemLevel provides LocalNavigationItemLevel.current + 1, 65 | ) { 66 | child?.invoke(this) 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | @Composable 74 | fun LintNavigationIconButton( 75 | modifier: Modifier = Modifier.size(navHeight), 76 | width: Dp = 320.dp, 77 | minimize: Boolean = false, 78 | selected: Boolean = false, 79 | title: @Composable (() -> Unit)? = null, 80 | onClick: () -> Unit, 81 | content: @Composable () -> Unit 82 | ) { 83 | val onSelectedColor by animateColorAsState( 84 | targetValue = if (selected) { 85 | MaterialTheme.colorScheme.surfaceVariant 86 | } else { 87 | Color.Transparent 88 | }, 89 | tween(durationMillis = 300, easing = LinearOutSlowInEasing) 90 | ) 91 | 92 | val animatedWidth by animateDpAsState( 93 | if (minimize) navHeight else width, 94 | animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) 95 | ) 96 | 97 | Row ( 98 | modifier = Modifier.width(animatedWidth), 99 | verticalAlignment = Alignment.CenterVertically, 100 | ){ 101 | Box( 102 | modifier = Modifier.then(modifier) 103 | .background(onSelectedColor, RoundedCornerShape(6.dp)) 104 | .clip(RoundedCornerShape(6.dp)) 105 | .clickable { 106 | onClick() 107 | }, 108 | ) { 109 | NavThumb(selected) 110 | Surface( 111 | modifier = Modifier.size(16.dp).align(Alignment.Center), 112 | color = Color.Transparent, 113 | content = content 114 | ) 115 | } 116 | title?.also { 117 | AnimatedVisibility( 118 | !minimize, 119 | ) { 120 | Row { 121 | Spacer(modifier = Modifier.size(16.dp)) 122 | ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { 123 | it() 124 | } 125 | Spacer(modifier = Modifier.size(16.dp)) 126 | } 127 | } 128 | } 129 | } 130 | } 131 | 132 | @Composable 133 | fun LintSearchSide( 134 | modifier: Modifier = Modifier, 135 | width: Dp = 320.dp, 136 | minimize: Boolean = false, 137 | value: String, 138 | onValueChange: (String) -> Unit, 139 | icon: @Composable () -> Unit, 140 | onClick: () -> Unit = {}, 141 | onClean: () -> Unit = {}, 142 | ) { 143 | val animatedWidth by animateDpAsState( 144 | if (minimize) navHeight else width, 145 | animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) 146 | ) 147 | 148 | Row ( 149 | modifier = Modifier.height(navHeight).width(animatedWidth) 150 | .clip(RoundedCornerShape(6.dp)) 151 | .then(modifier) 152 | ) { 153 | AnimatedContent( 154 | targetState = minimize 155 | ) { 156 | if (it) { 157 | Box( 158 | modifier = Modifier.size(navHeight) 159 | .clip(RoundedCornerShape(6.dp)) 160 | .clickable { 161 | onClick() 162 | }, 163 | ) { 164 | Surface( 165 | modifier = Modifier.size(16.dp).align(Alignment.Center), 166 | color = Color.Transparent, 167 | content = icon 168 | ) 169 | } 170 | } else { 171 | ProvideTextStyle( 172 | MaterialTheme.typography.bodyMedium 173 | ) { 174 | LintTextField( 175 | modifier = Modifier.fillMaxWidth(), 176 | value = value, 177 | onValueChange = onValueChange, 178 | trailingIcon = { 179 | if (value.isNotEmpty()) { 180 | Icon( 181 | modifier = Modifier.size(16.dp) 182 | .clip(CircleShape) 183 | .clickable { 184 | onClean() 185 | }, 186 | imageVector = Icons.Default.Close, 187 | contentDescription = null, 188 | ) 189 | } 190 | }, 191 | singleLine = true 192 | ) 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | @Composable 200 | private fun NavItem( 201 | expanded: Boolean, 202 | selected: Boolean, 203 | icon: @Composable () -> Unit, 204 | title: @Composable () -> Unit, 205 | subtitle: @Composable() (() -> Unit)?, 206 | onClick: (() -> Unit)?, 207 | onExpandedClick: (() -> Unit)?, 208 | child: @Composable() (ColumnScope.() -> Unit)? 209 | ) { 210 | val onSelectedColor by animateColorAsState( 211 | targetValue = if (selected) { 212 | MaterialTheme.colorScheme.surfaceVariant 213 | } else { 214 | Color.Transparent 215 | }, 216 | tween(durationMillis = 300, easing = LinearOutSlowInEasing) 217 | ) 218 | 219 | Box( 220 | modifier = Modifier.height(navHeight).wrapContentWidth() 221 | .background(onSelectedColor, RoundedCornerShape(6.dp)) 222 | .clip(RoundedCornerShape(6.dp)) 223 | .then( 224 | if (onClick == null) Modifier 225 | else Modifier.clickable(onClick = onClick) 226 | ) 227 | ) { 228 | NavThumb(selected = selected) 229 | val marginStart = LocalNavigationItemLevel.current * 28.dp 230 | Row( 231 | modifier = Modifier.wrapContentWidth().padding(start = marginStart), 232 | verticalAlignment = Alignment.CenterVertically 233 | ) { 234 | Box( 235 | modifier = Modifier.size(navHeight), 236 | contentAlignment = Alignment.Center 237 | ) { 238 | Surface( 239 | modifier = Modifier.padding(16.dp) 240 | .size(16.dp), 241 | color = Color.Transparent 242 | ) { 243 | icon() 244 | } 245 | } 246 | NavLabel(expanded, title, subtitle, child, onExpandedClick) 247 | } 248 | } 249 | } 250 | 251 | @Composable 252 | private fun BoxScope.NavThumb( 253 | selected: Boolean, 254 | ) { 255 | val height by animateDpAsState( 256 | targetValue = if (selected) { 257 | 16.dp 258 | } else { 259 | 0.dp 260 | }, 261 | animationSpec = tween(durationMillis = 300, easing = FastOutSlowInEasing) 262 | ) 263 | Surface( 264 | modifier = Modifier.clip(RoundedCornerShape(1.dp)) 265 | .width(3.dp) 266 | .height(height) 267 | .align(Alignment.CenterStart), 268 | color = MaterialTheme.colorScheme.primary, 269 | ) {} 270 | } 271 | 272 | @Composable 273 | private fun RowScope.NavLabel( 274 | expanded: Boolean, 275 | title: @Composable () -> Unit, 276 | subtitle: @Composable (() -> Unit)?, 277 | child: @Composable (ColumnScope.() -> Unit)?, 278 | onExpandedClick: (() -> Unit)? 279 | ) { 280 | Row( 281 | modifier = Modifier.fillMaxWidth().weight(1f), 282 | verticalAlignment = Alignment.CenterVertically 283 | ) { 284 | Column( 285 | modifier = Modifier.padding(vertical = 8.dp).fillMaxWidth().weight(1f), 286 | ) { 287 | ProvideTextStyle(MaterialTheme.typography.bodyMedium) { 288 | title() 289 | } 290 | subtitle?.also { 291 | ProvideTextStyle(MaterialTheme.typography.labelMedium) { 292 | it() 293 | } 294 | } 295 | } 296 | Spacer(modifier = Modifier.size(16.dp)) 297 | child?.run { 298 | TrailingIconButton(expanded = expanded, onExpandedClick) 299 | Spacer(modifier = Modifier.size(16.dp)) 300 | } 301 | } 302 | } 303 | 304 | @Composable 305 | internal fun TrailingIconButton( 306 | expanded: Boolean, 307 | onClick: (() -> Unit)? = null, 308 | ) { 309 | val rotation by animateFloatAsState( 310 | targetValue = if (expanded) 0f else -180f, 311 | animationSpec = tween(easing = LinearEasing) 312 | ) 313 | Icon( 314 | imageVector = Icons.Filled.KeyboardArrowDown, 315 | contentDescription = null, 316 | modifier = Modifier.size(24.dp).rotate(rotation) 317 | .clip(CircleShape) 318 | .then( 319 | if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier 320 | ) 321 | ) 322 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/components/LintTextField.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.components 2 | 3 | import androidx.compose.foundation.interaction.MutableInteractionSource 4 | import androidx.compose.foundation.shape.RoundedCornerShape 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.material3.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.remember 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Shape 12 | import androidx.compose.ui.text.TextStyle 13 | import androidx.compose.ui.text.input.TextFieldValue 14 | import androidx.compose.ui.text.input.VisualTransformation 15 | import androidx.compose.ui.unit.dp 16 | 17 | @Composable 18 | fun LintTextField( 19 | value: String, 20 | onValueChange: (String) -> Unit, 21 | modifier: Modifier = Modifier, 22 | enabled: Boolean = true, 23 | readOnly: Boolean = false, 24 | textStyle: TextStyle = LocalTextStyle.current, 25 | label: @Composable (() -> Unit)? = null, 26 | placeholder: @Composable (() -> Unit)? = null, 27 | leadingIcon: @Composable (() -> Unit)? = null, 28 | trailingIcon: @Composable (() -> Unit)? = null, 29 | prefix: @Composable (() -> Unit)? = null, 30 | suffix: @Composable (() -> Unit)? = null, 31 | supportingText: @Composable (() -> Unit)? = null, 32 | isError: Boolean = false, 33 | visualTransformation: VisualTransformation = VisualTransformation.None, 34 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 35 | keyboardActions: KeyboardActions = KeyboardActions.Default, 36 | singleLine: Boolean = false, 37 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 38 | minLines: Int = 1, 39 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 40 | shape: Shape = RoundedCornerShape( 41 | topStart = 6.dp, 42 | topEnd = 6.dp 43 | ), 44 | colors: TextFieldColors = TextFieldDefaults.colors().copy( 45 | unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, 46 | focusedContainerColor = MaterialTheme.colorScheme.surface, 47 | ) 48 | ) { 49 | TextField( 50 | value, 51 | onValueChange, 52 | modifier, 53 | enabled, 54 | readOnly, 55 | textStyle, 56 | label, 57 | placeholder, 58 | leadingIcon, 59 | trailingIcon, 60 | prefix, 61 | suffix, 62 | supportingText, 63 | isError, 64 | visualTransformation, 65 | keyboardOptions, 66 | keyboardActions, 67 | singleLine, 68 | maxLines, 69 | minLines, 70 | interactionSource, 71 | shape, 72 | colors 73 | ) 74 | } 75 | 76 | @Composable 77 | fun LintTextField( 78 | value: TextFieldValue, 79 | onValueChange: (TextFieldValue) -> Unit, 80 | modifier: Modifier = Modifier, 81 | enabled: Boolean = true, 82 | readOnly: Boolean = false, 83 | textStyle: TextStyle = LocalTextStyle.current, 84 | label: @Composable (() -> Unit)? = null, 85 | placeholder: @Composable (() -> Unit)? = null, 86 | leadingIcon: @Composable (() -> Unit)? = null, 87 | trailingIcon: @Composable (() -> Unit)? = null, 88 | prefix: @Composable (() -> Unit)? = null, 89 | suffix: @Composable (() -> Unit)? = null, 90 | supportingText: @Composable (() -> Unit)? = null, 91 | isError: Boolean = false, 92 | visualTransformation: VisualTransformation = VisualTransformation.None, 93 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 94 | keyboardActions: KeyboardActions = KeyboardActions.Default, 95 | singleLine: Boolean = false, 96 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 97 | minLines: Int = 1, 98 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 99 | shape: Shape = RoundedCornerShape( 100 | topStart = 6.dp, 101 | topEnd = 6.dp 102 | ), 103 | colors: TextFieldColors = TextFieldDefaults.colors().copy( 104 | unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainer, 105 | focusedContainerColor = MaterialTheme.colorScheme.surface, 106 | ) 107 | ) { 108 | TextField( 109 | value, 110 | onValueChange, 111 | modifier, 112 | enabled, 113 | readOnly, 114 | textStyle, 115 | label, 116 | placeholder, 117 | leadingIcon, 118 | trailingIcon, 119 | prefix, 120 | suffix, 121 | supportingText, 122 | isError, 123 | visualTransformation, 124 | keyboardOptions, 125 | keyboardActions, 126 | singleLine, 127 | maxLines, 128 | minLines, 129 | interactionSource, 130 | shape, 131 | colors 132 | ) 133 | } 134 | 135 | @Composable 136 | fun LintOutlinedTextField( 137 | value: String, 138 | onValueChange: (String) -> Unit, 139 | modifier: Modifier = Modifier, 140 | enabled: Boolean = true, 141 | readOnly: Boolean = false, 142 | textStyle: TextStyle = LocalTextStyle.current, 143 | label: @Composable (() -> Unit)? = null, 144 | placeholder: @Composable (() -> Unit)? = null, 145 | leadingIcon: @Composable (() -> Unit)? = null, 146 | trailingIcon: @Composable (() -> Unit)? = null, 147 | prefix: @Composable (() -> Unit)? = null, 148 | suffix: @Composable (() -> Unit)? = null, 149 | supportingText: @Composable (() -> Unit)? = null, 150 | isError: Boolean = false, 151 | visualTransformation: VisualTransformation = VisualTransformation.None, 152 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 153 | keyboardActions: KeyboardActions = KeyboardActions.Default, 154 | singleLine: Boolean = false, 155 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 156 | minLines: Int = 1, 157 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 158 | shape: Shape = RoundedCornerShape(6.dp), 159 | colors: TextFieldColors = OutlinedTextFieldDefaults.colors() 160 | ) { 161 | OutlinedTextField( 162 | value, 163 | onValueChange, 164 | modifier, 165 | enabled, 166 | readOnly, 167 | textStyle, 168 | label, 169 | placeholder, 170 | leadingIcon, 171 | trailingIcon, 172 | prefix, 173 | suffix, 174 | supportingText, 175 | isError, 176 | visualTransformation, 177 | keyboardOptions, 178 | keyboardActions, 179 | singleLine, 180 | maxLines, 181 | minLines, 182 | interactionSource, 183 | shape, 184 | colors 185 | ) 186 | } 187 | 188 | @Composable 189 | fun LintOutlinedTextField( 190 | value: TextFieldValue, 191 | onValueChange: (TextFieldValue) -> Unit, 192 | modifier: Modifier = Modifier, 193 | enabled: Boolean = true, 194 | readOnly: Boolean = false, 195 | textStyle: TextStyle = LocalTextStyle.current, 196 | label: @Composable (() -> Unit)? = null, 197 | placeholder: @Composable (() -> Unit)? = null, 198 | leadingIcon: @Composable (() -> Unit)? = null, 199 | trailingIcon: @Composable (() -> Unit)? = null, 200 | prefix: @Composable (() -> Unit)? = null, 201 | suffix: @Composable (() -> Unit)? = null, 202 | supportingText: @Composable (() -> Unit)? = null, 203 | isError: Boolean = false, 204 | visualTransformation: VisualTransformation = VisualTransformation.None, 205 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default, 206 | keyboardActions: KeyboardActions = KeyboardActions.Default, 207 | singleLine: Boolean = false, 208 | maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, 209 | minLines: Int = 1, 210 | interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 211 | shape: Shape = RoundedCornerShape(6.dp), 212 | colors: TextFieldColors = OutlinedTextFieldDefaults.colors() 213 | ) { 214 | OutlinedTextField( 215 | value, 216 | onValueChange, 217 | modifier, 218 | enabled, 219 | readOnly, 220 | textStyle, 221 | label, 222 | placeholder, 223 | leadingIcon, 224 | trailingIcon, 225 | prefix, 226 | suffix, 227 | supportingText, 228 | isError, 229 | visualTransformation, 230 | keyboardOptions, 231 | keyboardActions, 232 | singleLine, 233 | maxLines, 234 | minLines, 235 | interactionSource, 236 | shape, 237 | colors 238 | ) 239 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/dialog/LintAlert.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.dialog 2 | 3 | import androidx.compose.animation.AnimatedContent 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.rememberScrollState 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.foundation.verticalScroll 9 | import androidx.compose.material3.* 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.LaunchedEffect 12 | import androidx.compose.runtime.MutableState 13 | import androidx.compose.runtime.mutableStateOf 14 | import androidx.compose.runtime.saveable.rememberSaveable 15 | import androidx.compose.ui.Alignment 16 | import androidx.compose.ui.Modifier 17 | import androidx.compose.ui.draw.clip 18 | import androidx.compose.ui.unit.dp 19 | import androidx.compose.ui.window.Dialog 20 | import androidx.compose.ui.window.DialogProperties 21 | import io.github.lumkit.desktop.ui.components.LintButton 22 | import io.github.lumkit.desktop.ui.components.LintHorizontalDivider 23 | 24 | @Composable 25 | fun LintAlert( 26 | visible: MutableState, 27 | isCancel: Boolean = true, 28 | title: String, 29 | cancelButtonText: String? = null, 30 | onCancel: (() -> Unit)? = null, 31 | confirmButtonText: String? = null, 32 | onConfirm: (() -> Unit)? = null, 33 | scrollable: Boolean = true, 34 | content: @Composable ColumnScope.() -> Unit 35 | ) { 36 | DialogBasic( 37 | visible, isCancel 38 | ) { 39 | AlertContent( 40 | title, scrollable, content, confirmButtonText, onConfirm, cancelButtonText, onCancel 41 | ) 42 | } 43 | } 44 | 45 | @Composable 46 | fun LintAlert( 47 | visible: Boolean, 48 | isCancel: Boolean = true, 49 | title: String, 50 | cancelButtonText: String? = null, 51 | onCancel: (() -> Unit)? = null, 52 | confirmButtonText: String? = null, 53 | onConfirm: (() -> Unit)? = null, 54 | scrollable: Boolean = true, 55 | content: @Composable ColumnScope.() -> Unit 56 | ) { 57 | val visibleState = rememberSaveable { mutableStateOf(visible) } 58 | 59 | LaunchedEffect(visible) { 60 | visibleState.value = visible 61 | } 62 | 63 | DialogBasic( 64 | visibleState, isCancel 65 | ) { 66 | AlertContent( 67 | title, 68 | scrollable, 69 | content, 70 | confirmButtonText, 71 | onConfirm, 72 | cancelButtonText, 73 | onCancel, 74 | ) 75 | } 76 | } 77 | 78 | @Composable 79 | private fun AlertContent( 80 | title: String, 81 | scrollable: Boolean, 82 | content: @Composable() (ColumnScope.() -> Unit), 83 | confirmButtonText: String?, 84 | onConfirm: (() -> Unit)?, 85 | cancelButtonText: String?, 86 | onCancel: (() -> Unit)? 87 | ) { 88 | Box( 89 | Modifier 90 | .fillMaxWidth() 91 | .clip(RoundedCornerShape(8.dp)) 92 | .background(MaterialTheme.colorScheme.background) 93 | ) { 94 | Column(Modifier.background(MaterialTheme.colorScheme.secondaryContainer.copy(alpha = .3f))) { 95 | Column( 96 | Modifier 97 | .background(MaterialTheme.colorScheme.background) 98 | .padding(horizontal = 28.dp) 99 | .fillMaxWidth() 100 | .weight(1f, fill = false) 101 | ) { 102 | Spacer(Modifier.height(28.dp)) 103 | Text( 104 | style = MaterialTheme.typography.titleLarge, 105 | text = title, 106 | ) 107 | Spacer(Modifier.height(12.dp)) 108 | Column( 109 | modifier = Modifier 110 | .fillMaxWidth() 111 | .then( 112 | if (scrollable) Modifier.verticalScroll(rememberScrollState()) 113 | else Modifier 114 | ) 115 | ) { 116 | ProvideTextStyle(MaterialTheme.typography.bodyMedium) { 117 | content() 118 | } 119 | } 120 | Spacer(Modifier.height(28.dp)) 121 | } 122 | // Button Grid 123 | if (!(onConfirm == null && onCancel == null)) { 124 | // Divider 125 | LintHorizontalDivider() 126 | Box( 127 | Modifier 128 | .height(80.dp) 129 | .padding(horizontal = 28.dp), 130 | contentAlignment = Alignment.Center 131 | ) { 132 | Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { 133 | if (onConfirm != null) { 134 | LintButton(modifier = Modifier.weight(1f), onClick = onConfirm) { 135 | Text(confirmButtonText ?: "") 136 | } 137 | } 138 | if (onCancel != null) { 139 | LintButton( 140 | modifier = Modifier.weight(1f), 141 | onClick = onCancel, 142 | colors = ButtonDefaults.filledTonalButtonColors() 143 | ) { 144 | Text(cancelButtonText ?: "") 145 | } 146 | } 147 | } 148 | } 149 | } 150 | } 151 | } 152 | } 153 | 154 | @Composable 155 | fun DialogBasic( 156 | visible: MutableState, 157 | isCancel: Boolean = true, 158 | content: @Composable () -> Unit 159 | ) { 160 | AnimatedContent( 161 | modifier = Modifier.fillMaxSize(), 162 | targetState = visible.value 163 | ) { 164 | if (it) { 165 | Dialog( 166 | onDismissRequest = { 167 | if (isCancel) { 168 | visible.value = false 169 | } 170 | }, 171 | properties = DialogProperties( 172 | dismissOnBackPress = isCancel, 173 | dismissOnClickOutside = isCancel, 174 | ) 175 | ) { 176 | Box( 177 | modifier = Modifier.padding(28.dp) 178 | ) { 179 | content() 180 | } 181 | } 182 | } 183 | } 184 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val primaryLight = Color(0xFF3F5F90) 6 | val onPrimaryLight = Color(0xFFFFFFFF) 7 | val primaryContainerLight = Color(0xFFD6E3FF) 8 | val onPrimaryContainerLight = Color(0xFF001B3C) 9 | val secondaryLight = Color(0xFF555F71) 10 | val onSecondaryLight = Color(0xFFFFFFFF) 11 | val secondaryContainerLight = Color(0xFFD9E3F8) 12 | val onSecondaryContainerLight = Color(0xFF121C2B) 13 | val tertiaryLight = Color(0xFF6F5675) 14 | val onTertiaryLight = Color(0xFFFFFFFF) 15 | val tertiaryContainerLight = Color(0xFFF9D8FE) 16 | val onTertiaryContainerLight = Color(0xFF28132F) 17 | val errorLight = Color(0xFFBA1A1A) 18 | val onErrorLight = Color(0xFFFFFFFF) 19 | val errorContainerLight = Color(0xFFFFDAD6) 20 | val onErrorContainerLight = Color(0xFF410002) 21 | val backgroundLight = Color(0xFFF9F9FF) 22 | val onBackgroundLight = Color(0xFF191C20) 23 | val surfaceLight = Color(0xFFF9F9FF) 24 | val onSurfaceLight = Color(0xFF191C20) 25 | val surfaceVariantLight = Color(0xFFE0E2EC) 26 | val onSurfaceVariantLight = Color(0xFF43474E) 27 | val outlineLight = Color(0xFF74777F) 28 | val outlineVariantLight = Color(0xFFC4C6CF) 29 | val scrimLight = Color(0xFF000000) 30 | val inverseSurfaceLight = Color(0xFF2E3035) 31 | val inverseOnSurfaceLight = Color(0xFFF0F0F7) 32 | val inversePrimaryLight = Color(0xFFA8C8FF) 33 | val surfaceDimLight = Color(0xFFD9D9E0) 34 | val surfaceBrightLight = Color(0xFFF9F9FF) 35 | val surfaceContainerLowestLight = Color(0xFFFFFFFF) 36 | val surfaceContainerLowLight = Color(0xFFF3F3FA) 37 | val surfaceContainerLight = Color(0xFFEDEDF4) 38 | val surfaceContainerHighLight = Color(0xFFE7E8EE) 39 | val surfaceContainerHighestLight = Color(0xFFE2E2E9) 40 | 41 | val primaryLightMediumContrast = Color(0xFF214373) 42 | val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) 43 | val primaryContainerLightMediumContrast = Color(0xFF5675A8) 44 | val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) 45 | val secondaryLightMediumContrast = Color(0xFF3A4354) 46 | val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) 47 | val secondaryContainerLightMediumContrast = Color(0xFF6B7588) 48 | val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) 49 | val tertiaryLightMediumContrast = Color(0xFF523A58) 50 | val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) 51 | val tertiaryContainerLightMediumContrast = Color(0xFF866B8C) 52 | val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) 53 | val errorLightMediumContrast = Color(0xFF8C0009) 54 | val onErrorLightMediumContrast = Color(0xFFFFFFFF) 55 | val errorContainerLightMediumContrast = Color(0xFFDA342E) 56 | val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) 57 | val backgroundLightMediumContrast = Color(0xFFF9F9FF) 58 | val onBackgroundLightMediumContrast = Color(0xFF191C20) 59 | val surfaceLightMediumContrast = Color(0xFFF9F9FF) 60 | val onSurfaceLightMediumContrast = Color(0xFF191C20) 61 | val surfaceVariantLightMediumContrast = Color(0xFFE0E2EC) 62 | val onSurfaceVariantLightMediumContrast = Color(0xFF3F434A) 63 | val outlineLightMediumContrast = Color(0xFF5C5F67) 64 | val outlineVariantLightMediumContrast = Color(0xFF787B83) 65 | val scrimLightMediumContrast = Color(0xFF000000) 66 | val inverseSurfaceLightMediumContrast = Color(0xFF2E3035) 67 | val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7) 68 | val inversePrimaryLightMediumContrast = Color(0xFFA8C8FF) 69 | val surfaceDimLightMediumContrast = Color(0xFFD9D9E0) 70 | val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF) 71 | val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) 72 | val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA) 73 | val surfaceContainerLightMediumContrast = Color(0xFFEDEDF4) 74 | val surfaceContainerHighLightMediumContrast = Color(0xFFE7E8EE) 75 | val surfaceContainerHighestLightMediumContrast = Color(0xFFE2E2E9) 76 | 77 | val primaryLightHighContrast = Color(0xFF002248) 78 | val onPrimaryLightHighContrast = Color(0xFFFFFFFF) 79 | val primaryContainerLightHighContrast = Color(0xFF214373) 80 | val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) 81 | val secondaryLightHighContrast = Color(0xFF192232) 82 | val onSecondaryLightHighContrast = Color(0xFFFFFFFF) 83 | val secondaryContainerLightHighContrast = Color(0xFF3A4354) 84 | val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) 85 | val tertiaryLightHighContrast = Color(0xFF2F1A36) 86 | val onTertiaryLightHighContrast = Color(0xFFFFFFFF) 87 | val tertiaryContainerLightHighContrast = Color(0xFF523A58) 88 | val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) 89 | val errorLightHighContrast = Color(0xFF4E0002) 90 | val onErrorLightHighContrast = Color(0xFFFFFFFF) 91 | val errorContainerLightHighContrast = Color(0xFF8C0009) 92 | val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) 93 | val backgroundLightHighContrast = Color(0xFFF9F9FF) 94 | val onBackgroundLightHighContrast = Color(0xFF191C20) 95 | val surfaceLightHighContrast = Color(0xFFF9F9FF) 96 | val onSurfaceLightHighContrast = Color(0xFF000000) 97 | val surfaceVariantLightHighContrast = Color(0xFFE0E2EC) 98 | val onSurfaceVariantLightHighContrast = Color(0xFF21242B) 99 | val outlineLightHighContrast = Color(0xFF3F434A) 100 | val outlineVariantLightHighContrast = Color(0xFF3F434A) 101 | val scrimLightHighContrast = Color(0xFF000000) 102 | val inverseSurfaceLightHighContrast = Color(0xFF2E3035) 103 | val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) 104 | val inversePrimaryLightHighContrast = Color(0xFFE4ECFF) 105 | val surfaceDimLightHighContrast = Color(0xFFD9D9E0) 106 | val surfaceBrightLightHighContrast = Color(0xFFF9F9FF) 107 | val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) 108 | val surfaceContainerLowLightHighContrast = Color(0xFFF3F3FA) 109 | val surfaceContainerLightHighContrast = Color(0xFFEDEDF4) 110 | val surfaceContainerHighLightHighContrast = Color(0xFFE7E8EE) 111 | val surfaceContainerHighestLightHighContrast = Color(0xFFE2E2E9) 112 | 113 | val primaryDark = Color(0xFFA8C8FF) 114 | val onPrimaryDark = Color(0xFF06305F) 115 | val primaryContainerDark = Color(0xFF254777) 116 | val onPrimaryContainerDark = Color(0xFFD6E3FF) 117 | val secondaryDark = Color(0xFFBDC7DC) 118 | val onSecondaryDark = Color(0xFF273141) 119 | val secondaryContainerDark = Color(0xFF3E4758) 120 | val onSecondaryContainerDark = Color(0xFFD9E3F8) 121 | val tertiaryDark = Color(0xFFDBBCE1) 122 | val onTertiaryDark = Color(0xFF3E2845) 123 | val tertiaryContainerDark = Color(0xFF563E5D) 124 | val onTertiaryContainerDark = Color(0xFFF9D8FE) 125 | val errorDark = Color(0xFFFFB4AB) 126 | val onErrorDark = Color(0xFF690005) 127 | val errorContainerDark = Color(0xFF93000A) 128 | val onErrorContainerDark = Color(0xFFFFDAD6) 129 | val backgroundDark = Color(0xFF111318) 130 | val onBackgroundDark = Color(0xFFE2E2E9) 131 | val surfaceDark = Color(0xFF111318) 132 | val onSurfaceDark = Color(0xFFE2E2E9) 133 | val surfaceVariantDark = Color(0xFF43474E) 134 | val onSurfaceVariantDark = Color(0xFFC4C6CF) 135 | val outlineDark = Color(0xFF8E9099) 136 | val outlineVariantDark = Color(0xFF43474E) 137 | val scrimDark = Color(0xFF000000) 138 | val inverseSurfaceDark = Color(0xFFE2E2E9) 139 | val inverseOnSurfaceDark = Color(0xFF2E3035) 140 | val inversePrimaryDark = Color(0xFF3F5F90) 141 | val surfaceDimDark = Color(0xFF111318) 142 | val surfaceBrightDark = Color(0xFF37393E) 143 | val surfaceContainerLowestDark = Color(0xFF0C0E13) 144 | val surfaceContainerLowDark = Color(0xFF191C20) 145 | val surfaceContainerDark = Color(0xFF1D2024) 146 | val surfaceContainerHighDark = Color(0xFF282A2F) 147 | val surfaceContainerHighestDark = Color(0xFF33353A) 148 | 149 | val primaryDarkMediumContrast = Color(0xFFAFCCFF) 150 | val onPrimaryDarkMediumContrast = Color(0xFF001633) 151 | val primaryContainerDarkMediumContrast = Color(0xFF7292C6) 152 | val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) 153 | val secondaryDarkMediumContrast = Color(0xFFC1CBE0) 154 | val onSecondaryDarkMediumContrast = Color(0xFF0D1626) 155 | val secondaryContainerDarkMediumContrast = Color(0xFF8791A5) 156 | val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) 157 | val tertiaryDarkMediumContrast = Color(0xFFE0C1E5) 158 | val onTertiaryDarkMediumContrast = Color(0xFF220E29) 159 | val tertiaryContainerDarkMediumContrast = Color(0xFFA487AA) 160 | val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) 161 | val errorDarkMediumContrast = Color(0xFFFFBAB1) 162 | val onErrorDarkMediumContrast = Color(0xFF370001) 163 | val errorContainerDarkMediumContrast = Color(0xFFFF5449) 164 | val onErrorContainerDarkMediumContrast = Color(0xFF000000) 165 | val backgroundDarkMediumContrast = Color(0xFF111318) 166 | val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9) 167 | val surfaceDarkMediumContrast = Color(0xFF111318) 168 | val onSurfaceDarkMediumContrast = Color(0xFFFBFAFF) 169 | val surfaceVariantDarkMediumContrast = Color(0xFF43474E) 170 | val onSurfaceVariantDarkMediumContrast = Color(0xFFC8CAD4) 171 | val outlineDarkMediumContrast = Color(0xFFA0A3AC) 172 | val outlineVariantDarkMediumContrast = Color(0xFF80838C) 173 | val scrimDarkMediumContrast = Color(0xFF000000) 174 | val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9) 175 | val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F) 176 | val inversePrimaryDarkMediumContrast = Color(0xFF274878) 177 | val surfaceDimDarkMediumContrast = Color(0xFF111318) 178 | val surfaceBrightDarkMediumContrast = Color(0xFF37393E) 179 | val surfaceContainerLowestDarkMediumContrast = Color(0xFF0C0E13) 180 | val surfaceContainerLowDarkMediumContrast = Color(0xFF191C20) 181 | val surfaceContainerDarkMediumContrast = Color(0xFF1D2024) 182 | val surfaceContainerHighDarkMediumContrast = Color(0xFF282A2F) 183 | val surfaceContainerHighestDarkMediumContrast = Color(0xFF33353A) 184 | 185 | val primaryDarkHighContrast = Color(0xFFFBFAFF) 186 | val onPrimaryDarkHighContrast = Color(0xFF000000) 187 | val primaryContainerDarkHighContrast = Color(0xFFAFCCFF) 188 | val onPrimaryContainerDarkHighContrast = Color(0xFF000000) 189 | val secondaryDarkHighContrast = Color(0xFFFBFAFF) 190 | val onSecondaryDarkHighContrast = Color(0xFF000000) 191 | val secondaryContainerDarkHighContrast = Color(0xFFC1CBE0) 192 | val onSecondaryContainerDarkHighContrast = Color(0xFF000000) 193 | val tertiaryDarkHighContrast = Color(0xFFFFF9FA) 194 | val onTertiaryDarkHighContrast = Color(0xFF000000) 195 | val tertiaryContainerDarkHighContrast = Color(0xFFE0C1E5) 196 | val onTertiaryContainerDarkHighContrast = Color(0xFF000000) 197 | val errorDarkHighContrast = Color(0xFFFFF9F9) 198 | val onErrorDarkHighContrast = Color(0xFF000000) 199 | val errorContainerDarkHighContrast = Color(0xFFFFBAB1) 200 | val onErrorContainerDarkHighContrast = Color(0xFF000000) 201 | val backgroundDarkHighContrast = Color(0xFF111318) 202 | val onBackgroundDarkHighContrast = Color(0xFFE2E2E9) 203 | val surfaceDarkHighContrast = Color(0xFF111318) 204 | val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) 205 | val surfaceVariantDarkHighContrast = Color(0xFF43474E) 206 | val onSurfaceVariantDarkHighContrast = Color(0xFFFBFAFF) 207 | val outlineDarkHighContrast = Color(0xFFC8CAD4) 208 | val outlineVariantDarkHighContrast = Color(0xFFC8CAD4) 209 | val scrimDarkHighContrast = Color(0xFF000000) 210 | val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9) 211 | val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) 212 | val inversePrimaryDarkHighContrast = Color(0xFF002A56) 213 | val surfaceDimDarkHighContrast = Color(0xFF111318) 214 | val surfaceBrightDarkHighContrast = Color(0xFF37393E) 215 | val surfaceContainerLowestDarkHighContrast = Color(0xFF0C0E13) 216 | val surfaceContainerLowDarkHighContrast = Color(0xFF191C20) 217 | val surfaceContainerDarkHighContrast = Color(0xFF1D2024) 218 | val surfaceContainerHighDarkHighContrast = Color(0xFF282A2F) 219 | val surfaceContainerHighestDarkHighContrast = Color(0xFF33353A) 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/theme/Design.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.theme 2 | 3 | -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/theme/LintTheme.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.theme 2 | 3 | import androidx.compose.material3.ColorScheme 4 | import androidx.compose.material3.darkColorScheme 5 | import androidx.compose.material3.lightColorScheme 6 | import androidx.compose.runtime.compositionLocalOf 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.mutableStateOf 9 | import androidx.compose.runtime.setValue 10 | import io.github.lumkit.desktop.Const 11 | import io.github.lumkit.desktop.annotates.MODE_FILES 12 | import io.github.lumkit.desktop.common.toColor 13 | import io.github.lumkit.desktop.context.Context 14 | import io.github.lumkit.desktop.data.DarkThemeMode 15 | import io.github.lumkit.desktop.data.SchemesBasic 16 | import io.github.lumkit.desktop.data.ThemeBean 17 | import io.github.lumkit.desktop.preferences.SharedPreferences 18 | import io.github.lumkit.desktop.util.json 19 | import kotlinx.coroutines.Dispatchers 20 | import kotlinx.coroutines.withContext 21 | import java.io.* 22 | 23 | val LocalLintTheme = compositionLocalOf { error("Not provided.") } 24 | val LocalThemeStore = compositionLocalOf { error("Not provided.") } 25 | val DefaultLintTheme = LintTheme.LintThemeColorSchemes( 26 | label = "Default", 27 | light = lightScheme, 28 | dark = darkScheme, 29 | ) 30 | 31 | /** 32 | * Global theme color status container. 33 | */ 34 | class ThemeStore { 35 | var darkTheme by mutableStateOf(DarkThemeMode.SYSTEM) 36 | var colorSchemes by mutableStateOf( 37 | DefaultLintTheme 38 | ) 39 | var isSystemDarkTheme by mutableStateOf(false) 40 | var isDarkTheme by mutableStateOf(false) 41 | } 42 | 43 | class LintTheme internal constructor( 44 | private val context: Context, 45 | private val sharedPreferences: SharedPreferences, 46 | ) { 47 | private val themesDir by lazy { 48 | File(context.getDir(MODE_FILES), Const.THEME_INSTALL_DIRECTORY).apply { 49 | if (!exists()) mkdirs() 50 | } 51 | } 52 | 53 | /** 54 | * Load theme profiles file. 55 | * Invalid theme profiles will not be displayed. 56 | */ 57 | fun loadThemesList(): List { 58 | val themeBeans = ArrayList() 59 | val listFiles = themesDir.listFiles() 60 | listFiles?.sortedBy { it.lastModified() }?.forEach { file -> 61 | themeBeans.add( 62 | FileInputStream(file).use { 63 | BufferedInputStream(it).use { buffered -> 64 | json.decodeFromString(String(buffered.readBytes(), Charsets.UTF_8)) 65 | } 66 | }.copy(label = file.name) 67 | ) 68 | } 69 | return themeBeans 70 | } 71 | 72 | /** 73 | * Install the custom theme profile on the hard disk. 74 | * Users can export the "material-theme.json" file from 75 | * the "Material Theme Builder" website (https://m3.material.io/theme-builder). 76 | * 77 | * @param themeFile Theme profile file. 78 | * 79 | * @throws RuntimeException When a theme with the same name as the custom theme file 80 | * already exists in the installation directory, a [RuntimeException] exception will be thrown. 81 | */ 82 | suspend fun installTheme(themeFile: File): Unit = withContext(Dispatchers.IO) { 83 | val themeFileName = themeFile.name 84 | val installFile = File(themesDir, themeFileName) 85 | if (installFile.exists()) throw RuntimeException("The theme profile with the name \"$themeFileName\" already exists in the theme installation directory.") 86 | FileInputStream(themeFile).use { 87 | BufferedInputStream(it).use { bufferedInputStream -> 88 | FileOutputStream(installFile).use { out -> 89 | BufferedOutputStream(out).use { bufferedOutputStream -> 90 | bufferedInputStream.copyTo(bufferedOutputStream) 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | /** 98 | * Tells the application that the [themeBean] has been used (but will not be loaded into memory). 99 | */ 100 | suspend fun useTheme(themeBean: ThemeBean, onCreate: (LintThemeColorSchemes) -> Unit): Unit = 101 | withContext(Dispatchers.IO) { 102 | sharedPreferences.putString(Const.USED_THEME_FILE_NAME, themeBean.label) 103 | onCreate( 104 | LintThemeColorSchemes( 105 | label = themeBean.label, 106 | light = this@LintTheme lightColorScheme themeBean.schemes.light, 107 | dark = this@LintTheme lightColorScheme themeBean.schemes.dark, 108 | ) 109 | ) 110 | } 111 | 112 | /** 113 | * Load the theme that has been used (provided that it has been installed). 114 | */ 115 | fun getUsedTheme(): LintThemeColorSchemes { 116 | val themeName = sharedPreferences.getString(Const.USED_THEME_FILE_NAME) 117 | val theme = read(themeName ?: "") 118 | val schemes = theme.schemes 119 | return LintThemeColorSchemes( 120 | label = theme.label, 121 | light = this lightColorScheme schemes.light, 122 | dark = this darkColorScheme schemes.dark, 123 | ) 124 | } 125 | 126 | fun read(themeName: String): ThemeBean = 127 | json.decodeFromString(File(themesDir, themeName).readText()).copy(label = themeName) 128 | 129 | data class LintThemeColorSchemes( 130 | val label: String? = null, 131 | val light: ColorScheme, 132 | val dark: ColorScheme, 133 | ) 134 | 135 | fun uninstallThemeByName(name: String, onFailed: () -> Unit = {}, onSuccess: () -> Unit) { 136 | val usedThemeName = sharedPreferences.getString(Const.USED_THEME_FILE_NAME) 137 | if (name == usedThemeName) 138 | throw RuntimeException("The current theme is in use and cannot be uninstalled!") 139 | val file = File(themesDir, name) 140 | if (file.delete()) { 141 | onSuccess() 142 | } else { 143 | onFailed() 144 | } 145 | } 146 | 147 | infix fun lightColorScheme(scheme: SchemesBasic): ColorScheme = lightColorScheme( 148 | primary = scheme.primary.toColor(), 149 | onPrimary = scheme.onPrimary.toColor(), 150 | primaryContainer = scheme.primaryContainer.toColor(), 151 | onPrimaryContainer = scheme.onPrimaryContainer.toColor(), 152 | inversePrimary = scheme.inversePrimary.toColor(), 153 | secondary = scheme.secondary.toColor(), 154 | onSecondary = scheme.onSecondary.toColor(), 155 | secondaryContainer = scheme.secondaryContainer.toColor(), 156 | onSecondaryContainer = scheme.onSecondaryContainer.toColor(), 157 | tertiary = scheme.tertiary.toColor(), 158 | onTertiary = scheme.onTertiary.toColor(), 159 | tertiaryContainer = scheme.tertiaryContainer.toColor(), 160 | onTertiaryContainer = scheme.onTertiaryContainer.toColor(), 161 | background = scheme.background.toColor(), 162 | onBackground = scheme.onBackground.toColor(), 163 | surface = scheme.surface.toColor(), 164 | onSurface = scheme.onSurface.toColor(), 165 | surfaceVariant = scheme.surfaceVariant.toColor(), 166 | onSurfaceVariant = scheme.onSurfaceVariant.toColor(), 167 | surfaceTint = scheme.surfaceTint.toColor(), 168 | inverseSurface = scheme.inverseSurface.toColor(), 169 | inverseOnSurface = scheme.inverseOnSurface.toColor(), 170 | error = scheme.error.toColor(), 171 | onError = scheme.onError.toColor(), 172 | errorContainer = scheme.errorContainer.toColor(), 173 | onErrorContainer = scheme.onErrorContainer.toColor(), 174 | outline = scheme.outline.toColor(), 175 | outlineVariant = scheme.outlineVariant.toColor(), 176 | scrim = scheme.scrim.toColor(), 177 | surfaceBright = scheme.surfaceBright.toColor(), 178 | surfaceContainer = scheme.surfaceContainer.toColor(), 179 | surfaceContainerHigh = scheme.surfaceContainerHigh.toColor(), 180 | surfaceContainerHighest = scheme.surfaceContainerHighest.toColor(), 181 | surfaceContainerLow = scheme.surfaceContainerLow.toColor(), 182 | surfaceContainerLowest = scheme.surfaceContainerLowest.toColor(), 183 | surfaceDim = scheme.surfaceDim.toColor(), 184 | ) 185 | 186 | infix fun darkColorScheme(scheme: SchemesBasic): ColorScheme = darkColorScheme( 187 | primary = scheme.primary.toColor(), 188 | onPrimary = scheme.onPrimary.toColor(), 189 | primaryContainer = scheme.primaryContainer.toColor(), 190 | onPrimaryContainer = scheme.onPrimaryContainer.toColor(), 191 | inversePrimary = scheme.inversePrimary.toColor(), 192 | secondary = scheme.secondary.toColor(), 193 | onSecondary = scheme.onSecondary.toColor(), 194 | secondaryContainer = scheme.secondaryContainer.toColor(), 195 | onSecondaryContainer = scheme.onSecondaryContainer.toColor(), 196 | tertiary = scheme.tertiary.toColor(), 197 | onTertiary = scheme.onTertiary.toColor(), 198 | tertiaryContainer = scheme.tertiaryContainer.toColor(), 199 | onTertiaryContainer = scheme.onTertiaryContainer.toColor(), 200 | background = scheme.background.toColor(), 201 | onBackground = scheme.onBackground.toColor(), 202 | surface = scheme.surface.toColor(), 203 | onSurface = scheme.onSurface.toColor(), 204 | surfaceVariant = scheme.surfaceVariant.toColor(), 205 | onSurfaceVariant = scheme.onSurfaceVariant.toColor(), 206 | surfaceTint = scheme.surfaceTint.toColor(), 207 | inverseSurface = scheme.inverseSurface.toColor(), 208 | inverseOnSurface = scheme.inverseOnSurface.toColor(), 209 | error = scheme.error.toColor(), 210 | onError = scheme.onError.toColor(), 211 | errorContainer = scheme.errorContainer.toColor(), 212 | onErrorContainer = scheme.onErrorContainer.toColor(), 213 | outline = scheme.outline.toColor(), 214 | outlineVariant = scheme.outlineVariant.toColor(), 215 | scrim = scheme.scrim.toColor(), 216 | surfaceBright = scheme.surfaceBright.toColor(), 217 | surfaceContainer = scheme.surfaceContainer.toColor(), 218 | surfaceContainerHigh = scheme.surfaceContainerHigh.toColor(), 219 | surfaceContainerHighest = scheme.surfaceContainerHighest.toColor(), 220 | surfaceContainerLow = scheme.surfaceContainerLow.toColor(), 221 | surfaceContainerLowest = scheme.surfaceContainerLowest.toColor(), 222 | surfaceDim = scheme.surfaceDim.toColor(), 223 | ) 224 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.ui.theme 2 | 3 | import androidx.compose.animation.animateColorAsState 4 | import androidx.compose.animation.core.LinearEasing 5 | import androidx.compose.animation.core.tween 6 | import androidx.compose.material3.MaterialTheme 7 | import androidx.compose.material3.Surface 8 | import androidx.compose.material3.darkColorScheme 9 | import androidx.compose.material3.lightColorScheme 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.getValue 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.graphics.toArgb 15 | import com.formdev.flatlaf.FlatLaf 16 | import com.formdev.flatlaf.themes.FlatMacDarkLaf 17 | import com.formdev.flatlaf.themes.FlatMacLightLaf 18 | import io.github.lumkit.desktop.data.DarkThemeMode 19 | import javax.swing.UIManager 20 | 21 | internal val lightScheme = lightColorScheme( 22 | primary = primaryLight, 23 | onPrimary = onPrimaryLight, 24 | primaryContainer = primaryContainerLight, 25 | onPrimaryContainer = onPrimaryContainerLight, 26 | secondary = secondaryLight, 27 | onSecondary = onSecondaryLight, 28 | secondaryContainer = secondaryContainerLight, 29 | onSecondaryContainer = onSecondaryContainerLight, 30 | tertiary = tertiaryLight, 31 | onTertiary = onTertiaryLight, 32 | tertiaryContainer = tertiaryContainerLight, 33 | onTertiaryContainer = onTertiaryContainerLight, 34 | error = errorLight, 35 | onError = onErrorLight, 36 | errorContainer = errorContainerLight, 37 | onErrorContainer = onErrorContainerLight, 38 | background = backgroundLight, 39 | onBackground = onBackgroundLight, 40 | surface = surfaceLight, 41 | onSurface = onSurfaceLight, 42 | surfaceVariant = surfaceVariantLight, 43 | onSurfaceVariant = onSurfaceVariantLight, 44 | outline = outlineLight, 45 | outlineVariant = outlineVariantLight, 46 | scrim = scrimLight, 47 | inverseSurface = inverseSurfaceLight, 48 | inverseOnSurface = inverseOnSurfaceLight, 49 | inversePrimary = inversePrimaryLight, 50 | surfaceDim = surfaceDimLight, 51 | surfaceBright = surfaceBrightLight, 52 | surfaceContainerLowest = surfaceContainerLowestLight, 53 | surfaceContainerLow = surfaceContainerLowLight, 54 | surfaceContainer = surfaceContainerLight, 55 | surfaceContainerHigh = surfaceContainerHighLight, 56 | surfaceContainerHighest = surfaceContainerHighestLight, 57 | ) 58 | 59 | internal val darkScheme = darkColorScheme( 60 | primary = primaryDark, 61 | onPrimary = onPrimaryDark, 62 | primaryContainer = primaryContainerDark, 63 | onPrimaryContainer = onPrimaryContainerDark, 64 | secondary = secondaryDark, 65 | onSecondary = onSecondaryDark, 66 | secondaryContainer = secondaryContainerDark, 67 | onSecondaryContainer = onSecondaryContainerDark, 68 | tertiary = tertiaryDark, 69 | onTertiary = onTertiaryDark, 70 | tertiaryContainer = tertiaryContainerDark, 71 | onTertiaryContainer = onTertiaryContainerDark, 72 | error = errorDark, 73 | onError = onErrorDark, 74 | errorContainer = errorContainerDark, 75 | onErrorContainer = onErrorContainerDark, 76 | background = backgroundDark, 77 | onBackground = onBackgroundDark, 78 | surface = surfaceDark, 79 | onSurface = onSurfaceDark, 80 | surfaceVariant = surfaceVariantDark, 81 | onSurfaceVariant = onSurfaceVariantDark, 82 | outline = outlineDark, 83 | outlineVariant = outlineVariantDark, 84 | scrim = scrimDark, 85 | inverseSurface = inverseSurfaceDark, 86 | inverseOnSurface = inverseOnSurfaceDark, 87 | inversePrimary = inversePrimaryDark, 88 | surfaceDim = surfaceDimDark, 89 | surfaceBright = surfaceBrightDark, 90 | surfaceContainerLowest = surfaceContainerLowestDark, 91 | surfaceContainerLow = surfaceContainerLowDark, 92 | surfaceContainer = surfaceContainerDark, 93 | surfaceContainerHigh = surfaceContainerHighDark, 94 | surfaceContainerHighest = surfaceContainerHighestDark, 95 | ) 96 | 97 | @Composable 98 | fun LintTheme( 99 | content: @Composable () -> Unit 100 | ) { 101 | val themeStore = LocalThemeStore.current 102 | 103 | val colorScheme = when (themeStore.darkTheme) { 104 | DarkThemeMode.SYSTEM -> { 105 | if (themeStore.isSystemDarkTheme) { 106 | themeStore.isDarkTheme = true 107 | themeStore.colorSchemes.dark 108 | } else { 109 | themeStore.isDarkTheme = false 110 | themeStore.colorSchemes.light 111 | } 112 | } 113 | 114 | DarkThemeMode.LIGHT -> { 115 | themeStore.isDarkTheme = false 116 | themeStore.colorSchemes.light 117 | } 118 | 119 | DarkThemeMode.DARK -> { 120 | themeStore.isDarkTheme = true 121 | themeStore.colorSchemes.dark 122 | } 123 | } 124 | 125 | // update window theme 126 | try { 127 | val primary = colorScheme.primary 128 | FlatLaf.setSystemColorGetter { name: String -> 129 | if (name == "accent") 130 | java.awt.Color(primary.toArgb()) 131 | else null 132 | } 133 | if (themeStore.isDarkTheme) { 134 | FlatMacDarkLaf.setup() 135 | FlatMacDarkLaf.updateUILater() 136 | } else { 137 | FlatMacLightLaf.setup() 138 | FlatMacLightLaf.updateUILater() 139 | } 140 | } catch (_: Exception) {} 141 | 142 | val javaColor = UIManager.getColor("Panel.background") 143 | 144 | MaterialTheme( 145 | colorScheme = colorScheme.copy( 146 | background = Color(javaColor.red, javaColor.green, javaColor.blue, javaColor.alpha), 147 | ), 148 | content = content 149 | ) 150 | } 151 | 152 | @Composable 153 | fun AnimatedLintTheme( 154 | modifier: Modifier, 155 | content: @Composable () -> Unit 156 | ) { 157 | LintTheme { 158 | val background by animateColorAsState( 159 | MaterialTheme.colorScheme.background, 160 | animationSpec = tween(easing = LinearEasing) 161 | ) 162 | Surface( 163 | modifier = modifier, 164 | color = background, 165 | content = content 166 | ) 167 | } 168 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/kotlin/io/github/lumkit/desktop/util/Jsons.kt: -------------------------------------------------------------------------------- 1 | package io.github.lumkit.desktop.util 2 | 3 | import kotlinx.serialization.json.Json 4 | 5 | val json by lazy { 6 | Json{ 7 | ignoreUnknownKeys = true 8 | } 9 | } -------------------------------------------------------------------------------- /lint-compose-ui/src/desktopMain/resources/default-theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "seed": "#1A84F5", 3 | "schemes": { 4 | "light": { 5 | "primary": "#3F5F90", 6 | "surfaceTint": "#3F5F90", 7 | "onPrimary": "#FFFFFF", 8 | "primaryContainer": "#D6E3FF", 9 | "onPrimaryContainer": "#001B3C", 10 | "secondary": "#555F71", 11 | "onSecondary": "#FFFFFF", 12 | "secondaryContainer": "#D9E3F8", 13 | "onSecondaryContainer": "#121C2B", 14 | "tertiary": "#6F5675", 15 | "onTertiary": "#FFFFFF", 16 | "tertiaryContainer": "#F9D8FE", 17 | "onTertiaryContainer": "#28132F", 18 | "error": "#BA1A1A", 19 | "onError": "#FFFFFF", 20 | "errorContainer": "#FFDAD6", 21 | "onErrorContainer": "#410002", 22 | "background": "#F9F9FF", 23 | "onBackground": "#191C20", 24 | "surface": "#F9F9FF", 25 | "onSurface": "#191C20", 26 | "surfaceVariant": "#E0E2EC", 27 | "onSurfaceVariant": "#43474E", 28 | "outline": "#74777F", 29 | "outlineVariant": "#C4C6CF", 30 | "shadow": "#000000", 31 | "scrim": "#000000", 32 | "inverseSurface": "#2E3035", 33 | "inverseOnSurface": "#F0F0F7", 34 | "inversePrimary": "#A8C8FF", 35 | "primaryFixed": "#D6E3FF", 36 | "onPrimaryFixed": "#001B3C", 37 | "primaryFixedDim": "#A8C8FF", 38 | "onPrimaryFixedVariant": "#254777", 39 | "secondaryFixed": "#D9E3F8", 40 | "onSecondaryFixed": "#121C2B", 41 | "secondaryFixedDim": "#BDC7DC", 42 | "onSecondaryFixedVariant": "#3E4758", 43 | "tertiaryFixed": "#F9D8FE", 44 | "onTertiaryFixed": "#28132F", 45 | "tertiaryFixedDim": "#DBBCE1", 46 | "onTertiaryFixedVariant": "#563E5D", 47 | "surfaceDim": "#D9D9E0", 48 | "surfaceBright": "#F9F9FF", 49 | "surfaceContainerLowest": "#FFFFFF", 50 | "surfaceContainerLow": "#F3F3FA", 51 | "surfaceContainer": "#EDEDF4", 52 | "surfaceContainerHigh": "#E7E8EE", 53 | "surfaceContainerHighest": "#E2E2E9" 54 | }, 55 | "dark": { 56 | "primary": "#A8C8FF", 57 | "surfaceTint": "#A8C8FF", 58 | "onPrimary": "#06305F", 59 | "primaryContainer": "#254777", 60 | "onPrimaryContainer": "#D6E3FF", 61 | "secondary": "#BDC7DC", 62 | "onSecondary": "#273141", 63 | "secondaryContainer": "#3E4758", 64 | "onSecondaryContainer": "#D9E3F8", 65 | "tertiary": "#DBBCE1", 66 | "onTertiary": "#3E2845", 67 | "tertiaryContainer": "#563E5D", 68 | "onTertiaryContainer": "#F9D8FE", 69 | "error": "#FFB4AB", 70 | "onError": "#690005", 71 | "errorContainer": "#93000A", 72 | "onErrorContainer": "#FFDAD6", 73 | "background": "#111318", 74 | "onBackground": "#E2E2E9", 75 | "surface": "#111318", 76 | "onSurface": "#E2E2E9", 77 | "surfaceVariant": "#43474E", 78 | "onSurfaceVariant": "#C4C6CF", 79 | "outline": "#8E9099", 80 | "outlineVariant": "#43474E", 81 | "shadow": "#000000", 82 | "scrim": "#000000", 83 | "inverseSurface": "#E2E2E9", 84 | "inverseOnSurface": "#2E3035", 85 | "inversePrimary": "#3F5F90", 86 | "primaryFixed": "#D6E3FF", 87 | "onPrimaryFixed": "#001B3C", 88 | "primaryFixedDim": "#A8C8FF", 89 | "onPrimaryFixedVariant": "#254777", 90 | "secondaryFixed": "#D9E3F8", 91 | "onSecondaryFixed": "#121C2B", 92 | "secondaryFixedDim": "#BDC7DC", 93 | "onSecondaryFixedVariant": "#3E4758", 94 | "tertiaryFixed": "#F9D8FE", 95 | "onTertiaryFixed": "#28132F", 96 | "tertiaryFixedDim": "#DBBCE1", 97 | "onTertiaryFixedVariant": "#563E5D", 98 | "surfaceDim": "#111318", 99 | "surfaceBright": "#37393E", 100 | "surfaceContainerLowest": "#0C0E13", 101 | "surfaceContainerLow": "#191C20", 102 | "surfaceContainer": "#1D2024", 103 | "surfaceContainerHigh": "#282A2F", 104 | "surfaceContainerHighest": "#33353A" 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") 2 | 3 | pluginManagement { 4 | repositories { 5 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 6 | google() 7 | gradlePluginPortal() 8 | mavenCentral() 9 | maven(url = "https://jitpack.io") 10 | maven(url = "https://maven.aliyun.com/repository/public/") 11 | maven(url = "https://maven.aliyun.com/repository/google/") 12 | maven { 13 | url = uri("https://plugins.gradle.org/m2/") 14 | } 15 | } 16 | } 17 | plugins { 18 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0" 19 | } 20 | 21 | dependencyResolutionManagement { 22 | repositories { 23 | google() 24 | mavenCentral() 25 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 26 | maven(url = "https://jitpack.io") 27 | maven(url = "https://maven.aliyun.com/repository/public/") 28 | maven(url = "https://maven.aliyun.com/repository/google/") 29 | maven { 30 | url = uri("https://plugins.gradle.org/m2/") 31 | } 32 | } 33 | } 34 | rootProject.name = "lint-ui" 35 | //includeBuild("build-plugin") 36 | include(":example") 37 | include(":lint-compose-ui") 38 | -------------------------------------------------------------------------------- /static/img/screen-shoot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumkit/lint-ui/1759cae755d6e22d1bd77c0853f90045c77abca6/static/img/screen-shoot.png --------------------------------------------------------------------------------