diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 4d36ae6..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.4 + 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,34 +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.4 - - - 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 + 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 \ 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/.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..965ac66 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 { @@ -61,16 +60,14 @@ android { } dependencies { - implementation(project(":shared")) + implementation(projects.shared) implementation(libs.androidx.activity.compose) 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/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/build.gradle.kts b/build.gradle.kts index f38f874..31805fa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,13 +1,12 @@ 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) 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/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.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 c37855f..72ab483 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.2.0" +kotlinx-coroutines = "1.10.2" +kotlinx-serialization = "1.8.1" +ktor = "3.1.3" +agp = "8.10.1" +desugar_jdk_libs = "2.1.5" +compose = "1.8.2" +composeHotReload = "1.0.0-alpha11" +androidx = "1.16.0" +androidx-activity = "1.10.1" +androidx-appcompat = "1.7.1" +voyager = "1.1.0-beta03" +napier = "2.7.1" +buildKonfig = "0.17.1" +koin = "4.1.0" +seiko = "1.10.0" +mapsCompose = "6.6.0" +playServicesLocation = "21.3.0" maps-secrets = "2.0.1" +javafx = "24.0.1" [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" } @@ -47,44 +40,46 @@ 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" } -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.russhwolf:multiplatform-settings", version = "1.3.0" } +encryptedprefs = { module = "dev.spght:encryptedprefs-ktx", version = "1.0.1" } -moko-resources = { module = "dev.icerock.moko:resources", version.ref = "moko" } -moko-resources-compose = { module = "dev.icerock.moko:resources-compose", version.ref = "moko" } +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" } -kotlin-cocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", 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" } 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..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.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-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..a9ac559 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -3,14 +3,12 @@ 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 +29,9 @@ /* 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 +40,6 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 6BE1F7DCD0DE294643E555EF /* Pods_iosApp.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -66,8 +59,6 @@ children = ( 7555FF7D242A565900829871 /* iosApp */, 7555FF7C242A565900829871 /* Products */, - 7555FFB0242A642200829871 /* Frameworks */, - 8BA20514D79FE6023D3FA30F /* Pods */, ); sourceTree = ""; }; @@ -87,28 +78,10 @@ 7555FF8C242A565B00829871 /* Info.plist */, 058557D7273AAEEB004C7B11 /* Preview Content */, A47FDF572A8CD56700CA069B /* AppDelegate.swift */, - 391C5BDA2AC582D400F44208 /* SwiftToKotlin.swift */, ); 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 +99,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 +122,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 1130; - LastUpgradeCheck = 1130; + LastUpgradeCheck = 1620; ORGANIZATIONNAME = orgName; TargetAttributes = { 7555FF7A242A565900829871 = { @@ -190,46 +161,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 +170,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 */ @@ -255,7 +188,6 @@ files = ( A47FDF582A8CD56700CA069B /* AppDelegate.swift in Sources */, 7555FF83242A565900829871 /* ContentView.swift in Sources */, - 391C5BDB2AC582D400F44208 /* SwiftToKotlin.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -380,12 +312,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 = "${TEAM_ID}"; ENABLE_PREVIEWS = NO; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -412,7 +343,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/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/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/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/settings.gradle.kts b/settings.gradle.kts index 788266a..32e2bbc 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,23 +1,33 @@ -@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") } } include(":androidApp") +include(":desktopApp") include(":shared") diff --git a/shared/build.gradle.kts b/shared/build.gradle.kts index d5c44a7..0909a2d 100644 --- a/shared/build.gradle.kts +++ b/shared/build.gradle.kts @@ -1,11 +1,12 @@ 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) - alias(libs.plugins.kotlin.cocoapods) alias(libs.plugins.kotlinx.serialization) alias(libs.plugins.buildKonfig) } @@ -47,157 +48,126 @@ 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() - } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) } } + iosArm64 { binaries.framework { baseName = "shared" } } + iosSimulatorArm64 { binaries.framework { baseName = "shared" } } + + jvm() + sourceSets { - all { - languageSettings.optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") + commonMain.dependencies { + // Compose + 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) + + // 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) + + //Settings + implementation(libs.settings) } - 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) - } + commonTest.dependencies { + implementation(kotlin("test")) + implementation(libs.kotlinx.coroutines.test) } - val commonTest by getting { - dependencies { - implementation(kotlin("test")) - implementation(libs.kotlinx.coroutines.test) - } + androidMain.dependencies { + implementation(compose.uiTooling) + // 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) + + implementation(libs.encryptedprefs) } - 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) - } + iosMain.dependencies { + implementation(libs.ktor.client.darwin) } - 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) - } + + 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) }) } } } 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() } - buildFeatures { - compose = true - } - composeOptions { - kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get() - } compileOptions { sourceCompatibility = JavaVersion.VERSION_11 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/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/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/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/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/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/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/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/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/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/nav/AccountScreen.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/nav/AccountScreen.kt index 7cca76c..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 @@ -1,16 +1,17 @@ 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 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 @@ -21,9 +22,5 @@ class AccountScreen : Screen { viewModel = viewModel, navigateToSplash = { mainNavigator.replaceAll(SplashScreen()) }, navigateToFavorite = { navigator.push(FavoriteScreen(it)) }) - - BackHandler(isEnabled = 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..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 @@ -1,10 +1,10 @@ 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 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 +13,7 @@ class ArtistDetailScreen( private val actorId: Int, ) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -25,9 +26,5 @@ class ArtistDetailScreen( navigateToTv = { navigator.push(TvDetailScreen(tvId = it)) }, onBackPressed = navigator::pop ) - - BackHandler(isEnabled = 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..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 @@ -1,10 +1,10 @@ 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 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 +12,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 @@ -24,9 +25,5 @@ class FavoriteScreen(private val mediaType: MediaType) : Screen { navigateToTv = { navigator.push(TvDetailScreen(it)) }, navigateBack = { navigator.pop() } ) - - BackHandler(isEnabled = 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..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 @@ -1,10 +1,10 @@ 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 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 +13,7 @@ class MovieDetailScreen( private val movieId: Int, ) : Screen { + @OptIn(ExperimentalComposeUiApi::class) @Composable override fun Content() { val navigator = LocalNavigator.currentOrThrow @@ -24,9 +25,5 @@ class MovieDetailScreen( navigateToActor = { navigator.push(ArtistDetailScreen(it)) }, onBackPressed = navigator::pop, ) - - BackHandler(isEnabled = 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 51927af..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 @@ -1,16 +1,17 @@ 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 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 @@ -22,9 +23,5 @@ class SearchScreen : Screen { navigateToTv = { navigator.push(TvDetailScreen(it)) }, navigateToActor = { navigator.push(ArtistDetailScreen(it)) } ) - - BackHandler(isEnabled = 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 93e09cf..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 @@ -1,16 +1,17 @@ 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 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 @@ -21,9 +22,5 @@ class TvDetailScreen(private val tvId: Int) : Screen { navigateToActor = { navigator.push(ArtistDetailScreen(it)) }, onBackPressed = navigator::pop ) - - BackHandler(isEnabled = true) { - navigator.pop() - } } } 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/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/ImageItem.kt b/shared/src/commonMain/kotlin/com/example/moveeapp_compose_kmm/ui/components/ImageItem.kt index fc5fae8..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 @@ -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,8 +43,9 @@ fun PosterImageItem( ) { Image( modifier = modifier.fillMaxSize(), - painter = rememberAsyncImagePainter( - Constants.IMAGE_BASE.plus(imagePath) + painter = rememberImagePainter( + Constants.IMAGE_BASE.plus(imagePath), + placeholderPainter = { painterResource(Res.drawable.search_place_holder) } ), contentDescription = null, contentScale = contentScale @@ -72,16 +72,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 +115,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/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..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 @@ -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 @@ -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 71664b2..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 @@ -11,7 +14,8 @@ 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.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 74208e2..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,24 +3,32 @@ 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 -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 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( @@ -38,11 +46,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,13 +71,45 @@ 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, 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 eb610c9..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 @@ -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 @@ -23,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 = { @@ -56,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, - uiState = uiState, - onMarkerClick = viewModel::setSelectedCinema, - onPositionChange = viewModel::getUpdates - ) + mapContent() AnimatedVisibility( visible = uiState.selectedCinema != null, @@ -91,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 fe6f5f2..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 @@ -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 @@ -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 fab0ec1..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 @@ -20,16 +21,17 @@ 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 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,33 +196,38 @@ 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), icon = Icons.Default.Share, onClick = { - share(platformContext, uiState.movieDetailData.homepage) + share( + platformContext, + data.title, + data.overview, + data.posterPath.ifBlank { null } + ) } ) }) - Divider( + HorizontalDivider( modifier = Modifier.padding(vertical = 10.dp), thickness = 1.dp, color = MaterialTheme.colorScheme.secondaryContainer ) TextItem( - text = uiState.movieDetailData.overview, + text = data.overview, maxLines = Int.MAX_VALUE ) } @@ -221,7 +235,7 @@ fun MovieDetailContent( @Composable fun MovieCreditLazyRow( - uiState: MovieDetailUiState, + credit: List, onDetailClick: (Int) -> Unit, ) { TextItem( @@ -235,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) }) } } @@ -268,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 ca6bbee..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 @@ -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 @@ -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 a65590c..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 @@ -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 @@ -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 = {} + ) + } +} 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..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 @@ -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 @@ -193,12 +193,16 @@ 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 }) } ) }) - 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/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/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/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/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/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 acaaf47..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 @@ -2,31 +2,13 @@ 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() - }) { + return ComposeUIViewController { + KoinApplication(application = { init() }) { App() } } - Holder.viewController = uiViewController - return uiViewController } - -internal object Holder { - var viewController: UIViewController? = null -} - -fun onBackGesture() { - store.send(Action.OnBackPressed) -} - -val store = CoroutineScope(SupervisorJob()).createStore() 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..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 @@ -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 @@ -116,11 +118,13 @@ actual fun Map( } UIKitView( - modifier = modifier.fillMaxSize(), - interactive = true, - factory = { - mkMapView - }, update = { view -> + modifier = modifier, + 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) } 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..8caf0c0 --- /dev/null +++ b/shared/src/jvmMain/kotlin/com/example/moveeapp_compose_kmm/core/Share.jvm.kt @@ -0,0 +1,10 @@ +package com.example.moveeapp_compose_kmm.core + +actual fun share( + context: PlatformContext, + title: String, + description: String?, + imageUrl: 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() { + } +}