├── .gitignore
├── LICENSE.txt
├── README.md
├── androidApp
├── build.gradle.kts
└── src
│ └── androidMain
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── kotlin
│ └── com
│ │ └── myapplication
│ │ ├── App.kt
│ │ └── MainActivity.kt
│ └── res
│ ├── drawable-v24
│ └── ic_launcher_foreground.xml
│ ├── drawable
│ └── ic_launcher_background.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-mdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.webp
│ ├── ic_launcher_foreground.webp
│ └── ic_launcher_round.webp
│ └── values
│ ├── ic_launcher_background.xml
│ └── strings.xml
├── app_preview.gif
├── build.gradle.kts
├── cleanup.sh
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── iosApp
├── Configuration
│ └── Config.xcconfig
├── Podfile
├── Podfile.swift
├── iosApp.xcodeproj
│ └── project.pbxproj
└── iosApp
│ ├── Assets.xcassets
│ ├── AccentColor.colorset
│ │ └── Contents.json
│ ├── AppIcon.appiconset
│ │ ├── Contents.json
│ │ └── app-icon-1024.png
│ └── Contents.json
│ ├── ContentView.swift
│ ├── Info.plist
│ ├── Preview Content
│ └── Preview Assets.xcassets
│ │ └── Contents.json
│ └── iOSApp.swift
├── readme_images
├── android_app_running.png
├── banner.png
├── edit_run_config.png
├── hello_world_ios.png
├── open_project_view.png
├── run_on_android.png
├── target_device.png
└── text_field_added.png
├── settings.gradle.kts
└── shared
├── build.gradle.kts
└── src
├── androidMain
├── AndroidManifest.xml
└── kotlin
│ ├── core
│ ├── Context.kt
│ ├── DataStore.kt
│ └── platformModule.kt
│ ├── di
│ └── Utils.kt
│ └── main.android.kt
├── commonMain
├── kotlin
│ ├── App.kt
│ ├── core
│ │ ├── Context.kt
│ │ ├── DataStore.kt
│ │ ├── platformModule.kt
│ │ └── viewModelDefinition.kt
│ ├── data
│ │ ├── core
│ │ │ └── AppDataStoreManager.kt
│ │ ├── model
│ │ │ ├── Category.kt
│ │ │ ├── Product.kt
│ │ │ ├── Rating.kt
│ │ │ ├── TextFieldState.kt
│ │ │ ├── request
│ │ │ │ ├── LoginRequest.kt
│ │ │ │ └── RegisterModel.kt
│ │ │ └── response
│ │ │ │ ├── BaseResponse.kt
│ │ │ │ ├── LoginResponse.kt
│ │ │ │ └── RegisterResponse.kt
│ │ ├── network
│ │ │ ├── Resource.kt
│ │ │ └── Urls.kt
│ │ └── repository
│ │ │ ├── AppPreferences.kt
│ │ │ ├── AuthRepositoryImp.kt
│ │ │ ├── CreateDatastore.kt
│ │ │ └── HomeRepositoryImp.kt
│ ├── di
│ │ ├── AppModule.kt
│ │ └── Utils.kt
│ ├── domain
│ │ ├── core
│ │ │ └── AppDataStore.kt
│ │ ├── repository
│ │ │ ├── AuthRepository.kt
│ │ │ └── HomeRepository.kt
│ │ └── usecase
│ │ │ ├── CategoryUseCase.kt
│ │ │ ├── GetProfileUseCase.kt
│ │ │ ├── LoginUseCase.kt
│ │ │ ├── ProductUseCase.kt
│ │ │ └── RegisterUseCase.kt
│ ├── presentation
│ │ ├── base
│ │ │ ├── BaseViewModel.kt
│ │ │ └── DataStoreKeys.kt
│ │ ├── components
│ │ │ ├── AppBar.kt
│ │ │ ├── AppButtons.kt
│ │ │ ├── AppSlider.kt
│ │ │ ├── AppText.kt
│ │ │ ├── CategoryCardTag.kt
│ │ │ ├── CustomDialog.kt
│ │ │ ├── Gap.kt
│ │ │ ├── LoadingIndicator.kt
│ │ │ ├── ModalBottomSheet.kt
│ │ │ ├── ProductCard.kt
│ │ │ ├── ProfileSectionCard.kt
│ │ │ └── TabNavigationItem.kt
│ │ ├── screens
│ │ │ ├── auth
│ │ │ │ ├── login
│ │ │ │ │ ├── LoginScreen.kt
│ │ │ │ │ └── LoginViewModel.kt
│ │ │ │ ├── register
│ │ │ │ │ ├── RegisterScreen.kt
│ │ │ │ │ └── RegisterViewModel.kt
│ │ │ │ └── updateProfile
│ │ │ │ │ ├── UpdateProfileScreen.kt
│ │ │ │ │ └── UpdateProfileViewModel.kt
│ │ │ ├── category
│ │ │ │ ├── SelectedCategoryScreen.kt
│ │ │ │ └── SelectedCategoryViewModel.kt
│ │ │ ├── main
│ │ │ │ ├── MainScreen.kt
│ │ │ │ ├── MainViewModel.kt
│ │ │ │ └── taps
│ │ │ │ │ ├── category
│ │ │ │ │ ├── CategoriesViewModel.kt
│ │ │ │ │ ├── CategoryScreen.kt
│ │ │ │ │ └── CategoryTab.kt
│ │ │ │ │ ├── home
│ │ │ │ │ ├── HomeScreen.kt
│ │ │ │ │ ├── HomeTab.kt
│ │ │ │ │ └── HomeViewModel.kt
│ │ │ │ │ ├── profile
│ │ │ │ │ ├── ProfileScreen.kt
│ │ │ │ │ ├── ProfileTab.kt
│ │ │ │ │ └── ProfileViewModel.kt
│ │ │ │ │ └── search
│ │ │ │ │ ├── SearchScreen.kt
│ │ │ │ │ ├── SearchTab.kt
│ │ │ │ │ └── SearchViewModel.kt
│ │ │ ├── product
│ │ │ │ └── DetailScreen.kt
│ │ │ ├── settings
│ │ │ │ └── SettingsScreen.kt
│ │ │ └── splash
│ │ │ │ ├── SplashScreen.kt
│ │ │ │ └── SplashViewModel.kt
│ │ └── theme
│ │ │ ├── Color.kt
│ │ │ ├── Dimens.kt
│ │ │ └── Type.kt
│ └── utils
│ │ ├── AppStrings.kt
│ │ └── CommonUtil.kt
└── resources
│ ├── arrow_right.xml
│ ├── banner1.png
│ ├── banner2.png
│ ├── banner3.png
│ ├── compose-multiplatform.xml
│ ├── flag.xml
│ ├── ic_arrow_down.xml
│ ├── not_found.png
│ ├── visibility.xml
│ └── visibility_off.xml
└── iosMain
└── kotlin
├── core
├── Context.kt
├── DataStore.kt
├── platformModule.kt
└── viewModelDefinition.kt
├── di
└── Utils.kt
└── main.ios.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea
5 | .DS_Store
6 | build/
7 | /captures
8 | .externalNativeBuild
9 | .cxx
10 | iosApp/Podfile.lock
11 | iosApp/Pods/*
12 | iosApp/iosApp.xcworkspace/*
13 | iosApp/iosApp.xcodeproj/*
14 | !iosApp/iosApp.xcodeproj/project.pbxproj
15 | shared/shared.podspec
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Store KMP
2 | Compose Multiplatform Project ( android , ios )
3 |
4 | 
5 |
6 | 🔍 Features Snapshot:
7 | - Login, Sign Up ✅
8 | - Profile and update Profile✅
9 | - Home ✅
10 | - Products And ProductDetails✅
11 | - Categories And Category Details ✅
12 | - Search, Cart ,Setting ,Logout ✅
13 |
14 |
15 | 💻 Tech Stack Highlights:
16 | - Clean Archetecture with MVI
17 | - Kotlin Multiplatform
18 | - Kotlin Coroutines
19 | - Compose Multiplatform
20 | - Material3
21 | - Ktor
22 | - Datastore
23 | - Precompose
24 | - Koin
25 | - Voyager
26 | - Moko
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/androidApp/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | id("com.android.application")
4 | id("org.jetbrains.compose")
5 | }
6 |
7 | kotlin {
8 | android()
9 | sourceSets {
10 | val androidMain by getting {
11 | dependencies {
12 | implementation(project(":shared"))
13 | }
14 | }
15 | }
16 | }
17 |
18 | android {
19 | compileSdk = (findProperty("android.compileSdk") as String).toInt()
20 | namespace = "com.myapplication"
21 |
22 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
23 |
24 | defaultConfig {
25 | applicationId = "com.myapplication.MyApplication"
26 | minSdk = (findProperty("android.minSdk") as String).toInt()
27 | targetSdk = (findProperty("android.targetSdk") as String).toInt()
28 | versionCode = 1
29 | versionName = "1.0"
30 | }
31 | compileOptions {
32 | sourceCompatibility = JavaVersion.VERSION_17
33 | targetCompatibility = JavaVersion.VERSION_17
34 | }
35 | kotlin {
36 | jvmToolchain(17)
37 | }
38 | }
39 | dependencies {
40 | implementation("io.insert-koin:koin-android:3.4.0")
41 | }
42 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/androidApp/src/androidMain/kotlin/com/myapplication/App.kt:
--------------------------------------------------------------------------------
1 | package com.myapplication
2 |
3 |
4 | import android.app.Application
5 | import org.koin.core.component.KoinComponent
6 | class App : Application(), KoinComponent {
7 |
8 | override fun onCreate() {
9 | super.onCreate()
10 | // startKoin {
11 | // androidContext(this@App)
12 | // modules(appModule(this@App))
13 | // }
14 | }
15 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/kotlin/com/myapplication/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.myapplication
2 |
3 | import MainView
4 | import android.os.Bundle
5 | import androidx.activity.compose.setContent
6 | import androidx.appcompat.app.AppCompatActivity
7 |
8 | class MainActivity : AppCompatActivity() {
9 | override fun onCreate(savedInstanceState: Bundle?) {
10 | super.onCreate(savedInstanceState)
11 |
12 | setContent {
13 | MainView(application)
14 | }
15 | }
16 | }
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/androidApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/values/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #4285F4
4 |
--------------------------------------------------------------------------------
/androidApp/src/androidMain/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | KMP Store
3 |
--------------------------------------------------------------------------------
/app_preview.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/app_preview.gif
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | // this is necessary to avoid the plugins to be loaded multiple times
3 | // in each subproject's classloader
4 | kotlin("multiplatform").apply(false)
5 | id("com.android.application").apply(false)
6 | id("com.android.library").apply(false)
7 | id("org.jetbrains.compose").apply(false)
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/cleanup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | rm -rf .idea
3 | ./gradlew clean
4 | rm -rf .gradle
5 | rm -rf build
6 | rm -rf */build
7 | rm -rf iosApp/iosApp.xcworkspace
8 | rm -rf iosApp/Pods
9 | rm -rf iosApp/iosApp.xcodeproj/project.xcworkspace
10 | rm -rf iosApp/iosApp.xcodeproj/xcuserdata
11 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | #Gradle
2 | org.gradle.jvmargs=-Xmx2048M -Dkotlin.daemon.jvm.options\="-Xmx2048M"
3 |
4 | #Kotlin
5 | kotlin.code.style=official
6 |
7 | #MPP
8 | kotlin.mpp.stability.nowarn=true
9 | kotlin.mpp.enableCInteropCommonization=true
10 | kotlin.mpp.androidSourceSetLayoutVersion=2
11 |
12 | #Compose
13 | org.jetbrains.compose.experimental.uikit.enabled=true
14 | kotlin.native.cacheKind=none
15 |
16 | #Android
17 | android.useAndroidX=true
18 | android.enableJetifier=true
19 | android.compileSdk=34
20 | android.targetSdk=34
21 | android.minSdk=24
22 |
23 | #Versions
24 | kotlin.version=1.9.10
25 | agp.version=8.0.2
26 | compose.version=1.5.3
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Dec 05 01:48:10 PST 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%"=="" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%"=="" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if %ERRORLEVEL% equ 0 goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if %ERRORLEVEL% equ 0 goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | set EXIT_CODE=%ERRORLEVEL%
84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
86 | exit /b %EXIT_CODE%
87 |
88 | :mainEnd
89 | if "%OS%"=="Windows_NT" endlocal
90 |
91 | :omega
92 |
--------------------------------------------------------------------------------
/iosApp/Configuration/Config.xcconfig:
--------------------------------------------------------------------------------
1 | TEAM_ID=
2 | BUNDLE_ID=com.myapplication.MyApplication
3 | APP_NAME=StoreAppKMP
4 |
--------------------------------------------------------------------------------
/iosApp/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'iosApp' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for iosApp
9 | pod 'shared', :path => '../shared'
10 |
11 |
12 | end
13 |
--------------------------------------------------------------------------------
/iosApp/Podfile.swift:
--------------------------------------------------------------------------------
1 | # Uncomment the next line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | target 'iosApp' do
5 | # Comment the next line if you don't want to use dynamic frameworks
6 | use_frameworks!
7 |
8 | # Pods for iosApp
9 | pod 'shared', :path => '../shared'
10 |
11 |
12 | end
13 |
14 |
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "colors" : [
3 | {
4 | "idiom" : "universal"
5 | }
6 | ],
7 | "info" : {
8 | "author" : "xcode",
9 | "version" : 1
10 | }
11 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "filename" : "app-icon-1024.png",
5 | "idiom" : "universal",
6 | "platform" : "ios",
7 | "size" : "1024x1024"
8 | }
9 | ],
10 | "info" : {
11 | "author" : "xcode",
12 | "version" : 1
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/app-icon-1024.png
--------------------------------------------------------------------------------
/iosApp/iosApp/Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/iosApp/iosApp/ContentView.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import SwiftUI
3 | import shared
4 |
5 | struct ComposeView: UIViewControllerRepresentable {
6 | func makeUIViewController(context: Context) -> UIViewController {
7 | Main_iosKt.MainViewController()
8 | }
9 |
10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {}
11 | }
12 |
13 | struct ContentView: View {
14 | var body: some View {
15 | ComposeView()
16 | .ignoresSafeArea(.all, edges: .bottom) // Compose has own keyboard handler
17 | }
18 | }
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CADisableMinimumFrameDurationOnPhone
6 |
7 | CFBundleDevelopmentRegion
8 | $(DEVELOPMENT_LANGUAGE)
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleLocalizations
16 |
17 | en
18 | ar
19 |
20 | CFBundleName
21 | $(PRODUCT_NAME)
22 | CFBundlePackageType
23 | $(PRODUCT_BUNDLE_PACKAGE_TYPE)
24 | CFBundleShortVersionString
25 | 1.0
26 | CFBundleVersion
27 | 1
28 | LSRequiresIPhoneOS
29 |
30 | UIApplicationSceneManifest
31 |
32 | UIApplicationSupportsMultipleScenes
33 |
34 |
35 | UILaunchScreen
36 |
37 | UIRequiredDeviceCapabilities
38 |
39 | armv7
40 |
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "info" : {
3 | "author" : "xcode",
4 | "version" : 1
5 | }
6 | }
--------------------------------------------------------------------------------
/iosApp/iosApp/iOSApp.swift:
--------------------------------------------------------------------------------
1 | import SwiftUI
2 | import shared
3 | @main
4 | struct iOSApp: App {
5 | // init(){
6 | // AppModuleKt.doInitKoin()
7 | // }
8 | var body: some Scene {
9 | WindowGroup {
10 | ZStack {
11 | Color.white.ignoresSafeArea(.all) // status bar color
12 | ContentView()
13 | }.preferredColorScheme(.light)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/readme_images/android_app_running.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/android_app_running.png
--------------------------------------------------------------------------------
/readme_images/banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/banner.png
--------------------------------------------------------------------------------
/readme_images/edit_run_config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/edit_run_config.png
--------------------------------------------------------------------------------
/readme_images/hello_world_ios.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/hello_world_ios.png
--------------------------------------------------------------------------------
/readme_images/open_project_view.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/open_project_view.png
--------------------------------------------------------------------------------
/readme_images/run_on_android.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/run_on_android.png
--------------------------------------------------------------------------------
/readme_images/target_device.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/target_device.png
--------------------------------------------------------------------------------
/readme_images/text_field_added.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/readme_images/text_field_added.png
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "MyApplication"
2 |
3 | include(":androidApp")
4 | include(":shared")
5 |
6 | pluginManagement {
7 | repositories {
8 | gradlePluginPortal()
9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
10 | google()
11 | }
12 |
13 | plugins {
14 | val kotlinVersion = extra["kotlin.version"] as String
15 | val agpVersion = extra["agp.version"] as String
16 | val composeVersion = extra["compose.version"] as String
17 |
18 | kotlin("jvm").version(kotlinVersion)
19 | kotlin("multiplatform").version(kotlinVersion)
20 | kotlin("android").version(kotlinVersion)
21 |
22 | id("com.android.application").version(agpVersion)
23 | id("com.android.library").version(agpVersion)
24 |
25 | id("org.jetbrains.compose").version(composeVersion)
26 | id("dev.icerock.moko").version("0.23.0")
27 | }
28 | }
29 |
30 | dependencyResolutionManagement {
31 | repositories {
32 | google()
33 | mavenCentral()
34 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/shared/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | kotlin("multiplatform")
3 | kotlin("native.cocoapods")
4 | id("com.android.library")
5 | id("org.jetbrains.compose")
6 | // kotlin("plugin.serialization") version "1.8.21"
7 | id("org.jetbrains.kotlin.plugin.serialization") version "2.0.0-Beta1"
8 |
9 | // id("dev.icerock.mobile.multiplatform-resources") version "0.23.0"
10 |
11 | }
12 |
13 | kotlin {
14 | androidTarget()
15 |
16 | iosX64()
17 | iosArm64()
18 | iosSimulatorArm64()
19 |
20 | listOf(
21 | iosX64(),
22 | iosArm64(),
23 | iosSimulatorArm64()
24 | ).forEach {
25 | it.binaries.framework {
26 | isStatic = true
27 | baseName = "shared"
28 | // export("dev.icerock.moko:resources:0.23.0")
29 | // export("dev.icerock.moko:resources-compose:0.23.0")
30 |
31 | }
32 | }
33 |
34 | val myAttribute = Attribute.of("myOwnAttribute", String::class.java)
35 |
36 |
37 | configurations.all {
38 | if (name == "podDebugFrameworkIosFat") {
39 | attributes {
40 | // put a unique attribute
41 | attribute(myAttribute, "pod-debug")
42 | }
43 | }
44 | if (name == "podReleaseFrameworkIosFat") {
45 | attributes {
46 | // put a unique attribute
47 | attribute(myAttribute, "pod-release")
48 | }
49 | }
50 | }
51 |
52 |
53 | cocoapods {
54 | version = "1.0.0"
55 | summary = "Some description for the Shared Module"
56 | homepage = "Link to the Shared Module homepage"
57 | ios.deploymentTarget = "14.1"
58 | podfile = project.file("../iosApp/Podfile")
59 | framework {
60 | baseName = "shared"
61 | isStatic = true
62 | }
63 | // extraSpecAttributes["resources"] = "['src/commonMain/resources/**', 'src/iosMain/resources/**']"
64 | }
65 |
66 | sourceSets {
67 |
68 | val voyagerVersion = "1.0.0-rc07"
69 |
70 | val commonMain by getting {
71 | dependencies {
72 | implementation(compose.runtime)
73 | implementation(compose.foundation)
74 | implementation(compose.material)
75 |
76 | @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
77 | implementation(compose.components.resources)
78 | implementation("media.kamel:kamel-image:0.6.0")
79 | implementation("io.ktor:ktor-client-core:2.3.1")
80 | implementation("io.ktor:ktor-client-content-negotiation:2.3.1")
81 | implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.1")
82 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
83 | implementation("io.ktor:ktor-client-json:2.1.3")
84 | implementation("io.ktor:ktor-client-logging:2.3.1")
85 | api("dev.icerock.moko:mvvm-core:0.16.1") // only ViewModel, EventsDispatcher, Dispatchers.UI
86 | api("dev.icerock.moko:mvvm-compose:0.16.1") // api mvvm-core, getViewModel for Compose Multiplatfrom
87 | api("io.insert-koin:koin-core:3.4.0")
88 | api("io.insert-koin:koin-test:3.4.0")
89 | implementation("io.insert-koin:koin-compose:1.0.4")
90 |
91 | // Voyager
92 | // implementation("cafe.adriel.voyager:voyager-koin:$voyagerVersion")
93 | implementation("cafe.adriel.voyager:voyager-navigator:$voyagerVersion")
94 | implementation("cafe.adriel.voyager:voyager-tab-navigator:$voyagerVersion")
95 | implementation("cafe.adriel.voyager:voyager-transitions:$voyagerVersion")
96 |
97 | // implementation("dev.icerock.moko:resources:0.23.0")
98 | // implementation("dev.icerock.moko:resources-compose:0.23.0") // for compose multiplatform
99 | // implementation("dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.3.1")
100 |
101 | implementation("androidx.datastore:datastore-preferences-core:1.1.0-alpha03")
102 | implementation("co.touchlab:kermit:2.0.0-RC4")
103 |
104 |
105 | }
106 |
107 | }
108 | val androidMain by getting {
109 | dependencies {
110 | api("androidx.activity:activity-compose:1.6.1")
111 | api("androidx.appcompat:appcompat:1.6.1")
112 | api("androidx.core:core-ktx:1.9.0")
113 | implementation("io.ktor:ktor-client-android:2.3.1")
114 | implementation("io.insert-koin:koin-core:3.4.0")
115 | implementation("io.insert-koin:koin-android:3.4.0")
116 | implementation("androidx.compose.ui:ui-tooling-preview-android:1.5.4")
117 | implementation("androidx.datastore:datastore-preferences:1.0.0")
118 |
119 | }
120 | }
121 | val iosX64Main by getting
122 | val iosArm64Main by getting
123 | val iosSimulatorArm64Main by getting
124 | val iosMain by creating {
125 | dependsOn(commonMain)
126 | iosX64Main.dependsOn(this)
127 | iosArm64Main.dependsOn(this)
128 | iosSimulatorArm64Main.dependsOn(this)
129 |
130 | dependencies {
131 | implementation("io.ktor:ktor-client-darwin:2.3.1")
132 | }
133 | }
134 | }
135 | 0
136 |
137 |
138 | }
139 | //multiplatformResources {
140 | // multiplatformResourcesPackage = "com.bn.store.kmp"
141 | // disableStaticFrameworkWarning = true
142 | //}
143 |
144 | android {
145 | compileSdk = (findProperty("android.compileSdk") as String).toInt()
146 | namespace = "com.bn.store.kmp"
147 |
148 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml")
149 | sourceSets["main"].res.srcDirs("src/androidMain/res")
150 | sourceSets["main"].resources.srcDirs("src/commonMain/resources")
151 |
152 | defaultConfig {
153 | minSdk = (findProperty("android.minSdk") as String).toInt()
154 | }
155 | compileOptions {
156 | sourceCompatibility = JavaVersion.VERSION_17
157 | targetCompatibility = JavaVersion.VERSION_17
158 | }
159 | kotlin {
160 | jvmToolchain(17)
161 | }
162 | }
163 |
164 |
--------------------------------------------------------------------------------
/shared/src/androidMain/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/core/Context.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 |
4 | import android.app.Application
5 | import java.lang.ref.WeakReference
6 |
7 | actual typealias Context = Application
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/core/DataStore.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.Preferences
5 | import androidx.datastore.preferences.core.edit
6 | import androidx.datastore.preferences.core.stringPreferencesKey
7 | import androidx.datastore.preferences.preferencesDataStore
8 | import data.core.APP_DATASTORE
9 | import kotlinx.coroutines.flow.first
10 |
11 |
12 | val Context.dataStore: DataStore by preferencesDataStore(APP_DATASTORE)
13 |
14 | actual suspend fun Context.getData(key: String): String? {
15 | return dataStore.data.first()[stringPreferencesKey(key)] ?: ""
16 | }
17 |
18 | actual suspend fun Context.putData(key: String, `object`: String) {
19 | dataStore.edit {
20 | it[stringPreferencesKey(key)] = `object`
21 | }
22 | }
23 |
24 |
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/core/platformModule.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import io.ktor.client.engine.android.Android
4 | import org.koin.dsl.module
5 |
6 | actual fun platformModule() = module {
7 | single {
8 | Android.create()
9 | }
10 | }
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/di/Utils.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
4 | import org.koin.androidx.viewmodel.dsl.viewModel
5 | import org.koin.core.definition.Definition
6 | import org.koin.core.definition.KoinDefinition
7 | import org.koin.core.module.Module
8 | import org.koin.core.qualifier.Qualifier
9 |
10 | actual inline fun Module.viewModelDefinition(
11 | qualifier: Qualifier?,
12 | noinline definition: Definition,
13 | ): KoinDefinition = viewModel(qualifier = qualifier, definition = definition)
--------------------------------------------------------------------------------
/shared/src/androidMain/kotlin/main.android.kt:
--------------------------------------------------------------------------------
1 | import android.app.Application
2 | import androidx.compose.runtime.Composable
3 | import core.Context
4 |
5 | actual fun getPlatformName(): String = "Android"
6 |
7 | @Composable
8 | fun MainView(application: Application) = App(application)
9 |
10 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/App.kt:
--------------------------------------------------------------------------------
1 |
2 | import androidx.compose.foundation.shape.AbsoluteCutCornerShape
3 |
4 | import androidx.compose.material.MaterialTheme
5 | import androidx.compose.runtime.Composable
6 |
7 | import androidx.compose.ui.graphics.Color
8 | import androidx.compose.ui.unit.dp
9 | import cafe.adriel.voyager.navigator.Navigator
10 | import core.Context
11 | import di.appModule
12 | import org.koin.compose.KoinApplication
13 | import presentation.screens.splash.SplashScreen
14 |
15 | @Composable
16 | fun StoreAppTheme(
17 | content: @Composable () -> Unit
18 | ) {
19 | MaterialTheme(
20 | colors = MaterialTheme.colors.copy(primary = Color.Black),
21 | shapes = MaterialTheme.shapes.copy(
22 | small = AbsoluteCutCornerShape(0.dp),
23 | medium = AbsoluteCutCornerShape(0.dp),
24 | large = AbsoluteCutCornerShape(0.dp)
25 | )
26 | ) {
27 | content()
28 | }
29 | }
30 |
31 | @Composable
32 | fun App(context:Context) {
33 | KoinApplication(application = {
34 | modules(appModule(context))
35 | }) {
36 | StoreAppTheme {
37 | Navigator(SplashScreen())
38 | }
39 | }
40 |
41 | }
42 |
43 |
44 | expect fun getPlatformName(): String
45 |
46 |
47 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/core/Context.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | expect class Context
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/core/DataStore.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import kotlinx.coroutines.flow.Flow
4 |
5 | expect suspend fun Context.putData(key: String, `object`: String)
6 |
7 | expect suspend fun Context.getData(key: String): String?
8 |
9 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/core/platformModule.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import org.koin.core.module.Module
4 |
5 | expect fun platformModule(): Module
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/core/viewModelDefinition.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/core/AppDataStoreManager.kt:
--------------------------------------------------------------------------------
1 | package data.core
2 |
3 | import core.Context
4 | import core.getData
5 | import core.putData
6 | import domain.core.AppDataStore
7 |
8 | const val APP_DATASTORE = "com.bn.store.kmp"
9 |
10 | class AppDataStoreManager(val context: Context) : AppDataStore {
11 |
12 | override suspend fun setValue(
13 | key: String,
14 | value: String
15 | ) {
16 | context.putData(key, value)
17 | }
18 |
19 | override suspend fun readValue(
20 | key: String,
21 | ): String? {
22 | return context.getData(key)
23 | }
24 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/Category.kt:
--------------------------------------------------------------------------------
1 | package data.model
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 |
6 | @Serializable
7 | data class Category(
8 | val creationAt: String?=null,
9 | val id: Int?=null,
10 | val image: String?=null,
11 | val name: String?=null,
12 | val updatedAt: String?=null
13 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/Product.kt:
--------------------------------------------------------------------------------
1 | package data.model
2 |
3 |
4 | import data.model.response.BaseResponse
5 | import kotlinx.serialization.Serializable
6 |
7 | @Serializable
8 | data class Product(
9 | val category: Category,
10 | val creationAt: String,
11 | val description: String,
12 | val id: Int,
13 | val images: List,
14 | val price: Float,
15 | val title: String,
16 | val updatedAt: String,
17 | val rate: Double? = 4.5,
18 | val count: Int? = 49,
19 | ):BaseResponse()
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/Rating.kt:
--------------------------------------------------------------------------------
1 | package data.model
2 |
3 |
4 | import kotlinx.serialization.Serializable
5 |
6 | @Serializable
7 |
8 | data class Rating(
9 | val count: Int,
10 | val rate: Double
11 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/TextFieldState.kt:
--------------------------------------------------------------------------------
1 | package data.model
2 |
3 | data class TextFieldState(
4 | val text: String = "",
5 | val hint: String = "",
6 | val isHintVisible: Boolean = true
7 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/request/LoginRequest.kt:
--------------------------------------------------------------------------------
1 | package data.model.request
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 |
6 | @Serializable
7 | data class LoginRequest(
8 | val email: String,
9 | val password: String,
10 | )
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/request/RegisterModel.kt:
--------------------------------------------------------------------------------
1 | package data.model.request
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RegisterModel(
7 | val email: String,
8 | val password: String,
9 | val name: String,
10 | val avatar: String,
11 | val id: Int?=null,
12 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/response/BaseResponse.kt:
--------------------------------------------------------------------------------
1 | package data.model.response
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | open class BaseResponse(
7 | val statusCode: Int?=null,
8 | val message: String?=null
9 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/response/LoginResponse.kt:
--------------------------------------------------------------------------------
1 | package data.model.response
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class LoginResponse(
7 | val access_token: String?=null,
8 | val refresh_token: String?=null,
9 | ): BaseResponse()
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/model/response/RegisterResponse.kt:
--------------------------------------------------------------------------------
1 | package data.model.response
2 |
3 | import kotlinx.serialization.Serializable
4 |
5 | @Serializable
6 | data class RegisterResponse(
7 | val avatar: String? = null,
8 | val creationAt: String? = null,
9 | val email: String? = null,
10 | val id: Int? = null,
11 | val name: String? = null,
12 | val password: String? = null,
13 | val role: String? = null,
14 | val updatedAt: String? = null,
15 | val error: String? = null,
16 | val message: List? = null,
17 | val statusCode: Int? = null
18 | )
19 |
20 |
21 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/network/Resource.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 |
4 | sealed class Resource {
5 | data class Success(val result: R): Resource()
6 | data class Failure(val exception: Exception): Resource()
7 | object Loading: Resource()
8 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/network/Urls.kt:
--------------------------------------------------------------------------------
1 | package data.network
2 |
3 | object Urls {
4 | const val BASE_URL = "https://api.escuelajs.co/api/v1/"
5 | const val LOGIN = "${BASE_URL}auth/login"
6 | const val REGISTER = "${BASE_URL}users"
7 | const val GET_PROFILE_DATA = "${BASE_URL}auth/profile"
8 | const val UPDATE_PROFILE = "${BASE_URL}users/"
9 | const val GET_ALL_PRODUCTS = "${BASE_URL}products"
10 | const val PRODUCT_DETAILS = "${BASE_URL}products"
11 | const val CATEGORIES = "${BASE_URL}categories"
12 | const val GET_CATEGORY_PRODUCTS = "products"
13 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/repository/AppPreferences.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.core.IOException
5 | import androidx.datastore.preferences.core.Preferences
6 | import androidx.datastore.preferences.core.booleanPreferencesKey
7 | import androidx.datastore.preferences.core.edit
8 | import androidx.datastore.preferences.core.emptyPreferences
9 | import androidx.datastore.preferences.core.intPreferencesKey
10 | import androidx.datastore.preferences.core.stringPreferencesKey
11 | import co.touchlab.kermit.Logger
12 | import kotlinx.coroutines.flow.Flow
13 | import kotlinx.coroutines.flow.catch
14 | import kotlinx.coroutines.flow.first
15 | import kotlinx.coroutines.flow.map
16 | import org.koin.core.component.KoinComponent
17 |
18 |
19 | data class AppPreferences(
20 | val token: String = "",
21 | )
22 |
23 | class AppPreferencesRepository(
24 | private val dataStore: DataStore
25 | ) : KoinComponent {
26 |
27 | private val logger = Logger.withTag("UserPreferencesManager")
28 |
29 | private object PreferencesKeys {
30 | val USER_TOKEN = stringPreferencesKey("USER_TOKEN")
31 | }
32 |
33 | suspend fun clear() {
34 | dataStore.edit {
35 | it.clear()
36 | }
37 | }
38 |
39 | /**
40 | * Use this if you don't want to observe a flow.
41 | */
42 | suspend fun fetchInitialPreferences() =
43 | mapAppPreferences(dataStore.data.first().toPreferences())
44 |
45 | /**
46 | * Get the user preferences flow. When it's collected, keys are mapped to the
47 | * [UserPreferences] data class.
48 | */
49 | val userPreferencesFlow: Flow = dataStore.data
50 | .catch { exception ->
51 | // dataStore.data throws an IOException when an error is encountered when reading data
52 | if (exception is IOException) {
53 | logger.d { "Error reading preferences: $exception" }
54 | emit(emptyPreferences())
55 | } else {
56 | throw exception
57 | }
58 | }.map { preferences ->
59 | mapAppPreferences(preferences)
60 | }
61 |
62 |
63 | /**
64 | * Sets the userId that we get from the Ktor API (on button click).
65 | */
66 | suspend fun setUserToken(userToken: String) {
67 | dataStore.edit { preferences ->
68 | preferences[PreferencesKeys.USER_TOKEN] = userToken
69 | }
70 | }
71 |
72 |
73 | /**
74 | * Get the preferences key, then map it to the data class.
75 | */
76 | private fun mapAppPreferences(preferences: Preferences): AppPreferences {
77 | val userToken = preferences[PreferencesKeys.USER_TOKEN] ?: ""
78 | Logger.d { "lastScreen: $userToken" }
79 | return AppPreferences(userToken)
80 | }
81 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/repository/AuthRepositoryImp.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import data.core.AppDataStoreManager
4 | import data.model.request.LoginRequest
5 | import data.model.response.LoginResponse
6 | import data.model.request.RegisterModel
7 | import data.model.response.RegisterResponse
8 | import data.network.Resource
9 | import data.network.Urls
10 | import domain.core.AppDataStore
11 | import domain.repository.AuthRepository
12 | import io.ktor.client.HttpClient
13 | import io.ktor.client.call.body
14 | import io.ktor.client.request.get
15 | import io.ktor.client.request.header
16 | import io.ktor.client.request.post
17 | import io.ktor.client.request.put
18 | import io.ktor.client.request.setBody
19 | import presentation.base.DataStoreKeys.TOKEN
20 |
21 | class AuthRepositoryImp(private val httpClient: HttpClient, private val appDataStoreManager: AppDataStore,
22 | ) : AuthRepository {
23 | override suspend fun login(email: String, password: String): Resource {
24 | val response = httpClient.post(Urls.LOGIN) {
25 | setBody(LoginRequest(email,password))
26 | }.body()
27 | val isFailed = response.message != null
28 | return if(!isFailed){
29 | try {
30 | Resource.Success(
31 | response
32 | )
33 | } catch (e: Exception) {
34 | e.printStackTrace()
35 | Resource.Failure(e)
36 | }
37 | }else{
38 | Resource.Failure(Exception(response.message))
39 | }
40 |
41 | }
42 |
43 | override suspend fun register(registerModel: RegisterModel): Resource {
44 | val response = httpClient.post(Urls.REGISTER) {
45 | setBody(registerModel)
46 | }.body()
47 | return try {
48 | if(response.message.isNullOrEmpty()){
49 | Resource.Success(
50 | response
51 | )
52 | }else{
53 | Resource.Failure(Exception(response.message[0]))
54 | }
55 | } catch (e: Exception) {
56 | e.printStackTrace()
57 | Resource.Failure(e)
58 | }
59 | }
60 |
61 | override suspend fun getProfile() :Resource {
62 | val token = appDataStoreManager.readValue(TOKEN)
63 | val response = httpClient.get(Urls.GET_PROFILE_DATA) {
64 | header("Authorization","Bearer $token")
65 | }.body()
66 | return try {
67 | if(response.message.isNullOrEmpty()){
68 | Resource.Success(
69 | response
70 | )
71 | }else{
72 | Resource.Failure(Exception(response.message[0]))
73 | }
74 | } catch (e: Exception) {
75 | e.printStackTrace()
76 | Resource.Failure(e)
77 | }
78 | }
79 |
80 |
81 | override suspend fun updateProfile(registerModel: RegisterModel):Resource {
82 | val response = httpClient.put(Urls.UPDATE_PROFILE+registerModel.id) {
83 | }.body()
84 | return try {
85 | if(response.message.isNullOrEmpty()){
86 | Resource.Success(
87 | response
88 | )
89 | }else{
90 | Resource.Failure(Exception(response.message[0]))
91 | }
92 | } catch (e: Exception) {
93 | e.printStackTrace()
94 | Resource.Failure(e)
95 | }
96 | }
97 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/repository/CreateDatastore.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import androidx.datastore.core.DataStore
4 | import androidx.datastore.preferences.core.PreferenceDataStoreFactory
5 | import androidx.datastore.preferences.core.Preferences
6 | import kotlinx.atomicfu.locks.SynchronizedObject
7 | import kotlinx.atomicfu.locks.synchronized
8 | import okio.Path.Companion.toPath
9 |
10 | private lateinit var dataStore: DataStore
11 |
12 | private val lock = SynchronizedObject()
13 |
14 | /**
15 | * Gets the singleton DataStore instance, creating it if necessary.
16 | */
17 | fun getDataStore(producePath: () -> String): DataStore =
18 | synchronized(lock) {
19 | if (::dataStore.isInitialized) {
20 | dataStore
21 | } else {
22 | PreferenceDataStoreFactory.createWithPath(produceFile = { producePath().toPath() })
23 | .also { dataStore = it }
24 | }
25 | }
26 |
27 | const val dataStoreFileName = "com.bn.store.kmp.preferences"
28 |
29 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/data/repository/HomeRepositoryImp.kt:
--------------------------------------------------------------------------------
1 | package data.repository
2 |
3 | import data.model.Category
4 | import data.model.Product
5 | import data.model.request.LoginRequest
6 | import data.model.response.LoginResponse
7 | import data.model.request.RegisterModel
8 | import data.model.response.RegisterResponse
9 | import data.network.Resource
10 | import data.network.Urls
11 | import data.network.Urls.CATEGORIES
12 | import data.network.Urls.GET_CATEGORY_PRODUCTS
13 | import domain.repository.AuthRepository
14 | import domain.repository.HomeRepository
15 | import io.ktor.client.HttpClient
16 | import io.ktor.client.call.body
17 | import io.ktor.client.request.get
18 | import io.ktor.client.request.post
19 | import io.ktor.client.request.setBody
20 |
21 | class HomeRepositoryImp(private val httpClient: HttpClient) : HomeRepository {
22 | override suspend fun getAllProducts(): Resource> {
23 | val response = httpClient.get(Urls.GET_ALL_PRODUCTS) {
24 | }.body>()
25 | return try {
26 | if(!response.isEmpty()){
27 | Resource.Success(
28 | response
29 | )
30 | }else{
31 | Resource.Failure(Exception("Empty Products"))
32 | }
33 | } catch (e: Exception) {
34 | e.printStackTrace()
35 | Resource.Failure(e)
36 | }
37 | }
38 |
39 | override suspend fun getProductDetails(productId:Int): Resource {
40 | val response = httpClient.get(Urls.PRODUCT_DETAILS+"/$productId") {
41 | }.body()
42 | return try {
43 | if(response.message.isNullOrEmpty()){
44 | Resource.Success(
45 | response
46 | )
47 | }else{
48 | Resource.Failure(Exception("Empty Products"))
49 | }
50 | } catch (e: Exception) {
51 | e.printStackTrace()
52 | Resource.Failure(e)
53 | }
54 | }
55 |
56 | override suspend fun getCategories(): Resource> {
57 | val response = httpClient.get(CATEGORIES) {
58 |
59 | }.body>()
60 | return try {
61 | if(!response.isEmpty()){
62 | Resource.Success(
63 | response
64 | )
65 | }else{
66 | Resource.Failure(Exception("Empty Categories"))
67 | }
68 | } catch (e: Exception) {
69 | e.printStackTrace()
70 | Resource.Failure(e)
71 | }
72 |
73 | }
74 |
75 | override suspend fun getCategoryProducts(catId: Int): Resource> {
76 | val response = httpClient.get("$CATEGORIES/$catId/$GET_CATEGORY_PRODUCTS") {
77 |
78 | }.body>()
79 | return try {
80 | if(!response.isEmpty()){
81 | Resource.Success(
82 | response
83 | )
84 | }else{
85 | Resource.Failure(Exception("Empty Products"))
86 | }
87 | } catch (e: Exception) {
88 | e.printStackTrace()
89 | Resource.Failure(e)
90 | }
91 |
92 | }
93 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/di/AppModule.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import core.Context
4 | import core.platformModule
5 | import data.core.AppDataStoreManager
6 | import data.repository.AppPreferencesRepository
7 | import data.repository.AuthRepositoryImp
8 | import data.repository.HomeRepositoryImp
9 | import domain.core.AppDataStore
10 | import domain.repository.AuthRepository
11 | import domain.repository.HomeRepository
12 | import domain.usecase.CategoryUseCase
13 | import domain.usecase.GetProfileUseCase
14 | import domain.usecase.LoginUseCase
15 | import domain.usecase.ProductUseCase
16 | import domain.usecase.RegisterUseCase
17 | import io.ktor.client.HttpClient
18 | import io.ktor.client.plugins.DefaultRequest
19 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
20 | import io.ktor.client.plugins.logging.LogLevel
21 | import io.ktor.client.plugins.logging.Logger
22 | import io.ktor.client.plugins.logging.Logging
23 | import io.ktor.client.plugins.observer.ResponseObserver
24 | import io.ktor.client.request.header
25 | import io.ktor.http.ContentType
26 | import io.ktor.http.HttpHeaders
27 | import io.ktor.serialization.kotlinx.json.json
28 | import org.koin.core.context.startKoin
29 | import org.koin.dsl.KoinAppDeclaration
30 | import org.koin.dsl.module
31 | import presentation.screens.auth.login.LoginViewModel
32 | import presentation.screens.auth.register.RegisterViewModel
33 | import presentation.screens.auth.updateProfile.UpdateProfileViewModel
34 | import presentation.screens.category.SelectedCategoryViewModel
35 | import presentation.screens.main.MainViewModel
36 | import presentation.screens.main.taps.category.CategoriesViewModel
37 | import presentation.screens.main.taps.home.HomeViewModel
38 | import presentation.screens.main.taps.profile.ProfileViewModel
39 | import presentation.screens.main.taps.search.SearchViewModel
40 | import presentation.screens.splash.SplashViewModel
41 |
42 |
43 | fun initKoin(context: Context, appDeclaration: KoinAppDeclaration = {}) = startKoin {
44 | appDeclaration()
45 | modules(platformModule(), appModule(context))
46 | }
47 |
48 | fun initKoin(context: Context) = initKoin(context) {}
49 |
50 |
51 | fun appModule(context: Context) = module {
52 | single { createKtorClient() }
53 | single { AuthRepositoryImp(get(),get()) }
54 | single { HomeRepositoryImp(get()) }
55 |
56 | single { LoginUseCase(get()) }
57 | single { RegisterUseCase(get()) }
58 | single { CategoryUseCase(get()) }
59 | single { ProductUseCase(get()) }
60 | single { GetProfileUseCase(get()) }
61 |
62 | viewModelDefinition { LoginViewModel(get(), get()) }
63 | viewModelDefinition { RegisterViewModel(get()) }
64 | viewModelDefinition { MainViewModel(get()) }
65 | viewModelDefinition { SplashViewModel(get()) }
66 | viewModelDefinition { ProfileViewModel(get(),get()) }
67 | viewModelDefinition { CategoriesViewModel(get()) }
68 | viewModelDefinition { SelectedCategoryViewModel(get()) }
69 | viewModelDefinition { SearchViewModel(get()) }
70 | viewModelDefinition { UpdateProfileViewModel(get(),get()) }
71 | viewModelDefinition { HomeViewModel(get(),get(),get()) }
72 |
73 |
74 | single { AppDataStoreManager(context) }
75 | single { AppPreferencesRepository(get()) }
76 |
77 | }
78 |
79 |
80 | private const val TIME_OUT = 60_000
81 |
82 |
83 | fun createKtorClient(): HttpClient {
84 | return HttpClient() {
85 | // Configure your Ktor client here
86 | install(ContentNegotiation) {
87 | json(kotlinx.serialization.json.Json {
88 | ignoreUnknownKeys = true
89 | explicitNulls = false
90 | })
91 | }
92 | install(Logging) {
93 | logger = object : Logger {
94 | override fun log(message: String) {
95 | println("HTTP:Logger=>$message")
96 | }
97 |
98 | }
99 | level = LogLevel.ALL
100 | }
101 |
102 | install(ResponseObserver) {
103 | onResponse { response ->
104 | println("HTTP:status:=>${response.status.value}")
105 | }
106 | }
107 | install(DefaultRequest) {
108 | header(HttpHeaders.ContentType, ContentType.Application.Json)
109 | }
110 | }
111 | }
112 |
113 |
114 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/di/Utils.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
4 | import org.koin.core.definition.Definition
5 | import org.koin.core.definition.KoinDefinition
6 | import org.koin.core.module.Module
7 | import org.koin.core.qualifier.Qualifier
8 |
9 | expect inline fun Module.viewModelDefinition(
10 | qualifier: Qualifier? = null,
11 | noinline definition: Definition
12 | ): KoinDefinition
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/core/AppDataStore.kt:
--------------------------------------------------------------------------------
1 | package domain.core
2 |
3 |
4 | interface AppDataStore {
5 |
6 | suspend fun setValue(
7 | key: String,
8 | value: String
9 | )
10 |
11 | suspend fun readValue(
12 | key: String,
13 | ): String?
14 |
15 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/repository/AuthRepository.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import data.model.response.LoginResponse
4 | import data.model.request.RegisterModel
5 | import data.model.response.RegisterResponse
6 | import data.network.Resource
7 |
8 | interface AuthRepository {
9 | suspend fun login(userName: String, password: String): Resource
10 | suspend fun register(registerModel: RegisterModel): Resource
11 | suspend fun getProfile(): Resource
12 | suspend fun updateProfile(registerModel: RegisterModel): Resource
13 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/repository/HomeRepository.kt:
--------------------------------------------------------------------------------
1 | package domain.repository
2 |
3 | import data.model.Category
4 | import data.model.Product
5 | import data.model.response.LoginResponse
6 | import data.model.request.RegisterModel
7 | import data.model.response.RegisterResponse
8 | import data.network.Resource
9 |
10 | interface HomeRepository {
11 | suspend fun getAllProducts(): Resource>
12 | suspend fun getProductDetails(productId:Int): Resource
13 | suspend fun getCategories(): Resource>
14 | suspend fun getCategoryProducts(catId:Int): Resource>
15 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/usecase/CategoryUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import domain.repository.HomeRepository
4 |
5 |
6 | class CategoryUseCase
7 | constructor(
8 | private val repo: HomeRepository,
9 | ) {
10 | suspend fun invoke() = repo.getCategories()
11 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/usecase/GetProfileUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import data.model.request.RegisterModel
4 | import domain.repository.AuthRepository
5 |
6 |
7 | class GetProfileUseCase
8 | constructor(
9 | private val repo: AuthRepository,
10 | ) {
11 | suspend fun invoke() = repo.getProfile()
12 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/usecase/LoginUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import domain.repository.AuthRepository
4 |
5 |
6 | class LoginUseCase
7 | constructor(
8 | private val repo: AuthRepository,
9 | ) {
10 | suspend fun invoke(username:String,password:String) = repo.login(username,password)
11 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/usecase/ProductUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import data.model.request.RegisterModel
4 | import domain.repository.AuthRepository
5 | import domain.repository.HomeRepository
6 |
7 |
8 | class ProductUseCase
9 | constructor(
10 | private val repo: HomeRepository,
11 | ) {
12 | suspend fun getProductDetailsUseCase(productId:Int) = repo.getProductDetails(productId)
13 | suspend fun getAllProducts() = repo.getAllProducts()
14 | suspend fun getCategoryProducts(categoryId:Int) = repo.getCategoryProducts(categoryId)
15 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/domain/usecase/RegisterUseCase.kt:
--------------------------------------------------------------------------------
1 | package domain.usecase
2 |
3 | import data.model.request.RegisterModel
4 | import domain.repository.AuthRepository
5 |
6 |
7 | class RegisterUseCase
8 | constructor(
9 | private val repo: AuthRepository,
10 | ) {
11 | suspend fun invoke(registerModel: RegisterModel) = repo.register(registerModel)
12 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/base/BaseViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.base
2 |
3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
4 | import kotlinx.coroutines.CoroutineExceptionHandler
5 | import kotlinx.coroutines.Job
6 | import kotlinx.coroutines.launch
7 |
8 | abstract class BaseViewModel : ViewModel() {
9 | protected lateinit var launchIn: Job
10 | protected lateinit var secondIn: Job
11 |
12 |
13 | val handlerException by lazy {
14 | CoroutineExceptionHandler { _, ex ->
15 | viewModelScope.launch {
16 |
17 | }
18 | }
19 | }
20 |
21 | abstract fun setStateEvent(state: AllStateEvent)
22 | abstract fun setUiEvent(state: AllStateEvent)
23 | open class AllStateEvent {
24 | }
25 |
26 |
27 | override fun onCleared() {
28 | super.onCleared()
29 | if (::launchIn.isInitialized) launchIn.cancel()
30 | if (::secondIn.isInitialized) secondIn.cancel()
31 | }
32 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/base/DataStoreKeys.kt:
--------------------------------------------------------------------------------
1 | package presentation.base
2 |
3 | object DataStoreKeys {
4 | val TOKEN = "com.bn.store.kmp.TOKEN"
5 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/AppBar.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Spacer
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.material.Icon
11 | import androidx.compose.material.MaterialTheme
12 | import androidx.compose.material.Text
13 | import androidx.compose.material.TopAppBar
14 | import androidx.compose.runtime.Composable
15 | import androidx.compose.ui.Alignment
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.Color
18 | import androidx.compose.ui.text.font.FontWeight
19 | import androidx.compose.ui.unit.dp
20 | import org.jetbrains.compose.resources.ExperimentalResourceApi
21 | import org.jetbrains.compose.resources.painterResource
22 | import presentation.theme.keyLine2
23 |
24 |
25 | @OptIn(ExperimentalResourceApi::class)
26 | @Composable
27 | fun AppBar(
28 | appBarBackground: Color = MaterialTheme.colors.primary,
29 | appBarContentColor: Color = MaterialTheme.colors.onPrimary,
30 | barTitle: String,
31 | leadingIcon: String? = null,
32 | trailingIcon: String? = null,
33 | onLeadingIconClicked: (() -> Unit)? = null,
34 | onTrailingIconClicked: (() -> Unit)? = null,
35 | ) {
36 | TopAppBar(
37 | backgroundColor = appBarBackground,
38 | contentColor = appBarContentColor,
39 | elevation = 0.dp,
40 | modifier = Modifier
41 | .background(color = appBarBackground)
42 | .padding(start = 16.dp, end = 16.dp)
43 | ) {
44 | leadingIcon?.let {
45 | Icon(
46 | painter = painterResource(it),
47 | contentDescription = "leading icon of $barTitle",
48 | modifier = Modifier.clickable { onLeadingIconClicked?.let { clicked -> clicked() } }
49 |
50 | )
51 | Spacer(modifier = Modifier.size(keyLine2))
52 | }
53 | Text(
54 | modifier = Modifier,
55 | text = barTitle,
56 | style = MaterialTheme.typography.body1.copy(
57 | color = appBarContentColor,
58 | fontWeight = FontWeight.SemiBold
59 | )
60 | )
61 |
62 | trailingIcon?.let {
63 | Column(modifier = Modifier.fillMaxWidth()) {
64 | Icon(
65 | painter = painterResource(trailingIcon),
66 | contentDescription = it.toString(),
67 | modifier = Modifier
68 | .size(20.dp)
69 | .align(Alignment.End)
70 | .clickable { onTrailingIconClicked?.let { clicked -> clicked() } }
71 | )
72 | }
73 | }
74 | }
75 |
76 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/AppButtons.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.animation.AnimatedVisibility
4 | import androidx.compose.foundation.layout.Row
5 | import androidx.compose.foundation.layout.fillMaxWidth
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.foundation.shape.RoundedCornerShape
10 | import androidx.compose.material.Button
11 | import androidx.compose.material.ButtonColors
12 | import androidx.compose.material.ButtonDefaults
13 | import androidx.compose.material.CircularProgressIndicator
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.ui.Alignment
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.draw.drawBehind
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.graphics.drawscope.Stroke
21 | import androidx.compose.ui.text.TextStyle
22 | import androidx.compose.ui.text.font.Font
23 | import androidx.compose.ui.text.font.FontFamily
24 | import androidx.compose.ui.text.font.FontWeight
25 | import androidx.compose.ui.text.style.TextAlign
26 | import androidx.compose.ui.unit.dp
27 | import androidx.compose.ui.unit.sp
28 | import presentation.theme.DarkPurple
29 | import presentation.theme.yellow
30 |
31 | @Composable
32 | fun AppPrimaryButtonPreview() {
33 | // AppPrimaryButton(
34 | // modifier = Modifier
35 | // .padding(horizontal = 40.dp, vertical = 12.dp)
36 | // .width(400.dp)
37 | // .height(90.dp),
38 | // buttonText = stringResource(id = R.string.sign_in),
39 | // isLoading = false,
40 | //
41 | // shape = RoundedCornerShape(16.dp),
42 | // colors = ButtonDefaults.buttonColors(
43 | // contentColor = yellow,
44 | // containerColor = DarkPurple,
45 | // disabledContainerColor = grayTextColor
46 | // ),
47 | // textColor = yellow,
48 | // style = TextStyle(
49 | // fontFamily = semiBoldFont,
50 | // fontSize = 18.sp,
51 | // fontWeight = FontWeight.SemiBold,
52 | // textAlign = TextAlign.Center
53 | // )
54 | // ) {
55 | // // navController?.navigate(AuthScreens.RegisterStep1.route)
56 | // }
57 |
58 | }
59 |
60 | @Composable
61 | fun AppPrimaryButton(
62 | modifier: Modifier = Modifier,
63 | buttonText: String = "",
64 | isLoading: Boolean = false,
65 | colors: ButtonColors = ButtonDefaults.buttonColors(
66 | contentColor = yellow,
67 | backgroundColor = DarkPurple
68 | ),
69 | shape: RoundedCornerShape =
70 | RoundedCornerShape(16.dp),
71 | style: TextStyle = TextStyle(
72 | // fontFamily = fontFamilyResource(MR.fonts.somar_bold),
73 | fontWeight = FontWeight.Normal,
74 | fontSize = 14.sp,
75 | ),
76 | textColor: Color = yellow,
77 | onClicked: () -> Unit,
78 | ) {
79 | Button(modifier = modifier
80 | .fillMaxWidth()
81 | .height(61.dp)
82 | .padding(top = 34.dp),
83 | shape = shape,
84 | colors = colors,
85 | enabled = !isLoading,
86 | onClick = {
87 | onClicked()
88 | }) {
89 | val strokeWidth = 2.dp
90 | Row(verticalAlignment = Alignment.CenterVertically) {
91 | AnimatedVisibility(visible = isLoading) {
92 | CircularProgressIndicator(
93 | modifier = Modifier.drawBehind {
94 | drawCircle(
95 | Color.White,
96 | radius = size.width / 2 - strokeWidth.toPx() / 2,
97 | style = Stroke(strokeWidth.toPx())
98 | )
99 | },
100 | color = Color.LightGray,
101 | strokeWidth = strokeWidth
102 | )
103 | }
104 |
105 | Text(
106 | text = buttonText,
107 | modifier = Modifier.weight(1f),
108 | style = style,
109 | color = textColor,
110 | textAlign = TextAlign.Center,
111 | )
112 | }
113 |
114 | }
115 | }
116 |
117 |
118 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/AppSlider.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.animation.core.animateDpAsState
4 | import androidx.compose.foundation.ExperimentalFoundationApi
5 | import androidx.compose.foundation.Image
6 | import androidx.compose.foundation.background
7 | import androidx.compose.foundation.layout.Arrangement
8 | import androidx.compose.foundation.layout.Box
9 | import androidx.compose.foundation.layout.Column
10 | import androidx.compose.foundation.layout.Row
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.padding
15 | import androidx.compose.foundation.layout.size
16 | import androidx.compose.foundation.layout.wrapContentHeight
17 | import androidx.compose.foundation.layout.wrapContentSize
18 | import androidx.compose.foundation.pager.HorizontalPager
19 | import androidx.compose.foundation.pager.rememberPagerState
20 | import androidx.compose.foundation.shape.CircleShape
21 | import androidx.compose.material.Card
22 | import androidx.compose.material.Icon
23 | import androidx.compose.material.IconButton
24 | import androidx.compose.material.icons.Icons
25 | import androidx.compose.material.icons.filled.KeyboardArrowLeft
26 | import androidx.compose.material.icons.filled.KeyboardArrowRight
27 | import androidx.compose.runtime.Composable
28 | import androidx.compose.runtime.LaunchedEffect
29 | import androidx.compose.runtime.rememberCoroutineScope
30 | import androidx.compose.ui.Alignment
31 | import androidx.compose.ui.Modifier
32 | import androidx.compose.ui.draw.clip
33 | import androidx.compose.ui.graphics.Color
34 | import androidx.compose.ui.unit.dp
35 | import kotlinx.coroutines.delay
36 | import kotlinx.coroutines.launch
37 | import org.jetbrains.compose.resources.ExperimentalResourceApi
38 | import org.jetbrains.compose.resources.painterResource
39 |
40 |
41 | @OptIn(ExperimentalFoundationApi::class, ExperimentalResourceApi::class)
42 | @Composable
43 | fun AppSlider() {
44 | val images = listOf(
45 | "banner1.png",
46 | "banner2.png",
47 | "banner3.png",
48 | )
49 |
50 | val pagerState = rememberPagerState { images.size }
51 |
52 | LaunchedEffect(Unit) {
53 | while (true) {
54 | delay(4000)
55 | val nextPage = (pagerState.currentPage + 1) % pagerState.pageCount
56 | pagerState.scrollToPage(nextPage)
57 | }
58 | }
59 | val scope = rememberCoroutineScope()
60 |
61 | Column(
62 | modifier = Modifier.wrapContentHeight().fillMaxWidth(),
63 | horizontalAlignment = Alignment.CenterHorizontally
64 | ) {
65 | Box(modifier = Modifier.wrapContentSize()) {
66 | HorizontalPager(
67 | state = pagerState,
68 | Modifier
69 | .wrapContentSize()
70 |
71 | ) { currentPage ->
72 | Card(
73 | Modifier
74 | .wrapContentSize()
75 | .padding(26.dp),
76 | elevation = 8.dp
77 | ) {
78 | Image(
79 | modifier = Modifier.fillMaxWidth().height(200.dp),
80 | painter = painterResource(images[currentPage]),
81 | contentDescription = ""
82 | )
83 | }
84 | }
85 | IconButton(
86 | onClick = {
87 | val nextPage = pagerState.currentPage + 1
88 | if (nextPage < images.size) {
89 | scope.launch {
90 | pagerState.scrollToPage(nextPage)
91 | }
92 | }
93 | },
94 | Modifier
95 | .padding(30.dp)
96 | .size(48.dp)
97 | .align(Alignment.CenterEnd)
98 | .clip(CircleShape),
99 | // colors = IconButtonDefaults.iconButtonColors(
100 | // containerColor = Color(0x52373737)
101 | // )
102 | ) {
103 | Icon(
104 | imageVector = Icons.Filled.KeyboardArrowRight, contentDescription = "",
105 | Modifier.fillMaxSize(),
106 | tint = Color.LightGray
107 | )
108 | }
109 | IconButton(
110 | onClick = {
111 | val prevPage = pagerState.currentPage - 1
112 | if (prevPage >= 0) {
113 | scope.launch {
114 | pagerState.scrollToPage(prevPage)
115 | }
116 | }
117 | },
118 | Modifier
119 | .padding(30.dp)
120 | .size(48.dp)
121 | .align(Alignment.CenterStart)
122 | .clip(CircleShape),
123 | // colors = IconButtonDefaults.iconButtonColors(
124 | // containerColor = Color(0x52373737)
125 | // )
126 | ) {
127 | Icon(
128 | imageVector = Icons.Filled.KeyboardArrowLeft, contentDescription = "",
129 | Modifier.fillMaxSize(),
130 | tint = Color.LightGray
131 | )
132 | }
133 | }
134 |
135 | PageIndicator(
136 | pageCount = images.size,
137 | currentPage = pagerState.currentPage,
138 | modifier = Modifier
139 | )
140 |
141 | }
142 |
143 | }
144 |
145 | @Composable
146 | fun PageIndicator(pageCount: Int, currentPage: Int, modifier: Modifier) {
147 | Row(
148 | horizontalArrangement = Arrangement.SpaceBetween,
149 | verticalAlignment = Alignment.CenterVertically,
150 | modifier = modifier
151 | ) {
152 | repeat(pageCount) {
153 | IndicatorDots(isSelected = it == currentPage, modifier = modifier)
154 | }
155 | }
156 | }
157 |
158 | @Composable
159 | fun IndicatorDots(isSelected: Boolean, modifier: Modifier) {
160 | val size = animateDpAsState(targetValue = if (isSelected) 12.dp else 10.dp, label = "")
161 | Box(
162 | modifier = modifier.padding(2.dp)
163 | .size(size.value)
164 | .clip(CircleShape)
165 | .background(if (isSelected) Color(0xff373737) else Color(0xA8373737))
166 | )
167 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/AppText.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.interaction.MutableInteractionSource
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.foundation.layout.wrapContentWidth
10 | import androidx.compose.foundation.text.ClickableText
11 | import androidx.compose.foundation.text.KeyboardOptions
12 | import androidx.compose.material.Icon
13 | import androidx.compose.material.IconButton
14 | import androidx.compose.material.MaterialTheme
15 | import androidx.compose.material.Text
16 | import androidx.compose.material.TextField
17 | import androidx.compose.material.TextFieldDefaults
18 | import androidx.compose.runtime.Composable
19 | import androidx.compose.runtime.getValue
20 | import androidx.compose.runtime.mutableStateOf
21 | import androidx.compose.runtime.saveable.rememberSaveable
22 | import androidx.compose.runtime.setValue
23 | import androidx.compose.ui.Alignment
24 | import androidx.compose.ui.Modifier
25 | import androidx.compose.ui.graphics.Color
26 | import androidx.compose.ui.text.SpanStyle
27 | import androidx.compose.ui.text.TextStyle
28 | import androidx.compose.ui.text.buildAnnotatedString
29 | import androidx.compose.ui.text.font.FontWeight
30 | import androidx.compose.ui.text.input.PasswordVisualTransformation
31 | import androidx.compose.ui.text.input.VisualTransformation
32 | import androidx.compose.ui.text.withStyle
33 | import androidx.compose.ui.unit.dp
34 | import androidx.compose.ui.unit.sp
35 | import org.jetbrains.compose.resources.ExperimentalResourceApi
36 | import presentation.theme.DarkPurple
37 | import presentation.theme.blackTextColor
38 | import presentation.theme.gray2
39 | import presentation.theme.grayTextColor
40 | import org.jetbrains.compose.resources.painterResource
41 |
42 |
43 | @Composable
44 | fun AppText(
45 | text: String = "Text Here",
46 | modifier: Modifier = Modifier,
47 | style: TextStyle = TextStyle(
48 | // fontFamily = boldFont,
49 | fontWeight = FontWeight.Normal,
50 | fontSize = 12.sp,
51 | ),
52 | color: Color = blackTextColor
53 | ) {
54 | Text(
55 | text = text,
56 | modifier = modifier,
57 | style = style,
58 | color = color,
59 | )
60 | }
61 |
62 |
63 | @Composable
64 | fun AnnotatedClickableText(
65 | modifier: Modifier,
66 | baseText: String? = "first ",
67 | baseTextColor: Color = blackTextColor,
68 | annotatedTextTag: String = "Annotated",
69 | annotatedText: String = "annotated",
70 | annotatedTextStyle: SpanStyle = SpanStyle(
71 | color = DarkPurple,
72 | fontSize = 14.sp,
73 | fontWeight = FontWeight.Bold,
74 | // fontFamily = boldFont
75 | ), onClicked: () -> Unit = {}
76 | ) {
77 | val annotatedText = buildAnnotatedString {
78 | //append your initial text
79 | withStyle(
80 | style = SpanStyle(
81 | color = baseTextColor, fontSize = 13.sp,
82 | // fontFamily = semiBoldFont
83 | )
84 | ) {
85 | append(baseText)
86 | }
87 | //Start of the pushing annotation which you want to color and make them clickable later
88 | pushStringAnnotation(
89 | tag = annotatedTextTag,// provide tag which will then be provided when you click the text
90 | annotation = annotatedText
91 | )
92 | //add text with your different color/style
93 | withStyle(
94 | style = annotatedTextStyle
95 | ) {
96 | append(annotatedText)
97 | }
98 | // when pop is called it means the end of annotation with current tag
99 | pop()
100 | }
101 |
102 | ClickableText(
103 | modifier = modifier,
104 | text = annotatedText,
105 | onClick = { offset ->
106 | if (annotatedText.getStringAnnotations(
107 | tag = annotatedTextTag,// tag which you used in the buildAnnotatedString
108 | start = offset,
109 | end = offset
110 | ).isNotEmpty()
111 | ) {
112 | annotatedText.getStringAnnotations(
113 | tag = annotatedTextTag,// tag which you used in the buildAnnotatedString
114 | start = offset,
115 | end = offset
116 | )[0].let { annotation ->
117 | //do your stuff when it gets clicked
118 | onClicked()
119 | // Log.d("Clicked", annotation.item)
120 | }
121 | }
122 |
123 |
124 | }
125 | )
126 | }
127 |
128 |
129 |
130 | @OptIn(ExperimentalResourceApi::class)
131 | @Composable
132 | fun AppTextField(
133 | modifier: Modifier,
134 | value: String,
135 | error: String? = null,
136 | hintLabel: String,
137 | placeHolder: String = "",
138 | visualTransformation: VisualTransformation = VisualTransformation.None,
139 | keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
140 | hintColor: Color = grayTextColor,
141 | singleLine: Boolean = true,
142 | maxLines: Int = 1,
143 | showingIndicator: Boolean = true,
144 | trailingIconAction: @Composable (() -> Unit)? = null,
145 | onValueChanged: (text: String) -> Unit = {}
146 | ) {
147 |
148 | var passwordVisible by rememberSaveable { mutableStateOf(visualTransformation == PasswordVisualTransformation()) }
149 |
150 | Column {
151 | TextField(
152 | value = value,
153 | keyboardOptions = keyboardOptions,
154 | visualTransformation = if (visualTransformation == PasswordVisualTransformation() && passwordVisible) PasswordVisualTransformation() else VisualTransformation.None,
155 | onValueChange = {
156 | onValueChanged.invoke(it)
157 | },
158 | label = {
159 | Text(hintLabel, color = hintColor)
160 | },
161 | maxLines = maxLines,
162 | placeholder = {
163 | Text(placeHolder, color = grayTextColor)
164 | },
165 | colors = TextFieldDefaults.textFieldColors(
166 | backgroundColor = Color.White,
167 | unfocusedIndicatorColor = if (showingIndicator) grayTextColor else Color.Transparent,
168 | focusedIndicatorColor = if (showingIndicator) gray2 else Color.Transparent,
169 | ),
170 | singleLine = singleLine,
171 | textStyle = TextStyle(
172 | color = blackTextColor,
173 | fontWeight = FontWeight.Bold,
174 | // fontFamily = regularFont,
175 | fontSize = 16.sp
176 | ),
177 | modifier = modifier,
178 | trailingIcon = {
179 | if (visualTransformation == PasswordVisualTransformation()) {
180 | val image = if (passwordVisible) "visibility.xml" else "visibility_off.xml"
181 | val description = if (passwordVisible) "Hide password" else "Show password"
182 | IconButton(onClick = { passwordVisible = !passwordVisible }) {
183 | Icon(painter = painterResource("flag.xml") , description)
184 | }
185 | } else if (trailingIconAction !== null) {
186 | trailingIconAction()
187 | }
188 |
189 | })
190 |
191 | if (error != null) {
192 | Text(
193 | text = error,
194 | modifier = modifier,
195 | color = MaterialTheme.colors.error
196 | )
197 | }
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/CategoryCardTag.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.padding
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.graphics.Color
16 | import androidx.compose.ui.text.TextStyle
17 | import androidx.compose.ui.text.style.TextAlign
18 | import androidx.compose.ui.unit.dp
19 | import androidx.compose.ui.unit.sp
20 | import data.model.Category
21 | import presentation.theme.textColorSemiBlack
22 |
23 |
24 | @Composable
25 | fun CategoryCardTag(
26 | category: Category,
27 | onTap: (category: Category) -> Unit,
28 | ) {
29 | Box(
30 | modifier = Modifier
31 | .padding(8.dp)
32 | .padding(horizontal = 4.dp)
33 | .clip(CircleShape)
34 | .width(100.dp)
35 | .height(30.dp)
36 | .clickable {
37 | onTap(category)
38 | }
39 | .background(color = textColorSemiBlack),
40 | contentAlignment = Alignment.Center,
41 | content = {
42 | Text(
43 | textAlign = TextAlign.Center,
44 | text = category.name ?: "",
45 | style = TextStyle(
46 | color = Color.White,
47 | fontSize = 12.sp
48 | )
49 | )
50 | })
51 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/CustomDialog.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 | import androidx.compose.foundation.background
3 | import androidx.compose.foundation.border
4 | import androidx.compose.foundation.layout.Box
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.height
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.shape.RoundedCornerShape
14 | import androidx.compose.material.Button
15 | import androidx.compose.material.ButtonDefaults
16 | import androidx.compose.material.Card
17 | import androidx.compose.material.ExperimentalMaterialApi
18 | import androidx.compose.material.Text
19 | import androidx.compose.runtime.Composable
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.graphics.Color
23 | import androidx.compose.ui.graphics.Color.Companion.DarkGray
24 | import androidx.compose.ui.graphics.Color.Companion.Red
25 | import androidx.compose.ui.graphics.Color.Companion.White
26 | import androidx.compose.ui.text.TextStyle
27 | import androidx.compose.ui.text.font.FontWeight
28 | import androidx.compose.ui.text.style.TextAlign
29 | import androidx.compose.ui.unit.dp
30 | import androidx.compose.ui.unit.sp
31 | import presentation.theme.DarkPurple
32 | import presentation.theme.Gold
33 | import presentation.theme.yellow
34 | import utils.AppStrings
35 |
36 | @OptIn(ExperimentalMaterialApi::class)
37 | @Composable
38 | fun CustomDialogSheet(
39 | title: String,
40 | buttonText: String,
41 | message: String,
42 | onAccept: () -> Unit,
43 | onReject: () -> Unit
44 | ) {
45 | ModalBottomSheet(
46 | onDismissRequest = { onReject() },
47 | ) {
48 | CustomDialog(title, buttonText, message, onAccept, onReject)
49 | }
50 |
51 | }
52 |
53 | @Composable
54 | fun CustomDialog(
55 | title: String,
56 | buttonText: String,
57 | message: String,
58 | onAccept: () -> Unit,
59 | onReject: () -> Unit
60 | ) {
61 |
62 | Card(
63 | modifier = Modifier
64 | .fillMaxWidth()
65 | .height(250.dp),
66 | shape = RoundedCornerShape(30.dp)
67 | ) {
68 | Column(
69 | modifier = Modifier
70 | .fillMaxSize()
71 | .background(White)
72 | .padding(horizontal = 24.dp),
73 | horizontalAlignment = Alignment.Start
74 | ) {
75 |
76 | Spacer(modifier = Modifier.height(21.dp))
77 | Box(
78 | modifier = Modifier
79 | .width(134.dp)
80 | .height(5.dp)
81 | .background(
82 | color = Color(0xFFD0D0DA),
83 | shape = RoundedCornerShape(size = 100.dp)
84 | )
85 | .align(Alignment.CenterHorizontally)
86 | )
87 | Spacer(modifier = Modifier.height(28.dp))
88 |
89 | Text(
90 | text = title,
91 | style = TextStyle(
92 | fontSize = 24.sp,
93 | fontWeight = FontWeight(700),
94 | color = Color.Red,
95 | )
96 | )
97 |
98 | Spacer(modifier = Modifier.height(20.dp))
99 |
100 | Text(
101 | text = message,
102 | style = TextStyle(
103 | fontSize = 16.sp,
104 | fontWeight = FontWeight(500),
105 | color = Color.Black,
106 | )
107 | )
108 |
109 | Spacer(modifier = Modifier.height(30.dp))
110 |
111 | Row {
112 |
113 | Button(
114 | onClick = { onReject() },
115 | modifier = Modifier
116 | .fillMaxWidth(0.5f)
117 | .border(
118 | width = 1.dp,
119 | color = DarkPurple,
120 | shape = RoundedCornerShape(size = 12.dp)
121 | ).height(60.dp)
122 | ,
123 |
124 | shape = RoundedCornerShape(size = 12.dp),
125 | colors = ButtonDefaults.buttonColors(
126 | contentColor = DarkGray,
127 | backgroundColor = Red,
128 | ),
129 | ) {
130 | Text(text = AppStrings.cancel.stringValue, style = TextStyle(color = Color.White))
131 | }
132 | Spacer(
133 | modifier = Modifier.width(15.dp),
134 | )
135 |
136 | Button(
137 | onClick = { onAccept() },
138 | shape = RoundedCornerShape(size = 12.dp),
139 | colors = ButtonDefaults.buttonColors(
140 | contentColor = Gold
141 | ),
142 | modifier = Modifier
143 | .fillMaxWidth()
144 | .height(60.dp)
145 | ) {
146 | Text(
147 | text = AppStrings.yes.stringValue + "! $buttonText",
148 | textAlign = TextAlign.Center
149 | )
150 | }
151 | }
152 | }
153 | }
154 |
155 | }
156 |
157 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/Gap.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.layout.Box
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Row
6 | import androidx.compose.foundation.layout.height
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.width
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.geometry.Offset
16 | import androidx.compose.ui.layout.onGloballyPositioned
17 | import androidx.compose.ui.layout.positionInRoot
18 | import androidx.compose.ui.unit.Dp
19 | import androidx.compose.ui.unit.dp
20 |
21 |
22 | /**
23 | * You can use it to add space in Flex layout
24 | * Ex. Gap(20) , to add space with 20 dp in [Column] or [Row]
25 | */
26 | @Composable
27 | fun Gap(
28 | size: Dp,
29 | ) {
30 | var positionInParent by remember { mutableStateOf(Offset.Zero) }
31 | Box(modifier = Modifier
32 | .size(size)
33 | .onGloballyPositioned { coordinates ->
34 | positionInParent = coordinates.positionInRoot()
35 | }
36 | .then(
37 | Modifier
38 | .width(
39 | if (positionInParent.x == 0f) {
40 | 0.dp
41 | } else {
42 | size
43 | }
44 | )
45 | .height(
46 | if (positionInParent.y == 0f) {
47 | 0.dp
48 | } else {
49 | size
50 | }
51 | )
52 | ))
53 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/LoadingIndicator.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.RepeatMode
5 | import androidx.compose.animation.core.infiniteRepeatable
6 | import androidx.compose.animation.core.tween
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.Arrangement
9 | import androidx.compose.foundation.layout.Box
10 | import androidx.compose.foundation.layout.Column
11 | import androidx.compose.foundation.layout.Row
12 | import androidx.compose.foundation.layout.Spacer
13 | import androidx.compose.foundation.layout.fillMaxSize
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.layout.width
16 | import androidx.compose.foundation.shape.CircleShape
17 | import androidx.compose.runtime.Composable
18 | import androidx.compose.runtime.LaunchedEffect
19 | import androidx.compose.runtime.remember
20 | import androidx.compose.ui.Alignment
21 | import androidx.compose.ui.Modifier
22 | import androidx.compose.ui.draw.clip
23 | import androidx.compose.ui.graphics.Color
24 | import androidx.compose.ui.unit.Dp
25 | import androidx.compose.ui.unit.dp
26 | import kotlinx.coroutines.delay
27 |
28 |
29 | @Composable
30 | fun imageLoadingIndicator(){
31 | Column(
32 | modifier = Modifier
33 | .fillMaxSize(1.0f),
34 | verticalArrangement = Arrangement.Center,
35 | horizontalAlignment = Alignment.CenterHorizontally
36 | ) {
37 | LoadingAnimation3()
38 | }
39 | }
40 |
41 | @Composable
42 | fun LoadingAnimation3(
43 | circleColor: Color = Color(0xFF35898F),
44 | circleSize: Dp = 36.dp,
45 | animationDelay: Int = 400,
46 | initialAlpha: Float = 0.3f
47 | ) {
48 |
49 | // 3 circles
50 | val circles = listOf(
51 | remember {
52 | Animatable(initialValue = initialAlpha)
53 | },
54 | remember {
55 | Animatable(initialValue = initialAlpha)
56 | },
57 | remember {
58 | Animatable(initialValue = initialAlpha)
59 | }
60 | )
61 |
62 | circles.forEachIndexed { index, animatable ->
63 |
64 | LaunchedEffect(Unit) {
65 |
66 | // Use coroutine delay to sync animations
67 | delay(timeMillis = (animationDelay / circles.size).toLong() * index)
68 |
69 | animatable.animateTo(
70 | targetValue = 1f,
71 | animationSpec = infiniteRepeatable(
72 | animation = tween(
73 | durationMillis = animationDelay
74 | ),
75 | repeatMode = RepeatMode.Reverse
76 | )
77 | )
78 | }
79 | }
80 |
81 | // container for circles
82 | Row(
83 | modifier = Modifier
84 | //.border(width = 2.dp, color = Color.Magenta)
85 | ) {
86 |
87 | // adding each circle
88 | circles.forEachIndexed { index, animatable ->
89 |
90 | // gap between the circles
91 | if (index != 0) {
92 | Spacer(modifier = Modifier.width(width = 6.dp))
93 | }
94 |
95 | Box(
96 | modifier = Modifier
97 | .size(size = circleSize)
98 | .clip(shape = CircleShape)
99 | .background(
100 | color = circleColor
101 | .copy(alpha = animatable.value)
102 | )
103 | ) {
104 | }
105 | }
106 | }
107 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/ProductCard.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.fillMaxWidth
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.offset
13 | import androidx.compose.foundation.layout.padding
14 | import androidx.compose.foundation.layout.size
15 | import androidx.compose.foundation.layout.width
16 | import androidx.compose.foundation.layout.wrapContentHeight
17 | import androidx.compose.foundation.shape.CircleShape
18 | import androidx.compose.foundation.shape.RoundedCornerShape
19 | import androidx.compose.material.Card
20 | import androidx.compose.material.CircularProgressIndicator
21 | import androidx.compose.material.ExperimentalMaterialApi
22 | import androidx.compose.material.Icon
23 | import androidx.compose.material.Text
24 | import androidx.compose.material.icons.Icons
25 | import androidx.compose.material.icons.outlined.Favorite
26 | import androidx.compose.runtime.Composable
27 | import androidx.compose.ui.Alignment
28 | import androidx.compose.ui.Modifier
29 | import androidx.compose.ui.draw.clip
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.graphics.ColorFilter
32 | import androidx.compose.ui.layout.ContentScale
33 | import androidx.compose.ui.text.TextStyle
34 | import androidx.compose.ui.text.font.FontWeight
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import data.model.Product
38 | import io.kamel.image.KamelImage
39 | import io.kamel.image.asyncPainterResource
40 | import org.jetbrains.compose.resources.ExperimentalResourceApi
41 | import org.jetbrains.compose.resources.painterResource
42 | import presentation.theme.PrimaryColor
43 |
44 |
45 | @OptIn(ExperimentalMaterialApi::class, ExperimentalResourceApi::class)
46 | @Composable
47 | fun ProductCard(
48 | productModel: Product,
49 | onTap: () -> Unit,
50 | ) {
51 | Card(
52 | onClick = {
53 | onTap()
54 | },
55 | elevation = 0.dp,
56 | modifier = Modifier.padding(8.dp)
57 | ) {
58 | Column(modifier = Modifier.width(170.dp).wrapContentHeight()) {
59 | Box {
60 | KamelImage(
61 | asyncPainterResource(productModel.images[0]),
62 | productModel.description,
63 | contentScale = ContentScale.Fit,
64 | modifier = Modifier.fillMaxWidth().height(200.dp),
65 | onFailure = {
66 | KamelImage(
67 | asyncPainterResource(productModel.images[0]),
68 | productModel.description,
69 | contentScale = ContentScale.Fit,
70 | onLoading = { progress -> CircularProgressIndicator(progress) },
71 | modifier = Modifier.fillMaxWidth().height(200.dp),
72 | onFailure = {
73 | Image(
74 | modifier = Modifier,
75 | contentScale = ContentScale.Fit,
76 | painter = painterResource("not_found.png"),
77 | contentDescription = null
78 | )
79 | }
80 | )
81 | }
82 | )
83 | Box(modifier = Modifier.padding(8.dp)) {
84 | Text(
85 | "20%",
86 | modifier = Modifier.background(Color.Red, shape = RoundedCornerShape(30.dp))
87 | .padding(vertical = 8.dp, horizontal = 12.dp),
88 | style = TextStyle(
89 | color = Color.White,
90 | )
91 | )
92 | }
93 | }
94 | Box(
95 | modifier = Modifier
96 | .align(Alignment.End)
97 | .offset(y = -24.dp)
98 | .padding(8.dp)
99 | .size(36.dp)
100 | .clip(CircleShape)
101 | .clickable {
102 | }
103 | .background(color = Color.White),
104 | content = {
105 | Icon(
106 | modifier = Modifier
107 | .padding(8.dp)
108 | .size(40.dp),
109 | imageVector = Icons.Outlined.Favorite,
110 | tint = Color.Red,
111 | contentDescription = "avatar",
112 | )
113 | })
114 |
115 |
116 | Column(
117 | modifier = Modifier.offset(y = -36.dp)
118 | .padding(8.dp)
119 | ) {
120 | Text(
121 | productModel.category.name?:"", style = TextStyle(
122 | fontSize = 11.sp,
123 | fontWeight = FontWeight(400),
124 | color = Color(0xFF9B9B9B),
125 |
126 | )
127 | )
128 | Gap(5.dp)
129 | Text(
130 | productModel.title, style = TextStyle(
131 | fontSize = 16.sp,
132 | fontWeight = FontWeight(400),
133 | color = Color(0xFF222222),
134 | ),
135 | maxLines = 2
136 | )
137 | Gap(5.dp)
138 |
139 | Text(
140 | "${productModel.price} $",
141 | style = TextStyle(
142 | fontSize = 14.sp,
143 | lineHeight = 20.sp,
144 | fontWeight = FontWeight(500),
145 | color = Color(0xFF222222),
146 | )
147 | )
148 | }
149 | }
150 |
151 | }
152 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/ProfileSectionCard.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 |
4 | import androidx.compose.foundation.Image
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.clickable
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.Column
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.layout.size
14 | import androidx.compose.foundation.layout.width
15 | import androidx.compose.material.Text
16 | import androidx.compose.runtime.Composable
17 | import androidx.compose.ui.Alignment
18 | import androidx.compose.ui.Modifier
19 | import androidx.compose.ui.graphics.Color
20 | import androidx.compose.ui.layout.ContentScale
21 | import androidx.compose.ui.platform.testTag
22 | import androidx.compose.ui.text.TextStyle
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import org.jetbrains.compose.resources.ExperimentalResourceApi
28 | import org.jetbrains.compose.resources.painterResource
29 | import presentation.theme.BorderColor
30 |
31 | @OptIn(ExperimentalResourceApi::class)
32 | @Composable
33 | fun ProfileSectionCard(
34 | icon: @Composable () -> Unit,
35 | title: String,
36 | color: Color = Color.Black,
37 | withLine: Boolean = true,
38 | onClicked: () -> Unit
39 | ) {
40 | Column {
41 | Row(
42 | modifier = Modifier
43 | .clickable {
44 | onClicked()
45 | }
46 | .testTag(title)
47 | .padding(vertical = 20.dp),
48 | verticalAlignment = Alignment.CenterVertically
49 | ) {
50 | icon()
51 | Spacer(modifier = Modifier.width(15.dp))
52 |
53 | Text(
54 | text = title,
55 | style = TextStyle(
56 | fontSize = 16.sp,
57 | fontWeight = FontWeight(500),
58 | color = color,
59 | textAlign = TextAlign.Center,
60 | )
61 | )
62 | Spacer(modifier = Modifier.weight(1f))
63 | Image(
64 | modifier = Modifier.size(16.dp),
65 | painter = painterResource("arrow_right.xml"),
66 | contentDescription = title,
67 | contentScale = ContentScale.None
68 | )
69 |
70 | }
71 | if (withLine) {
72 | Box(
73 | modifier = Modifier
74 | .padding(0.dp)
75 | .width(345.dp)
76 | .height(1.dp)
77 | .background(color = BorderColor)
78 | )
79 | }
80 | }
81 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/components/TabNavigationItem.kt:
--------------------------------------------------------------------------------
1 | package presentation.components
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.layout.RowScope
5 | import androidx.compose.material.BottomNavigationItem
6 | import androidx.compose.material.Icon
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.ui.Modifier
9 | import androidx.compose.ui.graphics.Color
10 | import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
11 | import cafe.adriel.voyager.navigator.tab.Tab
12 | import presentation.theme.PrimaryColor
13 |
14 | @Composable
15 | fun RowScope.TabNavigationItem(tab: Tab) {
16 | val tabNavigator = LocalTabNavigator.current
17 | BottomNavigationItem(
18 | selected = tabNavigator.current.key == tab.key,
19 | onClick = { tabNavigator.current = tab },
20 | icon = { Icon(painter = tab.options.icon!!, contentDescription = tab.options.title) },
21 | selectedContentColor = PrimaryColor,
22 | unselectedContentColor = Color.Black,
23 | modifier = Modifier.background(color = Color.White)
24 | )
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/auth/login/LoginViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.auth.login
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import data.model.response.LoginResponse
6 | import data.model.response.RegisterResponse
7 | import data.model.TextFieldState
8 | import data.network.Resource
9 | import domain.core.AppDataStore
10 | import domain.usecase.LoginUseCase
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 | import kotlinx.coroutines.flow.asStateFlow
14 | import kotlinx.coroutines.launch
15 | import presentation.base.BaseViewModel
16 | import presentation.base.BaseViewModel.AllStateEvent
17 | import presentation.base.DataStoreKeys
18 | import utils.AppStrings
19 |
20 |
21 | class LoginViewModel(
22 | private val loginUseCase: LoginUseCase,
23 | private val appDataStoreManager: AppDataStore,
24 | ) : BaseViewModel() {
25 |
26 | private val _userNameError: MutableStateFlow = MutableStateFlow(null)
27 | val nameError = _userNameError.asStateFlow()
28 |
29 | private val _passwordError: MutableStateFlow = MutableStateFlow(null)
30 | val passwordError = _passwordError.asStateFlow()
31 |
32 | private val _userNameState = mutableStateOf(
33 | TextFieldState(
34 | text = "john@mail.com",
35 | hint = "Enter your Email",
36 | isHintVisible = false,
37 | )
38 | )
39 | val userName: State = _userNameState
40 |
41 | private val _passwordState = mutableStateOf(
42 | TextFieldState(
43 | text = "changeme",
44 | hint = "Enter your password",
45 | isHintVisible = false,
46 | )
47 | )
48 | val password: State = _passwordState
49 |
50 | private val _login = MutableStateFlow?>(null)
51 | val login: StateFlow?> = _login
52 |
53 | private val _register = MutableStateFlow?>(null)
54 | val register: StateFlow?> = _register
55 |
56 |
57 | override fun setStateEvent(state: AllStateEvent) {
58 | when (state) {
59 | is LoginStateIntent.Login -> {
60 | viewModelScope.launch {
61 | if (userName.value.text.length >= 5 && password.value.text.length >= 6) {
62 | _login.value = Resource.Loading
63 | _login.value = loginUseCase.invoke(userName.value.text, password.value.text)
64 | }
65 | }
66 | }
67 | is LoginStateIntent.SaveToken -> {
68 | viewModelScope.launch {
69 | appDataStoreManager.setValue(DataStoreKeys.TOKEN,state.token)
70 | }
71 | }
72 | }
73 |
74 | }
75 |
76 | override fun setUiEvent(state: AllStateEvent) {
77 | when (state) {
78 | is LoginUIStateEvent.EnteredUserName -> {
79 | _userNameState.value = userName.value.copy(
80 | text = state.value
81 | )
82 | viewModelScope.launch {
83 | if (userName.value.text.isEmpty()) {
84 | _userNameError.emit(AppStrings.user_name_validation.stringValue)
85 | } else {
86 | _userNameError.emit(null)
87 | }
88 | }
89 | }
90 |
91 | is LoginUIStateEvent.EnteredPassword -> {
92 | _passwordState.value = password.value.copy(
93 | text = state.value
94 | )
95 | viewModelScope.launch {
96 | if (password.value.text.length < 6) {
97 | _passwordError.emit(AppStrings.PasswordValidation.stringValue)
98 | } else {
99 | _passwordError.emit(null)
100 | }
101 | }
102 |
103 | }
104 | }
105 | }
106 |
107 | fun clearLoginState() {
108 | viewModelScope.launch {
109 | _login.emit(null)
110 | }
111 | }
112 | }
113 |
114 |
115 | sealed class LoginStateIntent : AllStateEvent() {
116 | object Login : LoginStateIntent()
117 | data class SaveToken(val token: String) : LoginStateIntent()
118 | // data class Register(val registerModel: RegisterModel) : AuthStateIntent()
119 | }
120 |
121 | sealed class LoginUIStateEvent : AllStateEvent() {
122 | data class EnteredUserName(val value: String) : LoginUIStateEvent()
123 | data class EnteredPassword(val value: String) : LoginUIStateEvent()
124 | }
125 |
126 |
127 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/auth/register/RegisterViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.auth.register
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import data.model.request.RegisterModel
6 | import data.model.response.RegisterResponse
7 | import data.model.TextFieldState
8 | import data.network.Resource
9 | import domain.usecase.RegisterUseCase
10 | import kotlinx.coroutines.flow.MutableStateFlow
11 | import kotlinx.coroutines.flow.StateFlow
12 | import kotlinx.coroutines.flow.asStateFlow
13 | import kotlinx.coroutines.launch
14 | import presentation.base.BaseViewModel
15 | import presentation.base.BaseViewModel.AllStateEvent
16 | import utils.AppStrings
17 |
18 |
19 | class RegisterViewModel(
20 | private val registerUseCase: RegisterUseCase,
21 | ) : BaseViewModel() {
22 |
23 | private val _nameError: MutableStateFlow = MutableStateFlow(null)
24 | val nameError = _nameError.asStateFlow()
25 |
26 | private val _emailError: MutableStateFlow = MutableStateFlow(null)
27 | val emailError = _emailError.asStateFlow()
28 |
29 | private val _passwordError: MutableStateFlow = MutableStateFlow(null)
30 | val passwordError = _passwordError.asStateFlow()
31 |
32 | private val _nameState = mutableStateOf(
33 | TextFieldState(
34 | text = "",
35 | hint = "Enter your Name",
36 | isHintVisible = false,
37 | )
38 | )
39 | val userName: State = _nameState
40 |
41 | private val _emailState = mutableStateOf(
42 | TextFieldState(
43 | text = "",
44 | hint = "Enter your valid email",
45 | isHintVisible = false,
46 | )
47 | )
48 | val email: State = _emailState
49 |
50 | private val _passwordState = mutableStateOf(
51 | TextFieldState(
52 | text = "",
53 | hint = "Enter your valid password",
54 | isHintVisible = false,
55 | )
56 | )
57 | val password: State = _passwordState
58 |
59 |
60 | private val _register = MutableStateFlow?>(null)
61 | val register: StateFlow?> = _register
62 |
63 |
64 | override fun setStateEvent(state: AllStateEvent) {
65 | when (state) {
66 | is RegisterStateIntent.Register -> {
67 | viewModelScope.launch {
68 | if (userName.value.text.length >= 5 && password.value.text.length >= 6 && email.value.text.length >= 6) {
69 | _register.value = Resource.Loading
70 | _register.value = registerUseCase.invoke(
71 | RegisterModel(
72 | email=email.value.text,
73 | name = userName.value.text,
74 | password = password.value.text,
75 | avatar = "https://api.lorem.space/image/face?w=640&h=480",
76 | )
77 | )
78 | }
79 | }
80 | }
81 |
82 | }
83 |
84 | }
85 |
86 | override fun setUiEvent(state: AllStateEvent) {
87 | when (state) {
88 | is RegisterUIEvent.EnteredName -> {
89 | _nameState.value = userName.value.copy(
90 | text = state.value
91 | )
92 | viewModelScope.launch {
93 | if (userName.value.text.isEmpty()) {
94 | _nameError.emit(AppStrings.user_name_validation.stringValue)
95 | } else {
96 | _nameError.emit(null)
97 | }
98 | }
99 | }
100 |
101 | is RegisterUIEvent.EnteredPassword -> {
102 | _passwordState.value = password.value.copy(
103 | text = state.value
104 | )
105 | viewModelScope.launch {
106 | if (password.value.text.length < 6) {
107 | _passwordError.emit(AppStrings.PasswordValidation.stringValue)
108 | } else {
109 | _passwordError.emit(null)
110 | }
111 | }
112 | }
113 | is RegisterUIEvent.EnteredEmail -> {
114 | _emailState.value = email.value.copy(
115 | text = state.value
116 | )
117 | viewModelScope.launch {
118 | if (email.value.text.length < 6) {
119 | _emailError.emit(AppStrings.email_validation.stringValue)
120 | } else {
121 | _emailError.emit(null)
122 | }
123 | }
124 | }
125 | }
126 | }
127 |
128 | fun clearAllState() {
129 | viewModelScope.launch {
130 | _register.emit(null)
131 | }
132 | }
133 | }
134 |
135 |
136 |
137 | sealed class RegisterStateIntent : AllStateEvent() {
138 | object Register : RegisterStateIntent()
139 | }
140 |
141 | sealed class RegisterUIEvent : AllStateEvent() {
142 | data class EnteredName(val value: String) : RegisterUIEvent()
143 | data class EnteredEmail(val value: String) : RegisterUIEvent()
144 | data class EnteredPassword(val value: String) : RegisterUIEvent()
145 | }
146 |
147 |
148 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/auth/updateProfile/UpdateProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.auth.updateProfile
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import data.model.Category
6 | import data.model.request.RegisterModel
7 | import data.model.response.RegisterResponse
8 | import data.model.TextFieldState
9 | import data.network.Resource
10 | import domain.usecase.GetProfileUseCase
11 | import domain.usecase.RegisterUseCase
12 | import kotlinx.coroutines.flow.MutableStateFlow
13 | import kotlinx.coroutines.flow.StateFlow
14 | import kotlinx.coroutines.flow.asStateFlow
15 | import kotlinx.coroutines.launch
16 | import presentation.base.BaseViewModel
17 | import presentation.base.BaseViewModel.AllStateEvent
18 | import utils.AppStrings
19 |
20 |
21 | class UpdateProfileViewModel(
22 | private val registerUseCase: RegisterUseCase,
23 | private val getProfileUseCase: GetProfileUseCase,
24 | ) : BaseViewModel() {
25 |
26 | private val _nameError: MutableStateFlow = MutableStateFlow(null)
27 | val nameError = _nameError.asStateFlow()
28 |
29 | private val _emailError: MutableStateFlow = MutableStateFlow(null)
30 | val emailError = _emailError.asStateFlow()
31 |
32 | private val _passwordError: MutableStateFlow = MutableStateFlow(null)
33 | val passwordError = _passwordError.asStateFlow()
34 |
35 | private val _nameState = mutableStateOf(
36 | TextFieldState(
37 | text = "",
38 | hint = "Enter your Name",
39 | isHintVisible = false,
40 | )
41 | )
42 | val userName: State = _nameState
43 |
44 | private val _emailState = mutableStateOf(
45 | TextFieldState(
46 | text = "",
47 | hint = "Enter your valid email",
48 | isHintVisible = false,
49 | )
50 | )
51 | val email: State = _emailState
52 |
53 | private val _passwordState = mutableStateOf(
54 | TextFieldState(
55 | text = "",
56 | hint = "Enter your valid password",
57 | isHintVisible = false,
58 | )
59 | )
60 | val password: State = _passwordState
61 |
62 |
63 | private var userProfile:RegisterResponse?=null
64 |
65 |
66 | private val _updateProfile = MutableStateFlow?>(null)
67 | val updateProfile: StateFlow?> = _updateProfile
68 |
69 | init {
70 | setStateEvent(UpdateProfileStateIntent.GettingProfile)
71 | }
72 |
73 | override fun setStateEvent(state: AllStateEvent) {
74 | when (state) {
75 | is UpdateProfileStateIntent.GettingProfile -> {
76 | viewModelScope.launch {
77 | val response = getProfileUseCase.invoke()
78 | when(getProfileUseCase.invoke()){
79 | is Resource.Failure -> {}
80 | Resource.Loading -> {}
81 | is Resource.Success -> {
82 | userProfile = (response as Resource.Success).result
83 | userProfile?.let {
84 | setUiEvent(UpdateProfileUIEvent.EnteredName(userProfile?.name?:""))
85 | setUiEvent(UpdateProfileUIEvent.EnteredEmail(userProfile?.email?:""))
86 | setUiEvent(UpdateProfileUIEvent.EnteredPassword(userProfile?.password?:""))
87 | }
88 | }
89 | }
90 | }
91 | }
92 | is UpdateProfileStateIntent.UpdateProfile -> {
93 | viewModelScope.launch {
94 | if (userName.value.text.length >= 5 && password.value.text.length >= 6 && email.value.text.length >= 6) {
95 | _updateProfile.value = Resource.Loading
96 | _updateProfile.value = registerUseCase.invoke(
97 | RegisterModel(
98 | email=email.value.text,
99 | name = userName.value.text,
100 | password = password.value.text,
101 | avatar = "https://api.lorem.space/image/face?w=640&h=480",
102 | )
103 | )
104 | }
105 | }
106 | }
107 |
108 | }
109 |
110 | }
111 |
112 | override fun setUiEvent(state: AllStateEvent) {
113 | when (state) {
114 | is UpdateProfileUIEvent.EnteredName -> {
115 | _nameState.value = userName.value.copy(
116 | text = state.value
117 | )
118 | viewModelScope.launch {
119 | if (userName.value.text.isEmpty()) {
120 | _nameError.emit(AppStrings.user_name_validation.stringValue)
121 | } else {
122 | _nameError.emit(null)
123 | }
124 | }
125 | }
126 |
127 | is UpdateProfileUIEvent.EnteredPassword -> {
128 | _passwordState.value = password.value.copy(
129 | text = state.value
130 | )
131 | viewModelScope.launch {
132 | if (password.value.text.length < 6) {
133 | _passwordError.emit(AppStrings.PasswordValidation.stringValue)
134 | } else {
135 | _passwordError.emit(null)
136 | }
137 | }
138 | }
139 | is UpdateProfileUIEvent.EnteredEmail -> {
140 | _emailState.value = email.value.copy(
141 | text = state.value
142 | )
143 | viewModelScope.launch {
144 | if (email.value.text.length < 6) {
145 | _emailError.emit(AppStrings.email_validation.stringValue)
146 | } else {
147 | _emailError.emit(null)
148 | }
149 | }
150 | }
151 | }
152 | }
153 |
154 | fun clearAllState() {
155 | viewModelScope.launch {
156 | _updateProfile.emit(null)
157 | }
158 | }
159 | }
160 |
161 |
162 |
163 | sealed class UpdateProfileStateIntent : AllStateEvent() {
164 | object GettingProfile : UpdateProfileStateIntent()
165 | object UpdateProfile : UpdateProfileStateIntent()
166 | }
167 |
168 | sealed class UpdateProfileUIEvent : AllStateEvent() {
169 | data class EnteredName(val value: String) : UpdateProfileUIEvent()
170 | data class EnteredEmail(val value: String) : UpdateProfileUIEvent()
171 | data class EnteredPassword(val value: String) : UpdateProfileUIEvent()
172 | }
173 |
174 |
175 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/category/SelectedCategoryScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.category
2 |
3 |
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.lazy.grid.GridCells
11 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
12 | import androidx.compose.material.Icon
13 | import androidx.compose.material.Scaffold
14 | import androidx.compose.material.SnackbarHost
15 | import androidx.compose.material.SnackbarHostState
16 | import androidx.compose.material.Text
17 | import androidx.compose.material.TopAppBar
18 | import androidx.compose.material.icons.Icons
19 | import androidx.compose.material.icons.filled.ArrowBack
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.collectAsState
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.graphics.Color
27 | import androidx.compose.ui.text.TextStyle
28 | import androidx.compose.ui.text.font.FontWeight
29 | import androidx.compose.ui.text.style.TextAlign
30 | import androidx.compose.ui.unit.dp
31 | import androidx.compose.ui.unit.sp
32 | import cafe.adriel.voyager.core.screen.Screen
33 | import cafe.adriel.voyager.navigator.LocalNavigator
34 | import cafe.adriel.voyager.navigator.Navigator
35 | import cafe.adriel.voyager.navigator.currentOrThrow
36 | import data.model.Category
37 | import data.model.Product
38 | import data.network.Resource
39 | import kotlinx.coroutines.launch
40 | import org.koin.compose.koinInject
41 | import presentation.components.AppSlider
42 | import presentation.components.LoadingAnimation3
43 | import presentation.components.ProductCard
44 | import presentation.screens.product.DetailTopBar
45 | import presentation.screens.product.ProductDetailsScreen
46 |
47 | class SelectedCategoryScreen(val category: Category) : Screen {
48 |
49 | @Composable
50 | override fun Content() {
51 | val navigator: Navigator = LocalNavigator.currentOrThrow
52 | val viewModel: SelectedCategoryViewModel = koinInject()
53 |
54 |
55 | val snackState = remember { SnackbarHostState() }
56 | val snackScope = rememberCoroutineScope()
57 | val products = viewModel.products.collectAsState()
58 |
59 | viewModel.setStateEvent(SelectedCategoryStateIntent.GetProducts(category.id ?: 0))
60 |
61 | SnackbarHost(hostState = snackState, Modifier)
62 |
63 | fun launchSnackBar(message: String) {
64 | snackScope.launch { snackState.showSnackbar(message) }
65 | }
66 |
67 | Scaffold(
68 | modifier = Modifier
69 | .fillMaxSize()
70 | .padding(horizontal = 16.dp),
71 | topBar = { CategoryDetailTopBar(navigator = navigator,category) },
72 | ) { paddingValues ->
73 | Column(
74 | Modifier.padding(paddingValues)
75 | ) {
76 | AppSlider()
77 |
78 | Text(
79 | modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp),
80 | text = "Products", style = TextStyle(
81 | fontSize = 14.sp,
82 | lineHeight = 20.sp,
83 | fontWeight = FontWeight(500),
84 | color = Color(0xFF9B9B9B),
85 | textAlign = TextAlign.Center,
86 | )
87 | )
88 |
89 | products.value.let { result ->
90 | when (result) {
91 | is Resource.Failure -> {
92 | launchSnackBar("some failures occurred")
93 | }
94 |
95 | Resource.Loading -> {
96 | Column(
97 | modifier = Modifier
98 | .fillMaxSize(1.0f),
99 | verticalArrangement = Arrangement.Center,
100 | horizontalAlignment = Alignment.CenterHorizontally
101 | ) {
102 | LoadingAnimation3()
103 | }
104 | }
105 |
106 | is Resource.Success -> {
107 | LazyVerticalGrid(
108 | modifier = Modifier,
109 | columns = GridCells.Adaptive(168.dp),
110 | contentPadding = PaddingValues(
111 | start = 12.dp,
112 | top = 2.dp,
113 | end = 12.dp,
114 | bottom = 16.dp
115 | ),
116 | content = {
117 | items(result.result.size) { index ->
118 | val product = result.result[index]
119 | ProductCard(
120 | product
121 | ) {
122 | navigator?.push(ProductDetailsScreen(product))
123 | }
124 | }
125 | }
126 | )
127 | }
128 |
129 | null -> {}
130 | }
131 | }
132 | }
133 | }
134 |
135 |
136 | }
137 | }
138 |
139 | @Composable
140 | fun CategoryDetailTopBar(navigator: Navigator?=null,cateogry: Category) {
141 | TopAppBar(
142 | title = { Text(text = "${cateogry.name}") },
143 | navigationIcon = {
144 | Icon(
145 | modifier = Modifier.clickable {
146 | navigator?.pop()
147 | },
148 | imageVector = Icons.Default.ArrowBack,
149 | contentDescription = "Back Button"
150 | )
151 | },
152 | backgroundColor = Color.Transparent,
153 | elevation = 0.dp,
154 | )
155 | }
156 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/category/SelectedCategoryViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.category
2 |
3 | import data.model.Category
4 | import data.model.Product
5 | import data.network.Resource
6 | import domain.core.AppDataStore
7 | import domain.usecase.CategoryUseCase
8 | import domain.usecase.ProductUseCase
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.launch
12 | import presentation.base.BaseViewModel
13 | import presentation.base.BaseViewModel.AllStateEvent
14 |
15 |
16 | class SelectedCategoryViewModel(
17 | private val productUseCase: ProductUseCase,
18 | ) : BaseViewModel() {
19 |
20 |
21 | private val _products = MutableStateFlow>?>(null)
22 | val products: StateFlow>?> = _products
23 |
24 | override fun setStateEvent(state: AllStateEvent) {
25 | when (state) {
26 | is SelectedCategoryStateIntent.GetProducts -> {
27 | viewModelScope.launch {
28 | _products.value = Resource.Loading
29 | _products.value = productUseCase.getCategoryProducts(state.categoryId)
30 | }
31 | }
32 | }
33 |
34 | }
35 | override fun setUiEvent(state: AllStateEvent) {
36 | }
37 |
38 | fun clearStates() {
39 | viewModelScope.launch {
40 | _products.emit(null)
41 | }
42 | }
43 | }
44 |
45 |
46 | sealed class SelectedCategoryStateIntent : AllStateEvent() {
47 | data class GetProducts(val categoryId: Int) : SelectedCategoryStateIntent()
48 | }
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main
2 |
3 |
4 | import androidx.compose.animation.AnimatedVisibility
5 | import androidx.compose.animation.slideInVertically
6 | import androidx.compose.animation.slideOutVertically
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.material.BottomNavigation
9 | import androidx.compose.material.Scaffold
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.saveable.rememberSaveable
13 | import androidx.compose.ui.Modifier
14 | import cafe.adriel.voyager.core.screen.Screen
15 | import cafe.adriel.voyager.navigator.LocalNavigator
16 | import cafe.adriel.voyager.navigator.Navigator
17 | import cafe.adriel.voyager.navigator.currentOrThrow
18 | import cafe.adriel.voyager.navigator.tab.CurrentTab
19 | import cafe.adriel.voyager.navigator.tab.TabNavigator
20 | import org.jetbrains.compose.resources.ExperimentalResourceApi
21 | import org.koin.compose.koinInject
22 | import presentation.components.TabNavigationItem
23 | import presentation.screens.main.taps.category.CategoryScreen
24 | import presentation.screens.main.taps.category.CategoryTab
25 | import presentation.screens.main.taps.home.HomeScreen
26 | import presentation.screens.main.taps.home.HomeTab
27 | import presentation.screens.main.taps.profile.ProfileTab
28 | import presentation.screens.main.taps.search.SearchTab
29 |
30 | object MainScreen : Screen {
31 |
32 | @Composable
33 | override fun Content() {
34 | val navigator = LocalNavigator.currentOrThrow
35 | MainScreen.mainContent(navigator)
36 | }
37 |
38 | @Composable
39 | private fun mainContent(
40 | navigator: Navigator? = null,
41 | viewModel: MainViewModel = koinInject()
42 | ) {
43 |
44 | val navBackStackEntry = navigator?.lastItem
45 | val bottomBarState = rememberSaveable { (mutableStateOf(true)) }
46 | when(navBackStackEntry){
47 | is HomeScreen ->{
48 | bottomBarState.value = true
49 | }
50 | is CategoryScreen ->{
51 | bottomBarState.value = false
52 | }
53 | }
54 |
55 | TabNavigator(
56 | tab = HomeTab
57 | ) {
58 | Scaffold(
59 | modifier = Modifier.fillMaxSize(),
60 | bottomBar = {
61 |
62 | AnimatedVisibility(
63 | visible = bottomBarState.value,
64 | enter = slideInVertically(initialOffsetY = { it }),
65 | exit = slideOutVertically(targetOffsetY = { it }),
66 | ) {
67 | BottomNavigation {
68 | TabNavigationItem(HomeTab)
69 | TabNavigationItem(CategoryTab)
70 | TabNavigationItem(SearchTab)
71 | TabNavigationItem(ProfileTab)
72 | }
73 | }
74 | },
75 | content = { CurrentTab() },
76 | )
77 | }
78 |
79 | }
80 |
81 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import domain.core.AppDataStore
6 | import kotlinx.coroutines.launch
7 | import presentation.base.BaseViewModel
8 | import presentation.base.BaseViewModel.AllStateEvent
9 | import presentation.base.DataStoreKeys
10 |
11 |
12 | class MainViewModel(
13 | private val appDataStoreManager: AppDataStore,
14 | ) : BaseViewModel() {
15 |
16 | private val _userToken = mutableStateOf(String())
17 | val userToken: State = _userToken
18 |
19 | override fun setStateEvent(state: AllStateEvent) {
20 | when (state) {
21 | is MainStateIntent.GettingToken -> {
22 | viewModelScope.launch {
23 | val token = appDataStoreManager.readValue(DataStoreKeys.TOKEN)
24 | _userToken.value = token?:""
25 | }
26 | }
27 | }
28 |
29 | }
30 |
31 | override fun setUiEvent(state: AllStateEvent) {
32 | }
33 |
34 | }
35 |
36 |
37 | sealed class MainStateIntent : AllStateEvent() {
38 | object GettingToken : MainStateIntent()
39 | }
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoriesViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.category
2 |
3 | import data.model.Category
4 | import data.network.Resource
5 | import domain.usecase.CategoryUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 | import kotlinx.coroutines.launch
9 | import presentation.base.BaseViewModel
10 | import presentation.base.BaseViewModel.AllStateEvent
11 |
12 |
13 | class CategoriesViewModel(
14 | private val categoryUseCase: CategoryUseCase,
15 | ) : BaseViewModel() {
16 |
17 |
18 | private val _categories = MutableStateFlow>?>(null)
19 | val categories: StateFlow>?> = _categories
20 |
21 | init {
22 | setStateEvent(CategoriesStateIntent.GetCategories)
23 | }
24 |
25 | override fun setStateEvent(state: AllStateEvent) {
26 | when (state) {
27 | is CategoriesStateIntent.GetCategories -> {
28 | viewModelScope.launch {
29 | _categories.value = Resource.Loading
30 | _categories.value = categoryUseCase.invoke()
31 | }
32 | }
33 | }
34 |
35 | }
36 |
37 | override fun setUiEvent(state: AllStateEvent) {
38 | }
39 |
40 | fun clearCategoriesStates() {
41 | viewModelScope.launch {
42 | _categories.emit(null)
43 | }
44 | }
45 | }
46 |
47 |
48 | sealed class CategoriesStateIntent : AllStateEvent() {
49 | object GetCategories : CategoriesStateIntent()
50 | }
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoryScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.category
2 |
3 |
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.Row
9 | import androidx.compose.foundation.layout.Spacer
10 | import androidx.compose.foundation.layout.fillMaxSize
11 | import androidx.compose.foundation.layout.height
12 | import androidx.compose.foundation.layout.padding
13 | import androidx.compose.foundation.lazy.LazyColumn
14 | import androidx.compose.foundation.lazy.LazyRow
15 | import androidx.compose.foundation.shape.RoundedCornerShape
16 | import androidx.compose.material.Card
17 | import androidx.compose.material.SnackbarHost
18 | import androidx.compose.material.SnackbarHostState
19 | import androidx.compose.material.Text
20 | import androidx.compose.runtime.Composable
21 | import androidx.compose.runtime.collectAsState
22 | import androidx.compose.runtime.remember
23 | import androidx.compose.runtime.rememberCoroutineScope
24 | import androidx.compose.ui.Alignment
25 | import androidx.compose.ui.Modifier
26 | import androidx.compose.ui.draw.clip
27 | import androidx.compose.ui.graphics.Color
28 | import androidx.compose.ui.layout.ContentScale
29 | import androidx.compose.ui.text.TextStyle
30 | import androidx.compose.ui.text.font.FontWeight
31 | import androidx.compose.ui.text.style.TextAlign
32 | import androidx.compose.ui.unit.dp
33 | import androidx.compose.ui.unit.sp
34 | import cafe.adriel.voyager.core.screen.Screen
35 | import cafe.adriel.voyager.navigator.LocalNavigator
36 | import cafe.adriel.voyager.navigator.Navigator
37 | import cafe.adriel.voyager.navigator.currentOrThrow
38 | import data.model.Category
39 | import data.network.Resource
40 | import io.kamel.image.KamelImage
41 | import io.kamel.image.asyncPainterResource
42 | import kotlinx.coroutines.launch
43 | import org.jetbrains.compose.resources.ExperimentalResourceApi
44 | import org.koin.compose.koinInject
45 | import presentation.components.CategoryCardTag
46 | import presentation.components.Gap
47 | import presentation.components.LoadingAnimation3
48 | import presentation.screens.category.SelectedCategoryScreen
49 | import presentation.screens.main.taps.home.HomeStateIntent
50 | import presentation.screens.main.taps.home.HomeViewModel
51 |
52 | object CategoryScreen : Screen {
53 |
54 | @Composable
55 | override fun Content() {
56 | val navigator = LocalNavigator.currentOrThrow
57 | mainContent(navigator)
58 | }
59 |
60 | @OptIn(ExperimentalResourceApi::class)
61 | @Composable
62 | private fun mainContent(
63 | navigator: Navigator? = null,
64 | ) {
65 | val viewModel: CategoriesViewModel = koinInject()
66 |
67 | val categories = viewModel.categories.collectAsState()
68 |
69 | /*val userToken = viewModel?.userToken?.value
70 | viewModel.setStateEvent(MainStateIntent.GettingToken)
71 | Text("Hello Your Token is : $userToken")*/
72 |
73 | val snackState = remember { SnackbarHostState() }
74 | val snackScope = rememberCoroutineScope()
75 |
76 | SnackbarHost(hostState = snackState, Modifier)
77 |
78 | fun launchSnackBar(message: String) {
79 | snackScope.launch { snackState.showSnackbar(message) }
80 | }
81 |
82 | categories.value.let { result ->
83 | when (result) {
84 | is Resource.Failure -> {
85 | launchSnackBar("some failures occurred")
86 | }
87 | Resource.Loading -> {
88 | Column(
89 | modifier = Modifier
90 | .fillMaxSize(1.0f),
91 | verticalArrangement = Arrangement.Center,
92 | horizontalAlignment = Alignment.CenterHorizontally
93 | ) {
94 | LoadingAnimation3()
95 | }
96 | }
97 |
98 | is Resource.Success -> {
99 | CategoryContent(
100 | navigator,
101 | result.result
102 | )
103 | }
104 |
105 | null -> {}
106 | }
107 | }
108 |
109 |
110 | }
111 |
112 |
113 | @Composable
114 | private fun CategoryContent(navigator: Navigator?,categories: List) {
115 | Column(
116 | modifier = Modifier
117 | .background(color = Color(0xFFF9F9F9))
118 | .padding(16.dp)
119 | ) {
120 | Text(
121 | "Choose category", style = TextStyle(
122 | fontSize = 14.sp,
123 | lineHeight = 20.sp,
124 | fontWeight = FontWeight(500),
125 | color = Color(0xFF9B9B9B),
126 | textAlign = TextAlign.Center,
127 | )
128 | )
129 | Gap(10.dp)
130 | LazyColumn {
131 | items(categories.size) {
132 | Card(
133 | elevation = 4.dp,
134 | modifier = Modifier.background(
135 | color = androidx.compose.ui.graphics.Color.White,
136 | shape = RoundedCornerShape(10.dp)
137 | ).clickable {
138 | navigator?.push(SelectedCategoryScreen(categories[it]))
139 | }
140 | .clip(
141 | shape = RoundedCornerShape(10.dp),
142 | )
143 | ) {
144 | Row(
145 | verticalAlignment = Alignment.CenterVertically,
146 | modifier = Modifier.height(100.dp).clip(
147 | shape = RoundedCornerShape(10.dp),
148 | ).background(color = Color.White, shape = RoundedCornerShape(10.dp))
149 | .clip(
150 | shape = RoundedCornerShape(10.dp),
151 | )
152 | ) {
153 | Text(
154 | categories[it].name?:"",
155 | modifier = Modifier.weight(1f).padding(start = 16.dp),
156 | style = TextStyle(
157 | fontSize = 18.sp,
158 | lineHeight = 22.sp,
159 | fontWeight = FontWeight(400),
160 | color = Color(0xFF222222),
161 | )
162 | )
163 | KamelImage(
164 | asyncPainterResource(categories[it].image?:""),
165 | categories[it].name,
166 | contentScale = ContentScale.FillWidth,
167 | modifier = Modifier.weight(1f)
168 | )
169 | }
170 | }
171 | Gap(10.dp)
172 | Spacer(modifier = Modifier.height(10.dp))
173 | }
174 | }
175 | }
176 | }
177 |
178 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/category/CategoryTab.kt:
--------------------------------------------------------------------------------
1 |
2 | package presentation.screens.main.taps.category
3 |
4 | import androidx.compose.animation.ExperimentalAnimationApi
5 | import androidx.compose.material.icons.Icons
6 | import androidx.compose.material.icons.filled.Menu
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
10 | import cafe.adriel.voyager.navigator.Navigator
11 | import cafe.adriel.voyager.navigator.tab.Tab
12 | import cafe.adriel.voyager.navigator.tab.TabOptions
13 | import cafe.adriel.voyager.transitions.SlideTransition
14 |
15 | object CategoryTab : Tab {
16 |
17 | override val options: TabOptions
18 | @Composable
19 | get() {
20 | val title = "Cart"
21 | val icon = rememberVectorPainter(Icons.Default.Menu)
22 |
23 | return remember {
24 | TabOptions(
25 | index = 0u,
26 | title = title,
27 | icon = icon
28 | )
29 | }
30 | }
31 |
32 | @Composable
33 | override fun Content() {
34 | Navigator(screen = CategoryScreen) { navigator ->
35 | SlideTransition(navigator = navigator)
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.home
2 |
3 |
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.PaddingValues
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.lazy.LazyRow
10 | import androidx.compose.foundation.lazy.grid.GridCells
11 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
12 | import androidx.compose.material.SnackbarHost
13 | import androidx.compose.material.SnackbarHostState
14 | import androidx.compose.material.Text
15 | import androidx.compose.runtime.Composable
16 | import androidx.compose.runtime.collectAsState
17 | import androidx.compose.runtime.remember
18 | import androidx.compose.runtime.rememberCoroutineScope
19 | import androidx.compose.ui.Alignment
20 | import androidx.compose.ui.Modifier
21 | import androidx.compose.ui.graphics.Color
22 | import androidx.compose.ui.text.TextStyle
23 | import androidx.compose.ui.text.font.FontWeight
24 | import androidx.compose.ui.text.style.TextAlign
25 | import androidx.compose.ui.unit.dp
26 | import androidx.compose.ui.unit.sp
27 | import cafe.adriel.voyager.core.screen.Screen
28 | import cafe.adriel.voyager.navigator.LocalNavigator
29 | import cafe.adriel.voyager.navigator.Navigator
30 | import cafe.adriel.voyager.navigator.currentOrThrow
31 | import data.network.Resource
32 | import kotlinx.coroutines.launch
33 | import org.koin.compose.koinInject
34 | import presentation.components.AppSlider
35 | import presentation.components.CategoryCardTag
36 | import presentation.components.LoadingAnimation3
37 | import presentation.components.ProductCard
38 | import presentation.screens.product.ProductDetailsScreen
39 |
40 | class HomeScreen : Screen {
41 |
42 | @Composable
43 | override fun Content() {
44 | val navigator: Navigator = LocalNavigator.currentOrThrow
45 | val viewModel: HomeViewModel = koinInject()
46 |
47 | val categories = viewModel.categories.collectAsState()
48 | val products = viewModel.products.collectAsState()
49 |
50 |
51 | val snackState = remember { SnackbarHostState() }
52 | val snackScope = rememberCoroutineScope()
53 |
54 | SnackbarHost(hostState = snackState, Modifier)
55 |
56 | fun launchSnackBar(message: String) {
57 | snackScope.launch { snackState.showSnackbar(message) }
58 | }
59 |
60 | Column {
61 | AppSlider()
62 |
63 | Text(
64 | modifier = Modifier.padding(10.dp, 0.dp, 0.dp, 0.dp),
65 | text = "Choose category", style = TextStyle(
66 | fontSize = 14.sp,
67 | lineHeight = 20.sp,
68 | fontWeight = FontWeight(500),
69 | color = Color(0xFF9B9B9B),
70 | textAlign = TextAlign.Center,
71 | )
72 | )
73 |
74 | categories.value.let { result ->
75 | when (result) {
76 | is Resource.Failure -> {
77 | launchSnackBar("some failures occurred")
78 | }
79 |
80 | Resource.Loading -> {
81 | Column(
82 | modifier = Modifier
83 | .fillMaxSize(1.0f),
84 | verticalArrangement = Arrangement.Center,
85 | horizontalAlignment = Alignment.CenterHorizontally
86 | ) {
87 | LoadingAnimation3()
88 | }
89 | }
90 |
91 | is Resource.Success -> {
92 | LazyRow {
93 | items(result.result.size) {
94 | var categoryItem = result.result[it]
95 | CategoryCardTag(categoryItem) {
96 | viewModel.setStateEvent(HomeStateIntent.SelectCategory(it.id?:0))
97 | }
98 | }
99 | }
100 | }
101 |
102 | null -> {}
103 | }
104 | }
105 |
106 | products.value.let { result ->
107 | when (result) {
108 | is Resource.Failure -> {
109 | launchSnackBar("some failures occurred")
110 | }
111 |
112 | Resource.Loading -> {
113 | Column(
114 | modifier = Modifier
115 | .fillMaxSize(1.0f),
116 | verticalArrangement = Arrangement.Center,
117 | horizontalAlignment = Alignment.CenterHorizontally
118 | ) {
119 | LoadingAnimation3()
120 | }
121 | }
122 |
123 | is Resource.Success -> {
124 | LazyVerticalGrid(
125 | modifier = Modifier,
126 | columns = GridCells.Adaptive(168.dp),
127 | contentPadding = PaddingValues(
128 | start = 12.dp,
129 | top = 2.dp,
130 | end = 12.dp,
131 | bottom = 16.dp
132 | ),
133 | content = {
134 | items(result.result.size) { index ->
135 | val product = result.result[index]
136 | ProductCard(
137 | product
138 | ) {
139 | navigator?.push(ProductDetailsScreen(product))
140 | }
141 | }
142 | }
143 | )
144 | }
145 |
146 | null -> {}
147 | }
148 | }
149 | }
150 |
151 |
152 | }
153 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeTab.kt:
--------------------------------------------------------------------------------
1 | @file:OptIn(ExperimentalAnimationApi::class)
2 |
3 | package presentation.screens.main.taps.home
4 |
5 | import androidx.compose.animation.ExperimentalAnimationApi
6 | import androidx.compose.material.icons.Icons
7 | import androidx.compose.material.icons.filled.Home
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.remember
10 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
11 | import cafe.adriel.voyager.navigator.Navigator
12 | import cafe.adriel.voyager.navigator.tab.Tab
13 | import cafe.adriel.voyager.navigator.tab.TabOptions
14 | import cafe.adriel.voyager.transitions.SlideTransition
15 |
16 | object HomeTab : Tab {
17 |
18 | override val options: TabOptions
19 | @Composable
20 | get() {
21 | val title = "Home"
22 | val icon = rememberVectorPainter(Icons.Default.Home)
23 |
24 | return remember {
25 | TabOptions(
26 | index = 0u,
27 | title = title,
28 | icon = icon
29 | )
30 | }
31 | }
32 |
33 | @Composable
34 | override fun Content() {
35 | Navigator(screen = HomeScreen()) { navigator ->
36 | SlideTransition(navigator = navigator)
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/home/HomeViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.home
2 |
3 | import data.model.Category
4 | import data.model.Product
5 | import data.network.Resource
6 | import domain.core.AppDataStore
7 | import domain.usecase.CategoryUseCase
8 | import domain.usecase.ProductUseCase
9 | import kotlinx.coroutines.flow.MutableStateFlow
10 | import kotlinx.coroutines.flow.StateFlow
11 | import kotlinx.coroutines.launch
12 | import presentation.base.BaseViewModel
13 | import presentation.base.BaseViewModel.AllStateEvent
14 |
15 |
16 | class HomeViewModel(
17 | private val categoryUseCase: CategoryUseCase,
18 | private val productUseCase: ProductUseCase,
19 | private val appDataStoreManager: AppDataStore,
20 | ) : BaseViewModel() {
21 |
22 |
23 | private val _categories = MutableStateFlow>?>(null)
24 | val categories: StateFlow>?> = _categories
25 |
26 | private val _products = MutableStateFlow>?>(null)
27 | val products: StateFlow>?> = _products
28 |
29 |
30 | init {
31 | setStateEvent(HomeStateIntent.GetCategories)
32 | }
33 |
34 | override fun setStateEvent(state: AllStateEvent) {
35 | when (state) {
36 | is HomeStateIntent.GetCategories -> {
37 | viewModelScope.launch {
38 | _categories.value = Resource.Loading
39 | _categories.value = categoryUseCase.invoke()
40 | when(_categories.value){
41 | is Resource.Failure -> {}
42 | Resource.Loading -> {}
43 | is Resource.Success -> {
44 | _products.value = productUseCase.getCategoryProducts(
45 | (_categories.value as Resource.Success>).result[0].id ?:0)
46 | }
47 | null -> {}
48 | }
49 | }
50 | }
51 |
52 | is HomeStateIntent.SelectCategory -> {
53 | viewModelScope.launch {
54 | _products.value = Resource.Loading
55 | _products.value = productUseCase.getCategoryProducts(state.categoryId)
56 | }
57 | }
58 | }
59 |
60 | }
61 | override fun setUiEvent(state: AllStateEvent) {
62 | }
63 |
64 | fun clearHomeStates() {
65 | viewModelScope.launch {
66 | _categories.emit(null)
67 | _products.emit(null)
68 | }
69 | }
70 | }
71 |
72 |
73 | sealed class HomeStateIntent : AllStateEvent() {
74 | object GetCategories : HomeStateIntent()
75 | data class SelectCategory(val categoryId: Int) : HomeStateIntent()
76 | }
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.profile
2 |
3 |
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.clickable
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Column
8 | import androidx.compose.foundation.layout.PaddingValues
9 | import androidx.compose.foundation.layout.Row
10 | import androidx.compose.foundation.layout.Spacer
11 | import androidx.compose.foundation.layout.fillMaxSize
12 | import androidx.compose.foundation.layout.fillMaxWidth
13 | import androidx.compose.foundation.layout.height
14 | import androidx.compose.foundation.layout.heightIn
15 | import androidx.compose.foundation.layout.padding
16 | import androidx.compose.foundation.layout.size
17 | import androidx.compose.foundation.layout.width
18 | import androidx.compose.foundation.lazy.LazyColumn
19 | import androidx.compose.foundation.lazy.LazyRow
20 | import androidx.compose.foundation.lazy.grid.GridCells
21 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
22 | import androidx.compose.foundation.rememberScrollState
23 | import androidx.compose.foundation.shape.CircleShape
24 | import androidx.compose.foundation.shape.RoundedCornerShape
25 | import androidx.compose.foundation.verticalScroll
26 | import androidx.compose.material.Card
27 | import androidx.compose.material.Icon
28 | import androidx.compose.material.SnackbarHost
29 | import androidx.compose.material.SnackbarHostState
30 | import androidx.compose.material.Text
31 | import androidx.compose.material.TextField
32 | import androidx.compose.material.TextFieldDefaults
33 | import androidx.compose.material.icons.Icons
34 | import androidx.compose.material.icons.filled.Edit
35 | import androidx.compose.material.icons.filled.Search
36 | import androidx.compose.runtime.Composable
37 | import androidx.compose.runtime.collectAsState
38 | import androidx.compose.runtime.getValue
39 | import androidx.compose.runtime.mutableStateOf
40 | import androidx.compose.runtime.remember
41 | import androidx.compose.runtime.rememberCoroutineScope
42 | import androidx.compose.runtime.setValue
43 | import androidx.compose.ui.Alignment
44 | import androidx.compose.ui.Modifier
45 | import androidx.compose.ui.graphics.Color
46 | import androidx.compose.ui.text.TextStyle
47 | import androidx.compose.ui.text.font.FontWeight
48 | import androidx.compose.ui.text.style.TextAlign
49 | import androidx.compose.ui.unit.dp
50 | import androidx.compose.ui.unit.sp
51 | import cafe.adriel.voyager.core.screen.Screen
52 | import cafe.adriel.voyager.navigator.LocalNavigator
53 | import cafe.adriel.voyager.navigator.Navigator
54 | import cafe.adriel.voyager.navigator.currentOrThrow
55 | import data.network.Resource
56 | import io.kamel.image.KamelImage
57 | import io.kamel.image.asyncPainterResource
58 | import kotlinx.coroutines.launch
59 | import org.koin.compose.koinInject
60 | import presentation.components.AppSlider
61 | import presentation.components.CategoryCardTag
62 | import presentation.components.CustomDialogSheet
63 | import presentation.components.LoadingAnimation3
64 | import presentation.components.ProductCard
65 | import presentation.components.ProfileSectionCard
66 | import presentation.screens.auth.updateProfile.UpdateProfileScreen
67 | import presentation.screens.product.ProductDetailsScreen
68 | import presentation.screens.settings.SettingsScreen
69 | import presentation.theme.gray2
70 | import utils.AppStrings
71 |
72 | class ProfileScreen : Screen {
73 |
74 | @Composable
75 | override fun Content() {
76 | val navigator :Navigator = LocalNavigator.currentOrThrow
77 | val viewModel: ProfileViewModel = koinInject()
78 |
79 | var showLogOutSheet by remember { mutableStateOf(false) }
80 | val logoutState = viewModel.logout.collectAsState()
81 |
82 | val nameState = viewModel.userName.value
83 | val emailState = viewModel.email.value
84 |
85 | Column(modifier = Modifier.padding(16.dp)) {
86 |
87 | Text(
88 | text = "Profile",
89 | style = TextStyle(
90 | fontSize = 24.sp,
91 | fontWeight = FontWeight(700),
92 | color = Color.Black,
93 |
94 | ),
95 | )
96 |
97 | Spacer(modifier = Modifier.height(30.dp))
98 |
99 | Row(
100 | modifier = Modifier
101 | .fillMaxWidth()
102 | .height(90.dp),
103 | horizontalArrangement = Arrangement.Center,
104 | verticalAlignment = Alignment.CenterVertically
105 | ) {
106 |
107 | Card(modifier = Modifier.size(80.dp), shape = CircleShape) {
108 | KamelImage(
109 | asyncPainterResource("https://i.ibb.co/cyP8x9m/my-passport-photo.jpg"),
110 | contentDescription = "profile_pic"
111 | )
112 | }
113 |
114 | Spacer(modifier = Modifier.width(10.dp))
115 |
116 | Column {
117 | Text(
118 | text = nameState?.text ?: "",
119 | style = TextStyle(
120 | fontSize = 16.sp,
121 | fontWeight = FontWeight(700),
122 | color = Color.Black,
123 | )
124 | )
125 |
126 | Spacer(modifier = Modifier.height(5.dp))
127 |
128 | Text(
129 | text = emailState?.text ?: "",
130 | style = TextStyle(
131 | fontSize = 14.sp,
132 | fontWeight = FontWeight(600),
133 | color = gray2,
134 | )
135 | )
136 |
137 | Spacer(modifier = Modifier.height(10.dp))
138 |
139 | Spacer(modifier = Modifier.weight(1f))
140 |
141 |
142 | }
143 |
144 |
145 |
146 | Spacer(modifier = Modifier.weight(1f))
147 |
148 | Icon(
149 | modifier = Modifier
150 | .clickable {
151 | navigator?.push(UpdateProfileScreen)
152 | }
153 | .size(24.dp),
154 | tint = Color.Black,
155 | imageVector = Icons.Default.Edit,
156 | contentDescription = ""
157 | )
158 | }
159 |
160 |
161 | Spacer(modifier = Modifier.height(30.dp))
162 |
163 | Column(
164 | Modifier.verticalScroll(rememberScrollState())
165 |
166 | ) {
167 | ProfileSectionCard({ }, title = "My Orders") {
168 | }
169 |
170 | ProfileSectionCard({ }, title = "Cart") {
171 |
172 | }
173 |
174 | ProfileSectionCard(
175 | { },
176 | title = "Settings"
177 | ) {
178 | navigator?.push(SettingsScreen)
179 | }
180 | ProfileSectionCard(
181 | {
182 | },
183 | color = Color.Red,
184 | withLine = false,
185 | title = "Logout"
186 | ) {
187 | showLogOutSheet = true
188 | }
189 |
190 | }
191 |
192 |
193 | if (showLogOutSheet) {
194 | CustomDialogSheet(
195 | title = AppStrings.log_out.stringValue,
196 | buttonText = AppStrings.log_out.stringValue,
197 | message = AppStrings.are_you_sure_you_want_log_out.stringValue,
198 | onAccept = {
199 | showLogOutSheet = false
200 | viewModel.setStateEvent(ProfileStateIntent.LogoutUser)
201 | },
202 | onReject = {
203 | showLogOutSheet = false
204 | })
205 | }
206 |
207 | }
208 |
209 | when (logoutState?.value) {
210 | true -> {
211 | navigator?.popUntilRoot()
212 | }
213 | null -> {}
214 | else -> {}
215 | }
216 | }
217 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileTab.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.profile
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.Arrangement
5 | import androidx.compose.foundation.layout.Column
6 | import androidx.compose.foundation.layout.Row
7 | import androidx.compose.foundation.layout.Spacer
8 | import androidx.compose.foundation.layout.fillMaxWidth
9 | import androidx.compose.foundation.layout.height
10 | import androidx.compose.foundation.layout.padding
11 | import androidx.compose.foundation.layout.size
12 | import androidx.compose.foundation.layout.width
13 | import androidx.compose.foundation.rememberScrollState
14 | import androidx.compose.foundation.shape.CircleShape
15 | import androidx.compose.foundation.verticalScroll
16 | import androidx.compose.material.Card
17 | import androidx.compose.material.Icon
18 | import androidx.compose.material.Text
19 | import androidx.compose.material.icons.Icons
20 | import androidx.compose.material.icons.filled.Edit
21 | import androidx.compose.material.icons.filled.Person
22 | import androidx.compose.runtime.Composable
23 | import androidx.compose.runtime.collectAsState
24 | import androidx.compose.runtime.getValue
25 | import androidx.compose.runtime.mutableStateOf
26 | import androidx.compose.runtime.remember
27 | import androidx.compose.runtime.setValue
28 | import androidx.compose.ui.Alignment
29 | import androidx.compose.ui.Modifier
30 | import androidx.compose.ui.graphics.Color
31 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
32 | import androidx.compose.ui.text.TextStyle
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.unit.dp
35 | import androidx.compose.ui.unit.sp
36 | import cafe.adriel.voyager.navigator.LocalNavigator
37 | import cafe.adriel.voyager.navigator.Navigator
38 | import cafe.adriel.voyager.navigator.currentOrThrow
39 | import cafe.adriel.voyager.navigator.tab.Tab
40 | import cafe.adriel.voyager.navigator.tab.TabOptions
41 | import cafe.adriel.voyager.transitions.SlideTransition
42 | import io.kamel.image.KamelImage
43 | import io.kamel.image.asyncPainterResource
44 | import org.koin.compose.koinInject
45 | import presentation.components.CustomDialogSheet
46 | import presentation.components.ProfileSectionCard
47 | import presentation.screens.main.taps.search.SearchScreen
48 | import presentation.screens.settings.SettingsScreen
49 | import presentation.theme.gray2
50 | import utils.AppStrings
51 |
52 |
53 | object ProfileTab : Tab {
54 |
55 | override val options: TabOptions
56 | @Composable
57 | get() {
58 | val icon = rememberVectorPainter(Icons.Default.Person)
59 |
60 | return remember {
61 | TabOptions(
62 | index = 3u,
63 | title = "Profile",
64 | icon = icon
65 | )
66 | }
67 | }
68 |
69 | @Composable
70 | override fun Content() {
71 | Navigator(screen = ProfileScreen()) { navigator ->
72 | SlideTransition(navigator = navigator)
73 | }
74 | }
75 |
76 |
77 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/profile/ProfileViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.profile
2 |
3 | import androidx.compose.runtime.State
4 | import androidx.compose.runtime.mutableStateOf
5 | import data.model.TextFieldState
6 | import data.model.response.RegisterResponse
7 | import data.network.Resource
8 | import domain.core.AppDataStore
9 | import domain.usecase.GetProfileUseCase
10 | import kotlinx.coroutines.async
11 | import kotlinx.coroutines.flow.MutableStateFlow
12 | import kotlinx.coroutines.flow.StateFlow
13 |
14 | import kotlinx.coroutines.launch
15 | import presentation.base.BaseViewModel
16 | import presentation.base.BaseViewModel.AllStateEvent
17 | import presentation.base.DataStoreKeys
18 | import presentation.screens.auth.updateProfile.UpdateProfileStateIntent
19 | import presentation.screens.auth.updateProfile.UpdateProfileUIEvent
20 |
21 |
22 | class ProfileViewModel(
23 | private val appDataStoreManager: AppDataStore,
24 | private val getProfileUseCase: GetProfileUseCase,
25 | ) : BaseViewModel() {
26 |
27 |
28 | private val _logout = MutableStateFlow(null)
29 | val logout: StateFlow = _logout
30 |
31 | private var userProfile:RegisterResponse?=null
32 |
33 | private val _nameState = mutableStateOf(
34 | TextFieldState(
35 | text = "",
36 | hint = "Enter your Name",
37 | isHintVisible = false,
38 | )
39 | )
40 | val userName: State = _nameState
41 |
42 | private val _emailState = mutableStateOf(
43 | TextFieldState(
44 | text = "",
45 | hint = "Enter your valid email",
46 | isHintVisible = false,
47 | )
48 | )
49 | val email: State = _emailState
50 |
51 |
52 | init {
53 | setStateEvent(ProfileStateIntent.GettingProfile)
54 | }
55 | override fun setStateEvent(state: AllStateEvent) {
56 | when (state) {
57 | is ProfileStateIntent.LogoutUser -> {
58 | viewModelScope.launch {
59 | async {
60 | appDataStoreManager.setValue(DataStoreKeys.TOKEN,"")
61 | }.await()
62 | _logout.emit(true)
63 |
64 |
65 | }
66 | }
67 | is ProfileStateIntent.GettingProfile -> {
68 | viewModelScope.launch {
69 | val response = getProfileUseCase.invoke()
70 | when(getProfileUseCase.invoke()){
71 | is Resource.Failure -> {}
72 | Resource.Loading -> {}
73 | is Resource.Success -> {
74 | userProfile = (response as Resource.Success).result
75 | userProfile?.let {
76 | _nameState.value = userName.value.copy(
77 | text = userProfile?.name?:""
78 | )
79 | _emailState.value = email.value.copy(
80 | text = userProfile?.email?:""
81 | )
82 | }
83 | }
84 | }
85 | }
86 | }
87 | }
88 |
89 | }
90 |
91 | override fun setUiEvent(state: AllStateEvent) {
92 | }
93 |
94 | override fun onCleared() {
95 | super.onCleared()
96 | // viewModelScope.launch {
97 | // _login.emit(null)
98 | // }
99 | }
100 | }
101 |
102 |
103 |
104 | sealed class ProfileStateIntent : AllStateEvent() {
105 | object LogoutUser : ProfileStateIntent()
106 | object GettingProfile : ProfileStateIntent()
107 |
108 |
109 | }
110 |
111 |
112 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.search
2 |
3 |
4 | import androidx.compose.foundation.background
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.PaddingValues
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.fillMaxWidth
10 | import androidx.compose.foundation.layout.heightIn
11 | import androidx.compose.foundation.layout.padding
12 | import androidx.compose.foundation.lazy.LazyColumn
13 | import androidx.compose.foundation.lazy.LazyRow
14 | import androidx.compose.foundation.lazy.grid.GridCells
15 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
16 | import androidx.compose.foundation.shape.RoundedCornerShape
17 | import androidx.compose.material.Icon
18 | import androidx.compose.material.SnackbarHost
19 | import androidx.compose.material.SnackbarHostState
20 | import androidx.compose.material.Text
21 | import androidx.compose.material.TextField
22 | import androidx.compose.material.TextFieldDefaults
23 | import androidx.compose.material.icons.Icons
24 | import androidx.compose.material.icons.filled.Search
25 | import androidx.compose.runtime.Composable
26 | import androidx.compose.runtime.collectAsState
27 | import androidx.compose.runtime.remember
28 | import androidx.compose.runtime.rememberCoroutineScope
29 | import androidx.compose.ui.Alignment
30 | import androidx.compose.ui.Modifier
31 | import androidx.compose.ui.graphics.Color
32 | import androidx.compose.ui.text.TextStyle
33 | import androidx.compose.ui.text.font.FontWeight
34 | import androidx.compose.ui.text.style.TextAlign
35 | import androidx.compose.ui.unit.dp
36 | import androidx.compose.ui.unit.sp
37 | import cafe.adriel.voyager.core.screen.Screen
38 | import cafe.adriel.voyager.navigator.LocalNavigator
39 | import cafe.adriel.voyager.navigator.Navigator
40 | import cafe.adriel.voyager.navigator.currentOrThrow
41 | import data.network.Resource
42 | import kotlinx.coroutines.launch
43 | import org.koin.compose.koinInject
44 | import presentation.components.AppSlider
45 | import presentation.components.CategoryCardTag
46 | import presentation.components.LoadingAnimation3
47 | import presentation.components.ProductCard
48 | import presentation.screens.product.ProductDetailsScreen
49 |
50 | class SearchScreen : Screen {
51 |
52 | @Composable
53 | override fun Content() {
54 | val viewModel: SearchViewModel = koinInject()
55 | val products = viewModel.products.collectAsState()
56 | val snackState = remember { SnackbarHostState() }
57 | val snackScope = rememberCoroutineScope()
58 | val navigator: Navigator = LocalNavigator.currentOrThrow
59 |
60 | SnackbarHost(hostState = snackState, Modifier)
61 |
62 | fun launchSnackBar(message: String) {
63 | snackScope.launch { snackState.showSnackbar(message) }
64 | }
65 |
66 | LazyColumn {
67 | item {
68 | TextField(
69 | leadingIcon = {
70 | Icon(Icons.Default.Search, "Search")
71 | },
72 | label = {
73 | Text(
74 | text = "What Are you looking for ?",
75 | textAlign = TextAlign.Center,
76 | style = TextStyle(
77 | color = Color(0xFFADB3DA),
78 | )
79 | )
80 | },
81 | value = "", onValueChange = {},
82 | colors = TextFieldDefaults.outlinedTextFieldColors(
83 | textColor = Color(0xFFADB3DA),
84 | disabledTextColor = Color.Transparent,
85 | backgroundColor = Color(0xFFEFF1F8),
86 | focusedBorderColor = Color.Transparent,
87 | unfocusedBorderColor = Color.Transparent,
88 | disabledBorderColor = Color.Transparent,
89 | ),
90 | shape = RoundedCornerShape(10.dp),
91 | modifier = Modifier
92 | .padding(16.dp)
93 | .fillMaxWidth()
94 | .background(Color.White, shape = RoundedCornerShape(10.dp)),
95 |
96 | )
97 | }
98 |
99 | item {
100 | products.value.let { result ->
101 | when (result) {
102 | is Resource.Failure -> {
103 | launchSnackBar("some failures occurred")
104 | }
105 |
106 | Resource.Loading -> {
107 | Column(
108 | modifier = Modifier
109 | .fillMaxSize(1.0f),
110 | verticalArrangement = Arrangement.Center,
111 | horizontalAlignment = Alignment.CenterHorizontally
112 | ) {
113 | LoadingAnimation3()
114 | }
115 | }
116 |
117 | is Resource.Success -> {
118 | LazyVerticalGrid(
119 | columns = GridCells.Adaptive(minSize = 150.dp),
120 | modifier = Modifier.heightIn(max = 800.dp)
121 | ) {
122 | items(result.result.size) { index ->
123 | val product = result.result[index]
124 | ProductCard(
125 | product
126 | ) {
127 | navigator.push(ProductDetailsScreen(product))
128 | }
129 | }
130 | }
131 | }
132 | null -> {}
133 | }
134 | }
135 | }
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchTab.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.search
2 |
3 |
4 | import androidx.compose.material.icons.Icons
5 | import androidx.compose.material.icons.filled.Search
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.remember
8 | import androidx.compose.ui.graphics.vector.rememberVectorPainter
9 | import cafe.adriel.voyager.navigator.Navigator
10 | import cafe.adriel.voyager.navigator.tab.Tab
11 | import cafe.adriel.voyager.navigator.tab.TabOptions
12 | import cafe.adriel.voyager.transitions.SlideTransition
13 |
14 |
15 | object SearchTab : Tab {
16 |
17 | override val options: TabOptions
18 | @Composable
19 | get() {
20 | val icon = rememberVectorPainter(Icons.Default.Search)
21 |
22 | return remember {
23 | TabOptions(
24 | index = 2u,
25 | title = "Search",
26 | icon = icon
27 | )
28 | }
29 | }
30 |
31 | @Composable
32 | override fun Content() {
33 | Navigator(screen = SearchScreen()) { navigator ->
34 | SlideTransition(navigator = navigator)
35 | }
36 | }
37 |
38 |
39 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/main/taps/search/SearchViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.main.taps.search
2 |
3 | import data.model.Product
4 | import data.network.Resource
5 | import domain.usecase.ProductUseCase
6 | import kotlinx.coroutines.flow.MutableStateFlow
7 | import kotlinx.coroutines.flow.StateFlow
8 | import kotlinx.coroutines.launch
9 | import presentation.base.BaseViewModel
10 | import presentation.base.BaseViewModel.AllStateEvent
11 |
12 |
13 | class SearchViewModel(
14 | private val productUseCase: ProductUseCase,
15 | ) : BaseViewModel() {
16 |
17 |
18 | private val _products = MutableStateFlow>?>(null)
19 | val products: StateFlow>?> = _products
20 |
21 |
22 | init {
23 | setStateEvent(SearchStateIntent.GetAllProducts)
24 | }
25 |
26 | override fun setStateEvent(state: AllStateEvent) {
27 | when (state) {
28 | is SearchStateIntent.GetAllProducts -> {
29 | viewModelScope.launch {
30 | _products.value = Resource.Loading
31 | _products.value = productUseCase.getAllProducts()
32 | }
33 | }
34 | }
35 |
36 | }
37 |
38 | override fun setUiEvent(state: AllStateEvent) {
39 | }
40 |
41 | fun clearHomeStates() {
42 | viewModelScope.launch {
43 | _products.emit(null)
44 | }
45 | }
46 | }
47 |
48 |
49 | sealed class SearchStateIntent : AllStateEvent() {
50 | object GetAllProducts : SearchStateIntent()
51 | }
52 |
53 |
54 |
55 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/splash/SplashScreen.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.splash
2 |
3 | import androidx.compose.animation.core.Animatable
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.Canvas
6 | import androidx.compose.foundation.Image
7 | import androidx.compose.foundation.background
8 | import androidx.compose.foundation.layout.fillMaxSize
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.foundation.layout.width
11 | import androidx.compose.material.Surface
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.runtime.LaunchedEffect
14 | import androidx.compose.runtime.collectAsState
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.ui.Modifier
17 | import androidx.compose.ui.graphics.ColorFilter
18 | import androidx.compose.ui.graphics.drawscope.withTransform
19 | import androidx.compose.ui.layout.ContentScale
20 | import androidx.compose.ui.unit.dp
21 | import cafe.adriel.voyager.core.screen.Screen
22 | import cafe.adriel.voyager.navigator.LocalNavigator
23 | import cafe.adriel.voyager.navigator.Navigator
24 | import cafe.adriel.voyager.navigator.currentOrThrow
25 | import kotlinx.coroutines.delay
26 | import kotlinx.coroutines.launch
27 | import org.jetbrains.compose.resources.ExperimentalResourceApi
28 | import org.jetbrains.compose.resources.painterResource
29 | import org.koin.compose.koinInject
30 | import presentation.screens.auth.login.LoginScreen
31 | import presentation.screens.main.MainScreen
32 | import presentation.theme.BOLD_SILVER_BACKGROUND_COLOR
33 | import presentation.theme.PrimaryColor
34 | import presentation.theme.SPLASH_ANIMATED_BG_COLOR
35 |
36 | class SplashScreen : Screen {
37 | @Composable
38 | override fun Content() {
39 | val navigator = LocalNavigator.currentOrThrow
40 | SplashScreenContent(navigator)
41 | }
42 | }
43 |
44 |
45 | @Composable
46 | fun SplashScreenContent(navigator: Navigator? = null,
47 | viewModel:SplashViewModel = koinInject()) {
48 |
49 |
50 | val isLogin = viewModel?.isLogin?.collectAsState()
51 |
52 | val scale = remember {
53 | Animatable(0f)
54 | }
55 |
56 | LaunchedEffect(key1 = true, block = {
57 | scale.animateTo(
58 | targetValue = 0.9f,
59 | animationSpec = tween(
60 | durationMillis = 1500,
61 | )
62 | )
63 |
64 | delay(10L)
65 | when(isLogin?.value){
66 | true->{
67 | navigator?.push(MainScreen)
68 | }
69 | false->{
70 | navigator?.push(LoginScreen)
71 | }
72 | else->{}
73 | }
74 | })
75 | SplashAnimationWithContent()
76 | }
77 |
78 |
79 | @OptIn(ExperimentalResourceApi::class)
80 | @Composable
81 | fun SplashAnimationWithContent() {
82 | val logo = painterResource("compose-multiplatform.xml")
83 | val angle1Y = remember {
84 | Animatable(20f)
85 | }
86 | val angle2 = remember {
87 | Animatable(20f)
88 | }
89 | LaunchedEffect(angle1Y, angle2) {
90 | launch {
91 | angle1Y.animateTo(180f, animationSpec = tween(1500))
92 | }
93 | launch {
94 | angle2.animateTo(180f, animationSpec = tween(1500))
95 | }
96 | }
97 |
98 | Surface(Modifier.fillMaxSize()) {
99 | Canvas(modifier = Modifier
100 | .fillMaxSize()
101 | .background(SPLASH_ANIMATED_BG_COLOR),
102 | onDraw = {
103 | withTransform({
104 | // translate(angle1Y.value)
105 | scale(scaleX = angle1Y.value, scaleY = angle2.value)
106 |
107 | }) {
108 | drawCircle(color = BOLD_SILVER_BACKGROUND_COLOR, radius = 8f)
109 | }
110 | }
111 | )
112 | Image(
113 | modifier = Modifier
114 | .width(40.dp)
115 | .padding(horizontal = 96.dp),
116 | contentScale = ContentScale.Fit,
117 | painter = logo,
118 | colorFilter = ColorFilter.tint(PrimaryColor),
119 | contentDescription = null
120 | )
121 | }
122 |
123 | }
124 |
125 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/screens/splash/SplashViewModel.kt:
--------------------------------------------------------------------------------
1 | package presentation.screens.splash
2 |
3 | import domain.core.AppDataStore
4 | import kotlinx.coroutines.flow.MutableStateFlow
5 | import kotlinx.coroutines.flow.StateFlow
6 | import kotlinx.coroutines.flow.asStateFlow
7 | import kotlinx.coroutines.launch
8 | import presentation.base.BaseViewModel
9 | import presentation.base.BaseViewModel.AllStateEvent
10 | import presentation.base.DataStoreKeys
11 |
12 |
13 | class SplashViewModel(
14 | private val appDataStoreManager: AppDataStore,
15 | ) : BaseViewModel() {
16 |
17 | private val _isLogin = MutableStateFlow(null)
18 | val isLogin: StateFlow = _isLogin.asStateFlow()
19 |
20 |
21 | init {
22 | viewModelScope.launch {
23 | val token = appDataStoreManager.readValue(DataStoreKeys.TOKEN)?:""
24 | _isLogin.value = token.isNotBlank()
25 | }
26 | }
27 |
28 | override fun setStateEvent(state: AllStateEvent) {
29 | }
30 |
31 | override fun setUiEvent(state: AllStateEvent) {
32 | }
33 |
34 | }
35 |
36 |
37 | sealed class MainStateIntent : AllStateEvent() {
38 | object GettingToken : MainStateIntent()
39 | }
40 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
12 | val BOLD_SILVER_BACKGROUND_COLOR = Color(0xFFF7F5EF)
13 | val SPLASH_ANIMATED_BG_COLOR = Color(0xFFEBEBEB)
14 | val DarkPurple = Color(0xFFC41406)
15 | val Purple200 = Color(0xFF93448C)
16 | val blackTextColor = Color(0xFF101010)
17 | val blackTextColorLight = Color(0xFF1E1E1E)
18 | val gray2 = Color(0xFF8E8EA9)
19 | val yellow = Color(0xFFFFBF51)
20 | val Gold = Color(0xFFFFBF51)
21 | val grayTextColor = Color(0xFF8E8EA9)
22 | val grayBackground = Color(0xFFF0F0F6)
23 | val BorderColor = Color(0xFFE9E8F8)
24 | val PrimaryColor = Color(0xFFDB3022)
25 | val textColorSemiBlack = Color(0xFF222222)
26 |
27 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/theme/Dimens.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.ui.unit.dp
4 |
5 | val keyLine0 = 2.dp
6 | val keyLine1 = 4.dp
7 | val keyLine2 = 8.dp
8 | val keyLine3 = 16.dp
9 | val keyLine4 = 18.dp
10 | val bottomAvoidFloat = 76.dp
11 |
12 | val AppBarSize = 50.dp
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/presentation/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package presentation.theme
2 |
3 | import androidx.compose.material.Typography
4 | import androidx.compose.ui.graphics.Color
5 | import androidx.compose.ui.text.TextStyle
6 | import androidx.compose.ui.text.font.Font
7 | import androidx.compose.ui.text.font.FontFamily
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.text.style.TextAlign
10 | import androidx.compose.ui.unit.sp
11 |
12 | // Set of Material typography styles to start with
13 |
14 |
15 | //val fontFamily: FontFamily = fontFamilyResource(MR.fonts.)
16 |
17 | //val somarBoldFont = fontFamilyResource(
18 | // MR.fonts.som
19 | //)
20 | //val somarSemiBold = FontFamily(
21 | // Font(R.font.somar_semibold)
22 | //)
23 | //val somarRegular = FontFamily(
24 | // Font(R.font.somar_regular)
25 | //)
26 |
27 | val Typography = Typography(
28 | body1 = TextStyle(
29 | // fontFamily = somarRegular,
30 | fontWeight = FontWeight.Normal,
31 | fontSize = 20.sp,
32 | ),
33 | body2 = TextStyle(
34 | // fontFamily = somarRegular,
35 | fontWeight = FontWeight.Normal,
36 | fontSize = 20.sp,
37 | ),
38 | h5 = TextStyle(
39 | color = Color.Black,
40 | // fontFamily = somarBoldFont,
41 | fontWeight = FontWeight.ExtraBold,
42 | fontSize = 24.sp,
43 | ),
44 | h4 = TextStyle(
45 | color = textColorSemiBlack,
46 | // fontFamily = somarSemiBold,
47 | fontWeight = FontWeight.Bold,
48 | fontSize = 20.sp,
49 | textAlign = TextAlign.Center
50 | ),
51 | h3 = TextStyle(
52 | color = textColorSemiBlack,
53 | // fontFamily = somarRegular,
54 | fontWeight = FontWeight.Bold,
55 | fontSize = 14.sp,
56 | textAlign = TextAlign.Center
57 | ),
58 | h2 = TextStyle(
59 | color = textColorSemiBlack,
60 | // fontFamily = somarRegular,
61 | fontWeight = FontWeight.Bold,
62 | fontSize = 16.sp,
63 | textAlign = TextAlign.Center
64 | )
65 |
66 | /* Other default text styles to override
67 | button = TextStyle(
68 | fontFamily = FontFamily.Default,
69 | fontWeight = FontWeight.W500,
70 | fontSize = 14.sp
71 | ),
72 | caption = TextStyle(
73 | fontFamily = FontFamily.Default,
74 | fontWeight = FontWeight.Normal,
75 | fontSize = 12.sp
76 | )
77 | */
78 | )
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/utils/AppStrings.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | enum class AppStrings(val stringValue: String) {
4 | Login("Login"),
5 | register("Register"),
6 | update_profile("Update Profile"),
7 | PhoneNumber("Phone Number"),
8 | Password("Password"),
9 | Email("Email"),
10 | Username("User Name"),
11 | LoginDescription("login by email and password"),
12 | register_description("register your information to login"),
13 | update_profile_description("update your profile information"),
14 | DontHaveAccount("Don't Have an account ?"),
15 | RegitserNewAccount("Register New Account"),
16 | have_account("Have an Account?"),
17 | PasswordValidation("password must be upper than 6 char"),
18 | email_validation("please enter valid email to login"),
19 | user_name_validation("please enter valid user name"),
20 | yes("Yes"),
21 | cancel("Cancel"),
22 | log_out("Logout"),
23 | are_you_sure_you_want_log_out("Are you sure you want log out?")
24 | }
25 |
--------------------------------------------------------------------------------
/shared/src/commonMain/kotlin/utils/CommonUtil.kt:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import kotlinx.serialization.json.Json
4 |
5 | object CommonUtil {
6 | fun isValidEmail(email: String): Boolean {
7 | val emailPattern = Regex("^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$")
8 | return emailPattern.matches(email)
9 | }
10 |
11 | inline fun jsonToObject(jsonString: String): T {
12 | return Json.decodeFromString(jsonString)
13 | }
14 |
15 |
16 | }
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/arrow_right.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
11 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/banner1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner1.png
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/banner2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner2.png
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/banner3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/banner3.png
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/compose-multiplatform.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
18 |
24 |
30 |
36 |
37 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/flag.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
9 |
13 |
14 |
17 |
21 |
25 |
28 |
31 |
34 |
37 |
40 |
43 |
46 |
49 |
52 |
55 |
58 |
61 |
64 |
65 |
66 |
67 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/ic_arrow_down.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
14 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/not_found.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/BasemNasr/StoreKMP/efe9a8fe5bedcc2801474d4e85f4f9ba710160ae/shared/src/commonMain/resources/not_found.png
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/visibility.xml:
--------------------------------------------------------------------------------
1 |
6 |
10 |
14 |
15 |
--------------------------------------------------------------------------------
/shared/src/commonMain/resources/visibility_off.xml:
--------------------------------------------------------------------------------
1 |
6 |
13 |
20 |
27 |
28 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/core/Context.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import platform.darwin.NSObject
4 |
5 | actual typealias Context = NSObject
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/core/DataStore.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import platform.Foundation.NSUserDefaults
4 | import kotlinx.coroutines.flow.MutableSharedFlow
5 |
6 |
7 | actual suspend fun Context.putData(key: String, `object`: String) {
8 | val sharedFlow = MutableSharedFlow()
9 | NSUserDefaults.standardUserDefaults().setObject(`object`, key)
10 | sharedFlow.emit(`object`)
11 | }
12 |
13 | actual suspend inline fun Context.getData(key: String): String? {
14 | return NSUserDefaults.standardUserDefaults().stringForKey(key)
15 | }
16 |
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/core/platformModule.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import io.ktor.client.engine.darwin.Darwin
4 | import org.koin.dsl.module
5 |
6 | actual fun platformModule() = module {
7 | single { Darwin.create() }
8 |
9 | }
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/core/viewModelDefinition.kt:
--------------------------------------------------------------------------------
1 | package core
2 |
3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
4 | import org.koin.core.definition.Definition
5 | import org.koin.core.definition.KoinDefinition
6 | import org.koin.core.module.Module
7 | import org.koin.core.qualifier.Qualifier
8 |
9 |
10 | inline fun Module.viewModelDefinition(
11 | qualifier: Qualifier?,
12 | noinline definition: Definition,
13 | ): KoinDefinition = factory(qualifier = qualifier, definition = definition)
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/di/Utils.kt:
--------------------------------------------------------------------------------
1 | package di
2 |
3 | import dev.icerock.moko.mvvm.viewmodel.ViewModel
4 | import org.koin.core.definition.Definition
5 | import org.koin.core.definition.KoinDefinition
6 | import org.koin.core.module.Module
7 | import org.koin.core.qualifier.Qualifier
8 |
9 | actual inline fun Module.viewModelDefinition(
10 | qualifier: Qualifier?,
11 | noinline definition: Definition,
12 | ): KoinDefinition = factory(qualifier = qualifier, definition = definition)
--------------------------------------------------------------------------------
/shared/src/iosMain/kotlin/main.ios.kt:
--------------------------------------------------------------------------------
1 | import androidx.compose.ui.window.ComposeUIViewController
2 | import core.Context
3 |
4 |
5 | actual fun getPlatformName(): String = "iOS"
6 |
7 | fun MainViewController() = ComposeUIViewController { App(Context()) }
--------------------------------------------------------------------------------