├── android ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.webp │ │ │ │ └── ic_launcher_round.webp │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── mipmap-anydpi-v33 │ │ │ │ └── ic_launcher.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ ├── drawable-v24 │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable │ │ │ │ └── ic_launcher_background.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── devexperto │ │ │ │ └── kotlinexpert │ │ │ │ └── MainActivity.kt │ │ └── AndroidManifest.xml │ └── debug │ │ ├── res │ │ └── xml │ │ │ └── network_security_config.xml │ │ └── AndroidManifest.xml ├── proguard-rules.pro └── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── ios ├── NotesAppIos │ ├── Assets.xcassets │ │ ├── Contents.json │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ └── Contents.json │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ ├── NotesAppIosApp.swift │ └── ContentView.swift └── NotesAppIos.xcodeproj │ ├── project.xcworkspace │ ├── contents.xcworkspacedata │ ├── xcuserdata │ │ └── antonio.xcuserdatad │ │ │ └── UserInterfaceState.xcuserstate │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist │ ├── xcuserdata │ └── antonio.xcuserdatad │ │ └── xcschemes │ │ └── xcschememanagement.plist │ └── project.pbxproj ├── common ├── src │ ├── androidMain │ │ ├── AndroidManifest.xml │ │ └── kotlin │ │ │ └── com │ │ │ └── devexperto │ │ │ └── kotlinexpert │ │ │ ├── getPlatformName.kt │ │ │ ├── data │ │ │ └── remote │ │ │ │ └── NotesUrl.kt │ │ │ └── ui │ │ │ └── screens │ │ │ └── DropdownMenu.android.kt │ ├── iosMain │ │ └── kotlin │ │ │ └── com │ │ │ └── devexperto │ │ │ └── kotlinexpert │ │ │ ├── getPlatformName.kt │ │ │ ├── data │ │ │ └── remote │ │ │ │ └── NotesUrl.kt │ │ │ ├── main.kt │ │ │ └── ui │ │ │ └── screens │ │ │ └── DropdownMenu.ios.kt │ ├── jsMain │ │ └── kotlin │ │ │ └── com │ │ │ └── devexperto │ │ │ └── kotlinexpert │ │ │ ├── getPlatformName.kt │ │ │ ├── data │ │ │ └── remote │ │ │ │ └── NotesUrl.kt │ │ │ └── ui │ │ │ ├── common │ │ │ └── Icon.kt │ │ │ ├── screens │ │ │ ├── home │ │ │ │ ├── Home.kt │ │ │ │ ├── NotesList.kt │ │ │ │ └── TopBar.kt │ │ │ └── detail │ │ │ │ └── Detail.kt │ │ │ └── theme │ │ │ └── AppStyleSheet.kt │ ├── desktopMain │ │ └── kotlin │ │ │ └── com │ │ │ └── devexperto │ │ │ └── kotlinexpert │ │ │ ├── getPlatformName.kt │ │ │ ├── data │ │ │ └── remote │ │ │ │ └── NotesUrl.kt │ │ │ └── ui │ │ │ └── screens │ │ │ └── DropdownMenu.desktop.kt │ ├── commonMain │ │ └── kotlin │ │ │ └── com │ │ │ └── devexperto │ │ │ └── kotlinexpert │ │ │ ├── platform.kt │ │ │ ├── ui │ │ │ ├── Route.kt │ │ │ ├── App.kt │ │ │ ├── screens │ │ │ │ ├── detail │ │ │ │ │ └── Detail.kt │ │ │ │ └── home │ │ │ │ │ └── Home.kt │ │ │ └── viewmodels │ │ │ │ ├── HomeViewModel.kt │ │ │ │ └── DetailViewModel.kt │ │ │ └── data │ │ │ ├── Filter.kt │ │ │ ├── remote │ │ │ ├── NotesClient.kt │ │ │ └── NotesRepository.kt │ │ │ └── Note.kt │ └── commonComposeKmpMain │ │ └── kotlin │ │ └── com │ │ └── devexperto │ │ └── kotlinexpert │ │ └── ui │ │ └── screens │ │ ├── DropdownMenu.kt │ │ ├── home │ │ ├── TopBar.kt │ │ ├── Home.kt │ │ └── NotesList.kt │ │ └── detail │ │ └── Detail.kt ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties └── build.gradle.kts ├── web ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── src │ └── jsMain │ │ ├── resources │ │ ├── kotlin-js.html │ │ └── index.html │ │ └── kotlin │ │ └── com │ │ └── devexperto │ │ └── kotlinexpert │ │ └── Main.kt └── build.gradle.kts ├── desktop ├── src │ └── jvmMain │ │ └── kotlin │ │ └── com │ │ └── devexperto │ │ └── kotlinexpert │ │ └── Main.kt └── build.gradle.kts ├── gradle.properties ├── .gitignore ├── settings.gradle.kts ├── gradlew.bat ├── yarn.lock └── gradlew /android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | MyNotes 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/NotesAppIos/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /common/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /web/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/web/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /common/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/common/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /ios/NotesAppIos/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-hdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-hdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-mdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-mdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /common/src/iosMain/kotlin/com/devexperto/kotlinexpert/getPlatformName.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | actual fun getPlatformName(): String = "iOS" -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/getPlatformName.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | actual fun getPlatformName(): String = "Web" -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xxxhdpi/ic_launcher.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-hdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-hdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-mdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-mdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /common/src/androidMain/kotlin/com/devexperto/kotlinexpert/getPlatformName.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | actual fun getPlatformName(): String = "Android" -------------------------------------------------------------------------------- /common/src/desktopMain/kotlin/com/devexperto/kotlinexpert/getPlatformName.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | actual fun getPlatformName(): String = "Desktop" -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/android/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp -------------------------------------------------------------------------------- /common/src/androidMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesUrl.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | actual const val NOTES_URL: String = "http://10.0.2.2:8080/notes" -------------------------------------------------------------------------------- /common/src/iosMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesUrl.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | actual const val NOTES_URL: String = "http://localhost:8080/notes" -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesUrl.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | actual const val NOTES_URL: String = "http://localhost:8080/notes" -------------------------------------------------------------------------------- /common/src/desktopMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesUrl.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | actual const val NOTES_URL: String = "http://localhost:8080/notes" -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/platform.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | expect fun getPlatformName(): String 4 | 5 | fun getAppTitle(): String = "My Notes - ${getPlatformName()}" -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/Route.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui 2 | 3 | sealed interface Route { 4 | object Home : Route 5 | data class Detail(val id: Long) : Route 6 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/data/Filter.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data 2 | 3 | sealed interface Filter { 4 | object All : Filter 5 | class ByType(val type: Note.Type) : Filter 6 | } -------------------------------------------------------------------------------- /ios/NotesAppIos.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/NotesAppIos/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /web/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /common/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /ios/NotesAppIos.xcodeproj/project.xcworkspace/xcuserdata/antonio.xcuserdatad/UserInterfaceState.xcuserstate: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devexpert-io/kotlin-expert/HEAD/ios/NotesAppIos.xcodeproj/project.xcworkspace/xcuserdata/antonio.xcuserdatad/UserInterfaceState.xcuserstate -------------------------------------------------------------------------------- /ios/NotesAppIos/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /web/src/jsMain/resources/kotlin-js.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Kotlin/JS Example 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /android/src/debug/res/xml/network_security_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10.0.2.2 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /ios/NotesAppIos.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/App.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.navigator.Navigator 5 | import com.devexperto.kotlinexpert.ui.screens.home.HomeScreen 6 | 7 | @Composable 8 | fun App() { 9 | Navigator(HomeScreen) 10 | } -------------------------------------------------------------------------------- /ios/NotesAppIos/NotesAppIosApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NotesAppIosApp.swift 3 | // NotesAppIos 4 | // 5 | // Created by Antonio Leiva Gordillo on 11/7/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct NotesAppIosApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /web/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sample 6 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesClient.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | import io.ktor.client.* 4 | import io.ktor.client.plugins.contentnegotiation.* 5 | import io.ktor.serialization.kotlinx.json.* 6 | 7 | val notesClient = HttpClient() { 8 | install(ContentNegotiation) { 9 | json() 10 | } 11 | } -------------------------------------------------------------------------------- /common/src/iosMain/kotlin/com/devexperto/kotlinexpert/main.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | import com.devexperto.kotlinexpert.ui.App 5 | import platform.UIKit.UIViewController 6 | 7 | @Suppress("unused", "FunctionName") 8 | fun MainViewController(): UIViewController { 9 | return ComposeUIViewController { App() } 10 | } -------------------------------------------------------------------------------- /android/src/main/res/mipmap-anydpi-v33/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/data/Note.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Note(val title: String, val description: String, val type: Type, val id: Long = NEW_NOTE) { 7 | companion object { 8 | const val NEW_NOTE = -1L 9 | } 10 | enum class Type { TEXT, AUDIO } 11 | } -------------------------------------------------------------------------------- /desktop/src/jvmMain/kotlin/com/devexperto/kotlinexpert/Main.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | import androidx.compose.ui.window.Window 4 | import androidx.compose.ui.window.application 5 | import com.devexperto.kotlinexpert.ui.App 6 | 7 | fun main() { 8 | application { 9 | Window(onCloseRequest = ::exitApplication, title = getAppTitle()) { 10 | App() 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /android/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.version=1.8.20 3 | agp.version=7.3.0 4 | compose.version=1.4.0 5 | ktor_version=2.1.2 6 | org.gradle.jvmargs=-Xmx4096m 7 | android.useAndroidX=true 8 | kotlin.mpp.androidSourceSetLayoutVersion1.nowarn=true 9 | voyager_version=1.0.0-rc05 10 | 11 | #Compose 12 | org.jetbrains.compose.experimental.jscanvas.enabled=true 13 | org.jetbrains.compose.experimental.uikit.enabled=true 14 | kotlin.native.cacheKind=none -------------------------------------------------------------------------------- /android/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /android/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /android/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /common/src/commonComposeKmpMain/kotlin/com/devexperto/kotlinexpert/ui/screens/DropdownMenu.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | expect fun DropdownMenu(expanded: Boolean, onDismissRequest: () -> Unit, content: @Composable ColumnScope.() -> Unit) 8 | 9 | @Composable 10 | expect fun DropdownMenuItem(onClick: () -> Unit, text: @Composable () -> Unit) -------------------------------------------------------------------------------- /ios/NotesAppIos.xcodeproj/xcuserdata/antonio.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | NotesAppIos.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/src/main/java/com/devexperto/kotlinexpert/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.material.MaterialTheme 7 | import com.devexperto.kotlinexpert.ui.App 8 | 9 | class MainActivity : ComponentActivity() { 10 | override fun onCreate(savedInstanceState: Bundle?) { 11 | super.onCreate(savedInstanceState) 12 | 13 | setContent { 14 | MaterialTheme { 15 | App() 16 | } 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/common/Icon.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.common 2 | 3 | import androidx.compose.runtime.Composable 4 | import org.jetbrains.compose.web.attributes.AttrsScope 5 | import org.jetbrains.compose.web.dom.Span 6 | import org.jetbrains.compose.web.dom.Text 7 | import org.w3c.dom.HTMLSpanElement 8 | 9 | @Composable 10 | fun Icon(iconName: String, attrs: (AttrsScope.() -> Unit)? = null) { 11 | Span( 12 | attrs = { 13 | classes("material-icons") 14 | attrs?.invoke(this) 15 | } 16 | ) { 17 | Text(iconName) 18 | } 19 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | local.properties 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | 8 | ### IntelliJ IDEA ### 9 | .idea/* 10 | *.iws 11 | *.iml 12 | *.ipr 13 | out/ 14 | !**/src/main/**/out/ 15 | !**/src/test/**/out/ 16 | 17 | ### Eclipse ### 18 | .apt_generated 19 | .classpath 20 | .factorypath 21 | .project 22 | .settings 23 | .springBeans 24 | .sts4-cache 25 | bin/ 26 | !**/src/main/**/bin/ 27 | !**/src/test/**/bin/ 28 | 29 | ### NetBeans ### 30 | /nbproject/private/ 31 | /nbbuild/ 32 | /dist/ 33 | /nbdist/ 34 | /.nb-gradle/ 35 | 36 | ### VS Code ### 37 | .vscode/ 38 | 39 | ### Mac OS ### 40 | .DS_Store -------------------------------------------------------------------------------- /ios/NotesAppIos/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // NotesAppIos 4 | // 5 | // Created by Antonio Leiva Gordillo on 11/7/23. 6 | // 7 | 8 | import SwiftUI 9 | import common 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | ComposeView() 14 | } 15 | } 16 | 17 | struct ComposeView: UIViewControllerRepresentable { 18 | func makeUIViewController(context: Context) -> UIViewController { 19 | MainKt.MainViewController() 20 | } 21 | 22 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 23 | } 24 | 25 | struct ContentView_Previews: PreviewProvider { 26 | static var previews: some View { 27 | ContentView() 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/iosMain/kotlin/com/devexperto/kotlinexpert/ui/screens/DropdownMenu.ios.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun DropdownMenu( 8 | expanded: Boolean, 9 | onDismissRequest: () -> Unit, 10 | content: @Composable ColumnScope.() -> Unit 11 | ) { 12 | androidx.compose.material3.DropdownMenu( 13 | expanded = expanded, 14 | onDismissRequest = onDismissRequest, 15 | content = content 16 | ) 17 | } 18 | 19 | @Composable 20 | actual fun DropdownMenuItem(onClick: () -> Unit, text: @Composable () -> Unit) { 21 | androidx.compose.material3.DropdownMenuItem( 22 | onClick = onClick, 23 | text = text 24 | ) 25 | } -------------------------------------------------------------------------------- /common/src/androidMain/kotlin/com/devexperto/kotlinexpert/ui/screens/DropdownMenu.android.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun DropdownMenu( 8 | expanded: Boolean, 9 | onDismissRequest: () -> Unit, 10 | content: @Composable ColumnScope.() -> Unit 11 | ) { 12 | androidx.compose.material3.DropdownMenu( 13 | expanded = expanded, 14 | onDismissRequest = onDismissRequest, 15 | content = content 16 | ) 17 | } 18 | 19 | @Composable 20 | actual fun DropdownMenuItem(onClick: () -> Unit, text: @Composable () -> Unit) { 21 | androidx.compose.material3.DropdownMenuItem( 22 | onClick = onClick, 23 | text = text 24 | ) 25 | } -------------------------------------------------------------------------------- /common/src/desktopMain/kotlin/com/devexperto/kotlinexpert/ui/screens/DropdownMenu.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens 2 | 3 | import androidx.compose.foundation.layout.ColumnScope 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | actual fun DropdownMenu( 8 | expanded: Boolean, 9 | onDismissRequest: () -> Unit, 10 | content: @Composable ColumnScope.() -> Unit 11 | ) { 12 | androidx.compose.material3.DropdownMenu( 13 | expanded = expanded, 14 | onDismissRequest = onDismissRequest, 15 | content = content 16 | ) 17 | } 18 | 19 | @Composable 20 | actual fun DropdownMenuItem(onClick: () -> Unit, text: @Composable () -> Unit) { 21 | androidx.compose.material3.DropdownMenuItem( 22 | onClick = onClick, 23 | text = text 24 | ) 25 | } -------------------------------------------------------------------------------- /android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/screens/detail/Detail.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.detail 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import cafe.adriel.voyager.core.model.rememberScreenModel 6 | import cafe.adriel.voyager.core.screen.Screen 7 | import cafe.adriel.voyager.navigator.LocalNavigator 8 | import cafe.adriel.voyager.navigator.currentOrThrow 9 | import com.devexperto.kotlinexpert.ui.viewmodels.DetailViewModel 10 | 11 | data class DetailScreen(val noteId: Long) : Screen { 12 | 13 | @Composable 14 | override fun Content() { 15 | val navigator = LocalNavigator.currentOrThrow 16 | Detail( 17 | vm = rememberScreenModel { DetailViewModel(noteId) }, 18 | onClose = { navigator.pop() }) 19 | } 20 | 21 | } 22 | 23 | @Composable 24 | expect fun Detail(vm: DetailViewModel, onClose: () -> Unit) -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. 2 | pluginManagement { 3 | repositories { 4 | google() 5 | gradlePluginPortal() 6 | mavenCentral() 7 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 8 | } 9 | 10 | plugins { 11 | kotlin("multiplatform").version(extra["kotlin.version"] as String) 12 | kotlin("android").version(extra["kotlin.version"] as String) 13 | kotlin("plugin.serialization").version(extra["kotlin.version"] as String) 14 | id("org.jetbrains.compose").version(extra["compose.version"] as String) 15 | id("com.android.application").version(extra["agp.version"] as String) 16 | id("com.android.library").version(extra["agp.version"] as String) 17 | } 18 | } 19 | 20 | rootProject.name = "MyNotes" 21 | 22 | include(":common", ":desktop", ":web", ":android") 23 | 24 | -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/Home.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.rememberCoroutineScope 5 | import cafe.adriel.voyager.core.model.rememberScreenModel 6 | import cafe.adriel.voyager.core.screen.Screen 7 | import cafe.adriel.voyager.navigator.LocalNavigator 8 | import cafe.adriel.voyager.navigator.currentOrThrow 9 | import com.devexperto.kotlinexpert.ui.screens.detail.DetailScreen 10 | import com.devexperto.kotlinexpert.ui.viewmodels.HomeViewModel 11 | 12 | object HomeScreen : Screen { 13 | 14 | @Composable 15 | override fun Content() { 16 | val navigator = LocalNavigator.currentOrThrow 17 | Home( 18 | vm = rememberScreenModel { HomeViewModel() }, 19 | onNoteClick = { navigator.push(DetailScreen(it)) } 20 | ) 21 | } 22 | 23 | } 24 | 25 | @Composable 26 | expect fun Home(vm: HomeViewModel, onNoteClick: (Long) -> Unit) -------------------------------------------------------------------------------- /desktop/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UNUSED_VARIABLE") 2 | 3 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 4 | 5 | plugins { 6 | kotlin("multiplatform") 7 | id("org.jetbrains.compose") 8 | } 9 | 10 | group = "com.devexperto.kotlinexpert" 11 | version = "1.0-SNAPSHOT" 12 | 13 | kotlin { 14 | jvm { 15 | jvmToolchain(11) 16 | withJava() 17 | } 18 | sourceSets { 19 | val jvmMain by getting { 20 | dependencies { 21 | implementation(project(":common")) 22 | implementation(compose.desktop.currentOs) 23 | } 24 | } 25 | val jvmTest by getting 26 | } 27 | } 28 | 29 | compose.desktop { 30 | application { 31 | mainClass = "com.devexperto.kotlinexpert.MainKt" 32 | nativeDistributions { 33 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 34 | packageName = "MyNotes" 35 | packageVersion = "1.0.0" 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /web/src/jsMain/kotlin/com/devexperto/kotlinexpert/Main.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert 2 | 3 | import com.devexperto.kotlinexpert.ui.App 4 | import com.devexperto.kotlinexpert.ui.theme.AppStyleSheet 5 | import kotlinx.browser.document 6 | import kotlinx.browser.window 7 | import org.jetbrains.compose.web.css.Style 8 | import org.jetbrains.compose.web.renderComposable 9 | 10 | fun main() { 11 | composeSample() 12 | kotlinJsSample() 13 | } 14 | 15 | private fun composeSample() { 16 | document.getElementById("root") ?: return 17 | renderComposable(rootElementId = "root") { 18 | Style(AppStyleSheet) 19 | App() 20 | } 21 | } 22 | 23 | private fun kotlinJsSample() { 24 | window.onload = { 25 | val message = document.getElementById("message") 26 | if (message != null) { 27 | message.textContent = "Hello, Kotlin/JS!" 28 | 29 | val button = document.getElementById("button")!! 30 | button.addEventListener("click", { 31 | window.alert("You clicked the button!") 32 | }) 33 | } 34 | } 35 | } 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | plugins { 4 | id("org.jetbrains.compose") 5 | id("com.android.application") 6 | id("org.jetbrains.kotlin.android") 7 | } 8 | 9 | android { 10 | namespace = "com.devexperto.kotlinexpert" 11 | compileSdk = 33 12 | 13 | defaultConfig { 14 | applicationId = "com.devexperto.kotlinexpert" 15 | minSdk = 24 16 | targetSdk = 33 17 | versionCode = 1 18 | versionName = "1.0" 19 | 20 | testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" 21 | } 22 | 23 | buildTypes { 24 | release { 25 | isMinifyEnabled = false 26 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 27 | } 28 | } 29 | compileOptions { 30 | sourceCompatibility = JavaVersion.VERSION_1_8 31 | targetCompatibility = JavaVersion.VERSION_1_8 32 | } 33 | } 34 | 35 | dependencies { 36 | implementation(project(":common")) 37 | implementation("androidx.activity:activity-compose:1.7.0") 38 | implementation("androidx.appcompat:appcompat:1.6.1") 39 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/data/remote/NotesRepository.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.data.remote 2 | 3 | import com.devexperto.kotlinexpert.data.Note 4 | import io.ktor.client.call.* 5 | import io.ktor.client.request.* 6 | import io.ktor.http.* 7 | 8 | expect val NOTES_URL: String 9 | 10 | object NotesRepository { 11 | 12 | suspend fun save(note: Note) { 13 | notesClient.post(NOTES_URL) { 14 | setBody(note) 15 | contentType(ContentType.Application.Json) 16 | } 17 | } 18 | 19 | suspend fun getAll(): List { 20 | val response = notesClient.request(NOTES_URL) 21 | return response.body() 22 | } 23 | 24 | suspend fun getById(id: Long): Note { 25 | val response = notesClient.request("$NOTES_URL/$id") 26 | return response.body() 27 | } 28 | 29 | suspend fun update(note: Note) { 30 | notesClient.put(NOTES_URL) { 31 | setBody(note) 32 | contentType(ContentType.Application.Json) 33 | } 34 | } 35 | 36 | suspend fun delete(note: Note) { 37 | notesClient.delete("$NOTES_URL/${note.id}") 38 | } 39 | } -------------------------------------------------------------------------------- /web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("multiplatform") 3 | id("org.jetbrains.compose") 4 | } 5 | 6 | group "com.devexperto.kotlinexpert" 7 | version "1.0-SNAPSHOT" 8 | 9 | repositories { 10 | google() 11 | mavenCentral() 12 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 13 | } 14 | 15 | kotlin { 16 | js(IR) { 17 | browser { 18 | testTask { 19 | testLogging.showStandardStreams = true 20 | useKarma { 21 | useChromeHeadless() 22 | useFirefox() 23 | } 24 | } 25 | } 26 | binaries.executable() 27 | } 28 | sourceSets { 29 | val jsMain by getting { 30 | dependencies { 31 | implementation(project(":common")) 32 | implementation(compose.web.core) 33 | implementation(compose.runtime) 34 | } 35 | } 36 | val jsTest by getting { 37 | dependencies { 38 | implementation(kotlin("test-js")) 39 | } 40 | } 41 | } 42 | } 43 | 44 | // Workaround for https://youtrack.jetbrains.com/issue/KTIJ-16480 45 | tasks.register("prepareKotlinBuildScriptModel"){} -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/Home.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | 4 | import androidx.compose.runtime.Composable 5 | import com.devexperto.kotlinexpert.data.Note 6 | import com.devexperto.kotlinexpert.ui.common.Icon 7 | import com.devexperto.kotlinexpert.ui.theme.AppStyleSheet 8 | import com.devexperto.kotlinexpert.ui.viewmodels.HomeViewModel 9 | import org.jetbrains.compose.web.css.* 10 | import org.jetbrains.compose.web.dom.Div 11 | import org.jetbrains.compose.web.dom.Text 12 | 13 | @Composable 14 | actual fun Home(vm: HomeViewModel, onNoteClick: (noteId: Long) -> Unit) { 15 | Div(attrs = { 16 | style { 17 | display(DisplayStyle.Flex) 18 | flexDirection(FlexDirection.Column) 19 | width(100.percent) 20 | height(100.percent) 21 | } 22 | } 23 | 24 | ) { 25 | TopBar(onFilterClick = vm::onFilterClick) 26 | 27 | Div { 28 | 29 | if (vm.state.loading) { 30 | Text("Cargando...") 31 | } 32 | 33 | vm.state.filteredNotes?.let { notes -> 34 | NotesList( 35 | notes = notes, 36 | onNoteClick = { onNoteClick(it.id) } 37 | ) 38 | } 39 | } 40 | 41 | Div( 42 | attrs = { 43 | classes(AppStyleSheet.fab) 44 | onClick { onNoteClick(Note.NEW_NOTE) } 45 | } 46 | ) { 47 | Icon("add") 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/viewmodels/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.viewmodels 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import cafe.adriel.voyager.core.model.ScreenModel 7 | import cafe.adriel.voyager.core.model.coroutineScope 8 | import com.devexperto.kotlinexpert.data.Filter 9 | import com.devexperto.kotlinexpert.data.Note 10 | import com.devexperto.kotlinexpert.data.remote.NotesRepository 11 | import kotlinx.coroutines.CoroutineScope 12 | import kotlinx.coroutines.launch 13 | 14 | class HomeViewModel : ScreenModel { 15 | 16 | var state by mutableStateOf(UiState()) 17 | private set 18 | 19 | init { 20 | loadNotes() 21 | } 22 | 23 | private fun loadNotes() { 24 | coroutineScope.launch { 25 | state = UiState(loading = true) 26 | val response = NotesRepository.getAll() 27 | state = UiState(notes = response) 28 | } 29 | } 30 | 31 | fun onFilterClick(filter: Filter) { 32 | state = state.copy(filter = filter) 33 | } 34 | 35 | data class UiState( 36 | val notes: List? = null, 37 | val loading: Boolean = false, 38 | val filter: Filter = Filter.All, 39 | ) { 40 | val filteredNotes: List? 41 | get() = notes?.let { 42 | when (filter) { 43 | Filter.All -> notes 44 | is Filter.ByType -> notes.filter { it.type == filter.type } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /common/src/commonComposeKmpMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/TopBar.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.material3.* 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.filled.FilterList 6 | import androidx.compose.runtime.* 7 | import com.devexperto.kotlinexpert.data.Filter 8 | import com.devexperto.kotlinexpert.data.Note 9 | import com.devexperto.kotlinexpert.getAppTitle 10 | import com.devexperto.kotlinexpert.ui.screens.DropdownMenu 11 | import com.devexperto.kotlinexpert.ui.screens.DropdownMenuItem 12 | 13 | @OptIn(ExperimentalMaterial3Api::class) 14 | @Composable 15 | fun TopBar(onFilterClick: (Filter) -> Unit) { 16 | TopAppBar( 17 | title = { Text(getAppTitle()) }, 18 | actions = { FiltersAction(onFilterClick) } 19 | ) 20 | } 21 | 22 | @Composable 23 | private fun FiltersAction(onFilterClick: (Filter) -> Unit) { 24 | var expanded by remember { mutableStateOf(false) } 25 | 26 | IconButton(onClick = { expanded = true }) { 27 | 28 | Icon(imageVector = Icons.Default.FilterList, contentDescription = "Filter") 29 | 30 | DropdownMenu(expanded, onDismissRequest = { expanded = false }) { 31 | listOf( 32 | Filter.All to "All", 33 | Filter.ByType(Note.Type.TEXT) to "Text", 34 | Filter.ByType(Note.Type.AUDIO) to "Audio" 35 | ).forEach { (filter, text) -> 36 | DropdownMenuItem(onClick = { 37 | onFilterClick(filter) 38 | expanded = false 39 | }) { 40 | Text(text) 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/NotesList.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.devexperto.kotlinexpert.data.Note 5 | import com.devexperto.kotlinexpert.ui.theme.AppStyleSheet 6 | import org.jetbrains.compose.web.css.* 7 | import org.jetbrains.compose.web.dom.Div 8 | import org.jetbrains.compose.web.dom.H2 9 | import org.jetbrains.compose.web.dom.Span 10 | import org.jetbrains.compose.web.dom.Text 11 | 12 | @Composable 13 | fun NotesList(notes: List, onNoteClick: (Note) -> Unit) { 14 | Div( 15 | attrs = { 16 | style { 17 | display(DisplayStyle.Flex) 18 | flexDirection(FlexDirection.Column) 19 | alignItems(AlignItems.Center) 20 | width(100.percent) 21 | height(100.percent) 22 | } 23 | }, 24 | 25 | ) { 26 | notes.forEach { note -> 27 | NoteCard(note, onNoteClick) 28 | } 29 | } 30 | } 31 | 32 | @Composable 33 | fun NoteCard(note: Note, onNoteClick: (Note) -> Unit) { 34 | Div( 35 | attrs = { 36 | classes(AppStyleSheet.noteCard) 37 | onClick { onNoteClick(note) } 38 | } 39 | ) { 40 | Div(attrs = { classes(AppStyleSheet.noteCardHeader) }) { 41 | H2(attrs = { classes(AppStyleSheet.noteCardTitle) }) { 42 | Text(note.title) 43 | } 44 | 45 | if (note.type == Note.Type.AUDIO) { 46 | Span(attrs = { style { marginLeft(8.px) } }) { 47 | Text("🎤") 48 | } 49 | } 50 | } 51 | 52 | Div { 53 | Text(note.description) 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /common/src/commonComposeKmpMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/Home.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.* 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.Add 9 | import androidx.compose.runtime.Composable 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import com.devexperto.kotlinexpert.data.Note 13 | import com.devexperto.kotlinexpert.ui.viewmodels.HomeViewModel 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | actual fun Home(vm: HomeViewModel, onNoteClick: (Long) -> Unit) { 18 | 19 | MaterialTheme { 20 | Scaffold( 21 | topBar = { TopBar(onFilterClick = vm::onFilterClick) }, 22 | floatingActionButton = { 23 | FloatingActionButton(onClick = { onNoteClick(Note.NEW_NOTE) }) { 24 | Icon(imageVector = Icons.Default.Add, contentDescription = "Add Note") 25 | } 26 | } 27 | ) { padding -> 28 | Box( 29 | contentAlignment = Alignment.Center, 30 | modifier = Modifier.padding(padding).fillMaxSize() 31 | ) { 32 | if (vm.state.loading) { 33 | CircularProgressIndicator() 34 | } 35 | 36 | vm.state.filteredNotes?.let { notes -> 37 | NotesList( 38 | notes = notes, 39 | onNoteClick = { onNoteClick(it.id) } 40 | ) 41 | } 42 | } 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /common/src/commonMain/kotlin/com/devexperto/kotlinexpert/ui/viewmodels/DetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.viewmodels 2 | 3 | import androidx.compose.runtime.getValue 4 | import androidx.compose.runtime.mutableStateOf 5 | import androidx.compose.runtime.setValue 6 | import cafe.adriel.voyager.core.model.ScreenModel 7 | import cafe.adriel.voyager.core.model.coroutineScope 8 | import com.devexperto.kotlinexpert.data.Note 9 | import com.devexperto.kotlinexpert.data.remote.NotesRepository 10 | import kotlinx.coroutines.launch 11 | 12 | class DetailViewModel(private val noteId: Long) : ScreenModel { 13 | 14 | var state by mutableStateOf(UiState()) 15 | private set 16 | 17 | init { 18 | if (noteId != Note.NEW_NOTE) { 19 | loadNote() 20 | } 21 | } 22 | 23 | private fun loadNote() { 24 | coroutineScope.launch { 25 | state = UiState(loading = true) 26 | state = UiState(note = NotesRepository.getById(noteId)) 27 | } 28 | } 29 | 30 | fun save() { 31 | coroutineScope.launch { 32 | val note = state.note 33 | if (note.id == Note.NEW_NOTE) { 34 | NotesRepository.save(note) 35 | } else { 36 | NotesRepository.update(note) 37 | } 38 | state = state.copy(saved = true) 39 | } 40 | } 41 | 42 | fun update(note: Note) { 43 | state = state.copy(note = note) 44 | } 45 | 46 | fun delete() { 47 | coroutineScope.launch { 48 | NotesRepository.delete(state.note) 49 | state = state.copy(saved = true) 50 | } 51 | } 52 | 53 | data class UiState( 54 | val note: Note = Note("", "", Note.Type.TEXT), 55 | val loading: Boolean = false, 56 | val saved: Boolean = false 57 | ) 58 | } -------------------------------------------------------------------------------- /android/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 10 | 16 | 19 | 22 | 23 | 24 | 25 | 31 | -------------------------------------------------------------------------------- /common/src/commonComposeKmpMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/NotesList.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyColumn 6 | import androidx.compose.foundation.lazy.items 7 | import androidx.compose.material3.* 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.filled.Mic 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.unit.dp 14 | import com.devexperto.kotlinexpert.data.Note 15 | 16 | @Composable 17 | fun NotesList(notes: List, onNoteClick: (Note) -> Unit) { 18 | LazyColumn( 19 | modifier = Modifier.fillMaxSize(), horizontalAlignment = Alignment.CenterHorizontally 20 | ) { 21 | items(notes) { note -> 22 | Card( 23 | modifier = Modifier 24 | .padding(8.dp) 25 | .fillMaxWidth(0.8f) 26 | .clickable { onNoteClick(note) } 27 | ) { 28 | Column( 29 | modifier = Modifier.padding(16.dp) 30 | ) { 31 | Row { 32 | Text( 33 | text = note.title, style = MaterialTheme.typography.titleLarge, modifier = Modifier.weight(1f) 34 | ) 35 | if (note.type == Note.Type.AUDIO) { 36 | Icon( 37 | imageVector = Icons.Default.Mic, contentDescription = null 38 | ) 39 | } 40 | } 41 | 42 | Spacer(modifier = Modifier.height(8.dp)) 43 | Text(text = note.description) 44 | } 45 | } 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/screens/home/TopBar.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.home 2 | 3 | import androidx.compose.runtime.* 4 | import com.devexperto.kotlinexpert.data.Filter 5 | import com.devexperto.kotlinexpert.data.Note 6 | import com.devexperto.kotlinexpert.ui.common.Icon 7 | import com.devexperto.kotlinexpert.ui.theme.AppStyleSheet 8 | import org.jetbrains.compose.web.dom.Div 9 | import org.jetbrains.compose.web.dom.H1 10 | import org.jetbrains.compose.web.dom.Text 11 | 12 | @Composable 13 | fun TopBar(onFilterClick: (Filter) -> Unit) { 14 | Div(attrs = { classes(AppStyleSheet.topBar) }) { 15 | H1(attrs = { classes(AppStyleSheet.topBarTitle) }) { 16 | Text("Mis notas") 17 | } 18 | FiltersAction(onFilterClick) 19 | } 20 | } 21 | 22 | @Composable 23 | fun FiltersAction(onFilterClick: (Filter) -> Unit) { 24 | var expanded by remember { mutableStateOf(false) } 25 | 26 | Div { 27 | Icon( 28 | iconName = "filter_list", 29 | attrs = { 30 | classes(AppStyleSheet.topBarIcon) 31 | onClick { expanded = !expanded } 32 | }) 33 | 34 | if (expanded) { 35 | Div(attrs = { classes(AppStyleSheet.filtersActionExpanded) }) { 36 | listOf( 37 | Filter.All to "All", 38 | Filter.ByType(Note.Type.TEXT) to "Text", 39 | Filter.ByType(Note.Type.AUDIO) to "Audio" 40 | ).forEach { (filter, text) -> 41 | Div(attrs = { 42 | classes(AppStyleSheet.filtersActionExpandedItem) 43 | onClick { 44 | onFilterClick(filter) 45 | expanded = false 46 | } 47 | }) { 48 | Text(text) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/theme/AppStyleSheet.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.theme 2 | 3 | import org.jetbrains.compose.web.css.* 4 | import org.jetbrains.compose.web.css.keywords.auto 5 | 6 | object AppStyleSheet : StyleSheet() { 7 | 8 | init { 9 | "*" style { 10 | fontFamily("Verdana", "sans-serif") 11 | } 12 | 13 | "body" style { 14 | margin(0.px) 15 | } 16 | } 17 | 18 | val fab by style { 19 | display(DisplayStyle.Flex) 20 | justifyContent(JustifyContent.Center) 21 | alignItems(AlignItems.Center) 22 | position(Position.Fixed) 23 | bottom(16.px) 24 | right(16.px) 25 | width(64.px) 26 | height(64.px) 27 | borderRadius(50.percent) 28 | backgroundColor(Color("#6200EE")) 29 | color(Color.white) 30 | fontSize(24.px) 31 | lineHeight(1.em) 32 | cursor("pointer") 33 | property("box-shadow", "0 5px 5px 0 rgba(0, 0, 0, 0.4)") 34 | self + hover style { 35 | backgroundColor(Color("#6200eecc")) 36 | } 37 | } 38 | 39 | val topBar by style { 40 | display(DisplayStyle.Flex) 41 | flexDirection(FlexDirection.Row) 42 | alignItems(AlignItems.Center) 43 | gap(16.px) 44 | padding(16.px) 45 | backgroundColor(Color("#6200EE")) 46 | } 47 | 48 | val topBarTitle by style { 49 | color(Color.white) 50 | margin(0.px) 51 | fontSize(25.px) 52 | fontWeight("normal") 53 | property("margin-right", auto) 54 | } 55 | 56 | val topBarIcon by style { 57 | color(Color.white) 58 | fontSize(24.px) 59 | cursor("pointer") 60 | borderRadius(50.percent) 61 | padding(12.px) 62 | self + hover style { 63 | backgroundColor(Color("#ffffff4d")) 64 | } 65 | } 66 | 67 | val filtersActionExpanded by style { 68 | display(DisplayStyle.Flex) 69 | flexDirection(FlexDirection.Column) 70 | alignItems(AlignItems.Center) 71 | backgroundColor(Color.white) 72 | borderRadius(4.px) 73 | position(Position.Absolute) 74 | top(16.px) 75 | right(16.px) 76 | property("box-shadow", "0 0 4px rgba(0, 0, 0, 0.25)") 77 | property("z-index", 1) 78 | } 79 | 80 | val filtersActionExpandedItem by style { 81 | justifyContent(JustifyContent.Center) 82 | alignItems(AlignItems.Center) 83 | padding(16.px) 84 | minWidth(150.px) 85 | cursor("pointer") 86 | } 87 | 88 | val noteCard by style { 89 | display(DisplayStyle.Flex) 90 | flexDirection(FlexDirection.Column) 91 | width(80.percent) 92 | maxWidth(600.px) 93 | marginTop(8.px) 94 | marginBottom(8.px) 95 | border(1.px, LineStyle.Solid, Color.black) 96 | borderRadius(4.px) 97 | padding(16.px) 98 | cursor("pointer") 99 | } 100 | 101 | val noteCardHeader by style { 102 | display(DisplayStyle.Flex) 103 | flexDirection(FlexDirection.Row) 104 | alignItems(AlignItems.Center) 105 | width(100.percent) 106 | } 107 | 108 | val noteCardTitle by style { 109 | flex(1) 110 | lineHeight(1.5.em) 111 | margin(0.px) 112 | fontSize(20.px) 113 | fontWeight("normal") 114 | } 115 | 116 | val detailInput by style { 117 | padding(16.px) 118 | borderRadius(4.px) 119 | border(1.px, LineStyle.Solid, Color("#ccc")) 120 | } 121 | } -------------------------------------------------------------------------------- /common/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage", "OPT_IN_USAGE", "PropertyName") 2 | 3 | val ktor_version: String by rootProject.project 4 | val voyager_version: String by rootProject.project 5 | 6 | plugins { 7 | kotlin("multiplatform") 8 | kotlin("plugin.serialization") 9 | id("org.jetbrains.compose") 10 | id("com.android.library") 11 | } 12 | 13 | group = "com.example" 14 | version = "1.0-SNAPSHOT" 15 | 16 | kotlin { 17 | targetHierarchy.default() 18 | android() 19 | jvm("desktop") { 20 | jvmToolchain(11) 21 | 22 | } 23 | js(IR) { 24 | browser() 25 | } 26 | 27 | listOf( 28 | iosArm64(), 29 | iosX64(), 30 | iosSimulatorArm64() 31 | ).forEach { 32 | it.binaries.framework { 33 | baseName = "common" 34 | linkerOpts("-framework", "CoreGraphics") 35 | linkerOpts("-framework", "CoreText") 36 | linkerOpts("-framework", "Metal") 37 | } 38 | } 39 | 40 | sourceSets { 41 | val commonMain by getting { 42 | dependencies { 43 | api(compose.runtime) 44 | api(compose.foundation) 45 | api(compose.material3) 46 | implementation(compose.materialIconsExtended) 47 | implementation("io.ktor:ktor-client-core:$ktor_version") 48 | implementation("io.ktor:ktor-client-content-negotiation:$ktor_version") 49 | implementation("io.ktor:ktor-serialization-kotlinx-json:$ktor_version") 50 | implementation("cafe.adriel.voyager:voyager-navigator:$voyager_version") 51 | } 52 | } 53 | val commonTest by getting { 54 | dependencies { 55 | implementation(kotlin("test")) 56 | } 57 | } 58 | 59 | val commonComposeKmpMain by creating { 60 | dependsOn(commonMain) 61 | } 62 | 63 | val desktopMain by getting { 64 | dependsOn(commonComposeKmpMain) 65 | dependencies { 66 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-swing:1.6.4") 67 | implementation("io.ktor:ktor-client-okhttp:$ktor_version") 68 | } 69 | } 70 | val desktopTest by getting 71 | 72 | val androidMain by getting { 73 | dependsOn(commonComposeKmpMain) 74 | dependencies { 75 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4") 76 | implementation("io.ktor:ktor-client-okhttp:$ktor_version") 77 | } 78 | } 79 | 80 | val androidTest by getting 81 | 82 | val jsMain by getting { 83 | dependencies { 84 | implementation(compose.web.core) 85 | implementation(compose.runtime) 86 | implementation("io.ktor:ktor-client-js:$ktor_version") 87 | } 88 | } 89 | 90 | val jsTest by getting 91 | 92 | val iosMain by getting { 93 | dependsOn(commonComposeKmpMain) 94 | dependencies { 95 | implementation("io.ktor:ktor-client-darwin:$ktor_version") 96 | } 97 | } 98 | } 99 | } 100 | 101 | android { 102 | compileSdk = 33 103 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 104 | defaultConfig { 105 | minSdk = 24 106 | targetSdk = 33 107 | } 108 | compileOptions { 109 | sourceCompatibility = JavaVersion.VERSION_1_8 110 | targetCompatibility = JavaVersion.VERSION_1_8 111 | } 112 | } -------------------------------------------------------------------------------- /common/src/jsMain/kotlin/com/devexperto/kotlinexpert/ui/screens/detail/Detail.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.detail 2 | 3 | import androidx.compose.runtime.Composable 4 | import com.devexperto.kotlinexpert.data.Note 5 | import com.devexperto.kotlinexpert.ui.common.Icon 6 | import com.devexperto.kotlinexpert.ui.theme.AppStyleSheet 7 | import com.devexperto.kotlinexpert.ui.viewmodels.DetailViewModel 8 | import org.jetbrains.compose.web.attributes.placeholder 9 | import org.jetbrains.compose.web.attributes.selected 10 | import org.jetbrains.compose.web.css.* 11 | import org.jetbrains.compose.web.dom.* 12 | 13 | @Composable 14 | actual fun Detail(vm: DetailViewModel, onClose: () -> Unit) { 15 | 16 | val note = vm.state.note 17 | 18 | Div { 19 | TopBar( 20 | note = note, 21 | onClose = onClose, 22 | onSave = vm::save, 23 | onDelete = vm::delete 24 | ) 25 | 26 | if (vm.state.saved) { 27 | onClose() 28 | } 29 | 30 | if (vm.state.loading) { 31 | Text("Cargando...") 32 | } else { 33 | Div( 34 | attrs = { 35 | style { 36 | padding(32.px) 37 | display(DisplayStyle.Flex) 38 | flexDirection(FlexDirection.Column) 39 | gap(16.px) 40 | maxWidth(600.px) 41 | property("margin", "0 auto") 42 | } 43 | } 44 | ) { 45 | TextInput( 46 | value = note.title, 47 | attrs = { 48 | classes(AppStyleSheet.detailInput) 49 | placeholder("Title") 50 | onInput { vm.update(note.copy(title = it.value)) } 51 | } 52 | ) 53 | TypeDropdown( 54 | value = note.type, 55 | onValueChange = { vm.update(note.copy(type = it)) } 56 | ) 57 | TextArea( 58 | value = note.description, 59 | attrs = { 60 | classes(AppStyleSheet.detailInput) 61 | placeholder("Description") 62 | onInput { vm.update(note.copy(description = it.value)) } 63 | } 64 | ) 65 | } 66 | } 67 | } 68 | } 69 | 70 | @Composable 71 | private fun TypeDropdown(value: Note.Type, onValueChange: (Note.Type) -> Unit) { 72 | Select( 73 | attrs = { 74 | classes(AppStyleSheet.detailInput) 75 | onChange { onValueChange(Note.Type.valueOf(it.target.value)) } 76 | } 77 | ) { 78 | Note.Type.values().forEach { 79 | Option( 80 | value = it.name, 81 | attrs = { 82 | if (it == value) { 83 | selected() 84 | } 85 | }) { 86 | Text(it.name) 87 | } 88 | } 89 | } 90 | } 91 | 92 | @Composable 93 | private fun TopBar(note: Note, onClose: () -> Unit, onSave: () -> Unit, onDelete: () -> Unit) { 94 | Div(attrs = { classes(AppStyleSheet.topBar) }) { 95 | Icon( 96 | iconName = "arrow_back", 97 | attrs = { 98 | classes(AppStyleSheet.topBarIcon) 99 | onClick { onClose() } 100 | } 101 | ) 102 | 103 | H1(attrs = { classes(AppStyleSheet.topBarTitle) }) { 104 | Text(note.title) 105 | } 106 | 107 | Icon( 108 | iconName = "save", 109 | attrs = { 110 | classes(AppStyleSheet.topBarIcon) 111 | onClick { onSave() } 112 | } 113 | ) 114 | 115 | if (note.id != Note.NEW_NOTE) { 116 | Icon( 117 | iconName = "delete", 118 | attrs = { 119 | classes(AppStyleSheet.topBarIcon) 120 | onClick { onDelete() } 121 | } 122 | ) 123 | } 124 | } 125 | } -------------------------------------------------------------------------------- /common/src/commonComposeKmpMain/kotlin/com/devexperto/kotlinexpert/ui/screens/detail/Detail.kt: -------------------------------------------------------------------------------- 1 | package com.devexperto.kotlinexpert.ui.screens.detail 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.ArrowDropDown 9 | import androidx.compose.material.icons.filled.Close 10 | import androidx.compose.material.icons.filled.Delete 11 | import androidx.compose.material.icons.filled.Save 12 | import androidx.compose.material3.* 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.unit.dp 16 | import com.devexperto.kotlinexpert.data.Note 17 | import com.devexperto.kotlinexpert.ui.screens.DropdownMenu 18 | import com.devexperto.kotlinexpert.ui.screens.DropdownMenuItem 19 | import com.devexperto.kotlinexpert.ui.viewmodels.DetailViewModel 20 | 21 | @OptIn(ExperimentalMaterial3Api::class) 22 | @Composable 23 | actual fun Detail(vm: DetailViewModel, onClose: () -> Unit) { 24 | 25 | val note = vm.state.note 26 | 27 | Scaffold( 28 | topBar = { 29 | TopBar( 30 | note = note, 31 | onClose = onClose, 32 | onSave = vm::save, 33 | onDelete = vm::delete 34 | ) 35 | } 36 | ) { padding -> 37 | if (vm.state.saved) { 38 | onClose() 39 | } 40 | 41 | Box(modifier = Modifier.padding(padding)) { 42 | if (vm.state.loading) { 43 | CircularProgressIndicator() 44 | } else { 45 | Column( 46 | modifier = Modifier.padding(32.dp) 47 | ) { 48 | OutlinedTextField( 49 | value = note.title, 50 | onValueChange = { vm.update(note.copy(title = it)) }, 51 | modifier = Modifier.fillMaxWidth(), 52 | label = { Text("Title") }, 53 | maxLines = 1 54 | ) 55 | TypeDropdown( 56 | value = note.type, 57 | onValueChange = { vm.update(note.copy(type = it)) }, 58 | modifier = Modifier.fillMaxWidth() 59 | ) 60 | OutlinedTextField( 61 | value = note.description, 62 | onValueChange = { vm.update(note.copy(description = it)) }, 63 | label = { Text("Description") }, 64 | modifier = Modifier.fillMaxWidth().weight(1f) 65 | ) 66 | } 67 | } 68 | } 69 | } 70 | } 71 | 72 | @OptIn(ExperimentalMaterial3Api::class) 73 | @Composable 74 | private fun TypeDropdown(value: Note.Type, onValueChange: (Note.Type) -> Unit, modifier: Modifier = Modifier) { 75 | 76 | var expanded by remember { mutableStateOf(false) } 77 | 78 | Box(modifier = modifier, propagateMinConstraints = true) { 79 | OutlinedTextField( 80 | value = value.toString(), 81 | onValueChange = {}, 82 | readOnly = true, 83 | label = { Text("Type") }, 84 | trailingIcon = { 85 | IconButton(onClick = { expanded = true }) { 86 | Icon(imageVector = Icons.Default.ArrowDropDown, contentDescription = "Show note types") 87 | } 88 | } 89 | ) 90 | DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) { 91 | Note.Type.values().forEach { 92 | DropdownMenuItem(onClick = { onValueChange(it); expanded = false }) { 93 | Text(it.name) 94 | } 95 | } 96 | } 97 | } 98 | } 99 | 100 | @OptIn(ExperimentalMaterial3Api::class) 101 | @Composable 102 | private fun TopBar(note: Note, onClose: () -> Unit, onSave: () -> Unit, onDelete: () -> Unit) { 103 | TopAppBar( 104 | title = { Text(note.title) }, 105 | navigationIcon = { 106 | IconButton(onClick = onClose) { 107 | Icon(imageVector = Icons.Default.Close, contentDescription = "Close Detail") 108 | } 109 | }, 110 | actions = { 111 | IconButton(onClick = onSave) { 112 | Icon(imageVector = Icons.Default.Save, contentDescription = "Save Note") 113 | } 114 | 115 | if (note.id != Note.NEW_NOTE) { 116 | IconButton(onClick = onDelete) { 117 | Icon(imageVector = Icons.Default.Delete, contentDescription = "Delete Note") 118 | } 119 | } 120 | } 121 | ) 122 | } 123 | -------------------------------------------------------------------------------- /android/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | "@jridgewell/gen-mapping@^0.3.0": 6 | version "0.3.3" 7 | resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz" 8 | integrity sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ== 9 | dependencies: 10 | "@jridgewell/set-array" "^1.0.1" 11 | "@jridgewell/sourcemap-codec" "^1.4.10" 12 | "@jridgewell/trace-mapping" "^0.3.9" 13 | 14 | "@jridgewell/resolve-uri@3.1.0": 15 | version "3.1.0" 16 | resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz" 17 | integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== 18 | 19 | "@jridgewell/set-array@^1.0.1": 20 | version "1.1.2" 21 | resolved "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz" 22 | integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== 23 | 24 | "@jridgewell/source-map@^0.3.2": 25 | version "0.3.3" 26 | resolved "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz" 27 | integrity sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg== 28 | dependencies: 29 | "@jridgewell/gen-mapping" "^0.3.0" 30 | "@jridgewell/trace-mapping" "^0.3.9" 31 | 32 | "@jridgewell/sourcemap-codec@^1.4.10": 33 | version "1.4.15" 34 | resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz" 35 | integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== 36 | 37 | "@jridgewell/sourcemap-codec@1.4.14": 38 | version "1.4.14" 39 | resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz" 40 | integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== 41 | 42 | "@jridgewell/trace-mapping@^0.3.9": 43 | version "0.3.18" 44 | resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz" 45 | integrity sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA== 46 | dependencies: 47 | "@jridgewell/resolve-uri" "3.1.0" 48 | "@jridgewell/sourcemap-codec" "1.4.14" 49 | 50 | "@rollup/plugin-terser@^0.4.1": 51 | version "0.4.1" 52 | resolved "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.1.tgz" 53 | integrity sha512-aKS32sw5a7hy+fEXVy+5T95aDIwjpGHCTv833HXVtyKMDoVS7pBr5K3L9hEQoNqbJFjfANPrNpIXlTQ7is00eA== 54 | dependencies: 55 | serialize-javascript "^6.0.0" 56 | smob "^0.0.6" 57 | terser "^5.15.1" 58 | 59 | acorn@^8.5.0: 60 | version "8.8.2" 61 | resolved "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz" 62 | integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== 63 | 64 | buffer-from@^1.0.0: 65 | version "1.1.2" 66 | resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" 67 | integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== 68 | 69 | commander@^2.20.0: 70 | version "2.20.3" 71 | resolved "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz" 72 | integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== 73 | 74 | randombytes@^2.1.0: 75 | version "2.1.0" 76 | resolved "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz" 77 | integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== 78 | dependencies: 79 | safe-buffer "^5.1.0" 80 | 81 | safe-buffer@^5.1.0: 82 | version "5.2.1" 83 | resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz" 84 | integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== 85 | 86 | serialize-javascript@^6.0.0: 87 | version "6.0.1" 88 | resolved "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz" 89 | integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== 90 | dependencies: 91 | randombytes "^2.1.0" 92 | 93 | smob@^0.0.6: 94 | version "0.0.6" 95 | resolved "https://registry.npmjs.org/smob/-/smob-0.0.6.tgz" 96 | integrity sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw== 97 | 98 | source-map-support@~0.5.20: 99 | version "0.5.21" 100 | resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz" 101 | integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== 102 | dependencies: 103 | buffer-from "^1.0.0" 104 | source-map "^0.6.0" 105 | 106 | source-map@^0.6.0: 107 | version "0.6.1" 108 | resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz" 109 | integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== 110 | 111 | terser@^5.15.1: 112 | version "5.17.1" 113 | resolved "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz" 114 | integrity sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw== 115 | dependencies: 116 | "@jridgewell/source-map" "^0.3.2" 117 | acorn "^8.5.0" 118 | commander "^2.20.0" 119 | source-map-support "~0.5.20" 120 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /ios/NotesAppIos.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A9292E4A2A5D73A1002F8E64 /* NotesAppIosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9292E492A5D73A1002F8E64 /* NotesAppIosApp.swift */; }; 11 | A9292E4C2A5D73A1002F8E64 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9292E4B2A5D73A1002F8E64 /* ContentView.swift */; }; 12 | A9292E4E2A5D73A2002F8E64 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9292E4D2A5D73A2002F8E64 /* Assets.xcassets */; }; 13 | A9292E512A5D73A2002F8E64 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A9292E502A5D73A2002F8E64 /* Preview Assets.xcassets */; }; 14 | /* End PBXBuildFile section */ 15 | 16 | /* Begin PBXFileReference section */ 17 | A9292E462A5D73A1002F8E64 /* NotesAppIos.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NotesAppIos.app; sourceTree = BUILT_PRODUCTS_DIR; }; 18 | A9292E492A5D73A1002F8E64 /* NotesAppIosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotesAppIosApp.swift; sourceTree = ""; }; 19 | A9292E4B2A5D73A1002F8E64 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 20 | A9292E4D2A5D73A2002F8E64 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 21 | A9292E502A5D73A2002F8E64 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 22 | /* End PBXFileReference section */ 23 | 24 | /* Begin PBXFrameworksBuildPhase section */ 25 | A9292E432A5D73A1002F8E64 /* Frameworks */ = { 26 | isa = PBXFrameworksBuildPhase; 27 | buildActionMask = 2147483647; 28 | files = ( 29 | ); 30 | runOnlyForDeploymentPostprocessing = 0; 31 | }; 32 | /* End PBXFrameworksBuildPhase section */ 33 | 34 | /* Begin PBXGroup section */ 35 | A9292E3D2A5D73A1002F8E64 = { 36 | isa = PBXGroup; 37 | children = ( 38 | A9292E482A5D73A1002F8E64 /* NotesAppIos */, 39 | A9292E472A5D73A1002F8E64 /* Products */, 40 | ); 41 | sourceTree = ""; 42 | }; 43 | A9292E472A5D73A1002F8E64 /* Products */ = { 44 | isa = PBXGroup; 45 | children = ( 46 | A9292E462A5D73A1002F8E64 /* NotesAppIos.app */, 47 | ); 48 | name = Products; 49 | sourceTree = ""; 50 | }; 51 | A9292E482A5D73A1002F8E64 /* NotesAppIos */ = { 52 | isa = PBXGroup; 53 | children = ( 54 | A9292E492A5D73A1002F8E64 /* NotesAppIosApp.swift */, 55 | A9292E4B2A5D73A1002F8E64 /* ContentView.swift */, 56 | A9292E4D2A5D73A2002F8E64 /* Assets.xcassets */, 57 | A9292E4F2A5D73A2002F8E64 /* Preview Content */, 58 | ); 59 | path = NotesAppIos; 60 | sourceTree = ""; 61 | }; 62 | A9292E4F2A5D73A2002F8E64 /* Preview Content */ = { 63 | isa = PBXGroup; 64 | children = ( 65 | A9292E502A5D73A2002F8E64 /* Preview Assets.xcassets */, 66 | ); 67 | path = "Preview Content"; 68 | sourceTree = ""; 69 | }; 70 | /* End PBXGroup section */ 71 | 72 | /* Begin PBXNativeTarget section */ 73 | A9292E452A5D73A1002F8E64 /* NotesAppIos */ = { 74 | isa = PBXNativeTarget; 75 | buildConfigurationList = A9292E542A5D73A2002F8E64 /* Build configuration list for PBXNativeTarget "NotesAppIos" */; 76 | buildPhases = ( 77 | A9292E572A5D7446002F8E64 /* Run Script */, 78 | A9292E422A5D73A1002F8E64 /* Sources */, 79 | A9292E432A5D73A1002F8E64 /* Frameworks */, 80 | A9292E442A5D73A1002F8E64 /* Resources */, 81 | ); 82 | buildRules = ( 83 | ); 84 | dependencies = ( 85 | ); 86 | name = NotesAppIos; 87 | productName = NotesAppIos; 88 | productReference = A9292E462A5D73A1002F8E64 /* NotesAppIos.app */; 89 | productType = "com.apple.product-type.application"; 90 | }; 91 | /* End PBXNativeTarget section */ 92 | 93 | /* Begin PBXProject section */ 94 | A9292E3E2A5D73A1002F8E64 /* Project object */ = { 95 | isa = PBXProject; 96 | attributes = { 97 | BuildIndependentTargetsInParallel = 1; 98 | LastSwiftUpdateCheck = 1430; 99 | LastUpgradeCheck = 1430; 100 | TargetAttributes = { 101 | A9292E452A5D73A1002F8E64 = { 102 | CreatedOnToolsVersion = 14.3.1; 103 | }; 104 | }; 105 | }; 106 | buildConfigurationList = A9292E412A5D73A1002F8E64 /* Build configuration list for PBXProject "NotesAppIos" */; 107 | compatibilityVersion = "Xcode 14.0"; 108 | developmentRegion = en; 109 | hasScannedForEncodings = 0; 110 | knownRegions = ( 111 | en, 112 | Base, 113 | ); 114 | mainGroup = A9292E3D2A5D73A1002F8E64; 115 | productRefGroup = A9292E472A5D73A1002F8E64 /* Products */; 116 | projectDirPath = ""; 117 | projectRoot = ""; 118 | targets = ( 119 | A9292E452A5D73A1002F8E64 /* NotesAppIos */, 120 | ); 121 | }; 122 | /* End PBXProject section */ 123 | 124 | /* Begin PBXResourcesBuildPhase section */ 125 | A9292E442A5D73A1002F8E64 /* Resources */ = { 126 | isa = PBXResourcesBuildPhase; 127 | buildActionMask = 2147483647; 128 | files = ( 129 | A9292E512A5D73A2002F8E64 /* Preview Assets.xcassets in Resources */, 130 | A9292E4E2A5D73A2002F8E64 /* Assets.xcassets in Resources */, 131 | ); 132 | runOnlyForDeploymentPostprocessing = 0; 133 | }; 134 | /* End PBXResourcesBuildPhase section */ 135 | 136 | /* Begin PBXShellScriptBuildPhase section */ 137 | A9292E572A5D7446002F8E64 /* Run Script */ = { 138 | isa = PBXShellScriptBuildPhase; 139 | buildActionMask = 2147483647; 140 | files = ( 141 | ); 142 | inputFileListPaths = ( 143 | ); 144 | inputPaths = ( 145 | ); 146 | name = "Run Script"; 147 | outputFileListPaths = ( 148 | ); 149 | outputPaths = ( 150 | ); 151 | runOnlyForDeploymentPostprocessing = 0; 152 | shellPath = /bin/sh; 153 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :common:embedAndSignAppleFrameworkForXcode\n"; 154 | }; 155 | /* End PBXShellScriptBuildPhase section */ 156 | 157 | /* Begin PBXSourcesBuildPhase section */ 158 | A9292E422A5D73A1002F8E64 /* Sources */ = { 159 | isa = PBXSourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | A9292E4C2A5D73A1002F8E64 /* ContentView.swift in Sources */, 163 | A9292E4A2A5D73A1002F8E64 /* NotesAppIosApp.swift in Sources */, 164 | ); 165 | runOnlyForDeploymentPostprocessing = 0; 166 | }; 167 | /* End PBXSourcesBuildPhase section */ 168 | 169 | /* Begin XCBuildConfiguration section */ 170 | A9292E522A5D73A2002F8E64 /* Debug */ = { 171 | isa = XCBuildConfiguration; 172 | buildSettings = { 173 | ALWAYS_SEARCH_USER_PATHS = NO; 174 | CLANG_ANALYZER_NONNULL = YES; 175 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 176 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 177 | CLANG_ENABLE_MODULES = YES; 178 | CLANG_ENABLE_OBJC_ARC = YES; 179 | CLANG_ENABLE_OBJC_WEAK = YES; 180 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 181 | CLANG_WARN_BOOL_CONVERSION = YES; 182 | CLANG_WARN_COMMA = YES; 183 | CLANG_WARN_CONSTANT_CONVERSION = YES; 184 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 185 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 186 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 187 | CLANG_WARN_EMPTY_BODY = YES; 188 | CLANG_WARN_ENUM_CONVERSION = YES; 189 | CLANG_WARN_INFINITE_RECURSION = YES; 190 | CLANG_WARN_INT_CONVERSION = YES; 191 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 192 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 193 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 195 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 196 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 197 | CLANG_WARN_STRICT_PROTOTYPES = YES; 198 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 199 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 200 | CLANG_WARN_UNREACHABLE_CODE = YES; 201 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 202 | COPY_PHASE_STRIP = NO; 203 | DEBUG_INFORMATION_FORMAT = dwarf; 204 | ENABLE_STRICT_OBJC_MSGSEND = YES; 205 | ENABLE_TESTABILITY = YES; 206 | GCC_C_LANGUAGE_STANDARD = gnu11; 207 | GCC_DYNAMIC_NO_PIC = NO; 208 | GCC_NO_COMMON_BLOCKS = YES; 209 | GCC_OPTIMIZATION_LEVEL = 0; 210 | GCC_PREPROCESSOR_DEFINITIONS = ( 211 | "DEBUG=1", 212 | "$(inherited)", 213 | ); 214 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 215 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 216 | GCC_WARN_UNDECLARED_SELECTOR = YES; 217 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 218 | GCC_WARN_UNUSED_FUNCTION = YES; 219 | GCC_WARN_UNUSED_VARIABLE = YES; 220 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 221 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 222 | MTL_FAST_MATH = YES; 223 | ONLY_ACTIVE_ARCH = YES; 224 | SDKROOT = iphoneos; 225 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 226 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 227 | }; 228 | name = Debug; 229 | }; 230 | A9292E532A5D73A2002F8E64 /* Release */ = { 231 | isa = XCBuildConfiguration; 232 | buildSettings = { 233 | ALWAYS_SEARCH_USER_PATHS = NO; 234 | CLANG_ANALYZER_NONNULL = YES; 235 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 236 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 237 | CLANG_ENABLE_MODULES = YES; 238 | CLANG_ENABLE_OBJC_ARC = YES; 239 | CLANG_ENABLE_OBJC_WEAK = YES; 240 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 241 | CLANG_WARN_BOOL_CONVERSION = YES; 242 | CLANG_WARN_COMMA = YES; 243 | CLANG_WARN_CONSTANT_CONVERSION = YES; 244 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 245 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 246 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 247 | CLANG_WARN_EMPTY_BODY = YES; 248 | CLANG_WARN_ENUM_CONVERSION = YES; 249 | CLANG_WARN_INFINITE_RECURSION = YES; 250 | CLANG_WARN_INT_CONVERSION = YES; 251 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 252 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 253 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 255 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 256 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 257 | CLANG_WARN_STRICT_PROTOTYPES = YES; 258 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 259 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 260 | CLANG_WARN_UNREACHABLE_CODE = YES; 261 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 262 | COPY_PHASE_STRIP = NO; 263 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 264 | ENABLE_NS_ASSERTIONS = NO; 265 | ENABLE_STRICT_OBJC_MSGSEND = YES; 266 | GCC_C_LANGUAGE_STANDARD = gnu11; 267 | GCC_NO_COMMON_BLOCKS = YES; 268 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 269 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 270 | GCC_WARN_UNDECLARED_SELECTOR = YES; 271 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 272 | GCC_WARN_UNUSED_FUNCTION = YES; 273 | GCC_WARN_UNUSED_VARIABLE = YES; 274 | IPHONEOS_DEPLOYMENT_TARGET = 16.4; 275 | MTL_ENABLE_DEBUG_INFO = NO; 276 | MTL_FAST_MATH = YES; 277 | SDKROOT = iphoneos; 278 | SWIFT_COMPILATION_MODE = wholemodule; 279 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 280 | VALIDATE_PRODUCT = YES; 281 | }; 282 | name = Release; 283 | }; 284 | A9292E552A5D73A2002F8E64 /* Debug */ = { 285 | isa = XCBuildConfiguration; 286 | buildSettings = { 287 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 288 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 289 | CODE_SIGN_STYLE = Automatic; 290 | CURRENT_PROJECT_VERSION = 1; 291 | DEVELOPMENT_ASSET_PATHS = "\"NotesAppIos/Preview Content\""; 292 | ENABLE_PREVIEWS = YES; 293 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; 294 | GENERATE_INFOPLIST_FILE = YES; 295 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 296 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 297 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 298 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 299 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 300 | LD_RUNPATH_SEARCH_PATHS = ( 301 | "$(inherited)", 302 | "@executable_path/Frameworks", 303 | ); 304 | MARKETING_VERSION = 1.0; 305 | OTHER_LDFLAGS = ( 306 | "$(inherited)", 307 | "-framework", 308 | common, 309 | ); 310 | PRODUCT_BUNDLE_IDENTIFIER = com.devexperto.kotlinexpert.NotesAppIos; 311 | PRODUCT_NAME = "$(TARGET_NAME)"; 312 | SWIFT_EMIT_LOC_STRINGS = YES; 313 | SWIFT_VERSION = 5.0; 314 | TARGETED_DEVICE_FAMILY = "1,2"; 315 | }; 316 | name = Debug; 317 | }; 318 | A9292E562A5D73A2002F8E64 /* Release */ = { 319 | isa = XCBuildConfiguration; 320 | buildSettings = { 321 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 322 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 323 | CODE_SIGN_STYLE = Automatic; 324 | CURRENT_PROJECT_VERSION = 1; 325 | DEVELOPMENT_ASSET_PATHS = "\"NotesAppIos/Preview Content\""; 326 | ENABLE_PREVIEWS = YES; 327 | FRAMEWORK_SEARCH_PATHS = "$(SRCROOT)/../common/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)"; 328 | GENERATE_INFOPLIST_FILE = YES; 329 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 330 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 331 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 332 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 333 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 334 | LD_RUNPATH_SEARCH_PATHS = ( 335 | "$(inherited)", 336 | "@executable_path/Frameworks", 337 | ); 338 | MARKETING_VERSION = 1.0; 339 | OTHER_LDFLAGS = ( 340 | "$(inherited)", 341 | "-framework", 342 | common, 343 | ); 344 | PRODUCT_BUNDLE_IDENTIFIER = com.devexperto.kotlinexpert.NotesAppIos; 345 | PRODUCT_NAME = "$(TARGET_NAME)"; 346 | SWIFT_EMIT_LOC_STRINGS = YES; 347 | SWIFT_VERSION = 5.0; 348 | TARGETED_DEVICE_FAMILY = "1,2"; 349 | }; 350 | name = Release; 351 | }; 352 | /* End XCBuildConfiguration section */ 353 | 354 | /* Begin XCConfigurationList section */ 355 | A9292E412A5D73A1002F8E64 /* Build configuration list for PBXProject "NotesAppIos" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | A9292E522A5D73A2002F8E64 /* Debug */, 359 | A9292E532A5D73A2002F8E64 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | A9292E542A5D73A2002F8E64 /* Build configuration list for PBXNativeTarget "NotesAppIos" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | A9292E552A5D73A2002F8E64 /* Debug */, 368 | A9292E562A5D73A2002F8E64 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = A9292E3E2A5D73A1002F8E64 /* Project object */; 376 | } 377 | --------------------------------------------------------------------------------