From c18f6801e3bd9c52a26f1be4b3d4c79979b74b37 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 21 Apr 2025 14:30:13 +0300 Subject: [PATCH 01/10] feat: Update dependencies and refactor related code. This commit includes several updates and changes: - Updated Kotlin to 2.1.20 - Updated Ktor to 3.1.2 - Updated Compose to 1.7.3 - Updated Voyager to 1.0.1 - Updated Koin to 4.0.4 - Removed Moko Resources references - Updated other dependencies such as kotlinx-coroutines, kotlinx-serialization, kotlinx-datetime, Android Gradle Plugin, desugar_jdk_libs, AndroidX libraries, Seiko Image Loader, KVault, Maps Compose, and Play Services Location. - Replaced `ImageResult.Image.toImage()` extension function with direct access to the image property. - Updated Gradle wrapper to version 8.12.1. - Added `nsexception-kt` for handling unhandled exceptions on iOS. - Changed the shell script for building Kotlin code in Xcode to use zsh and load environment variables. - Updated various configurations and settings in the iOS project (iosApp/iosApp.xcodeproj/project.pbxproj). - Removed the Podfile and related configurations as the project now uses Kotlin Multiplatform's native dependency management. - Updated the permission checking logic on Android to use `LifecycleEventEffect` for improved efficiency. --- .gitignore | 3 +- androidApp/build.gradle.kts | 13 +- build.gradle.kts | 3 +- gradle/libs.versions.toml | 58 +++--- gradle/wrapper/gradle-wrapper.properties | 2 +- iosApp/Podfile | 5 - iosApp/iosApp.xcodeproj/project.pbxproj | 82 +------- iosApp/iosApp/Info.plist | 23 ++- shared/build.gradle.kts | 190 ++++++------------ .../core/GoogleMapsComponent.kt | 4 +- .../core/ImageResultExt.kt | 6 - .../permission/PermissionsUtil.kt | 25 +-- .../core/ImageResultExt.kt | 6 - .../moveeapp_compose_kmm/di/NetworkModule.kt | 11 +- .../ui/components/ImageItem.kt | 13 +- .../core/ImageResultExt.kt | 6 - .../example/moveeapp_compose_kmm/main.ios.kt | 5 + .../example/moveeapp_compose_kmm/map/Map.kt | 19 +- 18 files changed, 158 insertions(+), 316 deletions(-) delete mode 100644 iosApp/Podfile delete mode 100644 shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt delete mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt delete mode 100644 shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt diff --git a/.gitignore b/.gitignore index 822b994..41b7c2b 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ iosApp/Pods/* iosApp/iosApp.xcworkspace/* iosApp/iosApp.xcodeproj/* !iosApp/iosApp.xcodeproj/project.pbxproj -shared/shared.podspec \ No newline at end of file +shared/shared.podspec +/.kotlin/* diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index a388c52..e6a3547 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -4,12 +4,13 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.maps.secrets) } android { namespace = "com.example.moveeapp_compose_kmm.android" - compileSdk = libs.versions.targetSdk.get().toInt() + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { applicationId = "com.example.moveeapp_compose_kmm.android" @@ -29,12 +30,10 @@ android { buildFeatures { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } packaging { resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" + excludes += "META-INF/INDEX.LIST" } } buildTypes { @@ -67,10 +66,8 @@ dependencies { coreLibraryDesugaring(libs.desugar.jdk.libs) // Koin - with(libs.koin) { - implementation(android) - implementation(core) - } + implementation(libs.koin.android) + implementation(libs.koin.core) } secrets { diff --git a/build.gradle.kts b/build.gradle.kts index f38f874..b4176a6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,10 +4,9 @@ plugins { alias(libs.plugins.multiplatform).apply(false) alias(libs.plugins.kotlin.android).apply(false) alias(libs.plugins.compose).apply(false) + alias(libs.plugins.compose.compiler).apply(false) alias(libs.plugins.kotlinx.serialization).apply(false) - alias(libs.plugins.moko.resources).apply(false) alias(libs.plugins.buildKonfig).apply(false) - alias(libs.plugins.kotlin.cocoapods).apply(false) alias(libs.plugins.maps.secrets).apply(false) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c37855f..55b681f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,42 +2,35 @@ minSdk = "24" targetSdk = "34" +compileSdk = "35" -kotlin = "1.9.23" -kotlinx-coroutines = "1.8.0" -kotlinx-serialization = "1.6.0" -kotlinx-datetime = "0.5.0" -ktor = "2.3.5" -agp = "8.3.2" -desugar_jdk_libs = "2.0.4" -compose = "1.6.2" -compose-compiler = "1.5.11" -compose-ui-tooling = "1.6.4" -androidx = "1.13.0" -androidx-activity = "1.9.0" -androidx-appcompat = "1.6.1" -voyager = "1.0.0-rc10" -napier = "2.6.1" -buildKonfig = "0.14.0" -koin = "3.5.0" -koin-compose = "1.1.0" -moko = "0.23.0" -seiko = "1.5.1" -mapsCompose = "2.15.0" -playServicesMaps = "18.2.0" -playServicesLocation = "21.2.0" +kotlin = "2.1.20" +kotlinx-coroutines = "1.10.2" +kotlinx-serialization = "1.8.1" +kotlinx-datetime = "0.6.2" +ktor = "3.1.2" +agp = "8.9.1" +desugar_jdk_libs = "2.1.5" +compose = "1.7.3" +androidx = "1.16.0" +androidx-activity = "1.10.1" +androidx-appcompat = "1.7.0" +voyager = "1.0.1" +napier = "2.7.1" +buildKonfig = "0.17.0" +koin = "4.0.4" +seiko = "1.10.0" +mapsCompose = "6.5.2" +playServicesLocation = "21.3.0" maps-secrets = "2.0.1" +nsexceptionKt = "0.1.17" [libraries] androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx" } androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } -androidx-compose-ui-util = { module = "androidx.compose.ui:ui-util", version.ref = "compose-ui-tooling" } -androidx-compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "compose-ui-tooling" } -androidx-compose-ui-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "compose-ui-tooling" } maps-compose = { module = "com.google.maps.android:maps-compose", version.ref = "mapsCompose" } -play-services-maps = { module = "com.google.android.gms:play-services-maps", version.ref = "playServicesMaps" } play-services-location = { module = "com.google.android.gms:play-services-location", version.ref = "playServicesLocation" } desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "desugar_jdk_libs" } @@ -62,29 +55,26 @@ ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "kto ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } -ktor-logging-logback = { module = "ch.qos.logback:logback-classic", version = "1.2.11" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" } -koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose" } +koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" } koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } image-loader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "seiko" } -settings = { module = "com.liftric:kvault", version = "1.10.0" } +settings = { module = "com.liftric:kvault", version = "1.12.0" } -moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" } -moko-resources-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko" } +nsexceptionKt-core = { module = "com.rickclephas.kmp:nsexception-kt-core", version.ref = "nsexceptionKt" } [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } +compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" } kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -moko-resources = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" } maps-secrets = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", version.ref = "maps-secrets" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c24aee5..c1f3894 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 01 14:19:46 TRT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/iosApp/Podfile b/iosApp/Podfile deleted file mode 100644 index aff9c51..0000000 --- a/iosApp/Podfile +++ /dev/null @@ -1,5 +0,0 @@ -target 'iosApp' do - use_frameworks! - platform :ios, '14.1' - pod 'shared', :path => '../shared' -end \ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 1785cd3..43d7dd6 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -3,14 +3,13 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 50; objects = { /* Begin PBXBuildFile section */ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; 391C5BDB2AC582D400F44208 /* SwiftToKotlin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */; }; - 6BE1F7DCD0DE294643E555EF /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 308D17AA6829522377A26985 /* Pods_iosApp.framework */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; A47FDF582A8CD56700CA069B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47FDF572A8CD56700CA069B /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -31,13 +30,10 @@ /* Begin PBXFileReference section */ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 188C81B3366C19D30F745167 /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; - 308D17AA6829522377A26985 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftToKotlin.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 98CC46A27321B3E2296FCD33 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; A47FDF572A8CD56700CA069B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -46,7 +42,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6BE1F7DCD0DE294643E555EF /* Pods_iosApp.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -66,8 +61,6 @@ children = ( 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, - 7555FFB0242A642200829871 /* Frameworks */, - 8BA20514D79FE6023D3FA30F /* Pods */, ); sourceTree = ""; }; @@ -92,23 +85,6 @@ path = iosApp; sourceTree = ""; }; - 7555FFB0242A642200829871 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 308D17AA6829522377A26985 /* Pods_iosApp.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 8BA20514D79FE6023D3FA30F /* Pods */ = { - isa = PBXGroup; - children = ( - 188C81B3366C19D30F745167 /* Pods-iosApp.debug.xcconfig */, - 98CC46A27321B3E2296FCD33 /* Pods-iosApp.release.xcconfig */, - ); - path = Pods; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -126,14 +102,12 @@ isa = PBXNativeTarget; buildConfigurationList = 7555FFA5242A565B00829871 /* Build configuration list for PBXNativeTarget "iosApp" */; buildPhases = ( - 17CE07116C60D5E7D0EDF35E /* [CP] Check Pods Manifest.lock */, - 7555FFB5242A651A00829871 /* ShellScript */, + 7555FFB5242A651A00829871 /* Build Kotlin Code */, 39F2145E2A94BC9600A86704 /* Headers */, 7555FF77242A565900829871 /* Sources */, 7555FF78242A565900829871 /* Frameworks */, 7555FF79242A565900829871 /* Resources */, 7555FFB4242A642300829871 /* Embed Frameworks */, - 32063F414771D6D5A93F4A86 /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -151,7 +125,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = orgName; TargetAttributes = { 7555FF7A242A565900829871 = { @@ -190,46 +164,7 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 17CE07116C60D5E7D0EDF35E /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 32063F414771D6D5A93F4A86 /* [CP] Copy Pods Resources */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Copy Pods Resources"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; - showEnvVarsInLog = 0; - }; - 7555FFB5242A651A00829871 /* ShellScript */ = { + 7555FFB5242A651A00829871 /* Build Kotlin Code */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -238,13 +173,14 @@ ); inputPaths = ( ); + name = "Build Kotlin Code"; outputFileListPaths = ( ); outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "cd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; + shellPath = /bin/zsh; + shellScript = "source ~/.zprofile\nsource ~/.zshrc\ncd \"$SRCROOT/..\"\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\n"; }; /* End PBXShellScriptBuildPhase section */ @@ -380,12 +316,11 @@ }; 7555FFA6242A565B00829871 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 188C81B3366C19D30F745167 /* Pods-iosApp.debug.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = ET37P8CM79; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -412,7 +347,6 @@ }; 7555FFA7242A565B00829871 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 98CC46A27321B3E2296FCD33 /* Pods-iosApp.release.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist index 971fed5..21f9275 100644 --- a/iosApp/iosApp/Info.plist +++ b/iosApp/iosApp/Info.plist @@ -2,16 +2,23 @@ - CFBundleDisplayName - Movee + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Movee CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleLocalizations + + en + tr + CFBundleName $(PRODUCT_NAME) CFBundlePackageType @@ -20,17 +27,17 @@ 1.0 CFBundleVersion 1 - CFBundleLocalizations - en - tr - LSRequiresIPhoneOS + NSLocationWhenInUseUsageDescription + If you want to see the weather for your location, grant the permission. UIApplicationSceneManifest UIApplicationSupportsMultipleScenes + UILaunchScreen + UIRequiredDeviceCapabilities armv7 @@ -48,9 +55,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - NSLocationWhenInUseUsageDescription - If you want to see the weather for your location, grant the permission. - UILaunchScreen - diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d5c44a7..3d7e7ab 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -4,8 +4,8 @@ import movee.util.requireStringProperty plugins { alias(libs.plugins.multiplatform) alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) alias(libs.plugins.android.library) - alias(libs.plugins.kotlin.cocoapods) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.buildKonfig) } @@ -47,145 +47,90 @@ buildkonfig { } } -@OptIn(org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi::class) kotlin { androidTarget() - iosX64() - iosArm64() - iosSimulatorArm64() - - cocoapods { - version = "1.0.0" - summary = "Some description for the Shared Module" - homepage = "Link to the Shared Module homepage" - ios.deploymentTarget = "14.1" - podfile = project.file("../iosApp/Podfile") - framework { - baseName = "shared" - isStatic = true - } - } - - androidTarget { - compilations.all { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() - } - } - } + iosArm64 { binaries.framework { baseName = "shared" } } + iosSimulatorArm64 { binaries.framework { baseName = "shared" } } sourceSets { all { languageSettings.optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") } - val commonMain by getting { - dependencies { - - // Compose - with(compose) { - api(runtime) - api(foundation) - api(material) - api(material3) - api(materialIconsExtended) - api(animation) - implementation(components.resources) - } - - implementation(libs.logger) - - // Coroutines - api(libs.kotlinx.coroutines.core) - - // KotlinX Serialization Json - api(libs.kotlinx.serialization.json) - - // Ktor - with(libs.ktor) { - api(core) - api(json) - api(contentNegotiation) - api(logging) - api(logging.logback) - } - - // Koin - with(libs.koin) { - api(core) - api(test) - api(compose) - } - - //Navigation - with(libs.voyager) { - api(navigator) - api(koin) - api(tabs) - api(transitions) - } - - //Image loader - api(libs.image.loader) - - //KVault - api(libs.settings) - } + commonMain.dependencies { + // Compose + api(compose.runtime) + api(compose.foundation) + api(compose.material) + api(compose.material3) + api(compose.materialIconsExtended) + api(compose.animation) + implementation(compose.components.resources) + + implementation(libs.logger) + + // Coroutines + api(libs.kotlinx.coroutines.core) + + // KotlinX Serialization Json + api(libs.kotlinx.serialization.json) + + // Ktor + api(libs.ktor.core) + api(libs.ktor.json) + api(libs.ktor.contentNegotiation) + api(libs.ktor.logging) + + // Koin + api(libs.koin.core) + api(libs.koin.test) + api(libs.koin.compose) + + //Navigation + api(libs.voyager.navigator) + api(libs.voyager.koin) + api(libs.voyager.tabs) + api(libs.voyager.transitions) + + //Image loader + api(libs.image.loader) + + //KVault + api(libs.settings) } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - implementation(libs.kotlinx.coroutines.test) - } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) } - val androidMain by getting { - dependsOn(commonMain) - - dependencies { - // Ktor - api(libs.ktor.client.android) - - // Koin - api(libs.koin.android) - - with(libs.androidx) { - api(core) - api(appcompat) - api(activity.compose) - } - - with(libs.androidx.compose.ui) { - api(util) - api(tooling) - api(preview) - } - api(libs.maps.compose) - - //Location - api(libs.play.services.location) - api(libs.play.services.maps) - } + androidMain.dependencies { + // Ktor + api(libs.ktor.client.android) + + // Koin + api(libs.koin.android) + + api(libs.androidx.core) + api(libs.androidx.appcompat) + api(libs.androidx.activity.compose) + + api(libs.maps.compose) + + //Location + api(libs.play.services.location) } - val iosX64Main by getting - val iosArm64Main by getting - val iosSimulatorArm64Main by getting - val iosMain by creating { - dependsOn(commonMain) - iosX64Main.dependsOn(this) - iosArm64Main.dependsOn(this) - iosSimulatorArm64Main.dependsOn(this) - dependencies { - implementation(libs.ktor.client.darwin) - } + + iosMain.dependencies { + implementation(libs.ktor.client.darwin) + implementation(libs.nsexceptionKt.core) } } } android { namespace = "com.example.moveeapp_compose_kmm" - compileSdk = libs.versions.targetSdk.get().toInt() + compileSdk = libs.versions.compileSdk.get().toInt() defaultConfig { minSdk = libs.versions.minSdk.get().toInt() @@ -193,9 +138,6 @@ android { buildFeatures { compose = true } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/GoogleMapsComponent.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/GoogleMapsComponent.kt index 85ca919..49de9e1 100644 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/GoogleMapsComponent.kt +++ b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/GoogleMapsComponent.kt @@ -20,7 +20,7 @@ fun GoogleMapsComponent( modifier: Modifier = Modifier, cameraPositionState: CameraPositionState, onMapClick: (LatLng) -> Unit, - content: (@Composable @GoogleMapComposable () -> Unit)?, + content: (@Composable @GoogleMapComposable () -> Unit), ) { val context = LocalContext.current val mapProperties by remember { @@ -54,4 +54,4 @@ fun GoogleMapsComponent( onMapClick = onMapClick, content = content ) -} \ No newline at end of file +} diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt deleted file mode 100644 index 0cff4f1..0000000 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.moveeapp_compose_kmm.core - -import com.seiko.imageloader.Image -import com.seiko.imageloader.model.ImageResult - -actual fun ImageResult.Image.toImage(): Image = this.image \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionsUtil.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionsUtil.kt index 4ae6efb..cdb691b 100644 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionsUtil.kt +++ b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionsUtil.kt @@ -4,16 +4,12 @@ import android.app.Activity import android.content.Context import android.content.pm.PackageManager import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle -import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LifecycleEventEffect import com.example.moveeapp_compose_kmm.core.getActivity - internal fun Context.findActivity(): Activity { return getActivity() ?: throw IllegalStateException("Permissions should be called in the context of an Activity") @@ -35,20 +31,11 @@ internal fun PermissionLifecycleCheckerEffect( ) { // Check if the permission was granted when the lifecycle is resumed. // The user might've gone to the Settings screen and granted the permission. - val permissionCheckerObserver = remember(permissionState) { - LifecycleEventObserver { _, event -> - if (event == lifecycleEvent) { - // If the permission is revoked, check again. - // We don't check if the permission was denied as that triggers a process restart. - if (permissionState.status != PermissionStatus.Granted) { - permissionState.refreshPermissionStatus() - } - } + LifecycleEventEffect(lifecycleEvent) { + // If the permission is revoked, check again. + // We don't check if the permission was denied as that triggers a process restart. + if (permissionState.status != PermissionStatus.Granted) { + permissionState.refreshPermissionStatus() } } - val lifecycle = LocalLifecycleOwner.current.lifecycle - DisposableEffect(lifecycle, permissionCheckerObserver) { - lifecycle.addObserver(permissionCheckerObserver) - onDispose { lifecycle.removeObserver(permissionCheckerObserver) } - } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt deleted file mode 100644 index 76304f0..0000000 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.moveeapp_compose_kmm.core - -import com.seiko.imageloader.Image -import com.seiko.imageloader.model.ImageResult - -expect fun ImageResult.Image.toImage(): Image \ No newline at end of file diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/di/NetworkModule.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/di/NetworkModule.kt index 15409ab..b468d5b 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/di/NetworkModule.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/di/NetworkModule.kt @@ -6,12 +6,12 @@ import com.example.moveeapp_compose_kmm.data.artist.ArtistService import com.example.moveeapp_compose_kmm.data.artist.ArtistServiceImpl import com.example.moveeapp_compose_kmm.data.favorite.FavoriteService import com.example.moveeapp_compose_kmm.data.favorite.FavoriteServiceImpl +import com.example.moveeapp_compose_kmm.data.map.NominatimService +import com.example.moveeapp_compose_kmm.data.map.NominatimServiceImpl import com.example.moveeapp_compose_kmm.data.movie.MovieService import com.example.moveeapp_compose_kmm.data.movie.MovieServiceImpl import com.example.moveeapp_compose_kmm.data.rate.RatingService import com.example.moveeapp_compose_kmm.data.rate.RatingServiceImpl -import com.example.moveeapp_compose_kmm.data.map.NominatimServiceImpl -import com.example.moveeapp_compose_kmm.data.map.NominatimService import com.example.moveeapp_compose_kmm.data.search.SearchService import com.example.moveeapp_compose_kmm.data.search.SearchServiceImpl import com.example.moveeapp_compose_kmm.data.tv.TvService @@ -21,7 +21,6 @@ import io.ktor.client.HttpClient import io.ktor.client.plugins.HttpTimeout import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.client.plugins.defaultRequest -import io.ktor.client.plugins.logging.DEFAULT import io.ktor.client.plugins.logging.LogLevel import io.ktor.client.plugins.logging.Logger import io.ktor.client.plugins.logging.Logging @@ -51,8 +50,12 @@ val networkModule = module { socketTimeoutMillis = timeout } install(Logging) { - logger = Logger.DEFAULT level = LogLevel.ALL + logger = object : Logger { + override fun log(message: String) { + com.example.moveeapp_compose_kmm.log(tag = "HttpClient") { message } + } + } } install(ContentNegotiation) { json(Json { diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt index fc5fae8..9e45b82 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt @@ -23,14 +23,13 @@ import androidx.compose.ui.unit.dp import com.example.moveeapp_compose_kmm.core.StatusBarAppearance import com.example.moveeapp_compose_kmm.core.getDominantColor import com.example.moveeapp_compose_kmm.core.toComposeImageBitmap -import com.example.moveeapp_compose_kmm.core.toImage import com.example.moveeapp_compose_kmm.ui.theme.isLight import com.example.moveeapp_compose_kmm.utils.Constants import com.seiko.imageloader.LocalImageLoader import com.seiko.imageloader.asImageBitmap import com.seiko.imageloader.model.ImageRequest import com.seiko.imageloader.model.ImageResult -import com.seiko.imageloader.rememberAsyncImagePainter +import com.seiko.imageloader.rememberImagePainter import movee.shared.generated.resources.Res import movee.shared.generated.resources.search_place_holder import org.jetbrains.compose.resources.painterResource @@ -44,7 +43,7 @@ fun PosterImageItem( ) { Image( modifier = modifier.fillMaxSize(), - painter = rememberAsyncImagePainter( + painter = rememberImagePainter( Constants.IMAGE_BASE.plus(imagePath) ), contentDescription = null, @@ -72,16 +71,16 @@ fun DetailPosterImage( modifier = modifier.fillMaxSize().onGloballyPositioned { viewWidth = it.size.width }, - painter = rememberAsyncImagePainter(request, imageLoader), + painter = rememberImagePainter(request, imageLoader), contentDescription = null, contentScale = FillWidth ) LaunchedEffect(imagePath) { val imageBitmap = when (val imageResult = imageLoader.execute(request)) { - is ImageResult.Bitmap -> imageResult.bitmap.asImageBitmap() + is ImageResult.OfBitmap -> imageResult.bitmap.asImageBitmap() - is ImageResult.Image -> imageResult.toImage().toComposeImageBitmap() + is ImageResult.OfImage -> imageResult.image.toComposeImageBitmap() else -> null } @@ -115,7 +114,7 @@ fun CardImageItem( painter = if (imagePath.isNullOrEmpty()) { painterResource(Res.drawable.search_place_holder) } else { - rememberAsyncImagePainter( + rememberImagePainter( Constants.IMAGE_BASE.plus(imagePath) ) }, diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt deleted file mode 100644 index 0cff4f1..0000000 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageResultExt.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.example.moveeapp_compose_kmm.core - -import com.seiko.imageloader.Image -import com.seiko.imageloader.model.ImageResult - -actual fun ImageResult.Image.toImage(): Image = this.image \ No newline at end of file diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt index acaaf47..e4d0922 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt @@ -4,12 +4,17 @@ import androidx.compose.ui.window.ComposeUIViewController import com.example.moveeapp_compose_kmm.di.init import com.example.moveeapp_compose_kmm.utils.Action import com.example.moveeapp_compose_kmm.utils.createStore +import com.rickclephas.kmp.nsexceptionkt.core.wrapUnhandledExceptionHook +import io.github.aakira.napier.LogLevel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import org.koin.compose.KoinApplication import platform.UIKit.UIViewController fun mainViewController(): UIViewController { + wrapUnhandledExceptionHook { + log(priority = LogLevel.ERROR, tag = "Exception", throwable = it) { it.message.orEmpty() } + } val uiViewController = ComposeUIViewController { KoinApplication(application = { init() diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt index b30095a..fe83bbc 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt @@ -8,12 +8,14 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.interop.UIKitView -import com.example.moveeapp_compose_kmm.utils.asUiImage +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView import com.example.moveeapp_compose_kmm.domain.location.DeviceLocation import com.example.moveeapp_compose_kmm.ui.scene.map.Cinema import com.example.moveeapp_compose_kmm.ui.scene.map.MapUiState +import com.example.moveeapp_compose_kmm.utils.asUiImage import kotlinx.cinterop.ExperimentalForeignApi +import kotlinx.cinterop.ObjCSignatureOverride import kotlinx.cinterop.useContents import movee.shared.generated.resources.Res import movee.shared.generated.resources.ic_maps_marker @@ -117,10 +119,12 @@ actual fun Map( UIKitView( modifier = modifier.fillMaxSize(), - interactive = true, - factory = { - mkMapView - }, update = { view -> + properties = UIKitInteropProperties( + isInteractive = true, + isNativeAccessibilityEnabled = true + ), + factory = { mkMapView }, + update = { view -> mkMapView.setRegion( MKCoordinateRegionMakeWithDistance( centerCoordinate = location, @@ -149,7 +153,6 @@ actual fun Map( ) } -@Suppress("CONFLICTING_OVERLOADS", "PARAMETER_NAME_CHANGED_ON_OVERRIDE") private class MKDelegate( private val userLocationImage: UIImage?, private val markerImage: UIImage?, @@ -177,11 +180,13 @@ private class MKDelegate( } } + @ObjCSignatureOverride override fun mapView(mapView: MKMapView, didSelectAnnotationView: MKAnnotationView) { if (didSelectAnnotationView.annotation !is MKUserLocation) onAnnotationClicked(didSelectAnnotationView.annotation) } + @ObjCSignatureOverride override fun mapView(mapView: MKMapView, didDeselectAnnotationView: MKAnnotationView) { onAnnotationClicked(null) } From 24272fccc381ce132f990038344004ab5ed898a4 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Wed, 7 May 2025 14:22:15 +0300 Subject: [PATCH 02/10] Refactored UI to use Material 3 components and updated dependencies. - Replaced `androidx.compose.material` components with `androidx.compose.material3` equivalents across the project. - Updated some build configurations. --- androidApp/build.gradle.kts | 2 +- build.gradle.kts | 2 +- gradle.properties | 8 ++---- gradle/libs.versions.toml | 1 + settings.gradle.kts | 23 +++++++++++------ shared/build.gradle.kts | 17 ++++++------- .../ui/components/DetailScreensAppBar.kt | 4 +-- .../ui/components/FloatingActionButtonItem.kt | 4 +-- .../ui/components/LoadingScreen.kt | 2 +- .../ui/components/RateButton.kt | 2 +- .../ui/components/TransparentIconHolder.kt | 12 +++++---- .../ui/scene/account/AccountScreen.kt | 2 +- .../scene/artistdetail/ArtistDetailScreen.kt | 2 +- .../ui/scene/main/MainScreen.kt | 25 +++++++++++-------- .../ui/scene/movie/MovieScreen.kt | 8 +++--- .../ui/scene/moviedetail/MovieDetailScreen.kt | 6 ++--- .../ui/scene/search/SearchScreen.kt | 2 +- .../ui/scene/tv/TvScreen.kt | 4 +-- .../ui/scene/tvdetail/TvDetailScreen.kt | 6 ++--- 19 files changed, 71 insertions(+), 61 deletions(-) diff --git a/androidApp/build.gradle.kts b/androidApp/build.gradle.kts index e6a3547..965ac66 100644 --- a/androidApp/build.gradle.kts +++ b/androidApp/build.gradle.kts @@ -60,7 +60,7 @@ android { } dependencies { - implementation(project(":shared")) + implementation(projects.shared) implementation(libs.androidx.activity.compose) coreLibraryDesugaring(libs.desugar.jdk.libs) diff --git a/build.gradle.kts b/build.gradle.kts index b4176a6..31805fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,7 @@ plugins { alias(libs.plugins.android.application).apply(false) alias(libs.plugins.android.library).apply(false) - alias(libs.plugins.multiplatform).apply(false) + alias(libs.plugins.kotlin.multiplatform).apply(false) alias(libs.plugins.kotlin.android).apply(false) alias(libs.plugins.compose).apply(false) alias(libs.plugins.compose.compiler).apply(false) diff --git a/gradle.properties b/gradle.properties index 0d3326d..34cd4a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,18 +1,14 @@ #Gradle -org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" +org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 #Kotlin kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx2048M #Android android.useAndroidX=true android.nonTransitiveRClass=true -#MPP -kotlin.mpp.enableCInteropCommonization=true -kotlin.mpp.androidSourceSetLayoutVersion=2 -org.jetbrains.compose.experimental.uikit.enabled=true - #URL BASE_URL=https://api.themoviedb.org/3/ IMAGE_BASE_URL=https://image.tmdb.org/t/p/w342 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55b681f..df32327 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -71,6 +71,7 @@ nsexceptionKt-core = { module = "com.rickclephas.kmp:nsexception-kt-core", versi kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } android-application = { id = "com.android.application", version.ref = "agp" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 788266a..175408c 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,21 +1,30 @@ -@file:Suppress("UnstableApiUsage") - rootProject.name = "Movee" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") pluginManagement { repositories { - google() - gradlePluginPortal() + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + gradlePluginPortal() } } dependencyResolutionManagement { repositories { - google() + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } mavenCentral() - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") } } diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 3d7e7ab..93c6120 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,8 +1,9 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec import movee.util.requireStringProperty +import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { - alias(libs.plugins.multiplatform) + alias(libs.plugins.kotlin.multiplatform) alias(libs.plugins.compose) alias(libs.plugins.compose.compiler) alias(libs.plugins.android.library) @@ -48,20 +49,19 @@ buildkonfig { } kotlin { - androidTarget() + androidTarget{ + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } iosArm64 { binaries.framework { baseName = "shared" } } iosSimulatorArm64 { binaries.framework { baseName = "shared" } } sourceSets { - all { - languageSettings.optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") - } - commonMain.dependencies { // Compose api(compose.runtime) api(compose.foundation) - api(compose.material) api(compose.material3) api(compose.materialIconsExtended) api(compose.animation) @@ -135,9 +135,6 @@ android { defaultConfig { minSdk = libs.versions.minSdk.get().toInt() } - buildFeatures { - compose = true - } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/DetailScreensAppBar.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/DetailScreensAppBar.kt index 0ab1bf6..7471254 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/DetailScreensAppBar.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/DetailScreensAppBar.kt @@ -10,11 +10,11 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.IconButton -import androidx.compose.material.Surface import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/FloatingActionButtonItem.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/FloatingActionButtonItem.kt index b74374b..06ee522 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/FloatingActionButtonItem.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/FloatingActionButtonItem.kt @@ -4,9 +4,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Icon import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingActionButtonDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -51,4 +51,4 @@ fun FloatingActionButtonItem( fontSize = 15.sp ) } -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/LoadingScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/LoadingScreen.kt index 334e581..2d78b34 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/LoadingScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/LoadingScreen.kt @@ -2,7 +2,7 @@ package com.example.moveeapp_compose_kmm.ui.components import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.material.CircularProgressIndicator +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/RateButton.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/RateButton.kt index b5f3c8a..8c250e7 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/RateButton.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/RateButton.kt @@ -19,11 +19,11 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Star import androidx.compose.material.icons.outlined.StarOutline import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.State diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/TransparentIconHolder.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/TransparentIconHolder.kt index 7476452..28056f6 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/TransparentIconHolder.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/TransparentIconHolder.kt @@ -3,11 +3,12 @@ package com.example.moveeapp_compose_kmm.ui.components import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.CircleShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.IconButton import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Search +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -24,8 +25,9 @@ fun TransparentIconHolder( Card( modifier = modifier, shape = CircleShape, - backgroundColor = Color.LightGray.copy(alpha = 0.5f), - elevation = 0.dp) { + colors = CardDefaults.cardColors(containerColor = Color.LightGray.copy(alpha = 0.5f)), + elevation = CardDefaults.cardElevation(0.dp), + ) { IconButton(onClick = onClick) { Icon(imageVector = icon, "", tint = Color.White) } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt index 0c001a5..22b2801 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt @@ -12,11 +12,11 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.windowInsetsTopHeight -import androidx.compose.material.Surface import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt index 71664b2..1267512 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt @@ -11,7 +11,7 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card +import androidx.compose.material3.Card import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt index 74208e2..3b3ba23 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt @@ -6,12 +6,13 @@ import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding -import androidx.compose.material.BottomNavigation -import androidx.compose.material.BottomNavigationItem -import androidx.compose.material.Icon -import androidx.compose.material.Text +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemDefaults import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -38,11 +39,11 @@ fun MainScreen( } }, bottomBar = { - BottomNavigation( + NavigationBar( modifier = Modifier, - backgroundColor = MaterialTheme.colorScheme.primaryContainer, + containerColor = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.secondary, - elevation = 4.dp, + tonalElevation = 4.dp, ) { for (tabItem in tabItems) { TabNavigationItem(tab = tabItem, selected = isTabSelected(tabItem)) { @@ -63,10 +64,14 @@ private fun RowScope.TabNavigationItem( ) { val title = stringResource(tab.title) - BottomNavigationItem( + NavigationBarItem( modifier = Modifier, - unselectedContentColor = MaterialTheme.colorScheme.secondary, - selectedContentColor = MaterialTheme.colorScheme.primary, + colors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.primary, + selectedTextColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.secondary, + unselectedTextColor = MaterialTheme.colorScheme.secondary, + ), alwaysShowLabel = true, selected = selected, onClick = onClick, diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt index fe6f5f2..b9ffdd7 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt @@ -22,11 +22,11 @@ import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Card -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.Surface +import androidx.compose.material3.Card +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt index fab0ec1..b647fdc 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt @@ -20,10 +20,10 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share -import androidx.compose.material3.Divider +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -206,7 +206,7 @@ fun MovieDetailContent( ) }) - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 10.dp), thickness = 1.dp, color = MaterialTheme.colorScheme.secondaryContainer diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt index ca6bbee..252e47b 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt @@ -19,9 +19,9 @@ import androidx.compose.foundation.layout.windowInsetsTopHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.Icon import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt index a65590c..b267885 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt @@ -20,9 +20,9 @@ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState -import androidx.compose.material.Card -import androidx.compose.material.Surface +import androidx.compose.material3.Card import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt index 17d51ce..72d4d07 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt @@ -21,10 +21,10 @@ import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll -import androidx.compose.material.Card import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Share -import androidx.compose.material3.Divider +import androidx.compose.material3.Card +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -198,7 +198,7 @@ fun TvDetailContent( ) }) - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 10.dp), thickness = 1.dp, color = MaterialTheme.colorScheme.secondaryContainer From 52532a15dc02ac5bfdf54964fc4ec555fbace359 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Fri, 30 May 2025 14:01:44 +0300 Subject: [PATCH 03/10] "Compose Hot Reload" is added to project. - This addition requires adding a desktop target to application. Because of that, settings and webview implementation need to be updated. - The old back handling system is changed with official multiplatform library - Some dependencies are updated. --- desktopApp/build.gradle.kts | 23 +++++++ .../com/example/moveeapp_compose_kmm/main.kt | 24 +++++++ gradle/libs.versions.toml | 30 +++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- iosApp/iosApp.xcodeproj/project.pbxproj | 2 +- settings.gradle.kts | 1 + shared/build.gradle.kts | 37 +++++++++-- .../moveeapp_compose_kmm/core/BackHandler.kt | 8 --- .../moveeapp_compose_kmm/di/Modules.kt | 24 ++++--- .../example/moveeapp_compose_kmm/map/Map.kt | 2 +- .../core/ImageBitmapExt.kt | 3 +- .../data/KVaultSettingsProvider.kt | 25 -------- .../data/MultiplatformSettingsProvider.kt | 64 +++++++++++++++++++ .../moveeapp_compose_kmm/nav/AccountScreen.kt | 6 +- .../nav/ArtistDetailScreen.kt | 6 +- .../nav/FavoriteScreen.kt | 6 +- .../nav/MovieDetailScreen.kt | 6 +- .../moveeapp_compose_kmm/nav/SearchScreen.kt | 6 +- .../nav/TvDetailScreen.kt | 6 +- .../ui/scene/map/MapScreen.kt | 3 +- .../moveeapp_compose_kmm/core/BackHadler.kt | 16 ----- .../moveeapp_compose_kmm/di/Modules.kt | 8 ++- .../example/moveeapp_compose_kmm/main.ios.kt | 5 -- .../example/moveeapp_compose_kmm/map/Map.kt | 2 +- .../moveeapp_compose_kmm/core/ImageExt.jvm.kt | 9 +++ .../core/NavigateToMap.jvm.kt | 11 ++++ .../core/PlatformContext.jvm.kt | 8 +++ .../moveeapp_compose_kmm/core/Share.jvm.kt | 8 +++ .../core/StatusBarAppearance.jvm.kt} | 2 +- .../moveeapp_compose_kmm/core/WebView.jvm.kt | 45 +++++++++++++ .../moveeapp_compose_kmm/di/Modules.kt | 31 +++++++++ .../moveeapp_compose_kmm/map/Map.jvm.kt | 22 +++++++ .../permission/MutablePermissionState.jvm.kt | 21 ++++++ 33 files changed, 371 insertions(+), 101 deletions(-) create mode 100644 desktopApp/build.gradle.kts create mode 100644 desktopApp/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/main.kt delete mode 100644 shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt delete mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/KVaultSettingsProvider.kt create mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/MultiplatformSettingsProvider.kt delete mode 100644 shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHadler.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageExt.jvm.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/NavigateToMap.jvm.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/PlatformContext.jvm.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt rename shared/src/{commonMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt => jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/StatusBarAppearance.jvm.kt} (62%) create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/WebView.jvm.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.jvm.kt create mode 100644 shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/permission/MutablePermissionState.jvm.kt diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts new file mode 100644 index 0000000..f022d6b --- /dev/null +++ b/desktopApp/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.compose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.compose.hotReload) +} + +kotlin { + jvm() + + sourceSets { + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.foundation) + } + + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(projects.shared) + } + } +} diff --git a/desktopApp/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/main.kt b/desktopApp/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/main.kt new file mode 100644 index 0000000..5ff6fae --- /dev/null +++ b/desktopApp/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/main.kt @@ -0,0 +1,24 @@ +package com.example.moveeapp_compose_kmm + +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application +import androidx.compose.ui.window.rememberWindowState +import com.example.moveeapp_compose_kmm.di.init +import org.koin.compose.KoinApplication +import java.awt.Dimension + +fun main() = application { + Window( + title = "Movee", + state = rememberWindowState(width = 360.dp, height = 720.dp), + onCloseRequest = ::exitApplication, + ) { + window.minimumSize = Dimension(300, 600) + KoinApplication(application = { + init() + }) { + App() + } + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df32327..3705efa 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,26 +4,26 @@ minSdk = "24" targetSdk = "34" compileSdk = "35" -kotlin = "2.1.20" +kotlin = "2.1.21" kotlinx-coroutines = "1.10.2" kotlinx-serialization = "1.8.1" -kotlinx-datetime = "0.6.2" -ktor = "3.1.2" -agp = "8.9.1" +ktor = "3.1.3" +agp = "8.10.1" desugar_jdk_libs = "2.1.5" -compose = "1.7.3" +compose = "1.8.1" +composeHotReload = "1.0.0-alpha10" androidx = "1.16.0" androidx-activity = "1.10.1" androidx-appcompat = "1.7.0" voyager = "1.0.1" napier = "2.7.1" -buildKonfig = "0.17.0" +buildKonfig = "0.17.1" koin = "4.0.4" seiko = "1.10.0" -mapsCompose = "6.5.2" +mapsCompose = "6.6.0" playServicesLocation = "21.3.0" maps-secrets = "2.0.1" -nsexceptionKt = "0.1.17" +javafx = "24.0.1" [libraries] @@ -40,18 +40,18 @@ voyager-tabs = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.r voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" } voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" } +ui-backhandler = { module = "org.jetbrains.compose.ui:ui-backhandler", version.ref = "compose" } + logger = { module = "io.github.aakira:napier", version.ref = "napier" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } -kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } -ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } ktor-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-contentNegotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } @@ -63,17 +63,21 @@ koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" } image-loader = { module = "io.github.qdsfdhvh:image-loader", version.ref = "seiko" } -settings = { module = "com.liftric:kvault", version = "1.12.0" } +settings = { module = "com.russhwolf:multiplatform-settings", version = "1.3.0" } +encryptedprefs = { module = "dev.spght:encryptedprefs-ktx", version = "1.0.1" } -nsexceptionKt-core = { module = "com.rickclephas.kmp:nsexception-kt-core", version.ref = "nsexceptionKt" } +javafx-base = { module = "org.openjfx:javafx-base", version.ref = "javafx" } +javafx-graphics = { module = "org.openjfx:javafx-graphics", version.ref = "javafx" } +javafx-swing = { module = "org.openjfx:javafx-swing", version.ref = "javafx" } +javafx-web = { module = "org.openjfx:javafx-web", version.ref = "javafx" } [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } -multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } compose = { id = "org.jetbrains.compose", version.ref = "compose" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +compose-hotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } android-application = { id = "com.android.application", version.ref = "agp" } android-library = { id = "com.android.library", version.ref = "agp" } buildKonfig = { id = "com.codingfeline.buildkonfig", version.ref = "buildKonfig" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index c1f3894..21fdc55 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 01 14:19:46 TRT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 43d7dd6..74f670d 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -320,7 +320,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; - DEVELOPMENT_TEAM = ""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; ENABLE_PREVIEWS = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", diff --git a/settings.gradle.kts b/settings.gradle.kts index 175408c..32e2bbc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -29,4 +29,5 @@ dependencyResolutionManagement { } include(":androidApp") +include(":desktopApp") include(":shared") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 93c6120..7d1cc3e 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -49,14 +49,17 @@ buildkonfig { } kotlin { - androidTarget{ + androidTarget { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } + iosArm64 { binaries.framework { baseName = "shared" } } iosSimulatorArm64 { binaries.framework { baseName = "shared" } } + jvm() + sourceSets { commonMain.dependencies { // Compose @@ -65,6 +68,7 @@ kotlin { api(compose.material3) api(compose.materialIconsExtended) api(compose.animation) + api(libs.ui.backhandler) implementation(compose.components.resources) implementation(libs.logger) @@ -95,8 +99,8 @@ kotlin { //Image loader api(libs.image.loader) - //KVault - api(libs.settings) + //Settings + implementation(libs.settings) } commonTest.dependencies { @@ -119,11 +123,20 @@ kotlin { //Location api(libs.play.services.location) + + implementation(libs.encryptedprefs) } iosMain.dependencies { implementation(libs.ktor.client.darwin) - implementation(libs.nsexceptionKt.core) + } + + jvmMain.dependencies { + val osSuffix = getOsSuffix() + implementation(dependencies.variantOf(libs.javafx.base) { classifier(osSuffix) }) + implementation(dependencies.variantOf(libs.javafx.graphics) { classifier(osSuffix) }) + implementation(dependencies.variantOf(libs.javafx.swing) { classifier(osSuffix) }) + implementation(dependencies.variantOf(libs.javafx.web) { classifier(osSuffix) }) } } } @@ -140,3 +153,19 @@ android { targetCompatibility = JavaVersion.VERSION_11 } } + +private fun getOsSuffix(): String { + // https://stackoverflow.com/questions/73187027/use-javafx-in-kotlin-multiplatform + // As JavaFX have platform-specific dependencies, we need to add them manually + val os = org.gradle.internal.os.OperatingSystem.current() + val arch = System.getProperty("os.arch") + + return when { + os.isWindows -> "win" + os.isMacOsX && arch == "aarch64" -> "mac-aarch64" + os.isMacOsX -> "mac" + os.isLinux && arch == "aarch64" -> "linux-aarch64" + os.isLinux -> "linux" + else -> throw IllegalStateException("Unknown OS: ${os.name}") + } +} diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt deleted file mode 100644 index 2876947..0000000 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.example.moveeapp_compose_kmm.core - -import androidx.compose.runtime.Composable - -@Composable -actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) { - androidx.activity.compose.BackHandler(isEnabled, onBack) -} \ No newline at end of file diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt index 1868e74..852e1f4 100644 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt +++ b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt @@ -1,24 +1,32 @@ package com.example.moveeapp_compose_kmm.di -import com.example.moveeapp_compose_kmm.domain.account.SessionSettings -import com.example.moveeapp_compose_kmm.data.KVaultSettingsProvider import com.example.moveeapp_compose_kmm.core.AndroidLocationRepository +import com.example.moveeapp_compose_kmm.data.MultiplatformSettingsProvider +import com.example.moveeapp_compose_kmm.domain.account.SessionSettings import com.example.moveeapp_compose_kmm.domain.location.LocationRepository - -import com.liftric.kvault.KVault +import com.russhwolf.settings.SharedPreferencesSettings +import dev.spght.encryptedprefs.EncryptedSharedPreferences +import dev.spght.encryptedprefs.MasterKey import org.koin.android.ext.koin.androidContext import org.koin.dsl.module actual val settingsModule = module { single { + val masterKey = MasterKey(androidContext(), keyScheme = MasterKey.KeyScheme.AES256_GCM) + val prefs = EncryptedSharedPreferences.create( + androidContext(), + NameSessionSettings, + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM + ) + SessionSettings( - settingsProvider = KVaultSettingsProvider( - KVault(androidContext(), NameSessionSettings) - ) + MultiplatformSettingsProvider(SharedPreferencesSettings(prefs)) ) } } actual val locationModule = module { - factory { AndroidLocationRepository(context = androidContext()) } + factory { AndroidLocationRepository(context = androidContext()) } } diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt index dda8669..c4ee7d1 100644 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt +++ b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt @@ -58,7 +58,7 @@ actual fun Map( GoogleMapsComponent( - modifier = Modifier.fillMaxSize(), + modifier = modifier, cameraPositionState = cameraPositionState, onMapClick = { onMarkerClick.invoke(null) diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageBitmapExt.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageBitmapExt.kt index 8dc3c1a..73ab1d6 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageBitmapExt.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageBitmapExt.kt @@ -6,7 +6,8 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.IO import kotlinx.coroutines.withContext -suspend fun ImageBitmap.getDominantColor(height: Int): Color { +suspend fun ImageBitmap.getDominantColor(height: Int): Color? { + if (height <= 0) return null return withContext(Dispatchers.IO) { val pixels = IntArray(width * height) readPixels(pixels, width = width, height = height) diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/KVaultSettingsProvider.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/KVaultSettingsProvider.kt deleted file mode 100644 index 077bd28..0000000 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/KVaultSettingsProvider.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.example.moveeapp_compose_kmm.data - -import com.example.moveeapp_compose_kmm.domain.SettingsProvider -import com.liftric.kvault.KVault - -class KVaultSettingsProvider(private val kVault: KVault) : SettingsProvider { - override fun set(key: String, value: Boolean): Boolean = kVault.set(key, value) - override fun set(key: String, value: Double): Boolean = kVault.set(key, value) - override fun set(key: String, value: Float): Boolean = kVault.set(key, value) - override fun set(key: String, value: Int): Boolean = kVault.set(key, value) - override fun set(key: String, value: Long): Boolean = kVault.set(key, value) - override fun set(key: String, value: String): Boolean = kVault.set(key, value) - - override fun bool(forKey: String): Boolean? = kVault.bool(forKey) - override fun int(forKey: String): Int? = kVault.int(forKey) - override fun float(forKey: String): Float? = kVault.float(forKey) - override fun double(forKey: String): Double? = kVault.double(forKey) - override fun long(forKey: String): Long? = kVault.long(forKey) - override fun string(forKey: String): String? = kVault.string(forKey) - - override fun keys(): List = kVault.allKeys() - override fun exists(forKey: String): Boolean = kVault.existsObject(forKey) - override fun delete(forKey: String): Boolean = kVault.deleteObject(forKey) - override fun clear(): Boolean = kVault.clear() -} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/MultiplatformSettingsProvider.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/MultiplatformSettingsProvider.kt new file mode 100644 index 0000000..0ce15ff --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/data/MultiplatformSettingsProvider.kt @@ -0,0 +1,64 @@ +package com.example.moveeapp_compose_kmm.data + +import com.example.moveeapp_compose_kmm.domain.SettingsProvider +import com.russhwolf.settings.Settings +import com.russhwolf.settings.get +import com.russhwolf.settings.set + +class MultiplatformSettingsProvider(private val settings: Settings) : SettingsProvider { + override fun set(key: String, value: Boolean): Boolean { + settings.putBoolean(key, value) + return true + } + + override fun set(key: String, value: Double): Boolean { + settings[key] = value + return true + } + + override fun set(key: String, value: Float): Boolean { + settings[key] = value + return true + } + + override fun set(key: String, value: Int): Boolean { + settings[key] = value + return true + } + + override fun set(key: String, value: Long): Boolean { + settings[key] = value + return true + } + + override fun set(key: String, value: String): Boolean { + settings[key] = value + return true + } + + override fun bool(forKey: String): Boolean? = settings[forKey] + + override fun int(forKey: String): Int? = settings[forKey] + + override fun float(forKey: String): Float? = settings[forKey] + + override fun double(forKey: String): Double? = settings[forKey] + + override fun long(forKey: String): Long? = settings[forKey] + + override fun string(forKey: String): String? = settings[forKey] + + override fun keys(): List = settings.keys.toList() + + override fun exists(forKey: String): Boolean = settings.hasKey(forKey) + + override fun delete(forKey: String): Boolean { + settings.remove(forKey) + return true + } + + override fun clear(): Boolean { + settings.clear() + return true + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt index 7cca76c..727607f 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt @@ -1,16 +1,18 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.account.AccountDetailViewModel import com.example.moveeapp_compose_kmm.ui.scene.account.AccountScreen class AccountScreen : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -22,7 +24,7 @@ class AccountScreen : Screen { navigateToSplash = { mainNavigator.replaceAll(SplashScreen()) }, navigateToFavorite = { navigator.push(FavoriteScreen(it)) }) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt index 123b55f..fc2d1d1 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt @@ -1,10 +1,11 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler +import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.artistdetail.ArtistDetailScreen import com.example.moveeapp_compose_kmm.ui.scene.artistdetail.ArtistDetailViewModel @@ -13,6 +14,7 @@ class ArtistDetailScreen( private val actorId: Int, ) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -26,7 +28,7 @@ class ArtistDetailScreen( onBackPressed = navigator::pop ) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt index 8a0f405..4c2a416 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt @@ -1,10 +1,11 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler +import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.domain.MediaType import com.example.moveeapp_compose_kmm.ui.scene.account.favoritescreen.FavoriteScreen @@ -12,6 +13,7 @@ import com.example.moveeapp_compose_kmm.ui.scene.account.favoritescreen.Favorite class FavoriteScreen(private val mediaType: MediaType) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -25,7 +27,7 @@ class FavoriteScreen(private val mediaType: MediaType) : Screen { navigateBack = { navigator.pop() } ) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt index c9555a2..7605754 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt @@ -1,10 +1,11 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.backhandler.BackHandler import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.moviedetail.MovieDetailScreen import com.example.moveeapp_compose_kmm.ui.scene.moviedetail.MovieDetailViewModel @@ -13,6 +14,7 @@ class MovieDetailScreen( private val movieId: Int, ) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -25,7 +27,7 @@ class MovieDetailScreen( onBackPressed = navigator::pop, ) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt index 51927af..8fd6761 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt @@ -1,16 +1,18 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler +import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.search.SearchScreen import com.example.moveeapp_compose_kmm.ui.scene.search.SearchViewModel class SearchScreen : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -23,7 +25,7 @@ class SearchScreen : Screen { navigateToActor = { navigator.push(ArtistDetailScreen(it)) } ) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt index 93e09cf..776a645 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt @@ -1,16 +1,18 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable +import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import com.example.moveeapp_compose_kmm.core.BackHandler +import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.tvdetail.TvDetailScreen import com.example.moveeapp_compose_kmm.ui.scene.tvdetail.TvDetailViewModel class TvDetailScreen(private val tvId: Int) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -22,7 +24,7 @@ class TvDetailScreen(private val tvId: Int) : Screen { onBackPressed = navigator::pop ) - BackHandler(isEnabled = true) { + BackHandler(enabled = true) { navigator.pop() } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt index eb610c9..3851bb6 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.MaterialTheme @@ -63,7 +64,7 @@ class MapScreen : Screen { Box(modifier = Modifier.padding(top = paddingValues.calculateTopPadding())) { Map( - modifier = Modifier, + modifier = Modifier.fillMaxSize(), uiState = uiState, onMarkerClick = viewModel::setSelectedCinema, onPositionChange = viewModel::getUpdates diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHadler.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHadler.kt deleted file mode 100644 index 4ea9668..0000000 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHadler.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.example.moveeapp_compose_kmm.core - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import com.example.moveeapp_compose_kmm.store - -@Composable -actual fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) { - LaunchedEffect(isEnabled) { - store.events.collect { - if (isEnabled) { - onBack() - } - } - } -} diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt index 25f229e..d4d595f 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt @@ -1,16 +1,18 @@ package com.example.moveeapp_compose_kmm.di import com.example.moveeapp_compose_kmm.core.IosLocationRepository +import com.example.moveeapp_compose_kmm.data.MultiplatformSettingsProvider import com.example.moveeapp_compose_kmm.domain.account.SessionSettings -import com.example.moveeapp_compose_kmm.data.KVaultSettingsProvider import com.example.moveeapp_compose_kmm.domain.location.LocationRepository -import com.liftric.kvault.KVault +import com.russhwolf.settings.ExperimentalSettingsImplementation +import com.russhwolf.settings.KeychainSettings import org.koin.dsl.module +@OptIn(ExperimentalSettingsImplementation::class) actual val settingsModule = module { single { SessionSettings( - KVaultSettingsProvider(KVault(NameSessionSettings)) + MultiplatformSettingsProvider(KeychainSettings(NameSessionSettings)) ) } } diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt index e4d0922..acaaf47 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt @@ -4,17 +4,12 @@ import androidx.compose.ui.window.ComposeUIViewController import com.example.moveeapp_compose_kmm.di.init import com.example.moveeapp_compose_kmm.utils.Action import com.example.moveeapp_compose_kmm.utils.createStore -import com.rickclephas.kmp.nsexceptionkt.core.wrapUnhandledExceptionHook -import io.github.aakira.napier.LogLevel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.SupervisorJob import org.koin.compose.KoinApplication import platform.UIKit.UIViewController fun mainViewController(): UIViewController { - wrapUnhandledExceptionHook { - log(priority = LogLevel.ERROR, tag = "Exception", throwable = it) { it.message.orEmpty() } - } val uiViewController = ComposeUIViewController { KoinApplication(application = { init() diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt index fe83bbc..65ab5b3 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.kt @@ -118,7 +118,7 @@ actual fun Map( } UIKitView( - modifier = modifier.fillMaxSize(), + modifier = modifier, properties = UIKitInteropProperties( isInteractive = true, isNativeAccessibilityEnabled = true diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageExt.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageExt.jvm.kt new file mode 100644 index 0000000..0b14215 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/ImageExt.jvm.kt @@ -0,0 +1,9 @@ +package com.example.moveeapp_compose_kmm.core + +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.toComposeImageBitmap +import com.seiko.imageloader.Image + +actual fun Image.toComposeImageBitmap(): ImageBitmap { + return this.toComposeImageBitmap() +} diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/NavigateToMap.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/NavigateToMap.jvm.kt new file mode 100644 index 0000000..85a7493 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/NavigateToMap.jvm.kt @@ -0,0 +1,11 @@ +package com.example.moveeapp_compose_kmm.core + +import com.example.moveeapp_compose_kmm.domain.location.DeviceLocation + +actual fun navigateToMap( + context: PlatformContext, + deviceLocation: DeviceLocation?, + destinationName: String +) { + TODO("Not yet implemented") +} diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/PlatformContext.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/PlatformContext.jvm.kt new file mode 100644 index 0000000..4355aa2 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/PlatformContext.jvm.kt @@ -0,0 +1,8 @@ +package com.example.moveeapp_compose_kmm.core + +import androidx.compose.runtime.Composable + +actual class PlatformContext + +@Composable +actual fun getPlatformContext(): PlatformContext = PlatformContext() diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt new file mode 100644 index 0000000..1c21953 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt @@ -0,0 +1,8 @@ +package com.example.moveeapp_compose_kmm.core + +actual fun share( + context: PlatformContext, + text: String +) { + +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/StatusBarAppearance.jvm.kt similarity index 62% rename from shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt rename to shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/StatusBarAppearance.jvm.kt index f68e46d..042cd99 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/BackHandler.kt +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/StatusBarAppearance.jvm.kt @@ -3,4 +3,4 @@ package com.example.moveeapp_compose_kmm.core import androidx.compose.runtime.Composable @Composable -expect fun BackHandler(isEnabled: Boolean, onBack: () -> Unit) +actual fun StatusBarAppearance(isBackgroundLight: Boolean) { } diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/WebView.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/WebView.jvm.kt new file mode 100644 index 0000000..4948906 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/WebView.jvm.kt @@ -0,0 +1,45 @@ +package com.example.moveeapp_compose_kmm.core + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.awt.SwingPanel +import javafx.application.Platform +import javafx.embed.swing.JFXPanel +import javafx.scene.Scene +import javafx.scene.web.WebView +import javax.swing.JPanel + +@Composable +actual fun WebView(modifier: Modifier, link: String) { + val jPanel: JPanel = remember { JPanel() } + val jfxPanel = JFXPanel() + + SwingPanel( + factory = { + jfxPanel.apply { buildWebView(link) } + jPanel.add(jfxPanel) + }, + modifier = modifier, + ) + + DisposableEffect(link) { onDispose { jPanel.remove(jfxPanel) } } +} + +@Suppress("SetJavaScriptEnabled") +private fun JFXPanel.buildWebView(url: String) { + Platform.runLater { + val webView = WebView() + val webEngine = webView.engine + + webEngine.userAgent = + "Mozilla/5.0 (Linux; Android 11; SAMSUNG SM-G973U) AppleWebKit/537.36 (KHTML, like Gecko) SamsungBrowser/14.2 Chrome/87.0.4280.141 Mobile Safari/537.36" + + webEngine.isJavaScriptEnabled = true + + webEngine.load(url) + val scene = Scene(webView) + setScene(scene) + } +} diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt new file mode 100644 index 0000000..a06a3a1 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/di/Modules.kt @@ -0,0 +1,31 @@ +package com.example.moveeapp_compose_kmm.di + +import com.example.moveeapp_compose_kmm.data.MultiplatformSettingsProvider +import com.example.moveeapp_compose_kmm.domain.account.SessionSettings +import com.example.moveeapp_compose_kmm.domain.location.DeviceLocation +import com.example.moveeapp_compose_kmm.domain.location.LocationRepository +import com.russhwolf.settings.PreferencesSettings +import org.koin.dsl.module +import java.util.prefs.Preferences + +actual val settingsModule = module { + single { + SessionSettings( + MultiplatformSettingsProvider( + PreferencesSettings(Preferences.userRoot().node(NameSessionSettings)) + ) + ) + } +} + +actual val locationModule = module { + factory { + object : LocationRepository { + override suspend fun getCurrentLocation(): DeviceLocation { + return DeviceLocation(0.0, 0.0) + } + } + } +} + + diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.jvm.kt new file mode 100644 index 0000000..70e4731 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/map/Map.jvm.kt @@ -0,0 +1,22 @@ +package com.example.moveeapp_compose_kmm.map + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import com.example.moveeapp_compose_kmm.domain.location.DeviceLocation +import com.example.moveeapp_compose_kmm.ui.scene.map.Cinema +import com.example.moveeapp_compose_kmm.ui.scene.map.MapUiState + +@Composable +actual fun Map( + modifier: Modifier, + uiState: MapUiState, + onMarkerClick: (Cinema?) -> Unit, + onPositionChange: (DeviceLocation) -> Unit +) { + Box(modifier) { + Text("Maps Content", Modifier.align(Alignment.Center)) + } +} diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/permission/MutablePermissionState.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/permission/MutablePermissionState.jvm.kt new file mode 100644 index 0000000..b293d9b --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/permission/MutablePermissionState.jvm.kt @@ -0,0 +1,21 @@ +package com.example.moveeapp_compose_kmm.permission + +import androidx.compose.runtime.Composable + +@Composable +actual fun rememberMutablePermissionState( + permission: Permission, + onPermissionResult: (Boolean) -> Unit +): PermissionState = object : PermissionState { + override val permission: Permission + get() = permission + + override val status: PermissionStatus + get() = PermissionStatus.Granted + + override fun launchPermissionRequest() { + } + + override fun openSettings() { + } +} From d56dd8900b73740e74edacda33bfe99deff999b6 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Tue, 24 Jun 2025 10:56:24 +0300 Subject: [PATCH 04/10] Refactor: Integrate predictive back gesture and update dependencies - Implemented predictive back gesture functionality using `PredictiveBackHandler` and custom screen transitions for a smoother user experience. - Removed the custom swipe gesture handling in iOS and the associated `Store` utility class. - Updated various library dependencies to their latest versions, including Compose, Voyager, Koin, and others. - Enabled `enableOnBackInvokedCallback` in the AndroidManifest for API 33 and above. - Removed explicit `BackHandler` composables from individual screens as back navigation is now handled by the navigator and predictive back gesture. --- androidApp/src/main/AndroidManifest.xml | 18 +- gradle/libs.versions.toml | 10 +- iosApp/iosApp.xcodeproj/project.pbxproj | 4 - iosApp/iosApp/AppDelegate.swift | 8 - iosApp/iosApp/ContentView.swift | 72 +------ iosApp/iosApp/SwiftToKotlin.swift | 14 -- .../moveeapp_compose_kmm/SwipeNavigator.kt | 45 +++++ .../moveeapp_compose_kmm/nav/AccountScreen.kt | 5 - .../nav/ArtistDetailScreen.kt | 5 - .../nav/FavoriteScreen.kt | 5 - .../nav/MovieDetailScreen.kt | 5 - .../nav/PredictiveBackTransition.kt | 185 ++++++++++++++++++ .../moveeapp_compose_kmm/nav/SearchScreen.kt | 5 - .../example/moveeapp_compose_kmm/nav/Tabs.kt | 32 ++- .../nav/TvDetailScreen.kt | 5 - .../moveeapp_compose_kmm/utils/Store.kt | 29 --- .../example/moveeapp_compose_kmm/main.ios.kt | 14 +- 17 files changed, 282 insertions(+), 179 deletions(-) delete mode 100644 iosApp/iosApp/SwiftToKotlin.swift create mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt create mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/PredictiveBackTransition.kt delete mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/utils/Store.kt diff --git a/androidApp/src/main/AndroidManifest.xml b/androidApp/src/main/AndroidManifest.xml index 8506eff..0753b48 100644 --- a/androidApp/src/main/AndroidManifest.xml +++ b/androidApp/src/main/AndroidManifest.xml @@ -1,15 +1,20 @@ - + + + + tools:targetApi="33"> @@ -18,7 +23,12 @@ - - + + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3705efa..bc99c02 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,15 +10,15 @@ kotlinx-serialization = "1.8.1" ktor = "3.1.3" agp = "8.10.1" desugar_jdk_libs = "2.1.5" -compose = "1.8.1" -composeHotReload = "1.0.0-alpha10" +compose = "1.8.2" +composeHotReload = "1.0.0-alpha11" androidx = "1.16.0" androidx-activity = "1.10.1" -androidx-appcompat = "1.7.0" -voyager = "1.0.1" +androidx-appcompat = "1.7.1" +voyager = "1.1.0-beta03" napier = "2.7.1" buildKonfig = "0.17.1" -koin = "4.0.4" +koin = "4.1.0" seiko = "1.10.0" mapsCompose = "6.6.0" playServicesLocation = "21.3.0" diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index 74f670d..a9ac559 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557BA273AAA24004C7B11 /* Assets.xcassets */; }; 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */; }; - 391C5BDB2AC582D400F44208 /* SwiftToKotlin.swift in Sources */ = {isa = PBXBuildFile; fileRef = 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */; }; 7555FF83242A565900829871 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7555FF82242A565900829871 /* ContentView.swift */; }; A47FDF582A8CD56700CA069B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = A47FDF572A8CD56700CA069B /* AppDelegate.swift */; }; /* End PBXBuildFile section */ @@ -30,7 +29,6 @@ /* Begin PBXFileReference section */ 058557BA273AAA24004C7B11 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 058557D8273AAEEB004C7B11 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; - 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftToKotlin.swift; sourceTree = ""; }; 7555FF7B242A565900829871 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7555FF82242A565900829871 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 7555FF8C242A565B00829871 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -80,7 +78,6 @@ 7555FF8C242A565B00829871 /* Info.plist */, 058557D7273AAEEB004C7B11 /* Preview Content */, A47FDF572A8CD56700CA069B /* AppDelegate.swift */, - 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */, ); path = iosApp; sourceTree = ""; @@ -191,7 +188,6 @@ files = ( A47FDF582A8CD56700CA069B /* AppDelegate.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, - 391C5BDB2AC582D400F44208 /* SwiftToKotlin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/iosApp/iosApp/AppDelegate.swift b/iosApp/iosApp/AppDelegate.swift index 009b28b..30f8d4d 100644 --- a/iosApp/iosApp/AppDelegate.swift +++ b/iosApp/iosApp/AppDelegate.swift @@ -1,11 +1,3 @@ -// -// AppDelegate.swift -// iosApp -// -// Created by Kilinc, Furkan on 16.08.2023. -// Copyright © 2023 orgName. All rights reserved. -// - import Foundation import shared import SwiftUI diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift index f80c8cb..58cdec4 100644 --- a/iosApp/iosApp/ContentView.swift +++ b/iosApp/iosApp/ContentView.swift @@ -4,80 +4,14 @@ import shared struct ContentView: View { var body: some View { - VStack { - SwipeGestureViewController { - onBackGesture() - } - }.ignoresSafeArea(.all) // Compose has own keyboard handler + ComposeView().ignoresSafeArea(.all) } } -class ContainerViewController: UIViewController { - private let onTouchDown: (CGPoint) -> Void - - init(child: UIViewController, onTouchDown: @escaping (CGPoint) -> Void) { - self.onTouchDown = onTouchDown - super.init(nibName: nil, bundle: nil) - addChild(child) - child.view.frame = view.frame - view.addSubview(child.view) - child.didMove(toParent: self) - } - - required init?(coder: NSCoder) { - fatalError("init(coder:) has not been implemented") - } - - override func touchesBegan(_ touches: Set, with event: UIEvent?) { - super.touchesBegan(touches, with: event) - if let startPoint = touches.first?.location(in: nil) { - onTouchDown(startPoint) - } - } -} - -struct SwipeGestureViewController: UIViewControllerRepresentable { - var onSwipe: () -> Void - +struct ComposeView: UIViewControllerRepresentable { func makeUIViewController(context: Context) -> UIViewController { - let viewController = Main_iosKt.mainViewController() - let containerController = ContainerViewController(child: viewController) { - context.coordinator.startPoint = $0 - } - - let swipeGestureRecognizer = UISwipeGestureRecognizer( - target: - context.coordinator, action: #selector(Coordinator.handleSwipe) - ) - swipeGestureRecognizer.direction = .right - swipeGestureRecognizer.numberOfTouchesRequired = 1 - containerController.view.addGestureRecognizer(swipeGestureRecognizer) - - return containerController + return Main_iosKt.mainViewController() } func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} - - func makeCoordinator() -> Coordinator { - Coordinator(onSwipe: onSwipe) - } - - class Coordinator: NSObject, UIGestureRecognizerDelegate { - var onSwipe: () -> Void - var startPoint: CGPoint? - - init(onSwipe: @escaping () -> Void) { - self.onSwipe = onSwipe - } - - @objc func handleSwipe(_ gesture: UISwipeGestureRecognizer) { - if gesture.state == .ended, let startPoint = startPoint, startPoint.x < 50 { - onSwipe() - } - } - - func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { - true - } - } } diff --git a/iosApp/iosApp/SwiftToKotlin.swift b/iosApp/iosApp/SwiftToKotlin.swift deleted file mode 100644 index b9747df..0000000 --- a/iosApp/iosApp/SwiftToKotlin.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// SwiftToKotlin.swift -// iosApp -// -// Created by Aydin, Senanur on 28.09.2023. -// Copyright © 2023 orgName. All rights reserved. -// - -import Foundation -import shared - -public func onBackGesture() { - Main_iosKt.onBackGesture() -} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt new file mode 100644 index 0000000..097efaa --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt @@ -0,0 +1,45 @@ +package com.example.moveeapp_compose_kmm + +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.key +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import cafe.adriel.voyager.navigator.Navigator + +@Composable +fun SwipeNavigator(navigator: Navigator) { + + val currentSceneEntry = navigator.lastItem + val prevSceneEntry = remember(navigator.items) { navigator.items.getOrNull(navigator.size - 2) } + + var prevWasSwiped by remember { + mutableStateOf(false) + } + + LaunchedEffect(currentSceneEntry) { + prevWasSwiped = false + } + + val dismissState = key(currentSceneEntry) { + rememberSwipeToDismissBoxState() + } + + LaunchedEffect(dismissState.currentValue) { + if (dismissState.currentValue == SwipeToDismissBoxValue.StartToEnd) { + prevWasSwiped = true + navigator.pop() + } + } + + val showPrev by remember(dismissState) { + derivedStateOf { + dismissState.progress > 0f + } + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt index 727607f..f5f619a 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt @@ -2,7 +2,6 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.backhandler.BackHandler import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -23,9 +22,5 @@ class AccountScreen : Screen { viewModel = viewModel, navigateToSplash = { mainNavigator.replaceAll(SplashScreen()) }, navigateToFavorite = { navigator.push(FavoriteScreen(it)) }) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt index fc2d1d1..c72e8f9 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/ArtistDetailScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.artistdetail.ArtistDetailScreen import com.example.moveeapp_compose_kmm.ui.scene.artistdetail.ArtistDetailViewModel @@ -27,9 +26,5 @@ class ArtistDetailScreen( navigateToTv = { navigator.push(TvDetailScreen(tvId = it)) }, onBackPressed = navigator::pop ) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt index 4c2a416..88255f6 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/FavoriteScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.domain.MediaType import com.example.moveeapp_compose_kmm.ui.scene.account.favoritescreen.FavoriteScreen @@ -26,9 +25,5 @@ class FavoriteScreen(private val mediaType: MediaType) : Screen { navigateToTv = { navigator.push(TvDetailScreen(it)) }, navigateBack = { navigator.pop() } ) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt index 7605754..0b53d6f 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/MovieDetailScreen.kt @@ -2,7 +2,6 @@ package com.example.moveeapp_compose_kmm.nav import androidx.compose.runtime.Composable import androidx.compose.ui.ExperimentalComposeUiApi -import androidx.compose.ui.backhandler.BackHandler import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow @@ -26,9 +25,5 @@ class MovieDetailScreen( navigateToActor = { navigator.push(ArtistDetailScreen(it)) }, onBackPressed = navigator::pop, ) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/PredictiveBackTransition.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/PredictiveBackTransition.kt new file mode 100644 index 0000000..521326b --- /dev/null +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/PredictiveBackTransition.kt @@ -0,0 +1,185 @@ +package com.example.moveeapp_compose_kmm.nav + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedContentTransitionScope +import androidx.compose.animation.ContentTransform +import androidx.compose.animation.EnterTransition +import androidx.compose.animation.ExitTransition +import androidx.compose.animation.SizeTransform +import androidx.compose.animation.core.AnimationSpec +import androidx.compose.animation.core.SeekableTransitionState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.animate +import androidx.compose.animation.core.rememberTransition +import androidx.compose.animation.core.spring +import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.backhandler.PredictiveBackHandler +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.core.stack.StackEvent +import cafe.adriel.voyager.navigator.Navigator +import cafe.adriel.voyager.transitions.ScreenTransitionContent +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.dropWhile +import kotlinx.coroutines.launch +import kotlin.coroutines.cancellation.CancellationException + +@OptIn(ExperimentalComposeUiApi::class) +@Composable +fun PredictiveBackTransition( + navigator: Navigator, + modifier: Modifier = Modifier, + enterTransition: AnimatedContentTransitionScope.(SwipeEdge) -> EnterTransition = { fadeIn() }, + exitTransition: AnimatedContentTransitionScope.(SwipeEdge) -> ExitTransition = { fadeOut() }, + popEnterTransition: AnimatedContentTransitionScope.(SwipeEdge) -> EnterTransition = enterTransition, + popExitTransition: AnimatedContentTransitionScope.(SwipeEdge) -> ExitTransition = exitTransition, + sizeTransform: (AnimatedContentTransitionScope.() -> SizeTransform?)? = null, + flingAnimationSpec: () -> AnimationSpec = { spring(stiffness = Spring.StiffnessLow) }, + content: ScreenTransitionContent = { it.Content() } +) { + val scope = rememberCoroutineScope() + var animationJob by remember { mutableStateOf?>(null) } + + var progress by remember { mutableFloatStateOf(0f) } + var swipeEdge by remember { mutableStateOf(SwipeEdge.Unknown) } + var isPredictiveBack by remember { mutableStateOf(false) } + val transition = if (sizeTransform != null) { + // Size transform seems broken atm so this is a more hacky way to keep the size transition working. + // this method breaks when the user spams back gesture or quick navigation action in general. + val transitionState = remember { SeekableTransitionState(navigator.lastItem) } + LaunchedEffect(progress) { + val previousEntry = navigator.items.getOrNull(navigator.size - 2) + if (previousEntry != null) transitionState.seekTo( + targetState = previousEntry, + fraction = progress + ) + } + LaunchedEffect(navigator) { + snapshotFlow { navigator.lastItem } + .collect { + if (animationJob?.second == AnimationType.Cancel) { + animationJob?.first?.cancel() + } + transitionState.animateTo(it) + } + } + rememberTransition(transitionState = transitionState) + } else { + if (isPredictiveBack) { + val transitionState = remember { SeekableTransitionState(navigator.lastItem) } + LaunchedEffect(progress) { + val previousEntry = navigator.items.getOrNull(navigator.size - 2) + if (previousEntry != null) transitionState.seekTo( + targetState = previousEntry, + fraction = progress + ) + } + rememberTransition(transitionState = transitionState) + } else { + updateTransition(targetState = navigator.lastItem) + } + } + + PredictiveBackHandler(enabled = navigator.canPop) { backEvent -> + if (animationJob?.second == AnimationType.Cancel) { + animationJob?.first?.cancel() + } + try { + backEvent + .dropWhile { animationJob != null } + .collect { + swipeEdge = when (it.swipeEdge) { + /* BackEventCompat.EDGE_LEFT */0 -> SwipeEdge.Left + /* BackEventCompat.EDGE_RIGHT */ 1 -> SwipeEdge.Right + else -> SwipeEdge.Unknown + } + progress = it.progress + isPredictiveBack = true + } + animationJob = scope.launch { + try { + if (isPredictiveBack) { + animate( + initialValue = progress, + targetValue = 1f, + animationSpec = flingAnimationSpec(), + block = { i, _ -> }, + ) + } + navigator.pop() + } catch (e: CancellationException) { + // Cancelled + progress = 0f + } finally { + isPredictiveBack = false + swipeEdge = SwipeEdge.Unknown + animationJob = null + } + } to AnimationType.Pop + } catch (e: CancellationException) { + animationJob = scope.launch { + try { + if (isPredictiveBack) { + animate( + initialValue = progress, + targetValue = 0f, + animationSpec = flingAnimationSpec(), + block = { i, _ -> progress = i }, + ) + } + } catch (e: CancellationException) { + // Cancelled + progress = 1f + } finally { + isPredictiveBack = false + swipeEdge = SwipeEdge.Unknown + animationJob = null + } + } to AnimationType.Cancel + } + } + + transition.AnimatedContent( + modifier = modifier, + transitionSpec = { + val pop = navigator.lastEvent == StackEvent.Pop || isPredictiveBack + ContentTransform( + targetContentEnter = if (pop) popEnterTransition(swipeEdge) else enterTransition( + swipeEdge + ), + initialContentExit = if (pop) popExitTransition(swipeEdge) else exitTransition( + swipeEdge + ), + targetContentZIndex = if (pop) 0f else 1f, + sizeTransform = sizeTransform?.invoke(this), + ) + }, + contentKey = { it.key }, + ) { + navigator.saveableState("transition", it) { + content(it) + } + } +} + +enum class SwipeEdge { + Unknown, + Left, + Right, +} + +private enum class AnimationType { + Pop, Cancel +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt index 8fd6761..07c6a0a 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/SearchScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.search.SearchScreen import com.example.moveeapp_compose_kmm.ui.scene.search.SearchViewModel @@ -24,9 +23,5 @@ class SearchScreen : Screen { navigateToTv = { navigator.push(TvDetailScreen(it)) }, navigateToActor = { navigator.push(ArtistDetailScreen(it)) } ) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/Tabs.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/Tabs.kt index a3c2c12..ef9a838 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/Tabs.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/Tabs.kt @@ -1,5 +1,9 @@ package com.example.moveeapp_compose_kmm.nav +import androidx.compose.animation.core.LinearEasing +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.runtime.staticCompositionLocalOf @@ -9,7 +13,6 @@ import cafe.adriel.voyager.navigator.Navigator import cafe.adriel.voyager.navigator.tab.LocalTabNavigator import cafe.adriel.voyager.navigator.tab.Tab import cafe.adriel.voyager.navigator.tab.TabOptions -import cafe.adriel.voyager.transitions.SlideTransition import com.example.moveeapp_compose_kmm.ui.tab.TabItem import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource @@ -44,8 +47,31 @@ private class TabData(@Transient private val tab: TabItem, private val screen: S @Composable override fun Content() { - Navigator(screen) { - SlideTransition(it) + Navigator(screen) { navigator -> + PredictiveBackTransition( + navigator = navigator, + enterTransition = { + slideInHorizontally( + animationSpec = tween(easing = LinearEasing), + initialOffsetX = { it }) + }, + popEnterTransition = { + slideInHorizontally( + animationSpec = tween(easing = LinearEasing), + initialOffsetX = { -it / 2 }) + }, + exitTransition = { + slideOutHorizontally( + animationSpec = tween(easing = LinearEasing), + targetOffsetX = { -it / 2 }) + }, + popExitTransition = { + slideOutHorizontally( + animationSpec = tween(easing = LinearEasing), + targetOffsetX = { it }) + }, + flingAnimationSpec = { tween(easing = LinearEasing) } + ) } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt index 776a645..c31fd7d 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/TvDetailScreen.kt @@ -5,7 +5,6 @@ import androidx.compose.ui.ExperimentalComposeUiApi import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.navigator.LocalNavigator import cafe.adriel.voyager.navigator.currentOrThrow -import androidx.compose.ui.backhandler.BackHandler import com.example.moveeapp_compose_kmm.core.viewModel import com.example.moveeapp_compose_kmm.ui.scene.tvdetail.TvDetailScreen import com.example.moveeapp_compose_kmm.ui.scene.tvdetail.TvDetailViewModel @@ -23,9 +22,5 @@ class TvDetailScreen(private val tvId: Int) : Screen { navigateToActor = { navigator.push(ArtistDetailScreen(it)) }, onBackPressed = navigator::pop ) - - BackHandler(enabled = true) { - navigator.pop() - } } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/utils/Store.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/utils/Store.kt deleted file mode 100644 index 01176e9..0000000 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/utils/Store.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.example.moveeapp_compose_kmm.utils - -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch - -sealed interface Action { - object OnBackPressed: Action -} - -interface Store { - fun send(action: Action) - val events: SharedFlow -} - -fun CoroutineScope.createStore(): Store { - val events = MutableSharedFlow() - - return object : Store { - override fun send(action: Action) { - launch { - events.emit(action) - } - } - override val events: SharedFlow = events.asSharedFlow() - } -} diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt index acaaf47..7983ef6 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt @@ -2,18 +2,12 @@ package com.example.moveeapp_compose_kmm import androidx.compose.ui.window.ComposeUIViewController import com.example.moveeapp_compose_kmm.di.init -import com.example.moveeapp_compose_kmm.utils.Action -import com.example.moveeapp_compose_kmm.utils.createStore -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.SupervisorJob import org.koin.compose.KoinApplication import platform.UIKit.UIViewController fun mainViewController(): UIViewController { val uiViewController = ComposeUIViewController { - KoinApplication(application = { - init() - }) { + KoinApplication(application = { init() }) { App() } } @@ -24,9 +18,3 @@ fun mainViewController(): UIViewController { internal object Holder { var viewController: UIViewController? = null } - -fun onBackGesture() { - store.send(Action.OnBackPressed) -} - -val store = CoroutineScope(SupervisorJob()).createStore() From a13662c87bd03054350df85e157a9eec2d792c9c Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Tue, 24 Jun 2025 15:37:57 +0300 Subject: [PATCH 05/10] Enhance share functionality and update Kotlin version This commit introduces several improvements to the sharing feature: - Updated the `share` function to accept `title`, `description`, and `imageUrl` parameters for richer sharing content. - Implemented platform-specific sharing for Android and iOS, including image sharing. - Updated Kotlin version to 2.2.0. - Removed unused `Holder` object from iOS main. The share functionality in movie and TV detail screens now utilizes the enhanced `share` function, allowing users to share titles, overviews, and posters. --- gradle/libs.versions.toml | 2 +- .../moveeapp_compose_kmm/core/Share.kt | 20 ++++++++++++--- .../moveeapp_compose_kmm/core/Share.kt | 7 +++++- .../ui/scene/moviedetail/MovieDetailScreen.kt | 7 +++++- .../ui/scene/tvdetail/TvDetailScreen.kt | 6 ++++- .../moveeapp_compose_kmm/core/Share.kt | 25 +++++++++++++++---- .../example/moveeapp_compose_kmm/main.ios.kt | 8 +----- .../moveeapp_compose_kmm/core/Share.jvm.kt | 4 ++- 8 files changed, 59 insertions(+), 20 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index bc99c02..72ab483 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ minSdk = "24" targetSdk = "34" compileSdk = "35" -kotlin = "2.1.21" +kotlin = "2.2.0" kotlinx-coroutines = "1.10.2" kotlinx-serialization = "1.8.1" ktor = "3.1.3" diff --git a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt index bf8944c..dd66b46 100644 --- a/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt +++ b/shared/src/androidMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt @@ -1,15 +1,29 @@ package com.example.moveeapp_compose_kmm.core import android.content.Intent +import androidx.core.net.toUri +import com.example.moveeapp_compose_kmm.utils.Constants -actual fun share(context: PlatformContext, text: String) { +actual fun share( + context: PlatformContext, + title: String, + description: String?, + imageUrl: String? +) { + val text = title + description?.let { "\n$it" } val sendIntent: Intent = Intent().apply { action = Intent.ACTION_SEND putExtra(Intent.EXTRA_TEXT, text) - type = "text/plain" + if (imageUrl != null) { + putExtra(Intent.EXTRA_STREAM, (Constants.IMAGE_BASE + imageUrl).toUri()) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + type = "image/*" + } else { + type = "text/*" + } } val shareIntent = Intent.createChooser(sendIntent, null) context.androidContext.startActivity(shareIntent) -} \ No newline at end of file +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt index f99cadc..ed54adc 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt @@ -1,3 +1,8 @@ package com.example.moveeapp_compose_kmm.core -expect fun share (context: PlatformContext, text: String) +expect fun share( + context: PlatformContext, + title: String, + description: String?, + imageUrl: String? = null +) diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt index b647fdc..b998995 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt @@ -201,7 +201,12 @@ fun MovieDetailContent( text = stringResource(Res.string.share), icon = Icons.Default.Share, onClick = { - share(platformContext, uiState.movieDetailData.homepage) + share( + platformContext, + uiState.movieDetailData.title, + uiState.movieDetailData.overview, + uiState.movieDetailData.posterPath.ifBlank { null } + ) } ) }) diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt index 72d4d07..a968d0a 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tvdetail/TvDetailScreen.kt @@ -193,7 +193,11 @@ fun TvDetailContent( text = stringResource(Res.string.share), icon = Icons.Default.Share, onClick = { - share(platformContext, uiState.tvDetailData.homepage) + share( + platformContext, + uiState.tvDetailData.title, + uiState.tvDetailData.overview, + uiState.tvDetailData.posterPath.ifBlank { null }) } ) }) diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt index 00d28d6..1b7a922 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.kt @@ -1,10 +1,25 @@ package com.example.moveeapp_compose_kmm.core -import com.example.moveeapp_compose_kmm.Holder +import com.example.moveeapp_compose_kmm.utils.Constants +import platform.Foundation.NSURL import platform.UIKit.UIActivityViewController +import platform.UIKit.UIApplication -actual fun share(context: PlatformContext, text: String) { - val controller = UIActivityViewController(listOf(text), null) +actual fun share( + context: PlatformContext, + title: String, + description: String?, + imageUrl: String? +) { + val text = title + description?.let { "\n$it" } + val items = mutableListOf(text) + imageUrl?.let { + NSURL(string = Constants.IMAGE_BASE + it) + }?.also { + items.add(it) + } - Holder.viewController?.presentViewController(controller, true, null) -} \ No newline at end of file + UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController( + UIActivityViewController(items, null), true, null + ) +} diff --git a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt index 7983ef6..a4822f5 100644 --- a/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt +++ b/shared/src/iosMain/kotlin/com/example/moveeapp_compose_kmm/main.ios.kt @@ -6,15 +6,9 @@ import org.koin.compose.KoinApplication import platform.UIKit.UIViewController fun mainViewController(): UIViewController { - val uiViewController = ComposeUIViewController { + return ComposeUIViewController { KoinApplication(application = { init() }) { App() } } - Holder.viewController = uiViewController - return uiViewController -} - -internal object Holder { - var viewController: UIViewController? = null } diff --git a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt index 1c21953..8caf0c0 100644 --- a/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt @@ -2,7 +2,9 @@ package com.example.moveeapp_compose_kmm.core actual fun share( context: PlatformContext, - text: String + title: String, + description: String?, + imageUrl: String? ) { } From 0531e25e47c7913a2a6bea3825a228533c99d825 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 30 Jun 2025 10:54:04 +0300 Subject: [PATCH 06/10] feat: Add Compose previews to all screens This commit introduces Compose previews to all screens within the application. This enhancement improves the development workflow by allowing developers to visualize UI components in isolation, facilitating faster iteration and easier debugging. Key changes: - Added `@Preview` annotations to composable functions in various screen files. - Made some internal composable functions private to encapsulate preview logic. - Utilized `LocalInspectionMode` in `rememberPermissionState` to provide a fake permission state for previews. - Added a placeholder painter to `PosterImageItem` for better preview rendering. --- shared/build.gradle.kts | 14 +- .../permission/PermissionState.kt | 26 ++- .../ui/components/ImageItem.kt | 3 +- .../ui/scene/account/AccountScreen.kt | 28 ++- .../account/favoritescreen/FavoriteScreen.kt | 167 +++++++++++------- .../scene/artistdetail/ArtistDetailScreen.kt | 33 +++- .../ui/scene/login/LoginScreen.kt | 53 +++++- .../ui/scene/main/MainScreen.kt | 35 ++++ .../ui/scene/map/MapScreen.kt | 48 +++-- .../ui/scene/movie/MovieScreen.kt | 70 +++++++- .../ui/scene/moviedetail/MovieDetailScreen.kt | 87 ++++++--- .../ui/scene/search/SearchScreen.kt | 44 ++++- .../ui/scene/tv/TvScreen.kt | 41 ++++- 13 files changed, 533 insertions(+), 116 deletions(-) diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index 7d1cc3e..0909a2d 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -63,13 +63,14 @@ kotlin { sourceSets { commonMain.dependencies { // Compose - api(compose.runtime) - api(compose.foundation) - api(compose.material3) - api(compose.materialIconsExtended) - api(compose.animation) - api(libs.ui.backhandler) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.materialIconsExtended) + implementation(compose.animation) + implementation(libs.ui.backhandler) implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) implementation(libs.logger) @@ -109,6 +110,7 @@ kotlin { } androidMain.dependencies { + implementation(compose.uiTooling) // Ktor api(libs.ktor.client.android) diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionState.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionState.kt index f4f9120..00bb5b4 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionState.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/permission/PermissionState.kt @@ -2,13 +2,19 @@ package com.example.moveeapp_compose_kmm.permission import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalInspectionMode @Composable fun rememberPermissionState( permission: Permission, onPermissionResult: (Boolean) -> Unit = {} ): PermissionState { - return rememberMutablePermissionState(permission, onPermissionResult) + return if (LocalInspectionMode.current) { + remember { FakePermissionState(permission) } + } else { + rememberMutablePermissionState(permission, onPermissionResult) + } } @Stable @@ -43,3 +49,21 @@ interface PermissionState { fun openSettings() } +/** + * A fake implementation of [PermissionState] that can be used in previews and tests. + * This class allows simulating different permission states by providing a custom [status]. + * The [launchPermissionRequest] and [openSettings] methods are no-ops. + */ +class FakePermissionState( + override val permission: Permission, + override val status: PermissionStatus = PermissionStatus.Granted +) : PermissionState { + + override fun launchPermissionRequest() { + // no-op + } + + override fun openSettings() { + // no-op + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt index 9e45b82..0b3bd17 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt @@ -44,7 +44,8 @@ fun PosterImageItem( Image( modifier = modifier.fillMaxSize(), painter = rememberImagePainter( - Constants.IMAGE_BASE.plus(imagePath) + Constants.IMAGE_BASE.plus(imagePath), + placeholderPainter = { painterResource(Res.drawable.search_place_holder) } ), contentDescription = null, contentScale = contentScale diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt index 22b2801..7b1186a 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/AccountScreen.kt @@ -30,7 +30,9 @@ import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.example.moveeapp_compose_kmm.domain.MediaType +import com.example.moveeapp_compose_kmm.domain.account.AccountDetail import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import com.example.moveeapp_compose_kmm.ui.theme.Fonts import movee.shared.generated.resources.Res import movee.shared.generated.resources.fav_movie @@ -38,6 +40,7 @@ import movee.shared.generated.resources.fav_tv import movee.shared.generated.resources.hello import movee.shared.generated.resources.tab_profile import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun AccountScreen( @@ -58,6 +61,15 @@ fun AccountScreen( } } + AccountScreen(uiState, logoutState, viewModel::logout, navigateToFavorite) +} + +@Composable +private fun AccountScreen( + uiState: AccountUiState, logoutState: Boolean, + onLogoutClick: () -> Unit, + navigateToFavorite: (MediaType) -> Unit, +) { Column(modifier = Modifier.background(MaterialTheme.colorScheme.secondaryContainer)) { Spacer( Modifier.fillMaxWidth().windowInsetsTopHeight(WindowInsets.statusBars) @@ -68,13 +80,13 @@ fun AccountScreen( uiState = uiState, onFavMovieClick = navigateToFavorite, onFavTvClick = navigateToFavorite, - onLogoutClick = { viewModel.logout() } + onLogoutClick = onLogoutClick ) } } @Composable -fun SuccessContent( +private fun SuccessContent( uiState: AccountUiState, onFavMovieClick: (MediaType) -> Unit, onFavTvClick: (MediaType) -> Unit, @@ -175,3 +187,15 @@ fun SuccessContent( } } +@Preview +@Composable +private fun AccountPreview() { + AppTheme { + AccountScreen( + uiState = AccountUiState(false, null, AccountDetail(1, "John", "Doe", "tr")), + logoutState = false, + onLogoutClick = {}, + navigateToFavorite = {} + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/favoritescreen/FavoriteScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/favoritescreen/FavoriteScreen.kt index 1e155f2..d26d764 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/favoritescreen/FavoriteScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/account/favoritescreen/FavoriteScreen.kt @@ -4,6 +4,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -37,14 +38,16 @@ import com.example.moveeapp_compose_kmm.ui.components.RateItem import com.example.moveeapp_compose_kmm.ui.components.TextItem import com.example.moveeapp_compose_kmm.ui.scene.account.FavoriteMovieUiState import com.example.moveeapp_compose_kmm.ui.scene.account.FavoriteTvUiState +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import com.example.moveeapp_compose_kmm.ui.theme.Fonts import movee.shared.generated.resources.Res import movee.shared.generated.resources.fav_movie import movee.shared.generated.resources.fav_tv +import org.jetbrains.compose.resources.StringResource import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable -@OptIn(ExperimentalMaterial3Api::class) fun FavoriteScreen( viewModel: FavoriteViewModel, mediaType: MediaType, @@ -59,60 +62,16 @@ fun FavoriteScreen( MediaType.MOVIE -> { viewModel.getPopularMovie() - Scaffold(topBar = { - TopAppBar( - title = { - TextItem( - text = stringResource(Res.string.fav_movie), - fontSize = 20.sp, - fontFamily = Fonts.bold, - textColor = MaterialTheme.colorScheme.primaryContainer - ) - }, - colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.primary), - navigationIcon = { - BackPressedItem(onBackPressed = navigateBack) - } - ) - }) { contentPadding -> - Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) { - Spacer( - modifier = Modifier.height(190.dp).fillMaxWidth() - .align(Alignment.TopCenter) - .background(MaterialTheme.colorScheme.primary) - ) - FavoriteMovieContent(favoriteMovieUiState, navigateToMovie) - } + FavoriteScreen(Res.string.fav_movie, navigateBack) { + FavoriteMovieContent(favoriteMovieUiState, navigateToMovie) } } MediaType.TV -> { viewModel.getPopularTv() - Scaffold(topBar = { - TopAppBar( - title = { - TextItem( - text = stringResource(Res.string.fav_tv), - fontSize = 20.sp, - fontFamily = Fonts.bold, - textColor = MaterialTheme.colorScheme.primaryContainer - ) - }, - colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.primary), - navigationIcon = { - BackPressedItem(onBackPressed = navigateBack) - } - ) - }) { contentPadding -> - Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) { - Spacer( - modifier = Modifier.height(190.dp).fillMaxWidth() - .align(Alignment.TopCenter) - .background(MaterialTheme.colorScheme.primary) - ) - FavoriteTvContent(favoriteTvUiState, navigateToTv) - } + FavoriteScreen(Res.string.fav_tv, navigateBack) { + FavoriteTvContent(favoriteTvUiState, navigateToTv) } } @@ -120,22 +79,42 @@ fun FavoriteScreen( } } +@OptIn(ExperimentalMaterial3Api::class) @Composable -fun FavoriteTvContent( - favoriteTvUiState: FavoriteTvUiState, - onTvDetailClick: (Int) -> Unit, +private fun FavoriteScreen( + title: StringResource, + navigateBack: () -> Unit, + content: @Composable BoxScope.() -> Unit, ) { - LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) { - items(favoriteTvUiState.favoriteTvData) { - FavoriteTvRow(favoriteTv = it) { tvId -> - onTvDetailClick(tvId) + Scaffold(topBar = { + TopAppBar( + title = { + TextItem( + text = stringResource(title), + fontSize = 20.sp, + fontFamily = Fonts.bold, + textColor = MaterialTheme.colorScheme.primaryContainer + ) + }, + colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.primary), + navigationIcon = { + BackPressedItem(onBackPressed = navigateBack) } + ) + }) { contentPadding -> + Box(modifier = Modifier.padding(top = contentPadding.calculateTopPadding())) { + Spacer( + modifier = Modifier.height(190.dp).fillMaxWidth() + .align(Alignment.TopCenter) + .background(MaterialTheme.colorScheme.primary) + ) + content() } } } @Composable -fun FavoriteMovieContent( +private fun FavoriteMovieContent( favoriteMovieUiState: FavoriteMovieUiState, onMovieDetailClick: (Int) -> Unit, ) { @@ -149,7 +128,21 @@ fun FavoriteMovieContent( } @Composable -fun FavoriteMovieRow( +private fun FavoriteTvContent( + favoriteTvUiState: FavoriteTvUiState, + onTvDetailClick: (Int) -> Unit, +) { + LazyColumn(modifier = Modifier.padding(horizontal = 16.dp)) { + items(favoriteTvUiState.favoriteTvData) { + FavoriteTvRow(favoriteTv = it) { tvId -> + onTvDetailClick(tvId) + } + } + } +} + +@Composable +private fun FavoriteMovieRow( favoriteMovie: FavoriteMovie, onDetailClick: (Int) -> Unit, ) { @@ -188,7 +181,7 @@ fun FavoriteMovieRow( } @Composable -fun FavoriteTvRow( +private fun FavoriteTvRow( favoriteTv: FavoriteTv, onDetailClick: (Int) -> Unit, ) { @@ -224,3 +217,59 @@ fun FavoriteTvRow( } } } + +@Preview +@Composable +private fun FavoriteMoviePreview() { + AppTheme { + FavoriteScreen(Res.string.fav_movie, {}) { + FavoriteMovieContent( + FavoriteMovieUiState( + favoriteMovieData = listOf( + FavoriteMovie( + movieId = 1, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 8.0 + ), + FavoriteMovie( + movieId = 2, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 8.0 + ) + ) + ) + ) {} + } + } +} + +@Preview +@Composable +private fun FavoriteTvPreview() { + AppTheme { + FavoriteScreen(Res.string.fav_tv, {}) { + FavoriteTvContent( + FavoriteTvUiState( + favoriteTvData = listOf( + FavoriteTv( + tvId = 1, + title = "The Office", + posterPath = "", + voteAverage = 8.0 + ), + FavoriteTv( + tvId = 2, + title = "The Office", + posterPath = "", + voteAverage = 8.0 + ) + ) + ) + ) {} + } + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt index 1267512..5ed70e7 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/artistdetail/ArtistDetailScreen.kt @@ -1,9 +1,12 @@ package com.example.moveeapp_compose_kmm.ui.scene.artistdetail +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width @@ -12,6 +15,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Card +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState @@ -30,9 +34,12 @@ import com.example.moveeapp_compose_kmm.ui.components.DetailScreensAppBar import com.example.moveeapp_compose_kmm.ui.components.ExpandableText import com.example.moveeapp_compose_kmm.ui.components.PosterImageItem import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.scene.artistdetail.model.ArtistDetailUiModel +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import movee.shared.generated.resources.Res import movee.shared.generated.resources.actor_born import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun ArtistDetailScreen( @@ -57,7 +64,7 @@ fun ArtistDetailScreen( } @Composable -fun SuccessContent( +private fun SuccessContent( uiState: ArtistDetailUiState, onDetailClick: (Pair) -> Unit, onBackPressed: () -> Unit, @@ -147,3 +154,27 @@ fun PersonCreditCardView( ) } } + +@Preview +@Composable +private fun ArtistDetailPreview() { + AppTheme { + Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.background)) { + SuccessContent( + ArtistDetailUiState( + false, + ArtistDetailUiModel( + "Tom Cruise", + "Thomas Cruise Mapother IV (born July 3, 1962) is an American actor and producer. Regarded as a Hollywood icon, he has received various accolades, including an Honorary Palme d'Or, three Golden Globe Awards, and nominations for four Academy Awards. His films have grossed over $12 billion worldwide, placing him among the highest-grossing actors ever.", + "1962-07-03", + "Syracuse, New York, USA", + credit = listOf( + ArtistCredit(name = "War fo the Worlds"), + ArtistCredit(name = "Minority Report"), + ArtistCredit(name = "The Outsiders"), + ) + ) + ), {}, {}) + } + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/login/LoginScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/login/LoginScreen.kt index a7e5d0f..45b93a4 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/login/LoginScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/login/LoginScreen.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.unit.dp import com.example.moveeapp_compose_kmm.data.account.LoginState import com.example.moveeapp_compose_kmm.ui.components.TextInputItem import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import com.example.moveeapp_compose_kmm.utils.Constants import movee.shared.generated.resources.Res import movee.shared.generated.resources.ic_login_movee @@ -42,6 +43,7 @@ import movee.shared.generated.resources.login_title import movee.shared.generated.resources.login_username import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun LoginScreen( @@ -49,8 +51,29 @@ fun LoginScreen( navigateToWebViewScreen: (String) -> Unit, navigateToMainScreen: () -> Unit, ) { - val isLoggedState by viewModel.isLoggedIn.collectAsState() + val loginState by viewModel.isLoggedIn.collectAsState() + LoginScreen( + viewModel.loginUiState, + loginState, + viewModel::onUserNameChange, + viewModel::onPasswordChange, + viewModel::login, + navigateToWebViewScreen, + navigateToMainScreen + ) +} + +@Composable +private fun LoginScreen( + loginUiState: LoginUiState, + loginState: LoginState, + onUserNameChange: (String) -> Unit, + onPasswordChange: (String) -> Unit, + onLogin: () -> Unit, + navigateToWebViewScreen: (String) -> Unit, + navigateToMainScreen: () -> Unit +) { Box(modifier = Modifier.fillMaxSize()) { Image( painter = painterResource(Res.drawable.login_background), @@ -73,11 +96,11 @@ fun LoginScreen( } LoginContent( - loginUiState = viewModel.loginUiState, - onUserNameChange = viewModel::onUserNameChange, - onPasswordChange = viewModel::onPasswordChange, - onLogin = viewModel::login, - isLoggedIn = isLoggedState, + loginUiState = loginUiState, + onUserNameChange = onUserNameChange, + onPasswordChange = onPasswordChange, + onLogin = onLogin, + isLoggedIn = loginState, navigateToWebViewScreen = navigateToWebViewScreen, navigateToMainScreen = navigateToMainScreen, ) @@ -86,7 +109,7 @@ fun LoginScreen( } @Composable -fun LoginContent( +private fun LoginContent( loginUiState: LoginUiState, onUserNameChange: (String) -> Unit, onPasswordChange: (String) -> Unit, @@ -183,3 +206,19 @@ fun LoginContent( } } } + +@Preview +@Composable +private fun LoginPreview() { + AppTheme { + LoginScreen( + loginUiState = LoginUiState(), + loginState = LoginState.LOGGED_OUT, + onUserNameChange = {}, + onPasswordChange = {}, + onLogin = {}, + navigateToWebViewScreen = {}, + navigateToMainScreen = {} + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt index 3b3ba23..1b90052 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/main/MainScreen.kt @@ -3,6 +3,7 @@ package com.example.moveeapp_compose_kmm.ui.scene.main import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.windowInsetsPadding @@ -18,10 +19,16 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.screen.ScreenKey +import cafe.adriel.voyager.navigator.tab.Tab +import cafe.adriel.voyager.navigator.tab.TabNavigator +import cafe.adriel.voyager.navigator.tab.TabOptions import com.example.moveeapp_compose_kmm.nav.CurrentTab import com.example.moveeapp_compose_kmm.ui.tab.TabItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun MainScreen( @@ -78,3 +85,31 @@ private fun RowScope.TabNavigationItem( label = { Text(text = title) }, icon = { Icon(painter = painterResource(tab.icon), contentDescription = title) }) } + +@Preview +@Composable +fun MainPreview() { + class TestTab : Tab { + override val options: TabOptions + @Composable get() { + return TabOptions(0u, "") + } + + override val key: ScreenKey get() = "test" + + @Composable + override fun Content() { + Box(Modifier.fillMaxSize()) + } + } + + AppTheme { + TabNavigator(TestTab()) { + MainScreen( + viewModel = MainViewModel(), + isTabSelected = { it.key == "MoviesTab" }, + onTabSelected = {}, + ) + } + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt index 3851bb6..9996f59 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/map/MapScreen.kt @@ -24,27 +24,45 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.example.moveeapp_compose_kmm.core.getPlatformContext import com.example.moveeapp_compose_kmm.core.navigateToMap import com.example.moveeapp_compose_kmm.core.viewModel +import com.example.moveeapp_compose_kmm.domain.location.DeviceLocation import com.example.moveeapp_compose_kmm.map.Map import com.example.moveeapp_compose_kmm.ui.components.BackPressedItem import com.example.moveeapp_compose_kmm.ui.components.MapsMarkerDialog import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import com.example.moveeapp_compose_kmm.ui.theme.Fonts import movee.shared.generated.resources.Res import movee.shared.generated.resources.cinema import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview class MapScreen : Screen { - @OptIn(ExperimentalMaterial3Api::class) + @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow val viewModel: MapViewModel = viewModel() val uiState by viewModel.uiState.collectAsState() - val platformContext = getPlatformContext() - viewModel.loadForecastWithLocation() + MapScaffold(uiState, navigator::pop) { + Map( + modifier = Modifier.fillMaxSize(), + uiState = uiState, + onMarkerClick = viewModel::setSelectedCinema, + onPositionChange = viewModel::getUpdates + ) + } + } + + @OptIn(ExperimentalMaterial3Api::class) + @Composable + private fun MapScaffold( + uiState: MapUiState, + onNavigateBack: () -> Unit = {}, + mapContent: @Composable () -> Unit + ) { Scaffold(topBar = { TopAppBar( title = { @@ -57,18 +75,14 @@ class MapScreen : Screen { }, colors = TopAppBarDefaults.topAppBarColors(MaterialTheme.colorScheme.primary), navigationIcon = { - BackPressedItem { navigator.pop() } + BackPressedItem(onBackPressed = onNavigateBack) } ) }) { paddingValues -> + val platformContext = getPlatformContext() Box(modifier = Modifier.padding(top = paddingValues.calculateTopPadding())) { - Map( - modifier = Modifier.fillMaxSize(), - uiState = uiState, - onMarkerClick = viewModel::setSelectedCinema, - onPositionChange = viewModel::getUpdates - ) + mapContent() AnimatedVisibility( visible = uiState.selectedCinema != null, @@ -92,4 +106,18 @@ class MapScreen : Screen { } } } + + @Preview + @Composable + fun MapScreenPreview() { + AppTheme { + MapScreen().MapScaffold( + MapUiState( + selectedCinema = Cinema("test", "test", DeviceLocation(1.0, 1.0)) + ) + ) { + Box(Modifier.fillMaxSize()) + } + } + } } diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt index b9ffdd7..0d1f1ac 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/movie/MovieScreen.kt @@ -50,12 +50,14 @@ import com.example.moveeapp_compose_kmm.ui.components.LoadingScreen import com.example.moveeapp_compose_kmm.ui.components.PosterImageItem import com.example.moveeapp_compose_kmm.ui.components.RateItem import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import com.example.moveeapp_compose_kmm.ui.theme.Fonts import movee.shared.generated.resources.Res import movee.shared.generated.resources.ic_map import movee.shared.generated.resources.tab_movies import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview import kotlin.math.absoluteValue @Composable @@ -94,7 +96,7 @@ fun MovieScreen( } @Composable -fun SuccessContent( +private fun SuccessContent( modifier: Modifier = Modifier, popularMovieData: List, nowPlayingMovieData: List, @@ -247,3 +249,69 @@ fun HorizontalMoviePager( } } } + +@Preview +@Composable +fun MovieScreenPreview() { + AppTheme { + SuccessContent( + modifier = Modifier.fillMaxSize() + .background(color = MaterialTheme.colorScheme.primaryContainer), + popularMovieData = listOf( + PopularMovie( + movieId = 1, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 8.0 + ), + PopularMovie( + movieId = 2, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 8.0 + ), + PopularMovie( + movieId = 3, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 8.0 + ), + ), + nowPlayingMovieData = listOf( + NowPlayingMovie( + movieId = 1, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 5.0 + ), + NowPlayingMovie( + movieId = 2, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 5.0 + ), + NowPlayingMovie( + movieId = 3, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 5.0 + ), + NowPlayingMovie( + movieId = 4, + title = "Final Destination", + posterPath = "", + releaseDate = "2025-05-14", + voteAverage = 5.0 + ), + ), + onDetailClick = {}, + onMapClick = {} + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt index b998995..4394366 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/moviedetail/MovieDetailScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -30,6 +31,7 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight @@ -54,10 +56,13 @@ import com.example.moveeapp_compose_kmm.ui.components.RateItem import com.example.moveeapp_compose_kmm.ui.components.RateRow import com.example.moveeapp_compose_kmm.ui.components.RuntimeItem import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.scene.moviedetail.model.MovieDetailUiModel +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import movee.shared.generated.resources.Res import movee.shared.generated.resources.movie_detail_cast import movee.shared.generated.resources.share import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview import kotlin.math.round @Composable @@ -89,7 +94,7 @@ fun MovieDetailScreen( } SuccessContent( - uiState = uiState, + movieDetailData = uiState.movieDetailData, onDetailClick = navigateToActor, onFavouriteClicked = { isFav, movieId -> viewModel.addFavorite( @@ -107,9 +112,9 @@ fun MovieDetailScreen( } @Composable -fun SuccessContent( +private fun SuccessContent( modifier: Modifier = Modifier, - uiState: MovieDetailUiState, + movieDetailData: MovieDetailUiModel, isFavorite: Boolean, onDetailClick: (Int) -> Unit, onBackPressed: () -> Unit, @@ -129,18 +134,18 @@ fun SuccessContent( FavouriteItem( isFavorite = isFavorite, onFavouriteClicked = { - onFavouriteClicked(!isFavorite, uiState.movieDetailData.movieId) + onFavouriteClicked(!isFavorite, movieDetailData.movieId) } ) }, content = { DetailPosterImage( - imagePath = uiState.movieDetailData.backdropPath, - modifier = Modifier.padding(bottom = 12.dp) + imagePath = movieDetailData.backdropPath, + modifier = Modifier.padding(bottom = 12.dp).defaultMinSize(minHeight = 128.dp) ) RateItem( - rate = round(uiState.movieDetailData.voteAverage).toString(), + rate = round(movieDetailData.voteAverage).toString(), modifier = Modifier.align( Alignment.BottomStart ).padding(start = 16.dp) @@ -148,15 +153,19 @@ fun SuccessContent( } ) - MovieDetailContent(uiState = uiState, ratingValue = ratingValue, onRateMovie = onRateMovie) + MovieDetailContent( + data = movieDetailData, + ratingValue = ratingValue, + onRateMovie = onRateMovie + ) - MovieCreditLazyRow(uiState = uiState, onDetailClick = onDetailClick) + MovieCreditLazyRow(credit = movieDetailData.credit, onDetailClick = onDetailClick) } } @Composable fun MovieDetailContent( - uiState: MovieDetailUiState, + data: MovieDetailUiModel, ratingValue: State, onRateMovie: (rate: Int, movieId: Int) -> Unit, ) { @@ -166,7 +175,7 @@ fun MovieDetailContent( TextItem( modifier = Modifier.padding(top = 8.dp), - text = uiState.movieDetailData.title, + text = data.title, fontSize = 32.sp, fontWeight = FontWeight.Bold, maxLines = Int.MAX_VALUE, @@ -175,7 +184,7 @@ fun MovieDetailContent( TextItem( modifier = Modifier.padding(top = 8.dp), - text = uiState.movieDetailData.genre, + text = data.genre, textColor = MaterialTheme.colorScheme.secondary ) @@ -187,15 +196,15 @@ fun MovieDetailContent( ) { RuntimeItem( modifier = Modifier.padding(end = 8.dp), - runtime = uiState.movieDetailData.runtime.toString() + runtime = data.runtime.toString() ) - DateItem(date = uiState.movieDetailData.releaseDate) + DateItem(date = data.releaseDate) } RateRow( modifier = Modifier.padding(vertical = 12.dp).height(IntrinsicSize.Min), ratingValue = ratingValue, - onRatingValueChange = { onRateMovie.invoke(it, uiState.movieDetailData.movieId) }, + onRatingValueChange = { onRateMovie.invoke(it, data.movieId) }, hidableContent = { FloatingActionButtonItem( text = stringResource(Res.string.share), @@ -203,9 +212,9 @@ fun MovieDetailContent( onClick = { share( platformContext, - uiState.movieDetailData.title, - uiState.movieDetailData.overview, - uiState.movieDetailData.posterPath.ifBlank { null } + data.title, + data.overview, + data.posterPath.ifBlank { null } ) } ) @@ -218,7 +227,7 @@ fun MovieDetailContent( ) TextItem( - text = uiState.movieDetailData.overview, + text = data.overview, maxLines = Int.MAX_VALUE ) } @@ -226,7 +235,7 @@ fun MovieDetailContent( @Composable fun MovieCreditLazyRow( - uiState: MovieDetailUiState, + credit: List, onDetailClick: (Int) -> Unit, ) { TextItem( @@ -240,9 +249,9 @@ fun MovieCreditLazyRow( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(10.dp) ) { - items(uiState.movieDetailData.credit.size) { index -> + items(credit.size) { index -> MovieCreditCardView( - credit = uiState.movieDetailData.credit[index], + credit = credit[index], onClick = { id -> onDetailClick(id) }) } } @@ -273,3 +282,37 @@ fun MovieCreditCardView( ) } } + +@Preview +@Composable +fun SuccessContentPreview() { + AppTheme { + SuccessContent( + modifier = Modifier + .fillMaxSize() + .background(color = MaterialTheme.colorScheme.primaryContainer), + movieDetailData = MovieDetailUiModel( + movieId = 1, + title = "Movie Title", + genre = "Action", + runtime = 120, + releaseDate = "2023-01-01", + overview = "This is a great movie.", + voteAverage = 8.5, + posterPath = "", + backdropPath = "", + credit = listOf( + Credits(1, "John Doe", "profile_path_1"), + Credits(2, "Jane Smith", "profile_path_2"), + Credits(3, "Bob Johnson", "profile_path_3"), + ) + ), + isFavorite = false, + onDetailClick = {}, + onBackPressed = {}, + onFavouriteClicked = { _, _ -> }, + ratingValue = mutableStateOf(3), + onRateMovie = { _, _ -> } + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt index 252e47b..ce2de2d 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/search/SearchScreen.kt @@ -41,12 +41,17 @@ import com.example.moveeapp_compose_kmm.ui.components.LoadingScreen import com.example.moveeapp_compose_kmm.ui.components.SearchTextField import com.example.moveeapp_compose_kmm.ui.components.TextItem import com.example.moveeapp_compose_kmm.ui.scene.search.model.SearchUiModel +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme import movee.shared.generated.resources.Res +import movee.shared.generated.resources.ic_search_actor +import movee.shared.generated.resources.ic_search_movie +import movee.shared.generated.resources.ic_search_tv import movee.shared.generated.resources.search_empty_text import movee.shared.generated.resources.search_place_holder import movee.shared.generated.resources.tab_search import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun SearchScreen( @@ -72,7 +77,8 @@ fun SearchScreen( ErrorScreen(it) } - SearchContent(uiState = uiState, + SearchContent( + uiState = uiState, query = queryState, handleQueryState = viewModel::handleQueryChange, onDetailClick = { id, mediaType -> @@ -88,7 +94,7 @@ fun SearchScreen( } @Composable -fun SearchContent( +private fun SearchContent( uiState: SearchUiState, query: String, handleQueryState: (String) -> Unit, @@ -204,3 +210,37 @@ fun SearchRow(searchItem: SearchUiModel, onDetailClick: (Int, String) -> Unit) { } } } + +@Preview +@Composable +private fun SearchPreview() { + AppTheme { + SearchContent( + uiState = SearchUiState( + data = listOf( + SearchUiModel( + id = 1, + name = "Spider-Man: No Way Home", + mediaType = "Movie", + iconType = Res.drawable.ic_search_movie + ), + SearchUiModel( + id = 2, + name = "The Office", + mediaType = "TV", + iconType = Res.drawable.ic_search_tv + ), + SearchUiModel( + id = 3, + name = "Tom Cruise", + mediaType = "Actor", + iconType = Res.drawable.ic_search_actor + ) + ) + ), + query = "", + handleQueryState = {}, + onDetailClick = { _, _ -> } + ) + } +} diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt index b267885..5920be0 100644 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt +++ b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/scene/tv/TvScreen.kt @@ -38,6 +38,8 @@ import com.example.moveeapp_compose_kmm.ui.components.LoadingScreen import com.example.moveeapp_compose_kmm.ui.components.PosterImageItem import com.example.moveeapp_compose_kmm.ui.components.RateItem import com.example.moveeapp_compose_kmm.ui.components.TextItem +import com.example.moveeapp_compose_kmm.ui.theme.AppTheme +import org.jetbrains.compose.ui.tooling.preview.Preview import kotlin.math.absoluteValue @Composable @@ -72,7 +74,7 @@ fun TvScreen( } @Composable -fun SuccessContent( +private fun SuccessContent( modifier: Modifier = Modifier, popularTv: List, topRatedTv: List, @@ -93,7 +95,9 @@ fun TvLazyVerticalGrid( popularTv: List, onclick: (Int) -> Unit, ) { - LazyVerticalGrid(modifier = modifier, columns = GridCells.Fixed(2), + LazyVerticalGrid( + modifier = modifier, + columns = GridCells.Fixed(2), content = { item(span = { GridItemSpan(2) }) { Box { @@ -114,7 +118,8 @@ fun TvLazyVerticalGrid( shape = MaterialTheme.shapes.small ) { Column { - PosterImageItem(imagePath = tvShow.posterPath, + PosterImageItem( + imagePath = tvShow.posterPath, modifier = Modifier.clickable { onclick(tvShow.tvId) } @@ -133,7 +138,8 @@ fun TvLazyVerticalGrid( } } - }) + } + ) } @@ -170,3 +176,30 @@ fun HorizontalMoviePager( } } } + +@Preview +@Composable +private fun TvPreview() { + AppTheme { + SuccessContent( + modifier = Modifier + .fillMaxSize() + .background(MaterialTheme.colorScheme.primaryContainer), + popularTv = listOf( + PopularTv(1, "The Office", null), + PopularTv(2, "The Office", null), + PopularTv(3, "The Office", null), + PopularTv(4, "The Office", null), + PopularTv(5, "The Office", null), + ), + topRatedTv = listOf( + TopRatedTv(1, 8.5, "The Office", ""), + TopRatedTv(2, 8.5, "The Office", ""), + TopRatedTv(3, 8.5, "The Office", ""), + TopRatedTv(4, 8.5, "The Office", ""), + TopRatedTv(5, 8.5, "The Office", ""), + ), + navigateToDetail = {} + ) + } +} From 60eaf157a382db19abd6573486d9008143d53038 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 30 Jun 2025 11:48:01 +0300 Subject: [PATCH 07/10] Removed unused SwipeNavigator file. --- .../moveeapp_compose_kmm/SwipeNavigator.kt | 45 ------------------- 1 file changed, 45 deletions(-) delete mode 100644 shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt diff --git a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt deleted file mode 100644 index 097efaa..0000000 --- a/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/SwipeNavigator.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.example.moveeapp_compose_kmm - -import androidx.compose.material3.SwipeToDismissBoxValue -import androidx.compose.material3.rememberSwipeToDismissBoxState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.key -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import cafe.adriel.voyager.navigator.Navigator - -@Composable -fun SwipeNavigator(navigator: Navigator) { - - val currentSceneEntry = navigator.lastItem - val prevSceneEntry = remember(navigator.items) { navigator.items.getOrNull(navigator.size - 2) } - - var prevWasSwiped by remember { - mutableStateOf(false) - } - - LaunchedEffect(currentSceneEntry) { - prevWasSwiped = false - } - - val dismissState = key(currentSceneEntry) { - rememberSwipeToDismissBoxState() - } - - LaunchedEffect(dismissState.currentValue) { - if (dismissState.currentValue == SwipeToDismissBoxValue.StartToEnd) { - prevWasSwiped = true - navigator.pop() - } - } - - val showPrev by remember(dismissState) { - derivedStateOf { - dismissState.progress > 0f - } - } -} From 7e0edc5bf8194ddaae8f54448954298f36af47b6 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 30 Jun 2025 13:08:32 +0300 Subject: [PATCH 08/10] Refactor: Update Gradle version in CI and remove Cocoapods setup The Gradle version used in the GitHub Actions workflow has been updated from 8.4 to 8.14.2. Additionally, the steps for setting up Cocoapods and building the xcworkspace using `gradle podInstall` have been removed from the iOS build process. --- .github/workflows/actions.yml | 14 +++----------- gradle/wrapper/gradle-wrapper.properties | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 4d36ae6..1f68881 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -26,7 +26,7 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: - gradle-version: 8.4 + gradle-version: 8.14.2 - name: Build androidApp run: gradle :androidApp:buildDebug @@ -61,21 +61,13 @@ jobs: - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: - gradle-version: 8.4 + gradle-version: 8.14.2 - name: Generate framework file run: gradle :shared:generateDummyFramework - - name: Set up cocoapods - uses: maxim-lobanov/setup-cocoapods@v1 - with: - version: latest - - - name: Build xcworkspace - run: gradle podInstall - - name: Build iosApp run: xcodebuild clean build -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO #- name: Run Ios tests - # run: bash xcodebuild test -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \ No newline at end of file + # run: bash xcodebuild test -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 21fdc55..d6e3148 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Aug 01 14:19:46 TRT 2023 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists From 4e100e2fa316c18ef3f2b89f25eceab61f68aaa5 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 30 Jun 2025 13:21:16 +0300 Subject: [PATCH 09/10] Refactor: Update Gradle version in CI and remove Cocoapods setup The Gradle version used in the GitHub Actions workflow has been updated from 8.4 to 8.14.2. Additionally, the steps for setting up Cocoapods and building the xcworkspace using `gradle podInstall` have been removed from the iOS build process. --- .github/workflows/actions.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 1f68881..48f7f23 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -63,9 +63,6 @@ jobs: with: gradle-version: 8.14.2 - - name: Generate framework file - run: gradle :shared:generateDummyFramework - - name: Build iosApp run: xcodebuild clean build -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO From e4c942a77357ee74c211a6b963391b2be22f6253 Mon Sep 17 00:00:00 2001 From: Murat Bilir Date: Mon, 30 Jun 2025 18:00:23 +0300 Subject: [PATCH 10/10] CI: Update Github Actions workflow --- .github/workflows/actions.yml | 27 ++++++++------------------- 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 48f7f23..1c27867 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -17,16 +17,10 @@ jobs: with: ref: ${{ github.event.pull_request.head.ref }} - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: 'oracle' - java-version: '17.0.5' - - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: - gradle-version: 8.14.2 + gradle-version: wrapper - name: Build androidApp run: gradle :androidApp:buildDebug @@ -36,7 +30,7 @@ jobs: ios-build-check: name: Build iosApp - runs-on: macos-latest + runs-on: macos-15 env: API_KEY_TMDB: '' @@ -48,23 +42,18 @@ jobs: ref: ${{ github.event.pull_request.head.ref }} - name: Set up Xcode - uses: maxim-lobanov/setup-xcode@v1 - with: - xcode-version: latest-stable - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - distribution: 'oracle' - java-version: '17.0.5' + run: | + sudo xcode-select -s /Applications/Xcode_16.2.app/Contents/Developer + xcodebuild -version # Optional: verify selected version - name: Setup Gradle uses: gradle/gradle-build-action@v2 with: - gradle-version: 8.14.2 + gradle-version: wrapper - name: Build iosApp - run: xcodebuild clean build -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 14,OS=16.2' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO + run: | + xcodebuild clean build -project iosApp/iosApp.xcodeproj -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 16 Pro,OS=18.3.1' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO #- name: Run Ios tests # run: bash xcodebuild test -workspace iosApp/iosApp.xcworkspace -scheme iosApp -destination 'platform=iOS Simulator,name=iPhone 15,OS=17.0' CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO