diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index fe63bb6..6d0ee1c 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/.idea/runConfigurations/iosApp.xml b/.idea/runConfigurations/iosApp.xml new file mode 100644 index 0000000..296ddc1 --- /dev/null +++ b/.idea/runConfigurations/iosApp.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 72e814c..067e602 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,20 @@ # 🌳 JsonTree -![](https://img.shields.io/maven-central/v/com.sebastianneubauer.jsontree/jsontree) ![](https://img.shields.io/badge/Kotlin-1.9.23-orange) ![](https://img.shields.io/badge/SDK-21+-brightgreen) ![](https://img.shields.io/badge/Android_Weekly-Issue_584-yellow) +![](https://img.shields.io/maven-central/v/com.sebastianneubauer.jsontree/jsontree) ![](https://img.shields.io/badge/Kotlin-2.0.0-orange) ![](https://img.shields.io/badge/SDK-21+-brightgreen) ![](https://img.shields.io/badge/Android_Weekly-Issue_584-yellow)

-JsonTree is an Android library to display JSON data in Compose with syntax highlighting and more. +JsonTree is a Compose Multiplatform library to display JSON data in Compose with syntax highlighting and more.

- +

+ + ## About -JsonTree is an Android library to display formatted JSON data in Compose. +JsonTree is an Compose Multiplatform library to display formatted JSON data in Compose. Users can expand/collapse objects and arrays, which can also display additional info like item counts and item indices for arrays. JsonTree offers several customizations for visual appearance like syntax highlighting, text style and icons. @@ -20,9 +22,10 @@ JsonTree offers several customizations for visual appearance like syntax highlig See `maven-central` tag at the top for the latest version. -```groovy +```kotlin dependencies { - implementation 'com.sebastianneubauer.jsontree:jsontree:latest-version' + // add this to commonMain in a multiplatform project + implementation("com.sebastianneubauer.jsontree:jsontree:latest-version") } ``` @@ -65,7 +68,7 @@ JsonTree( // The size of the arrow icon iconSize = 20.dp, // The TextStyle to use for the json tree - textStyle = TextStyle(...), + textStyle = TextStyle(/*...*/), // If true, then array items will show their index. showIndices = false, // If true, then arrays and objects will show the amount of child items when collapsed. @@ -74,7 +77,7 @@ JsonTree( // until there are none or multiple on a level. expandSingleChildren = false, // A callback method which is called when the provided json data can't be parsed. - onError = { throwable -> /* Do something */ } + onError = { throwable: Throwable -> /* Do something */ } ) ``` diff --git a/app/.gitignore b/app/.gitignore deleted file mode 100644 index 42afabf..0000000 --- a/app/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 8c1fb25..0000000 --- a/app/build.gradle +++ /dev/null @@ -1,69 +0,0 @@ -plugins { - alias libs.plugins.android.application - alias libs.plugins.kotlin.android - alias(libs.plugins.compose.compiler) -} - -android { - namespace="com.sebastianneubauer.jsontreedemo" - compileSdk libs.versions.android.target.get().toInteger() - - defaultConfig { - applicationId "com.sebastianneubauer.jsontreedemo" - minSdk libs.versions.android.min.get().toInteger() - targetSdk libs.versions.android.target.get().toInteger() - versionCode 1 - versionName "1.0" - - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - vectorDrawables { - useSupportLibrary true - } - } - - signingConfigs { - debug {} - } - - buildTypes { - release { - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - //for testing only, don't do this in your app - signingConfig signingConfigs.debug - } - debug { - applicationIdSuffix ".debug" - signingConfig signingConfigs.debug - } - } - buildFeatures { - compose true - } - packagingOptions { - resources { - excludes += '/META-INF/{AL2.0,LGPL2.1}' - } - } -} - -composeCompiler { - enableStrongSkippingMode = true -} - -kotlin { - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get())) - } -} - -dependencies { - implementation platform(libs.androidx.compose.bom) - implementation libs.androidx.activity.compose - implementation libs.androidx.compose.ui - implementation libs.androidx.compose.material - implementation libs.androidx.compose.ui.tooling - implementation libs.androidx.compose.ui.tooling.preview - - implementation project(":jsontree") -} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro deleted file mode 100644 index 481bb43..0000000 --- a/app/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt b/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt deleted file mode 100644 index 42a5e8f..0000000 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/MainActivity.kt +++ /dev/null @@ -1,208 +0,0 @@ -package com.sebastianneubauer.jsontreedemo - -import android.os.Bundle -import androidx.activity.ComponentActivity -import androidx.activity.compose.setContent -import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.background -import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.ExperimentalLayoutApi -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.pager.HorizontalPager -import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.foundation.rememberScrollState -import androidx.compose.material3.Button -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.dp -import com.sebastianneubauer.jsontree.TreeColors -import com.sebastianneubauer.jsontree.JsonTree -import com.sebastianneubauer.jsontree.TreeState -import com.sebastianneubauer.jsontree.defaultDarkColors -import com.sebastianneubauer.jsontree.defaultLightColors -import com.sebastianneubauer.jsontreedemo.ui.theme.JsonTreeTheme -import java.lang.IllegalStateException - -internal class MainActivity : ComponentActivity() { - - @OptIn(ExperimentalFoundationApi::class, ExperimentalLayoutApi::class) - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContent { - JsonTreeTheme { - Column( - modifier = Modifier.fillMaxSize() - ) { - var errorMessage: String? by remember { mutableStateOf(null) } - var json: String by remember { mutableStateOf(simpleJson) } - var colors: TreeColors by remember { mutableStateOf(defaultLightColors) } - var initialState: TreeState by remember { mutableStateOf(TreeState.FIRST_ITEM_EXPANDED) } - var showIndices: Boolean by remember { mutableStateOf(true) } - var showItemCount: Boolean by remember { mutableStateOf(true) } - var expandSingleChildren: Boolean by remember { mutableStateOf(true) } - - Spacer(modifier = Modifier.height(16.dp)) - - Text( - modifier = Modifier.padding(horizontal = 16.dp), - text = "🌳 JsonTree", - style = MaterialTheme.typography.headlineMedium, - color = Color.Black - ) - - Spacer(modifier = Modifier.height(48.dp)) - - FlowRow { - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { - errorMessage = null - json = when (json) { - emptyJson -> simpleJson - simpleJson -> complexJson - complexJson -> invalidJson - invalidJson -> emptyJson - else -> throw IllegalStateException("No JSON selected!") - } - } - ) { - Text( - text = when (json) { - simpleJson -> "Simple Json" - emptyJson -> "Empty Json" - complexJson -> "Complex Json" - invalidJson -> "Invalid Json" - else -> throw IllegalStateException("No JSON selected!") - } - ) - } - - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { - val newState = when(initialState) { - TreeState.EXPANDED -> TreeState.COLLAPSED - TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED - TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED - } - initialState = newState - } - ) { - Text(text = initialState.name) - } - - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { showIndices = !showIndices } - ) { - Text(text = if(showIndices) "Hide indices" else "Show indices") - } - - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { showItemCount = !showItemCount } - ) { - Text(text = if(showItemCount) "Hide item count" else "Show item count") - } - - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { - colors = if(colors == defaultLightColors) defaultDarkColors else defaultLightColors - } - ) { - Text(text = if(colors == defaultLightColors) "Light" else "Dark") - } - - Button( - modifier = Modifier.padding(horizontal = 8.dp), - onClick = { expandSingleChildren = !expandSingleChildren } - ) { - Text(text = if(expandSingleChildren) "Expand children" else "Don't expand children") - } - } - - val pagerState = rememberPagerState( - initialPage = 0, - pageCount = { 3 } - ) - - //Pager to test leaving composition - HorizontalPager( - modifier = Modifier - .fillMaxWidth() - .weight(1F), - state = pagerState, - verticalAlignment = Alignment.Top - ) { pageIndex -> - when (pageIndex) { - 0 -> { - val error = errorMessage - if(error!= null) { - Text( - text = error, - color = Color.Black - ) - } else { - JsonTree( - modifier = Modifier - .fillMaxSize() - .horizontalScroll(rememberScrollState()) - .background( - if (colors == defaultLightColors) Color.White else Color.Black - ), - contentPadding = PaddingValues(16.dp), - json = json, - onLoading = { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.Center - ) { - Text( - text = "Loading...", - color = if (colors == defaultLightColors) Color.Black else Color.White - ) - } - }, - initialState = initialState, - colors = colors, - showIndices = showIndices, - showItemCount = showItemCount, - expandSingleChildren = expandSingleChildren, - onError = { errorMessage = it.localizedMessage }, - ) - } - } - 1 -> { - Text(text = "Page 1") - } - 2 -> { - Text(text = "Page 2") - } - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp deleted file mode 100644 index c209e78..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp deleted file mode 100644 index b2dfe3d..0000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp deleted file mode 100644 index 4f0f1d6..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp deleted file mode 100644 index 62b611d..0000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp deleted file mode 100644 index 948a307..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp deleted file mode 100644 index 1b9a695..0000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp deleted file mode 100644 index 28d4b77..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9287f50..0000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp deleted file mode 100644 index aa7d642..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp deleted file mode 100644 index 9126ae3..0000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp and /dev/null differ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f8c6127..0000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index c2aab59..0000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,3 +0,0 @@ - - JsonTree - \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml deleted file mode 100644 index 02a6fd1..0000000 --- a/app/src/main/res/values/themes.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - \ No newline at end of file diff --git a/build.gradle b/build.gradle deleted file mode 100644 index b9aa328..0000000 --- a/build.gradle +++ /dev/null @@ -1,63 +0,0 @@ -buildscript { - repositories { - mavenCentral() - } -} - -plugins { - alias libs.plugins.android.application apply false - alias libs.plugins.android.library apply false - alias libs.plugins.kotlin.android apply false - alias(libs.plugins.compose.compiler) apply false - alias libs.plugins.publish apply false - alias libs.plugins.api.validator - alias libs.plugins.detekt -} - -apiValidation { - ignoredProjects += ["app"] -} - -def projectSource = file(projectDir) -def configFile = files("$rootDir/detekt/config.yml") -def baselineFile = file("$rootDir/detekt/baseline.xml") -def kotlinFiles = "**/*.kt" -def appModuleFiles = "**/app/**" -def resourceFiles = "**/resources/**" -def buildFiles = "**/build/**" - -tasks.register("detektAll", io.gitlab.arturbosch.detekt.Detekt) { - def autoFix = project.hasProperty('detektAutoFix') - - description = "Custom DETEKT task for all modules" - parallel = true - ignoreFailures = false - autoCorrect = autoFix - buildUponDefaultConfig = true - setSource(projectSource) - config.setFrom(configFile) - baseline.set(baselineFile) - include(kotlinFiles) - exclude(appModuleFiles, resourceFiles, buildFiles) - reports { - html.enabled = true - xml.enabled = false - txt.enabled = false - } -} - -tasks.register("detektGenerateBaseline", io.gitlab.arturbosch.detekt.DetektCreateBaselineTask) { - description = "Custom DETEKT task to build baseline for all modules" - parallel = true - ignoreFailures = false - buildUponDefaultConfig = true - setSource(projectSource) - baseline.set(baselineFile) - config.setFrom(configFile) - include(kotlinFiles) - exclude(appModuleFiles, resourceFiles, buildFiles) -} - -dependencies { - detektPlugins libs.detekt.formatting -} \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..5c0c311 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,76 @@ +import io.gitlab.arturbosch.detekt.Detekt + +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath(libs.atomicfu) + } +} + +plugins { + alias(libs.plugins.android.application).apply(false) + alias(libs.plugins.android.library).apply(false) + alias(libs.plugins.kotlinMultiplatform).apply(false) + alias(libs.plugins.compose).apply(false) + alias(libs.plugins.compose.compiler).apply(false) + alias(libs.plugins.publish).apply(false) + alias(libs.plugins.api.validator) + alias(libs.plugins.detekt) +} + +apiValidation { + ignoredProjects.add("sample") +} + +val projectSource = file(projectDir) +val configFiles = files("$rootDir/detekt/config.yml") +val baselineFile = File("$rootDir/detekt/baseline.xml") +val kotlinFiles = "**/*.kt" +val sampleModuleFiles = "**/sample/**" +val resourceFiles = "**/resources/**" +val buildFiles = "**/build/**" + +tasks.register("detektAll") { + val autoFix = project.hasProperty("detektAutoFix") + + description = "Custom DETEKT task for all modules" + parallel = true + ignoreFailures = false + autoCorrect = autoFix + buildUponDefaultConfig = true + setSource(projectSource) + config.setFrom(configFiles) + baseline = baselineFile + reports { + html.required = true + xml.required = false + txt.required = false + } +} + +tasks.register("detektGenerateBaseline") { + description = "Custom DETEKT task to build baseline for all modules" + parallel = true + ignoreFailures = false + buildUponDefaultConfig = true + setSource(projectSource) + baseline.set(baselineFile) + config.setFrom(configFiles) + include(kotlinFiles) + exclude(sampleModuleFiles, resourceFiles, buildFiles) +} + + +tasks.withType().configureEach { + include(kotlinFiles) + exclude(sampleModuleFiles, resourceFiles, buildFiles) +} + +dependencies { + detektPlugins(libs.detekt.formatting) +} + + +apply(plugin = "kotlinx-atomicfu") diff --git a/common-android.gradle b/common-android.gradle deleted file mode 100644 index ff19a36..0000000 --- a/common-android.gradle +++ /dev/null @@ -1,24 +0,0 @@ -android { - namespace = 'com.sebastianneubauer.jsontree' - - compileSdk libs.versions.android.target.get().toInteger() - - defaultConfig { - minSdk libs.versions.android.min.get().toInteger() - targetSdk libs.versions.android.target.get().toInteger() - - aarMetadata { - minCompileSdk = libs.versions.android.min.get().toInteger() - } - } - - buildFeatures { - buildConfig = false - } -} - -kotlin{ - jvmToolchain { - languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get())) - } -} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a81301d..d2f55a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,49 +1,46 @@ [versions] -android-min = "21" -android-target = "34" +android-minSdk = "21" +android-compileSdk = "34" java-toolchain = "17" -gradle-plugin = "8.4.1" +compose = "1.6.11" +agp = "8.4.2" kotlin = "2.0.0" -publish = "0.28.0" -api-validator = "0.14.0" +publish = "0.29.0" +nexus-publish = "2.0.0" +api-validator = "0.16.3" +atomicfuGradlePlugin = "0.22.0" -androidx-activity-compose = "1.9.0" -androidx-compose-bom = "2024.05.00" +androidx-activity-compose = "1.9.1" -kotlinx-serialization-json = "1.7.0" -coroutines = "1.8.1" +kotlinx-serialization-json = "1.7.1" +kotlinx-coroutines = "1.8.1" junit = "4.13.2" detekt = "1.23.6" [libraries] -androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "androidx-compose-bom" } -androidx-compose-ui = { group = "androidx.compose.ui", name = "ui" } -androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } -androidx-compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } -androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation" } -androidx-compose-material = { group = "androidx.compose.material3", name = "material3" } -androidx-compose-runtime = { group = "androidx.compose.runtime", name = "runtime" } +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } - kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } +kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } -testing-junit = { module = "junit:junit", version.ref = "junit" } -testing-compose-ui-junit = { group = "androidx.compose.ui", name = "ui-test-junit4" } -testing-compose-ui-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } -testing-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } publish = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "publish" } +atomicfu = { module = "org.jetbrains.kotlinx:atomicfu-gradle-plugin", version.ref = "atomicfuGradlePlugin" } [plugins] -android-application = { id = "com.android.application", version.ref = "gradle-plugin" } -android-library = { id = "com.android.library", version.ref = "gradle-plugin" } -kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } +android-library = { id = "com.android.library", version.ref = "agp" } +android-application = { id = "com.android.application", version.ref = "agp" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +compose = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } publish = { id = "com.vanniktech.maven.publish", version.ref = "publish" } api-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "api-validator" } diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000..76674b4 --- /dev/null +++ b/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,3 @@ +TEAM_ID= +BUNDLE_ID=com.sebastianneubauer.jsontreesample.KotlinProject +APP_NAME=KotlinProject \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..42f290d --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,393 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2152FB032600AC8F00CF470E /* iOSApp.swift */; }; + 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2152FB032600AC8F00CF470E /* iOSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSApp.swift; sourceTree = ""; }; + 7555FF7B242A565900829871 /* KotlinProject.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = KotlinProject.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + AB3632DC29227652001CCB65 /* Config.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + B92378962B6B1156000C7307 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 058557D7273AAEEB004C7B11 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 42799AB246E5F90AF97AA0EF /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; + 7555FF72242A565900829871 = { + isa = PBXGroup; + children = ( + AB1DB47929225F7C00F7AF9C /* Configuration */, + 7555FF7D242A565900829871 /* iosApp */, + 7555FF7C242A565900829871 /* Products */, + 42799AB246E5F90AF97AA0EF /* Frameworks */, + ); + sourceTree = ""; + }; + 7555FF7C242A565900829871 /* Products */ = { + isa = PBXGroup; + children = ( + 7555FF7B242A565900829871 /* KotlinProject.app */, + ); + name = Products; + sourceTree = ""; + }; + 7555FF7D242A565900829871 /* iosApp */ = { + isa = PBXGroup; + children = ( + 058557BA273AAA24004C7B11 /* Assets.xcassets */, + 7555FF82242A565900829871 /* ContentView.swift */, + 7555FF8C242A565B00829871 /* Info.plist */, + 2152FB032600AC8F00CF470E /* iOSApp.swift */, + 058557D7273AAEEB004C7B11 /* Preview Content */, + ); + path = iosApp; + sourceTree = ""; + }; + AB1DB47929225F7C00F7AF9C /* Configuration */ = { + isa = PBXGroup; + children = ( + AB3632DC29227652001CCB65 /* Config.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7555FF7A242A565900829871 /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */, + 7555FF77242A565900829871 /* Sources */, + B92378962B6B1156000C7307 /* Frameworks */, + 7555FF79242A565900829871 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = 7555FF7B242A565900829871 /* KotlinProject.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7555FF73242A565900829871 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1130; + LastUpgradeCheck = 1540; + ORGANIZATIONNAME = orgName; + TargetAttributes = { + 7555FF7A242A565900829871 = { + CreatedOnToolsVersion = 11.3.1; + }; + }; + }; + buildConfigurationList = 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7555FF72242A565900829871; + packageReferences = ( + ); + productRefGroup = 7555FF7C242A565900829871 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7555FF7A242A565900829871 /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7555FF79242A565900829871 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, + 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + F36B1CEB2AD83DDC00CB74D5 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :sample:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7555FF77242A565900829871 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, + 7555FF83242A565900829871 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7555FFA3242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7555FFA4242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AB3632DC29227652001CCB65 /* Config.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7555FFA6242A565B00829871 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7555FFA7242A565B00829871 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\n$(SRCROOT)/../sample/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", + ); + INFOPLIST_FILE = iosApp/Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 15.3; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = "${BUNDLE_ID}${TEAM_ID}"; + PRODUCT_NAME = "${APP_NAME}"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7555FF76242A565900829871 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA3242A565B00829871 /* Debug */, + 7555FFA4242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7555FFA6242A565B00829871 /* Debug */, + 7555FFA7242A565B00829871 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7555FF73242A565900829871 /* Project object */; +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..ee7e3ca --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..8edf56e --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,14 @@ +{ + "images" : [ + { + "filename" : "app-icon-1024.png", + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png new file mode 100644 index 0000000..53fc536 Binary files /dev/null and b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png differ diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..ab75edd --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,21 @@ +import UIKit +import SwiftUI +import Sample + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea(.keyboard) // Compose has own keyboard handler + } +} + + + diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 0000000..412e378 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,50 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSRequiresIPhoneOS + + CADisableMinimumFrameDurationOnPhone + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + + UILaunchScreen + + UIRequiredDeviceCapabilities + + armv7 + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..4aa7c53 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} \ No newline at end of file diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..d83dca6 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} \ No newline at end of file diff --git a/jsontree/api/android/jsontree.api b/jsontree/api/android/jsontree.api new file mode 100644 index 0000000..ab9660f --- /dev/null +++ b/jsontree/api/android/jsontree.api @@ -0,0 +1,44 @@ +public final class com/sebastianneubauer/jsontree/JsonTreeKt { + public static final fun JsonTree-xKBSf-U (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Lcom/sebastianneubauer/jsontree/TreeState;Landroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/TreeColors;Landroidx/compose/ui/graphics/vector/ImageVector;FLandroidx/compose/ui/text/TextStyle;ZZZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V +} + +public final class com/sebastianneubauer/jsontree/TreeColors { + public static final field $stable I + public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3-0d7_KjU ()J + public final fun component4-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun component7-0d7_KjU ()J + public final fun component8-0d7_KjU ()J + public final fun copy-FD3wquc (JJJJJJJJ)Lcom/sebastianneubauer/jsontree/TreeColors; + public static synthetic fun copy-FD3wquc$default (Lcom/sebastianneubauer/jsontree/TreeColors;JJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/TreeColors; + public fun equals (Ljava/lang/Object;)Z + public final fun getBooleanValueColor-0d7_KjU ()J + public final fun getIconColor-0d7_KjU ()J + public final fun getIndexColor-0d7_KjU ()J + public final fun getKeyColor-0d7_KjU ()J + public final fun getNullValueColor-0d7_KjU ()J + public final fun getNumberValueColor-0d7_KjU ()J + public final fun getStringValueColor-0d7_KjU ()J + public final fun getSymbolColor-0d7_KjU ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/TreeColorsKt { + public static final fun getDefaultDarkColors ()Lcom/sebastianneubauer/jsontree/TreeColors; + public static final fun getDefaultLightColors ()Lcom/sebastianneubauer/jsontree/TreeColors; +} + +public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum { + public static final field COLLAPSED Lcom/sebastianneubauer/jsontree/TreeState; + public static final field EXPANDED Lcom/sebastianneubauer/jsontree/TreeState; + public static final field FIRST_ITEM_EXPANDED Lcom/sebastianneubauer/jsontree/TreeState; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/sebastianneubauer/jsontree/TreeState; + public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState; +} + diff --git a/jsontree/api/jvm/jsontree.api b/jsontree/api/jvm/jsontree.api new file mode 100644 index 0000000..ab9660f --- /dev/null +++ b/jsontree/api/jvm/jsontree.api @@ -0,0 +1,44 @@ +public final class com/sebastianneubauer/jsontree/JsonTreeKt { + public static final fun JsonTree-xKBSf-U (Ljava/lang/String;Lkotlin/jvm/functions/Function2;Landroidx/compose/ui/Modifier;Lcom/sebastianneubauer/jsontree/TreeState;Landroidx/compose/foundation/layout/PaddingValues;Lcom/sebastianneubauer/jsontree/TreeColors;Landroidx/compose/ui/graphics/vector/ImageVector;FLandroidx/compose/ui/text/TextStyle;ZZZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;III)V +} + +public final class com/sebastianneubauer/jsontree/TreeColors { + public static final field $stable I + public synthetic fun (JJJJJJJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2-0d7_KjU ()J + public final fun component3-0d7_KjU ()J + public final fun component4-0d7_KjU ()J + public final fun component5-0d7_KjU ()J + public final fun component6-0d7_KjU ()J + public final fun component7-0d7_KjU ()J + public final fun component8-0d7_KjU ()J + public final fun copy-FD3wquc (JJJJJJJJ)Lcom/sebastianneubauer/jsontree/TreeColors; + public static synthetic fun copy-FD3wquc$default (Lcom/sebastianneubauer/jsontree/TreeColors;JJJJJJJJILjava/lang/Object;)Lcom/sebastianneubauer/jsontree/TreeColors; + public fun equals (Ljava/lang/Object;)Z + public final fun getBooleanValueColor-0d7_KjU ()J + public final fun getIconColor-0d7_KjU ()J + public final fun getIndexColor-0d7_KjU ()J + public final fun getKeyColor-0d7_KjU ()J + public final fun getNullValueColor-0d7_KjU ()J + public final fun getNumberValueColor-0d7_KjU ()J + public final fun getStringValueColor-0d7_KjU ()J + public final fun getSymbolColor-0d7_KjU ()J + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/sebastianneubauer/jsontree/TreeColorsKt { + public static final fun getDefaultDarkColors ()Lcom/sebastianneubauer/jsontree/TreeColors; + public static final fun getDefaultLightColors ()Lcom/sebastianneubauer/jsontree/TreeColors; +} + +public final class com/sebastianneubauer/jsontree/TreeState : java/lang/Enum { + public static final field COLLAPSED Lcom/sebastianneubauer/jsontree/TreeState; + public static final field EXPANDED Lcom/sebastianneubauer/jsontree/TreeState; + public static final field FIRST_ITEM_EXPANDED Lcom/sebastianneubauer/jsontree/TreeState; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Lcom/sebastianneubauer/jsontree/TreeState; + public static fun values ()[Lcom/sebastianneubauer/jsontree/TreeState; +} + diff --git a/jsontree/build.gradle b/jsontree/build.gradle deleted file mode 100644 index 574e71b..0000000 --- a/jsontree/build.gradle +++ /dev/null @@ -1,41 +0,0 @@ -plugins { - alias libs.plugins.android.library - alias libs.plugins.kotlin.android - alias(libs.plugins.compose.compiler) - alias libs.plugins.publish -} - -apply from: "$rootProject.projectDir/common-android.gradle" - -android { - kotlinOptions { - freeCompilerArgs += ["-Xexplicit-api=strict", "-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn"] - } - buildFeatures { - compose = true - } - defaultConfig { - testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' - } -} - -composeCompiler { - enableStrongSkippingMode = true -} - -dependencies { - implementation platform(libs.androidx.compose.bom) - implementation libs.androidx.compose.ui - implementation libs.androidx.compose.ui.tooling - implementation libs.androidx.compose.foundation - implementation libs.androidx.compose.material - implementation libs.androidx.compose.runtime - - implementation libs.kotlinx.serialization.json - - testImplementation libs.testing.junit - testImplementation libs.testing.coroutines - testImplementation platform(libs.androidx.compose.bom) - androidTestImplementation libs.testing.compose.ui.junit - debugImplementation libs.testing.compose.ui.manifest -} \ No newline at end of file diff --git a/jsontree/build.gradle.kts b/jsontree/build.gradle.kts new file mode 100644 index 0000000..6e57616 --- /dev/null +++ b/jsontree/build.gradle.kts @@ -0,0 +1,114 @@ +import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.android.library) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) + id("kotlinx-atomicfu") + alias(libs.plugins.publish) +} + +kotlin { + jvm() + androidTarget { + publishLibraryVariants("release") + @OptIn(ExperimentalKotlinGradlePluginApi::class) + instrumentedTestVariant.sourceSetTree.set(KotlinSourceSetTree.test) + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + } + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + instrumentedTestVariant { + sourceSetTree.set(KotlinSourceSetTree.test) + dependencies { + implementation(libs.kotlinx.coroutines.test) + } + } + } + iosX64() + iosArm64() + iosSimulatorArm64() + + @OptIn(ExperimentalKotlinGradlePluginApi::class) + compilerOptions { + freeCompilerArgs.addAll("-Xexplicit-api=strict", "-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn") + } + + jvmToolchain { + languageVersion.set(JavaLanguageVersion.of(libs.versions.java.toolchain.get())) + } + + sourceSets { + val commonMain by getting { + dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.kotlinx.coroutines.core) + + implementation (libs.kotlinx.serialization.json) + + } + } + + val commonTest by getting { + dependencies { + implementation(libs.kotlin.test) + implementation(libs.kotlinx.coroutines.test) + + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + implementation(compose.uiTest) + } + } + + val androidMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.android) + } + } + + val jvmMain by getting { + dependencies { + implementation(libs.kotlinx.coroutines.swing) + implementation(compose.desktop.currentOs) + } + } + + val jvmTest by getting { + dependencies { + implementation(compose.desktop.uiTestJUnit4) + implementation(compose.desktop.currentOs) + } + } + } +} + + +android { + namespace = "com.sebastianneubauer.jsontree" + compileSdk = libs.versions.android.compileSdk.get().toInt() + buildFeatures { + buildConfig = false + compose = true + } + defaultConfig { + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + minSdk = libs.versions.android.minSdk.get().toInt() + aarMetadata { + minCompileSdk = libs.versions.android.minSdk.get().toInt() + } + } +} + +composeCompiler { + enableStrongSkippingMode = true +} + +apply(plugin = "kotlinx-atomicfu") \ No newline at end of file diff --git a/jsontree/proguard-rules.pro b/jsontree/proguard-rules.pro index 481bb43..ff59496 100644 --- a/jsontree/proguard-rules.pro +++ b/jsontree/proguard-rules.pro @@ -1,6 +1,6 @@ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. +# proguardFiles setting in build.gradle.kts. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt b/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt deleted file mode 100644 index 0992ed3..0000000 --- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/JsonTreeTest.kt +++ /dev/null @@ -1,455 +0,0 @@ -package com.sebastianneubauer.jsontree - -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.material3.Button -import androidx.compose.material3.Text -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.testTag -import androidx.compose.ui.test.ExperimentalTestApi -import androidx.compose.ui.test.assertCountEquals -import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.hasTestTag -import androidx.compose.ui.test.hasText -import androidx.compose.ui.test.junit4.createComposeRule -import androidx.compose.ui.test.onAllNodesWithText -import androidx.compose.ui.test.onNodeWithTag -import androidx.compose.ui.test.onNodeWithText -import androidx.compose.ui.test.performClick -import org.junit.Rule -import org.junit.Test - -@OptIn(ExperimentalTestApi::class) -internal class JsonTreeTest { - - @JvmField - @Rule - val composeTestRule = createComposeRule() - - private fun setJson( - json: String, - initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED, - ) { - composeTestRule.setContent { - JsonTree( - json = json, - initialState = initialState, - onLoading = {} - ) - } - } - - @Test - fun initial_state_is_first_item_expanded() { - setJson(nestedJson) - - composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed() - composeTestRule.onNodeWithText("\"topLevelArray\": [ 2 items ],").assertIsDisplayed() - composeTestRule.onNodeWithText("\"emptyObject\": { 0 items }").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertDoesNotExist() - composeTestRule.onNodeWithText("\"hello\",").assertDoesNotExist() - } - - @Test - fun initial_state_is_collapsed() { - setJson(nestedJson, initialState = TreeState.COLLAPSED) - - composeTestRule.onNodeWithText("{ 3 items }").assertIsDisplayed() - composeTestRule.onNodeWithText("\"topLevelObject\"", substring = true).assertDoesNotExist() - } - - @Test - fun initial_state_is_expanded() { - setJson(nestedJson, initialState = TreeState.EXPANDED) - - // fix flakiness by waiting until first item appears - composeTestRule.waitUntilExactlyOneExists(hasText("\"topLevelObject\": {")) - - // every collapsable is expanded - composeTestRule.onNodeWithText("\"topLevelObject\": {").assertIsDisplayed() - composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"nestedObject\": {").assertIsDisplayed() - composeTestRule.onNodeWithText("\"int\": 42,").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"nestedArray\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("\"nestedArrayValue\",").assertIsDisplayed() - composeTestRule.onNodeWithText("\"nestedArrayValue\"").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"arrayOfObjects\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("\"anotherString\": \"anotherStringValue\"").assertIsDisplayed() - composeTestRule.onNodeWithText("\"anotherInt\": 52").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"topLevelArray\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("\"hello\",").assertIsDisplayed() - composeTestRule.onNodeWithText("\"emptyObject\": {").assertIsDisplayed() - } - - @Test - fun click_on_collapsed_object_or_array_expands_it() { - setJson(nestedJson) - // fix flakiness by waiting until first item appears - composeTestRule.waitUntilExactlyOneExists(hasText("\"topLevelObject\": { 2 items },")) - - composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").performClick() - composeTestRule.onNodeWithText("\"topLevelObject\": {").assertIsDisplayed() - composeTestRule.onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed() - composeTestRule.onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"topLevelArray\": [ 2 items ],").performClick() - composeTestRule.onNodeWithText("\"topLevelArray\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("\"hello\",").assertIsDisplayed() - composeTestRule.onNodeWithText("\"world\"").assertIsDisplayed() - } - - @Test - fun click_on_expanded_object_or_array_collapses_it() { - setJson(nestedJson, initialState = TreeState.EXPANDED) - - composeTestRule.onNodeWithText("\"arrayOfObjects\": [").performClick() - composeTestRule.onNodeWithText("\"arrayOfObjects\": [ 2 items ]").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"nestedObject\": {").performClick() - composeTestRule.onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed() - - composeTestRule.onNodeWithText("\"topLevelObject\": {").performClick() - composeTestRule.onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed() - } - - @Test - fun array_of_arrays_is_rendered_correctly() { - setJson(arrayOfArraysJson) - // fix flakiness by waiting until first item appears - composeTestRule.waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]")) - - composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() - composeTestRule.onNodeWithText("\"array\": [ 2 items ]").performClick() - composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed() - composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed() - - composeTestRule.onNodeWithText("[ 1 item ],").performClick() - composeTestRule.onNodeWithText("[ 2 items ]").performClick() - - composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed() - composeTestRule.onNodeWithText("],").assertIsDisplayed() - composeTestRule.onNodeWithText("42,").assertIsDisplayed() - composeTestRule.onNodeWithText("52").assertIsDisplayed() - } - - @Test - fun root_array_is_rendered_correctly() { - setJson(rootArrayJson) - - composeTestRule.assertRootArrayIsDisplayed() - } - - @Test - fun root_string_is_rendered_correctly() { - setJson(rootStringJson) - - composeTestRule.assertRootStringIsDisplayed() - } - - @Test - fun empty_object_is_rendered_correctly() { - setJson(EMPTY_OBJECT_JSON) - - composeTestRule.assertEmptyObjectIsDisplayed() - - composeTestRule.onNodeWithText("{").performClick() - composeTestRule.onNodeWithText("{ 0 items }").assertIsDisplayed() - } - - @Test - fun invalid_json_shows_an_error() { - composeTestRule.setContent { - Box { - var errorMessage: String? by remember { mutableStateOf(null) } - - JsonTree( - modifier = Modifier.testTag("jsonTree"), - json = INVALID_JSON, - onError = { throwable -> errorMessage = throwable.localizedMessage }, - onLoading = {} - ) - - errorMessage?.let { - Text( - modifier = Modifier.testTag("errorText"), - text = it - ) - } - } - } - - composeTestRule.onNodeWithTag("errorText").assertIsDisplayed() - } - - @Test - fun changing_json_is_handled_correctly() { - composeTestRule.setContent { - var jsonString: String by remember { mutableStateOf(rootStringJson) } - - Column { - JsonTree(json = jsonString, onLoading = {}) - - Button( - modifier = Modifier.testTag("button"), - onClick = { jsonString = rootArrayJson } - ) {} - } - } - - composeTestRule.assertRootStringIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.assertRootArrayIsDisplayed() - } - - @Test - fun changing_json_while_collapsed_is_handled_correctly() { - composeTestRule.setContent { - var jsonString: String by remember { mutableStateOf(nestedJson) } - - Column { - JsonTree( - json = jsonString, - initialState = TreeState.COLLAPSED, - onLoading = {} - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { jsonString = rootArrayJson } - ) {} - } - } - - composeTestRule.onNodeWithText("{ 3 items }").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed() - } - - @Test - fun changing_json_from_invalid_to_valid_is_handled_correctly() { - composeTestRule.setContent { - var jsonString: String by remember { mutableStateOf(INVALID_JSON) } - - Column { - JsonTree( - modifier = Modifier.testTag("jsonTree"), - json = jsonString, - onLoading = {} - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { jsonString = rootStringJson } - ) {} - } - } - - composeTestRule.onNodeWithTag("button").performClick() - composeTestRule.assertRootStringIsDisplayed() - } - - @Test - fun changing_json_from_valid_to_invalid_is_handled_correctly() { - composeTestRule.setContent { - var jsonString: String by remember { mutableStateOf(rootStringJson) } - var errorMessage: String? by remember { mutableStateOf(null) } - - Column { - JsonTree( - modifier = Modifier.testTag("jsonTree"), - json = jsonString, - onError = { throwable -> errorMessage = throwable.localizedMessage }, - onLoading = {} - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { jsonString = INVALID_JSON } - ) {} - - errorMessage?.let { - Text( - modifier = Modifier.testTag("errorText"), - text = it - ) - } - } - } - - // fix flakiness by waiting until first item appears - composeTestRule.waitUntilExactlyOneExists(hasTestTag("jsonTree")) - - composeTestRule.onNodeWithTag("jsonTree").assertIsDisplayed() - composeTestRule.onNodeWithTag("errorText").assertDoesNotExist() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithTag("errorText").assertIsDisplayed() - } - - @Test - fun changing_initial_state_is_handled_correctly() { - composeTestRule.setContent { - var initalState: TreeState by remember { mutableStateOf(TreeState.EXPANDED) } - - Column { - JsonTree( - json = arrayOfArraysJson, - initialState = initalState, - onLoading = {} - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { - initalState = when (initalState) { - TreeState.EXPANDED -> TreeState.COLLAPSED - TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED - TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED - } - } - ) {} - } - } - - composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed() - composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed() - composeTestRule.onNodeWithText("42,").assertIsDisplayed() - composeTestRule.onNodeWithText("52").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() - } - - @Test - fun showing_and_hiding_indices_is_handled_correctly() { - composeTestRule.setContent { - var showIndices: Boolean by remember { mutableStateOf(true) } - - Column { - JsonTree( - json = arrayOfArraysJson, - onLoading = {}, - showIndices = showIndices, - initialState = TreeState.EXPANDED - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { showIndices = !showIndices } - ) {} - } - } - - composeTestRule.onNodeWithText("0: [").assertIsDisplayed() - composeTestRule.onNodeWithText("0: \"stringValue\"").assertIsDisplayed() - composeTestRule.onNodeWithText("1: [").assertIsDisplayed() - composeTestRule.onNodeWithText("0: 42,").assertIsDisplayed() - composeTestRule.onNodeWithText("1: 52").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onAllNodesWithText("[").assertCountEquals(2) - composeTestRule.onNodeWithText("\"stringValue\"").assertIsDisplayed() - composeTestRule.onNodeWithText("42,").assertIsDisplayed() - composeTestRule.onNodeWithText("52").assertIsDisplayed() - } - - @Test - fun showing_and_hiding_item_count_is_handled_correctly() { - composeTestRule.setContent { - var showItemCount: Boolean by remember { mutableStateOf(true) } - - Column { - JsonTree( - json = rootArrayJson, - onLoading = {}, - showItemCount = showItemCount, - initialState = TreeState.COLLAPSED - ) - - Button( - modifier = Modifier.testTag("button"), - onClick = { showItemCount = !showItemCount } - ) {} - } - } - - composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithText("[ ... ]").assertIsDisplayed() - - composeTestRule.onNodeWithTag("button").performClick() - - composeTestRule.onNodeWithText("[ 1 item ]").assertIsDisplayed() - } - - @Test - fun expanding_single_children_is_handled_correctly() { - composeTestRule.setContent { - JsonTree( - json = arrayOfArraysJson, - initialState = TreeState.COLLAPSED, - onLoading = {}, - expandSingleChildren = true - ) - } - - composeTestRule.onNodeWithText("{ 1 item }").performClick() - // shows nested array - composeTestRule.onNodeWithText("\"array\": [").assertIsDisplayed() - // shows collapsed contents of nested array - composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed() - composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed() - - // collapse root - composeTestRule.onNodeWithText("{").performClick() - composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed() - } - - @Test - fun not_expanding_single_children_is_handled_correctly() { - composeTestRule.setContent { - JsonTree( - json = arrayOfArraysJson, - initialState = TreeState.COLLAPSED, - onLoading = {}, - expandSingleChildren = false - ) - } - - composeTestRule.onNodeWithText("{ 1 item }").performClick() - - composeTestRule.waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]")) - composeTestRule.onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() - composeTestRule.onNodeWithText("\"array\": [ 2 items ]").performClick() - - composeTestRule.onNodeWithText("[ 1 item ],").assertIsDisplayed() - composeTestRule.onNodeWithText("[ 2 items ]").assertIsDisplayed() - - // collapse root - composeTestRule.onNodeWithText("{").performClick() - composeTestRule.onNodeWithText("{ 1 item }").assertIsDisplayed() - } -} diff --git a/jsontree/src/main/res/drawable/jsontree_arrow_right.xml b/jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml similarity index 71% rename from jsontree/src/main/res/drawable/jsontree_arrow_right.xml rename to jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml index e3b8f37..4272a0e 100644 --- a/jsontree/src/main/res/drawable/jsontree_arrow_right.xml +++ b/jsontree/src/commonMain/composeResources/drawable/jsontree_arrow_right.xml @@ -1,5 +1,5 @@ - + diff --git a/jsontree/src/main/res/values/plurals.xml b/jsontree/src/commonMain/composeResources/values/plurals.xml similarity index 51% rename from jsontree/src/main/res/values/plurals.xml rename to jsontree/src/commonMain/composeResources/values/plurals.xml index cf8b5d3..a4ca7f9 100644 --- a/jsontree/src/main/res/values/plurals.xml +++ b/jsontree/src/commonMain/composeResources/values/plurals.xml @@ -1,7 +1,7 @@ - \u0020%d item\u0020 - \u0020%d items\u0020 + \u0020%1$d item\u0020 + \u0020%1$d items\u0020 \ No newline at end of file diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt similarity index 93% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt index 4811913..451c8cd 100644 --- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/AnnotatedText.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/AnnotatedText.kt @@ -2,18 +2,20 @@ package com.sebastianneubauer.jsontree import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.withStyle import com.sebastianneubauer.jsontree.JsonTreeElement.ParentType +import jsontree.jsontree.generated.resources.Res +import jsontree.jsontree.generated.resources.jsontree_collapsable_items import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.booleanOrNull import kotlinx.serialization.json.doubleOrNull import kotlinx.serialization.json.floatOrNull import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.longOrNull +import org.jetbrains.compose.resources.pluralStringResource @Composable internal fun rememberCollapsableText( @@ -27,7 +29,7 @@ internal fun rememberCollapsableText( showItemCount: Boolean, parentType: ParentType, ): AnnotatedString { - val itemCount = pluralStringResource(R.plurals.jsontree_collapsable_items, childItemCount, childItemCount) + val itemCount = pluralStringResource(Res.plurals.jsontree_collapsable_items, childItemCount, childItemCount) return remember(key, state, colors, isLastItem, itemCount, type, showIndices, showItemCount) { val openBracket = if (type == CollapsableType.OBJECT) "{" else "[" diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/CollapsableType.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/CollapsableType.kt similarity index 100% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/CollapsableType.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/CollapsableType.kt diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt similarity index 97% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt index 7b7b4fd..2f1c4a3 100644 --- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTree.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTree.kt @@ -21,14 +21,16 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.times +import jsontree.jsontree.generated.resources.Res +import jsontree.jsontree.generated.resources.jsontree_arrow_right import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import org.jetbrains.compose.resources.vectorResource /** * Renders JSON data as a formatted tree with collapsable objects and arrays. @@ -59,7 +61,7 @@ public fun JsonTree( initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED, contentPadding: PaddingValues = PaddingValues(0.dp), colors: TreeColors = defaultLightColors, - icon: ImageVector = ImageVector.vectorResource(R.drawable.jsontree_arrow_right), + icon: ImageVector = vectorResource(Res.drawable.jsontree_arrow_right), iconSize: Dp = 20.dp, textStyle: TextStyle = LocalTextStyle.current, showIndices: Boolean = false, diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeElement.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeElement.kt similarity index 100% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeElement.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeElement.kt diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt similarity index 97% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt index afe8367..d82c722 100644 --- a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParser.kt +++ b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParser.kt @@ -11,6 +11,7 @@ import com.sebastianneubauer.jsontree.JsonTreeParserState.Loading import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Parsed import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready +import kotlinx.atomicfu.atomic import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.withContext import kotlinx.serialization.json.Json @@ -20,7 +21,6 @@ import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject -import java.util.concurrent.atomic.AtomicLong internal class JsonTreeParser( private val json: String, @@ -42,7 +42,7 @@ internal class JsonTreeParser( Ready( list = parsingState.jsonElement .toJsonTree( - idGenerator = AtomicLong(), + idGenerator = AtomicLongWrapper(), state = initialState, level = 0, key = null, @@ -193,7 +193,7 @@ internal class JsonTreeParser( } private fun JsonElement.toJsonTree( - idGenerator: AtomicLong, + idGenerator: AtomicLongWrapper, state: TreeState, level: Int, key: String?, @@ -297,3 +297,10 @@ internal class JsonTreeParser( return list } } + +internal class AtomicLongWrapper { + private val atomicLong = atomic(0L) + fun incrementAndGet(): Long { + return atomicLong.incrementAndGet() + } +} diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParserState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserState.kt similarity index 100% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/JsonTreeParserState.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserState.kt diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeColors.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeColors.kt similarity index 100% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeColors.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeColors.kt diff --git a/jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeState.kt b/jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeState.kt similarity index 100% rename from jsontree/src/main/java/com/sebastianneubauer/jsontree/TreeState.kt rename to jsontree/src/commonMain/kotlin/com/sebastianneubauer/jsontree/TreeState.kt diff --git a/jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt similarity index 76% rename from jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt index 3cf8a14..c18cdbc 100644 --- a/jsontree/src/test/java/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/JsonTreeParserTest.kt @@ -3,14 +3,15 @@ package com.sebastianneubauer.jsontree import com.sebastianneubauer.jsontree.JsonTreeParserState.Parsing.Error import com.sebastianneubauer.jsontree.JsonTreeParserState.Ready import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestResult import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.serialization.json.JsonPrimitive -import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue -import org.junit.Test +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue -internal class JsonTreeParserTest { +public class JsonTreeParserTest { @OptIn(ExperimentalCoroutinesApi::class) private suspend fun underTest(json: String, initialState: TreeState): JsonTreeParser { @@ -22,14 +23,14 @@ internal class JsonTreeParserTest { } @Test - fun `invalid json - shows error state`() = runTest { + public fun `invalid json - shows error state`(): TestResult = runTest { val underTest = underTest(INVALID_JSON, TreeState.COLLAPSED) // produces Error state with any throwable assertTrue(underTest.state.value is Error) } @Test - fun `empty json - collapsed - shows collapsed empty json`() = runTest { + public fun `empty json - collapsed - shows collapsed empty json`(): TestResult = runTest { val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.COLLAPSED) assertEquals( Ready( @@ -50,7 +51,7 @@ internal class JsonTreeParserTest { } @Test - fun `empty json - first item expanded - shows expanded empty json`() = runTest { + public fun `empty json - first item expanded - shows expanded empty json`(): TestResult = runTest { val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.FIRST_ITEM_EXPANDED) assertEquals( Ready( @@ -77,7 +78,7 @@ internal class JsonTreeParserTest { } @Test - fun `empty json - expanded - shows expanded empty json`() = runTest { + public fun `empty json - expanded - shows expanded empty json`(): TestResult = runTest { val underTest = underTest(EMPTY_OBJECT_JSON, TreeState.EXPANDED) assertEquals( Ready( @@ -104,7 +105,7 @@ internal class JsonTreeParserTest { } @Test - fun `root string json - collapsed - shows root string`() = runTest { + public fun `root string json - collapsed - shows root string`(): TestResult = runTest { val underTest = underTest(rootStringJson, TreeState.COLLAPSED) assertEquals( Ready( @@ -124,7 +125,7 @@ internal class JsonTreeParserTest { } @Test - fun `root string json - expanded - shows root string`() = runTest { + public fun `root string json - expanded - shows root string`(): TestResult = runTest { val underTest = underTest(rootStringJson, TreeState.EXPANDED) assertEquals( Ready( @@ -144,7 +145,7 @@ internal class JsonTreeParserTest { } @Test - fun `root string json - first item expanded - shows root string`() = runTest { + public fun `root string json - first item expanded - shows root string`(): TestResult = runTest { val underTest = underTest(rootStringJson, TreeState.FIRST_ITEM_EXPANDED) assertEquals( Ready( @@ -164,7 +165,7 @@ internal class JsonTreeParserTest { } @Test - fun `root array json - collapsed - shows collapsed array`() = runTest { + public fun `root array json - collapsed - shows collapsed array`(): TestResult = runTest { val underTest = underTest(rootArrayJson, TreeState.COLLAPSED) assertEquals( Ready( @@ -194,7 +195,7 @@ internal class JsonTreeParserTest { } @Test - fun `root array json - first item expanded - shows expanded array`() = runTest { + public fun `root array json - first item expanded - shows expanded array`(): TestResult = runTest { val underTest = underTest(rootArrayJson, TreeState.FIRST_ITEM_EXPANDED) assertEquals( Ready( @@ -238,7 +239,7 @@ internal class JsonTreeParserTest { } @Test - fun `root array json - expanded - shows expanded array`() = runTest { + public fun `root array json - expanded - shows expanded array`(): TestResult = runTest { val underTest = underTest(rootArrayJson, TreeState.EXPANDED) assertEquals( Ready( @@ -282,7 +283,7 @@ internal class JsonTreeParserTest { } @Test - fun `nested json - collapsed - shows collapsed json`() = runTest { + public fun `nested json - collapsed - shows collapsed json`(): TestResult = runTest { val underTest = underTest(nestedJson, TreeState.COLLAPSED) assertEquals( Ready(list = listOf(jsonTreeElement())), @@ -291,12 +292,15 @@ internal class JsonTreeParserTest { } @Test - fun `nested json - first item expanded - shows first item expanded`() = runTest { + public fun `nested json - first item expanded - shows first item expanded`(): TestResult = runTest { val underTest = underTest(nestedJson, TreeState.FIRST_ITEM_EXPANDED) assertEquals( Ready( list = listOf( - jsonTreeElement(state = TreeState.FIRST_ITEM_EXPANDED, childrenState = TreeState.COLLAPSED), + jsonTreeElement( + state = TreeState.FIRST_ITEM_EXPANDED, + childrenState = TreeState.COLLAPSED + ), array(), JsonTreeElement.EndBracket( id = "8-b", @@ -311,7 +315,7 @@ internal class JsonTreeParserTest { } @Test - fun `nested json - expanded - shows expanded json`() = runTest { + public fun `nested json - expanded - shows expanded json`(): TestResult = runTest { val underTest = underTest(nestedJson, TreeState.EXPANDED) assertEquals( Ready( @@ -361,130 +365,146 @@ internal class JsonTreeParserTest { } @Test - fun `nested json - expandSingleChildren is false - expands and collapses correctly`() = runTest { - val underTest = underTest(nestedJson, TreeState.COLLAPSED) - assertEquals( - Ready(list = listOf(jsonTreeElement())), - underTest.state.value - ) - - // expand root - underTest.expandOrCollapseItem(jsonTreeElement(), expandSingleChildren = false) - - assertEquals( - Ready( - list = listOf( - jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - array(), - JsonTreeElement.EndBracket( - id = "8-b", - level = 0, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.OBJECT - ), - ) - ), - underTest.state.value - ) - - // expand array - underTest.expandOrCollapseItem(item = array(), expandSingleChildren = false) - - assertEquals( - Ready( - list = listOf( - jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - nestedArray1(), - nestedArray2(), - JsonTreeElement.EndBracket( - id = "7-b", - level = 1, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.ARRAY - ), - JsonTreeElement.EndBracket( - id = "8-b", - level = 0, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.OBJECT - ), - ) - ), - underTest.state.value - ) - - // expand nestedArray2 - underTest.expandOrCollapseItem(item = nestedArray2(), expandSingleChildren = false) - - assertEquals( - Ready( - list = listOf( - jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - nestedArray1(), - nestedArray2(state = TreeState.EXPANDED), - numberPrimitive1, - numberPrimitive2, - JsonTreeElement.EndBracket( - id = "6-b", - level = 2, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.ARRAY - ), - JsonTreeElement.EndBracket( - id = "7-b", - level = 1, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.ARRAY - ), - JsonTreeElement.EndBracket( - id = "8-b", - level = 0, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.OBJECT - ), - ) - ), - underTest.state.value - ) - - // collapse array - underTest.expandOrCollapseItem( - item = array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - expandSingleChildren = false - ) - - assertEquals( - Ready( - list = listOf( - jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - array(), - JsonTreeElement.EndBracket( - id = "8-b", - level = 0, - isLastItem = true, - type = JsonTreeElement.EndBracket.Type.OBJECT - ), - ) - ), - underTest.state.value - ) - - // collapse root - underTest.expandOrCollapseItem( - item = jsonTreeElement(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), - expandSingleChildren = false - ) - - assertEquals( - Ready(list = listOf(jsonTreeElement())), - underTest.state.value - ) - } + public fun `nested json - expandSingleChildren is false - expands and collapses correctly`(): TestResult = + runTest { + val underTest = underTest(nestedJson, TreeState.COLLAPSED) + assertEquals( + Ready(list = listOf(jsonTreeElement())), + underTest.state.value + ) + + // expand root + underTest.expandOrCollapseItem(jsonTreeElement(), expandSingleChildren = false) + + assertEquals( + Ready( + list = listOf( + jsonTreeElement( + state = TreeState.EXPANDED, + childrenState = TreeState.COLLAPSED + ), + array(), + JsonTreeElement.EndBracket( + id = "8-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ), + ) + ), + underTest.state.value + ) + + // expand array + underTest.expandOrCollapseItem(item = array(), expandSingleChildren = false) + + assertEquals( + Ready( + list = listOf( + jsonTreeElement( + state = TreeState.EXPANDED, + childrenState = TreeState.COLLAPSED + ), + array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), + nestedArray1(), + nestedArray2(), + JsonTreeElement.EndBracket( + id = "7-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.ARRAY + ), + JsonTreeElement.EndBracket( + id = "8-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ), + ) + ), + underTest.state.value + ) + + // expand nestedArray2 + underTest.expandOrCollapseItem(item = nestedArray2(), expandSingleChildren = false) + + assertEquals( + Ready( + list = listOf( + jsonTreeElement( + state = TreeState.EXPANDED, + childrenState = TreeState.COLLAPSED + ), + array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), + nestedArray1(), + nestedArray2(state = TreeState.EXPANDED), + numberPrimitive1, + numberPrimitive2, + JsonTreeElement.EndBracket( + id = "6-b", + level = 2, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.ARRAY + ), + JsonTreeElement.EndBracket( + id = "7-b", + level = 1, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.ARRAY + ), + JsonTreeElement.EndBracket( + id = "8-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ), + ) + ), + underTest.state.value + ) + + // collapse array + underTest.expandOrCollapseItem( + item = array(state = TreeState.EXPANDED, childrenState = TreeState.COLLAPSED), + expandSingleChildren = false + ) + + assertEquals( + Ready( + list = listOf( + jsonTreeElement( + state = TreeState.EXPANDED, + childrenState = TreeState.COLLAPSED + ), + array(), + JsonTreeElement.EndBracket( + id = "8-b", + level = 0, + isLastItem = true, + type = JsonTreeElement.EndBracket.Type.OBJECT + ), + ) + ), + underTest.state.value + ) + + // collapse root + underTest.expandOrCollapseItem( + item = jsonTreeElement( + state = TreeState.EXPANDED, + childrenState = TreeState.COLLAPSED + ), + expandSingleChildren = false + ) + + assertEquals( + Ready(list = listOf(jsonTreeElement())), + underTest.state.value + ) + } @Test - fun `nested json - expandSingleChildren is true - expands and collapses correctly`() = runTest { + public fun `nested json - expandSingleChildren is true - expands and collapses correctly`(): TestResult = runTest { val underTest = underTest(nestedJson, TreeState.COLLAPSED) assertEquals( Ready(list = listOf(jsonTreeElement())), diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt similarity index 81% rename from jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt index 46c34f4..dc37ae7 100644 --- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/TestData.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/TestData.kt @@ -1,6 +1,22 @@ package com.sebastianneubauer.jsontree internal val nestedJson = """ + { + "array": [ + [ + { + "string": "aString" + } + ], + [ + 42, + 52 + ] + ] + } +""".trimIndent() + +internal val nestedJson2 = """ { "topLevelObject": { "string": "stringValue", diff --git a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt similarity index 52% rename from jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt rename to jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt index 9b09c27..3403f94 100644 --- a/jsontree/src/androidTest/java/com/sebastianneubauer/jsontree/Assertions.kt +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/Assertions.kt @@ -1,20 +1,23 @@ -package com.sebastianneubauer.jsontree +@file:OptIn(ExperimentalTestApi::class) +package com.sebastianneubauer.jsontree.ui + +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi import androidx.compose.ui.test.assertIsDisplayed -import androidx.compose.ui.test.junit4.ComposeTestRule import androidx.compose.ui.test.onNodeWithText -internal fun ComposeTestRule.assertRootArrayIsDisplayed() { +internal fun ComposeUiTest.assertRootArrayIsDisplayed() { onNodeWithText("[").assertIsDisplayed() onNodeWithText("\"stringValue\"").assertIsDisplayed() onNodeWithText("]").assertIsDisplayed() } -internal fun ComposeTestRule.assertRootStringIsDisplayed() { +internal fun ComposeUiTest.assertRootStringIsDisplayed() { onNodeWithText("\"stringValue\"").assertIsDisplayed() } -internal fun ComposeTestRule.assertEmptyObjectIsDisplayed() { +internal fun ComposeUiTest.assertEmptyObjectIsDisplayed() { onNodeWithText("{").assertIsDisplayed() onNodeWithText("}").assertIsDisplayed() } diff --git a/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt new file mode 100644 index 0000000..624960c --- /dev/null +++ b/jsontree/src/commonTest/kotlin/com/sebastianneubauer/jsontree/ui/JsonTreeTest.kt @@ -0,0 +1,470 @@ +@file:OptIn(ExperimentalTestApi::class) + +package com.sebastianneubauer.jsontree.ui + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.material3.Button +import androidx.compose.material3.Text +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.ComposeUiTest +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertCountEquals +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.hasTestTag +import androidx.compose.ui.test.hasText +import androidx.compose.ui.test.onAllNodesWithText +import androidx.compose.ui.test.onNodeWithTag +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import androidx.compose.ui.test.waitUntilExactlyOneExists +import com.sebastianneubauer.jsontree.EMPTY_OBJECT_JSON +import com.sebastianneubauer.jsontree.INVALID_JSON +import com.sebastianneubauer.jsontree.JsonTree +import com.sebastianneubauer.jsontree.TreeState +import com.sebastianneubauer.jsontree.arrayOfArraysJson +import com.sebastianneubauer.jsontree.nestedJson2 +import com.sebastianneubauer.jsontree.rootArrayJson +import com.sebastianneubauer.jsontree.rootStringJson +import kotlin.test.Test + +public class JsonTreeTest { + + private fun ComposeUiTest.setJson( + json: String, + initialState: TreeState = TreeState.FIRST_ITEM_EXPANDED, + ) { + setContent { + JsonTree( + json = json, + initialState = initialState, + onLoading = {}, + ) + } + } + + @Test + public fun initial_state_is_first_item_expanded(): Unit = runComposeUiTest { + setJson(nestedJson2) + + waitForIdle() + onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed() + onNodeWithText("\"topLevelArray\": [ 2 items ],").assertIsDisplayed() + onNodeWithText("\"emptyObject\": { 0 items }").assertIsDisplayed() + + onNodeWithText("\"string\": \"stringValue\",").assertDoesNotExist() + onNodeWithText("\"hello\",").assertDoesNotExist() + } + + @Test + public fun initial_state_is_collapsed(): Unit = runComposeUiTest { + setJson(nestedJson2, initialState = TreeState.COLLAPSED) + + waitUntilExactlyOneExists(hasText("{ 3 items }")) + + onNodeWithText("{ 3 items }").assertIsDisplayed() + onNodeWithText("\"topLevelObject\"", substring = true).assertDoesNotExist() + } + + @Test + public fun initial_state_is_expanded(): Unit = runComposeUiTest { + setJson(nestedJson2, initialState = TreeState.EXPANDED) + + // fix flakiness by waiting until first item appears + waitUntilExactlyOneExists(hasText("\"topLevelObject\": {")) + + // every collapsable is expanded + onNodeWithText("\"topLevelObject\": {").assertIsDisplayed() + onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed() + + onNodeWithText("\"nestedObject\": {").assertIsDisplayed() + onNodeWithText("\"int\": 42,").assertIsDisplayed() + + onNodeWithText("\"nestedArray\": [").assertIsDisplayed() + onNodeWithText("\"nestedArrayValue\",").assertIsDisplayed() + onNodeWithText("\"nestedArrayValue\"").assertIsDisplayed() + + onNodeWithText("\"arrayOfObjects\": [").assertIsDisplayed() + onNodeWithText("\"anotherString\": \"anotherStringValue\"").assertIsDisplayed() + onNodeWithText("\"anotherInt\": 52").assertIsDisplayed() + + onNodeWithText("\"topLevelArray\": [").assertIsDisplayed() + onNodeWithText("\"hello\",").assertIsDisplayed() + onNodeWithText("\"emptyObject\": {").assertIsDisplayed() + } + + @Test + public fun click_on_collapsed_object_or_array_expands_it(): Unit = runComposeUiTest { + setJson(nestedJson2) + // fix flakiness by waiting until first item appears + waitUntilExactlyOneExists(hasText("\"topLevelObject\": { 2 items },")) + + onNodeWithText("\"topLevelObject\": { 2 items },").performClick() + onNodeWithText("\"topLevelObject\": {").assertIsDisplayed() + onNodeWithText("\"string\": \"stringValue\",").assertIsDisplayed() + onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed() + + onNodeWithText("\"topLevelArray\": [ 2 items ],").performClick() + onNodeWithText("\"topLevelArray\": [").assertIsDisplayed() + onNodeWithText("\"hello\",").assertIsDisplayed() + onNodeWithText("\"world\"").assertIsDisplayed() + } + + @Test + public fun click_on_expanded_object_or_array_collapses_it(): Unit = runComposeUiTest { + setJson(nestedJson2, initialState = TreeState.EXPANDED) + + waitForIdle() + + onNodeWithText("\"arrayOfObjects\": [").performClick() + waitForIdle() + onNodeWithText("\"arrayOfObjects\": [ 2 items ]").assertIsDisplayed() + + onNodeWithText("\"nestedObject\": {").performClick() + waitForIdle() + onNodeWithText("\"nestedObject\": { 3 items }").assertIsDisplayed() + + onNodeWithText("\"topLevelObject\": {").performClick() + waitForIdle() + onNodeWithText("\"topLevelObject\": { 2 items },").assertIsDisplayed() + } + + @Test + public fun array_of_arrays_is_rendered_correctly(): Unit = runComposeUiTest { + setJson(arrayOfArraysJson) + // fix flakiness by waiting until first item appears + waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]")) + + onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() + onNodeWithText("\"array\": [ 2 items ]").performClick() + + waitUntilExactlyOneExists(hasText("\"array\": [")) + onNodeWithText("\"array\": [").assertIsDisplayed() + onNodeWithText("[ 1 item ],").assertIsDisplayed() + onNodeWithText("[ 2 items ]").assertIsDisplayed() + + onNodeWithText("[ 1 item ],").performClick() + onNodeWithText("[ 2 items ]").performClick() + + onNodeWithText("\"stringValue\"").assertIsDisplayed() + onNodeWithText("],").assertIsDisplayed() + onNodeWithText("42,").assertIsDisplayed() + onNodeWithText("52").assertIsDisplayed() + } + + @Test + public fun root_array_is_rendered_correctly(): Unit = runComposeUiTest { + setJson(rootArrayJson) + + assertRootArrayIsDisplayed() + } + + @Test + public fun root_string_is_rendered_correctly(): Unit = runComposeUiTest { + setJson(rootStringJson) + + assertRootStringIsDisplayed() + } + + @Test + public fun empty_object_is_rendered_correctly(): Unit = runComposeUiTest { + setJson(EMPTY_OBJECT_JSON) + waitForIdle() + assertEmptyObjectIsDisplayed() + + onNodeWithText("{").performClick() + onNodeWithText("{ 0 items }").assertIsDisplayed() + } + + @Test + public fun invalid_json_shows_an_error(): Unit = runComposeUiTest { + setContent { + Box { + var errorMessage: String? by remember { mutableStateOf(null) } + + JsonTree( + modifier = Modifier.testTag("jsonTree"), + json = INVALID_JSON, + onError = { throwable -> errorMessage = throwable.message }, + onLoading = {}, + ) + + errorMessage?.let { + Text( + modifier = Modifier.testTag("errorText"), + text = it + ) + } + } + } + + onNodeWithTag("errorText").assertIsDisplayed() + } + + @Test + public fun changing_json_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var jsonString: String by remember { mutableStateOf(rootStringJson) } + + Column { + JsonTree(json = jsonString, onLoading = {}) + + Button( + modifier = Modifier.testTag("button"), + onClick = { jsonString = rootArrayJson } + ) {} + } + } + + assertRootStringIsDisplayed() + + onNodeWithTag("button").performClick() + + assertRootArrayIsDisplayed() + } + + @Test + public fun changing_json_while_collapsed_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var jsonString: String by remember { mutableStateOf(nestedJson2) } + + Column { + JsonTree( + json = jsonString, + initialState = TreeState.COLLAPSED, + onLoading = {}, + ) + + Button( + modifier = Modifier.testTag("button"), + onClick = { jsonString = rootArrayJson } + ) {} + } + } + + onNodeWithText("{ 3 items }").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onNodeWithText("[ 1 item ]").assertIsDisplayed() + } + + @Test + public fun changing_json_from_invalid_to_valid_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var jsonString: String by remember { mutableStateOf(INVALID_JSON) } + + Column { + JsonTree( + modifier = Modifier.testTag("jsonTree"), + json = jsonString, + onLoading = {}, + ) + + Button( + modifier = Modifier.testTag("button"), + onClick = { jsonString = rootStringJson } + ) {} + } + } + + onNodeWithTag("button").performClick() + assertRootStringIsDisplayed() + } + + @Test + public fun changing_json_from_valid_to_invalid_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var jsonString: String by remember { mutableStateOf(rootStringJson) } + var errorMessage: String? by remember { mutableStateOf(null) } + + Column { + JsonTree( + modifier = Modifier.testTag("jsonTree"), + json = jsonString, + onError = { throwable -> errorMessage = throwable.message }, + onLoading = {} + ) + + Button( + modifier = Modifier.testTag("button"), + onClick = { jsonString = INVALID_JSON } + ) {} + + errorMessage?.let { + Text( + modifier = Modifier.testTag("errorText"), + text = it + ) + } + } + } + + // fix flakiness by waiting until first item appears + waitUntilExactlyOneExists(hasTestTag("jsonTree")) + + onNodeWithTag("jsonTree").assertIsDisplayed() + onNodeWithTag("errorText").assertDoesNotExist() + + onNodeWithTag("button").performClick() + + onNodeWithTag("errorText").assertIsDisplayed() + } + + @Test + public fun changing_initial_state_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var initalState: TreeState by remember { mutableStateOf(TreeState.EXPANDED) } + + Column { + JsonTree(json = arrayOfArraysJson, initialState = initalState, onLoading = {}) + + Button( + modifier = Modifier.testTag("button"), + onClick = { + initalState = when (initalState) { + TreeState.EXPANDED -> TreeState.COLLAPSED + TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED + TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED + } + } + ) {} + } + } + + onNodeWithText("\"array\": [").assertIsDisplayed() + onNodeWithText("\"stringValue\"").assertIsDisplayed() + onNodeWithText("42,").assertIsDisplayed() + onNodeWithText("52").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onNodeWithText("{ 1 item }").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() + } + + @Test + public fun showing_and_hiding_indices_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var showIndices: Boolean by remember { mutableStateOf(true) } + + Column { + JsonTree( + json = arrayOfArraysJson, + onLoading = {}, + showIndices = showIndices, + initialState = TreeState.EXPANDED + ) + + Button( + modifier = Modifier.testTag("button"), + onClick = { showIndices = !showIndices } + ) {} + } + } + + onNodeWithText("0: [").assertIsDisplayed() + onNodeWithText("0: \"stringValue\"").assertIsDisplayed() + onNodeWithText("1: [").assertIsDisplayed() + onNodeWithText("0: 42,").assertIsDisplayed() + onNodeWithText("1: 52").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onAllNodesWithText("[").assertCountEquals(2) + onNodeWithText("\"stringValue\"").assertIsDisplayed() + onNodeWithText("42,").assertIsDisplayed() + onNodeWithText("52").assertIsDisplayed() + } + + @Test + public fun showing_and_hiding_item_count_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + var showItemCount: Boolean by remember { mutableStateOf(true) } + + Column { + JsonTree( + json = rootArrayJson, + onLoading = {}, + showItemCount = showItemCount, + initialState = TreeState.COLLAPSED + ) + + Button( + modifier = Modifier.testTag("button"), + onClick = { showItemCount = !showItemCount } + ) {} + } + } + + onNodeWithText("[ 1 item ]").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onNodeWithText("[ ... ]").assertIsDisplayed() + + onNodeWithTag("button").performClick() + + onNodeWithText("[ 1 item ]").assertIsDisplayed() + } + + @Test + public fun expanding_single_children_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + JsonTree( + json = arrayOfArraysJson, + initialState = TreeState.COLLAPSED, + onLoading = {}, + expandSingleChildren = true + ) + } + waitUntilExactlyOneExists(hasText("{ 1 item }")) + onNodeWithText("{ 1 item }").performClick() + waitUntilExactlyOneExists(hasText("\"array\": [")) + // shows nested array + onNodeWithText("\"array\": [").assertIsDisplayed() + // shows collapsed contents of nested array + onNodeWithText("[ 1 item ],").assertIsDisplayed() + onNodeWithText("[ 2 items ]").assertIsDisplayed() + + // collapse root + onNodeWithText("{").performClick() + onNodeWithText("{ 1 item }").assertIsDisplayed() + } + + @Test + public fun not_expanding_single_children_is_handled_correctly(): Unit = runComposeUiTest { + setContent { + JsonTree( + json = arrayOfArraysJson, + initialState = TreeState.COLLAPSED, + onLoading = {}, + expandSingleChildren = false + ) + } + waitUntilExactlyOneExists(hasText("{ 1 item }")) + onNodeWithText("{ 1 item }").performClick() + + waitUntilExactlyOneExists(hasText("\"array\": [ 2 items ]")) + onNodeWithText("\"array\": [ 2 items ]").assertIsDisplayed() + onNodeWithText("\"array\": [ 2 items ]").performClick() + + waitForIdle() + onNodeWithText("[ 1 item ],").assertIsDisplayed() + onNodeWithText("[ 2 items ]").assertIsDisplayed() + + waitForIdle() + // collapse root + onNodeWithText("{").performClick() + onNodeWithText("{ 1 item }").assertIsDisplayed() + } +} diff --git a/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt b/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt deleted file mode 100644 index 58df75a..0000000 --- a/jsontree/src/test/java/com/sebastianneubauer/jsontree/TestData.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.sebastianneubauer.jsontree - -internal val nestedJson = """ - { - "array": [ - [ - { - "string": "aString" - } - ], - [ - 42, - 52 - ] - ] - } -""".trimIndent() - -internal val rootArrayJson = """ - [ - "stringValue" - ] -""".trimIndent() - -internal val rootStringJson = """ - "stringValue" -""".trimIndent() - -internal const val EMPTY_OBJECT_JSON = "{}" - -internal const val INVALID_JSON = "" diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts new file mode 100644 index 0000000..a9b1c7d --- /dev/null +++ b/sample/build.gradle.kts @@ -0,0 +1,107 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) +} + +group = "com.sebastianneubauer.jsontreesample" +version = "1.0" + +kotlin { + androidTarget() + + jvm() + + listOf( + iosX64(), + iosArm64(), + iosSimulatorArm64() + ).forEach { + it.binaries.framework { + baseName = "Sample" + isStatic = true + } + } + + sourceSets { + all { + languageSettings { + optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") + } + } + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.kotlinx.serialization.json) + implementation(project(":jsontree")) + } + + commonTest.dependencies { + implementation(kotlin("test")) + } + + androidMain.dependencies { + implementation(compose.uiTooling) + implementation(libs.androidx.activity.compose) + } + + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + } + + + } + + //https://kotlinlang.org/docs/native-objc-interop.html#export-of-kdoc-comments-to-generated-objective-c-headers + targets.withType { + compilations["main"].compilerOptions.options.freeCompilerArgs.add("-Xexport-kdoc") + } + +} + + + +android { + namespace = "com.sebastianneubauer.jsontreesample" + compileSdk = 34 + + defaultConfig { + minSdk = 24 + } + sourceSets["main"].apply { + manifest.srcFile("src/androidMain/AndroidManifest.xml") + res.srcDirs("src/androidMain/res") + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.11" + } +} + +compose.desktop { + application { + mainClass = "MainKt" + buildTypes.release { + proguard { + configurationFiles.from("compose-desktop.pro") + } + } + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "com.sebastianneubauer.jsontreesample" + packageVersion = "1.0.0" + } + } +} diff --git a/sample/compose-desktop.pro b/sample/compose-desktop.pro new file mode 100644 index 0000000..c00d74f --- /dev/null +++ b/sample/compose-desktop.pro @@ -0,0 +1 @@ +-keep class org.sqlite.** { *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/sample/src/androidMain/AndroidManifest.xml similarity index 62% rename from app/src/main/AndroidManifest.xml rename to sample/src/androidMain/AndroidManifest.xml index 573c488..756d3b5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/sample/src/androidMain/AndroidManifest.xml @@ -1,7 +1,7 @@ - + + android:theme="@android:style/Theme.Material.Light.NoActionBar"> + android:launchMode="singleInstance" + android:windowSoftInputMode="adjustPan"> - diff --git a/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt b/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt new file mode 100644 index 0000000..f85c22a --- /dev/null +++ b/sample/src/androidMain/kotlin/com/sebastianneubauer/jsontreesample/MainActivity.kt @@ -0,0 +1,14 @@ +package com.sebastianneubauer.jsontreesample + +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge + +class MainActivity : ComponentActivity() { + + override fun onCreate(savedInstanceState: android.os.Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { App() } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to sample/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/sample/src/androidMain/res/drawable/ic_launcher_background.xml similarity index 99% rename from app/src/main/res/drawable/ic_launcher_background.xml rename to sample/src/androidMain/res/drawable/ic_launcher_background.xml index 07d5da9..e93e11a 100644 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ b/sample/src/androidMain/res/drawable/ic_launcher_background.xml @@ -167,4 +167,4 @@ android:pathData="M79,19L79,89" android:strokeWidth="0.8" android:strokeColor="#33FFFFFF" /> - + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml rename to sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml similarity index 100% rename from app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml rename to sample/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher.png differ diff --git a/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher.png differ diff --git a/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/sample/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/sample/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/sample/src/androidMain/res/values/strings.xml b/sample/src/androidMain/res/values/strings.xml new file mode 100644 index 0000000..93ac338 --- /dev/null +++ b/sample/src/androidMain/res/values/strings.xml @@ -0,0 +1,3 @@ + + JsonTree Sample + \ No newline at end of file diff --git a/sample/src/commonMain/kotlin/App.kt b/sample/src/commonMain/kotlin/App.kt new file mode 100644 index 0000000..2ef6c25 --- /dev/null +++ b/sample/src/commonMain/kotlin/App.kt @@ -0,0 +1,199 @@ +package com.sebastianneubauer.jsontreesample + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.safeDrawingPadding +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.rememberPagerState +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Button +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.sebastianneubauer.jsontree.JsonTree +import com.sebastianneubauer.jsontree.TreeColors +import com.sebastianneubauer.jsontree.TreeState +import com.sebastianneubauer.jsontree.defaultDarkColors +import com.sebastianneubauer.jsontree.defaultLightColors +import com.sebastianneubauer.jsontreesample.ui.theme.JsonTreeTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + + +@Composable +internal fun App() = JsonTreeTheme { + MainScreen() +} + +@OptIn( + ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class, ExperimentalLayoutApi::class +) +@Composable +fun MainScreen() { + + Scaffold( + modifier = Modifier.safeDrawingPadding(), + topBar = { + CenterAlignedTopAppBar( + title = { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = "🌳 JsonTree", + style = MaterialTheme.typography.headlineMedium, + color = Color.Black + ) + }, + ) + }, + contentWindowInsets = WindowInsets(top = 60.dp), + ) { padding -> + Column( + modifier = Modifier.fillMaxSize().padding(padding) + ) { + var errorMessage: String? by remember { mutableStateOf(null) } + var json: String by remember { mutableStateOf(simpleJson) } + var colors: TreeColors by remember { mutableStateOf(defaultLightColors) } + var initialState: TreeState by remember { mutableStateOf(TreeState.FIRST_ITEM_EXPANDED) } + var showIndices: Boolean by remember { mutableStateOf(true) } + var showItemCount: Boolean by remember { mutableStateOf(true) } + var expandSingleChildren: Boolean by remember { mutableStateOf(true) } + + FlowRow { + Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = { + errorMessage = null + json = when (json) { + emptyJson -> simpleJson + simpleJson -> complexJson + complexJson -> invalidJson + invalidJson -> emptyJson + else -> throw IllegalStateException("No JSON selected!") + } + }) { + Text( + text = when (json) { + simpleJson -> "Simple Json" + emptyJson -> "Empty Json" + complexJson -> "Complex Json" + invalidJson -> "Invalid Json" + else -> throw IllegalStateException("No JSON selected!") + } + ) + } + + Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = { + val newState = when (initialState) { + TreeState.EXPANDED -> TreeState.COLLAPSED + TreeState.COLLAPSED -> TreeState.FIRST_ITEM_EXPANDED + TreeState.FIRST_ITEM_EXPANDED -> TreeState.EXPANDED + } + initialState = newState + }) { + Text(text = initialState.name) + } + + Button(modifier = Modifier.padding(horizontal = 8.dp), + onClick = { showIndices = !showIndices }) { + Text(text = if (showIndices) "Hide indices" else "Show indices") + } + + Button(modifier = Modifier.padding(horizontal = 8.dp), + onClick = { showItemCount = !showItemCount }) { + Text(text = if (showItemCount) "Hide item count" else "Show item count") + } + + Button(modifier = Modifier.padding(horizontal = 8.dp), onClick = { + colors = + if (colors == defaultLightColors) defaultDarkColors else defaultLightColors + }) { + Text(text = if (colors == defaultLightColors) "Light" else "Dark") + } + + Button(modifier = Modifier.padding(horizontal = 8.dp), + onClick = { expandSingleChildren = !expandSingleChildren }) { + Text(text = if (expandSingleChildren) "Expand children" else "Don't expand children") + } + } + + val pagerState = rememberPagerState(initialPage = 0, pageCount = { 3 }) + + //Pager to test leaving composition + HorizontalPager( + modifier = Modifier.fillMaxWidth().weight(1F), + state = pagerState, + verticalAlignment = Alignment.Top + ) { pageIndex -> + when (pageIndex) { + 0 -> { + val error = errorMessage + if (error != null) { + Text( + text = error, color = Color.Black + ) + } else { + JsonTree( + modifier = Modifier.fillMaxSize() + .horizontalScroll(rememberScrollState()).background( + if (colors == defaultLightColors) Color.White else Color.Black + ), + contentPadding = PaddingValues(16.dp), + json = json, + onLoading = { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Text( + text = "Loading...", + color = if (colors == defaultLightColors) Color.Black else Color.White + ) + } + }, + initialState = initialState, + colors = colors, + showIndices = showIndices, + showItemCount = showItemCount, + expandSingleChildren = expandSingleChildren, + onError = { errorMessage = it.message }, + ) + } + } + + 1 -> { + Text(text = "Page 1") + } + + 2 -> { + Text(text = "Page 2") + } + } + } + } + } +} + + +@Preview +@Composable +fun PreviewMainScreen() = JsonTreeTheme { + MainScreen() +} \ No newline at end of file diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt b/sample/src/commonMain/kotlin/JsonStrings.kt similarity index 99% rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt rename to sample/src/commonMain/kotlin/JsonStrings.kt index d235a9a..5b74cdd 100644 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/JsonStrings.kt +++ b/sample/src/commonMain/kotlin/JsonStrings.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontreedemo +package com.sebastianneubauer.jsontreesample internal val complexJson = """ { diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt b/sample/src/commonMain/kotlin/ui/theme/Color.kt similarity index 79% rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt rename to sample/src/commonMain/kotlin/ui/theme/Color.kt index 672857d..4d63bba 100644 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Color.kt +++ b/sample/src/commonMain/kotlin/ui/theme/Color.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontreedemo.ui.theme +package com.sebastianneubauer.jsontreesample.ui.theme import androidx.compose.ui.graphics.Color diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt b/sample/src/commonMain/kotlin/ui/theme/Shape.kt similarity index 83% rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt rename to sample/src/commonMain/kotlin/ui/theme/Shape.kt index 9bed189..12a9092 100644 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Shape.kt +++ b/sample/src/commonMain/kotlin/ui/theme/Shape.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontreedemo.ui.theme +package com.sebastianneubauer.jsontreesample.ui.theme import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Shapes diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt b/sample/src/commonMain/kotlin/ui/theme/Theme.kt similarity index 95% rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt rename to sample/src/commonMain/kotlin/ui/theme/Theme.kt index 5f399c0..3c35e58 100644 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Theme.kt +++ b/sample/src/commonMain/kotlin/ui/theme/Theme.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontreedemo.ui.theme +package com.sebastianneubauer.jsontreesample.ui.theme import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme diff --git a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt b/sample/src/commonMain/kotlin/ui/theme/Type.kt similarity index 93% rename from app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt rename to sample/src/commonMain/kotlin/ui/theme/Type.kt index 5bf373a..032cf12 100644 --- a/app/src/main/java/com/sebastianneubauer/jsontreedemo/ui/theme/Type.kt +++ b/sample/src/commonMain/kotlin/ui/theme/Type.kt @@ -1,4 +1,4 @@ -package com.sebastianneubauer.jsontreedemo.ui.theme +package com.sebastianneubauer.jsontreesample.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle diff --git a/sample/src/iosMain/kotlin/main.kt b/sample/src/iosMain/kotlin/main.kt new file mode 100644 index 0000000..e4d28f7 --- /dev/null +++ b/sample/src/iosMain/kotlin/main.kt @@ -0,0 +1,5 @@ +import androidx.compose.ui.window.ComposeUIViewController +import com.sebastianneubauer.jsontreesample.App +import platform.UIKit.UIViewController + +fun MainViewController(): UIViewController = ComposeUIViewController { App() } diff --git a/sample/src/jvmMain/kotlin/main.kt b/sample/src/jvmMain/kotlin/main.kt new file mode 100644 index 0000000..8ebcf1a --- /dev/null +++ b/sample/src/jvmMain/kotlin/main.kt @@ -0,0 +1,17 @@ +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import com.sebastianneubauer.jsontreesample.App +import java.awt.Dimension + +fun main() = application { + Window( + title = "JsonTree Sample", + state = rememberWindowState(width = 800.dp, height = 600.dp), + onCloseRequest = ::exitApplication, + ) { + window.minimumSize = Dimension(350, 600) + App() + } +} \ No newline at end of file diff --git a/screenshots/jsonTree.png b/screenshots/jsonTree-android-dark.png similarity index 100% rename from screenshots/jsonTree.png rename to screenshots/jsonTree-android-dark.png diff --git a/screenshots/jsonTree-macos.png b/screenshots/jsonTree-macos.png new file mode 100644 index 0000000..adfa4b5 Binary files /dev/null and b/screenshots/jsonTree-macos.png differ diff --git a/settings.gradle b/settings.gradle.kts similarity index 88% rename from settings.gradle rename to settings.gradle.kts index 3818b8c..1e7ddb7 100644 --- a/settings.gradle +++ b/settings.gradle.kts @@ -13,5 +13,5 @@ dependencyResolutionManagement { } } rootProject.name = "JsonTree" -include ':app' -include ':jsontree' +include(":jsontree") +include(":sample")