├── .fleet ├── run.json └── settings.json ├── .github ├── FUNDING.yml └── workflows │ └── basic.yml ├── .gitignore ├── LICENSE ├── README.md ├── backend ├── .gitignore ├── .run │ ├── Adoptme [desktopRun].run.xml │ └── Server.run.xml ├── Dockerfile ├── README.md ├── build.gradle.kts ├── docs │ ├── .swagger-codegen-ignore │ ├── .swagger-codegen │ │ └── VERSION │ └── index.html └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── multiplatformkickstarter │ │ │ ├── Application.kt │ │ │ ├── AuthenticationConfig.kt │ │ │ ├── GeneralRoutingConfig.kt │ │ │ ├── SessionsConfig.kt │ │ │ ├── auth │ │ │ ├── Auth.kt │ │ │ ├── JwtService.kt │ │ │ └── UserSession.kt │ │ │ ├── models │ │ │ └── DatabaseUser.kt │ │ │ ├── plugins │ │ │ ├── Administration.kt │ │ │ ├── HTTP.kt │ │ │ ├── Monitoring.kt │ │ │ ├── Serialization.kt │ │ │ └── Templating.kt │ │ │ ├── repository │ │ │ ├── DatabaseFactory.kt │ │ │ ├── pets │ │ │ │ ├── Pets.kt │ │ │ │ ├── PetsRepository.kt │ │ │ │ └── PetsRepositoryImp.kt │ │ │ ├── profile │ │ │ │ ├── ProfileRepository.kt │ │ │ │ ├── ProfileRepositoryImpl.kt │ │ │ │ └── Profiles.kt │ │ │ └── user │ │ │ │ ├── UserRepository.kt │ │ │ │ ├── UserRepositoryImp.kt │ │ │ │ └── Users.kt │ │ │ └── routes │ │ │ ├── PetsRoutes.kt │ │ │ ├── ProfileRoutes.kt │ │ │ └── UserRoute.kt │ └── resources │ │ ├── logback.xml │ │ └── openapi │ │ └── documentation.yaml │ └── test │ └── kotlin │ └── com │ └── multiplatformkickstarter │ └── ApplicationTest.kt ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── kotlin │ │ └── com │ │ │ └── multiplatformkickstarter │ │ │ └── app │ │ │ └── android │ │ │ ├── MainActivity.kt │ │ │ ├── MainActivityUiState.kt │ │ │ ├── MultiplatformKickstarterApplication.kt │ │ │ ├── SplashActivity.kt │ │ │ └── core │ │ │ └── di │ │ │ └── DependencyContainer.kt │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── authentication_rafiki.xml │ │ ├── bg_splash.xml │ │ ├── cat_dog.xml │ │ ├── ethnic_friendship_pana.xml │ │ ├── features_overview_cuate.xml │ │ ├── ic_launcher_background.xml │ │ ├── mind_map.xml │ │ ├── mind_map_cuate.xml │ │ ├── mklogo.xml │ │ ├── no_data_cuate.xml │ │ ├── product_quality_amico.xml │ │ ├── reminders_pana.xml │ │ └── world_amico.xml │ │ ├── font │ │ ├── poppins_black.ttf │ │ ├── poppins_black_italic.ttf │ │ ├── poppins_bold.ttf │ │ ├── poppins_bold_italic.ttf │ │ ├── poppins_extra_bold.ttf │ │ ├── poppins_extra_bold_italic.ttf │ │ ├── poppins_extra_light.ttf │ │ ├── poppins_extra_light_italic.ttf │ │ ├── poppins_italic.ttf │ │ ├── poppins_light.ttf │ │ ├── poppins_light_italic.ttf │ │ ├── poppins_medium.ttf │ │ ├── poppins_medium_italic.ttf │ │ ├── poppins_regular.ttf │ │ ├── poppins_semi_bold.ttf │ │ ├── poppins_semi_bold_italic.ttf │ │ ├── poppins_thin.ttf │ │ └── poppins_thin_italic.ttf │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_round.png │ │ ├── values-land │ │ └── dimens.xml │ │ ├── values-w1240dp │ │ └── dimens.xml │ │ ├── values-w600dp │ │ └── dimens.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── placeholder_list.xml │ │ ├── strings.xml │ │ ├── strings_placeholder.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ └── data_extraction_rules.xml │ ├── commonMain │ ├── composeResources │ │ └── drawable │ │ │ └── compose-multiplatform.xml │ └── kotlin │ │ └── App.kt │ ├── desktopMain │ └── kotlin │ │ └── Main.kt │ ├── iosMain │ └── kotlin │ │ └── MainViewController.kt │ └── wasmJsMain │ ├── kotlin │ └── main.kt │ └── resources │ ├── index.html │ └── styles.css ├── config ├── detekt-config.yml └── images │ ├── multiplatform-kickstarter-logo.png │ └── multiplatform-kickstarter-screenshot.png ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── iosApp.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── iosApp.xcscheme └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ ├── Contents.json │ ├── MKLogo.imageset │ │ ├── Contents.json │ │ ├── MKLogo 1.png │ │ ├── MKLogo 2.png │ │ └── MKLogo.png │ └── mind_map.imageset │ │ ├── Contents.json │ │ └── mind_map.svg │ ├── ContentView.swift │ ├── Info.plist │ ├── LaunchScreen.storyboard │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iOSApp.swift ├── settings.gradle.kts └── shared ├── build.gradle.kts └── src ├── androidMain ├── drawable │ ├── bg_splash.xml │ ├── ethnic_friendship_pana.xml │ ├── ic_launcher_background.xml │ ├── mind_map.xml │ ├── mind_map_cuate.xml │ ├── mklogo.xml │ ├── no_data_cuate.xml │ ├── reminders_pana.xml │ └── world_amico.xml ├── kotlin │ └── com │ │ └── multiplatformkickstarter │ │ └── app │ │ ├── feature │ │ └── debugmenu │ │ │ └── Debug.android.kt │ │ ├── localization │ │ └── Localization.android.kt │ │ └── platform │ │ └── Resources.kt └── resources │ └── drawable │ ├── authentication_rafiki.xml │ ├── bg_splash.xml │ ├── cat_dog.xml │ ├── ethnic_friendship_pana.xml │ ├── features_overview_cuate.xml │ ├── ic_launcher_background.xml │ ├── mind_map.xml │ ├── mind_map_cuate.xml │ ├── mklogo.xml │ ├── no_data_cuate.xml │ ├── product_quality_amico.xml │ ├── reminders_pana.xml │ └── world_amico.xml ├── commonMain ├── composeResources │ ├── drawable │ │ ├── authentication_rafiki.xml │ │ ├── bg_splash.xml │ │ ├── cat_dog.xml │ │ ├── ethnic_friendship_pana.xml │ │ ├── features_overview_cuate.xml │ │ ├── ic_launcher_background.xml │ │ ├── mind_map.xml │ │ ├── mind_map_cuate.xml │ │ ├── mklogo.xml │ │ ├── no_data_cuate.xml │ │ ├── product_quality_amico.xml │ │ ├── reminders_pana.xml │ │ └── world_amico.xml │ └── font │ │ ├── poppins_black.ttf │ │ ├── poppins_black_italic.ttf │ │ ├── poppins_bold.ttf │ │ ├── poppins_bold_italic.ttf │ │ ├── poppins_extra_bold.ttf │ │ ├── poppins_extra_bold_italic.ttf │ │ ├── poppins_extra_light.ttf │ │ ├── poppins_extra_light_italic.ttf │ │ ├── poppins_italic.ttf │ │ ├── poppins_light.ttf │ │ ├── poppins_light_italic.ttf │ │ ├── poppins_medium.ttf │ │ ├── poppins_medium_italic.ttf │ │ ├── poppins_regular.ttf │ │ ├── poppins_semi_bold.ttf │ │ ├── poppins_semi_bold_italic.ttf │ │ ├── poppins_thin.ttf │ │ └── poppins_thin_italic.ttf ├── kotlin │ └── com │ │ └── multiplatformkickstarter │ │ └── app │ │ ├── MainApp.kt │ │ ├── common │ │ ├── model │ │ │ ├── GeoLocation.kt │ │ │ ├── PetCategory.kt │ │ │ ├── PetModel.kt │ │ │ ├── Profile.kt │ │ │ ├── SearchModel.kt │ │ │ ├── User.kt │ │ │ ├── UserData.kt │ │ │ └── theme │ │ │ │ ├── DarkThemeConfig.kt │ │ │ │ └── ThemeBrand.kt │ │ └── usecases │ │ │ └── UseCase.kt │ │ ├── data │ │ ├── repositories │ │ │ ├── AuthenticationRepository.kt │ │ │ ├── LastSearchAdsMockRepository.kt │ │ │ ├── NearMeAdsMockRepository.kt │ │ │ ├── ProfileRepository.kt │ │ │ └── SessionRepository.kt │ │ └── usecases │ │ │ ├── GetLastSearchUseCase.kt │ │ │ ├── GetNearMeAdsUseCase.kt │ │ │ ├── GetSearchUseCase.kt │ │ │ └── LoadProfileUseCase.kt │ │ ├── di │ │ ├── AppModule.kt │ │ └── CommonModule.kt │ │ ├── extensions │ │ └── KtorExtensions.kt │ │ ├── feature │ │ ├── debugmenu │ │ │ ├── Debug.kt │ │ │ ├── DebugMenuScreen.kt │ │ │ ├── repositories │ │ │ │ └── GlobalAppSettingsRepository.kt │ │ │ └── viewmodel │ │ │ │ └── DebugMenuViewModel.kt │ │ ├── loginsignup │ │ │ ├── EmailLoginScreen.kt │ │ │ ├── EmailSignUpScreen.kt │ │ │ ├── LoginSignUpLandingScreen.kt │ │ │ ├── di │ │ │ │ └── LogInSignUpModule.kt │ │ │ ├── usecases │ │ │ │ ├── EmailLogInUseCase.kt │ │ │ │ └── EmailSignUpUseCase.kt │ │ │ └── viewmodels │ │ │ │ ├── EmailLoginViewModel.kt │ │ │ │ └── EmailSignUpViewModel.kt │ │ ├── pets │ │ │ ├── MyPetsScreen.kt │ │ │ ├── di │ │ │ │ └── PetsModule.kt │ │ │ └── viewmodels │ │ │ │ └── MyPetsViewModel.kt │ │ ├── petupload │ │ │ ├── PetUploadScreen.kt │ │ │ ├── repositories │ │ │ │ └── PetUploadPublishRepository.kt │ │ │ ├── usecases │ │ │ │ └── PetUploadUseCase.kt │ │ │ └── viewmodel │ │ │ │ └── PetUploadViewModel.kt │ │ ├── profile │ │ │ ├── ProfileDetailScreen.kt │ │ │ └── viewmodels │ │ │ │ └── ProfileDetailViewModel.kt │ │ └── search │ │ │ └── repositories │ │ │ ├── LastSearchesRepository.kt │ │ │ └── PetsFromSearchRepository.kt │ │ ├── localization │ │ ├── Localization.kt │ │ └── translations │ │ │ ├── DeutschLocalization.kt │ │ │ ├── EnglishLocalization.kt │ │ │ ├── FrenchLocalization.kt │ │ │ ├── ItalianLocalization.kt │ │ │ └── SpanishLocalization.kt │ │ ├── navigation │ │ ├── FavoritesTab.kt │ │ ├── HomeTab.kt │ │ ├── InboxTab.kt │ │ ├── PetUploadTab.kt │ │ └── ProfileTab.kt │ │ ├── platform │ │ ├── Environment.kt │ │ ├── Extensions.kt │ │ ├── Helper.kt │ │ ├── MultiplatformKickstarterEventTracker.kt │ │ ├── Resources.kt │ │ ├── RootNavigatorRepository.kt │ │ ├── RootSnackbarHostStateRepository.kt │ │ ├── ServiceClient.kt │ │ └── TrackingComponent.kt │ │ └── ui │ │ ├── components │ │ ├── ColoredSnackBar.kt │ │ ├── Navigation.kt │ │ ├── NoResultsLayout.kt │ │ ├── OnBoarding.kt │ │ ├── PetCardBig.kt │ │ ├── PetCardSmall.kt │ │ ├── PetsSearchBar.kt │ │ ├── Picker.kt │ │ ├── RatingBar.kt │ │ ├── Toast.kt │ │ └── TopAppBar.kt │ │ ├── icon │ │ └── MultiplatformKickstarterIcons.kt │ │ ├── screens │ │ ├── HomeTabScreen.kt │ │ ├── MainScreen.kt │ │ ├── OnboardingScreen.kt │ │ ├── PetDetailScreen.kt │ │ ├── ProTemplateFeatureScreen.kt │ │ ├── ProfileTabScreen.kt │ │ ├── SearchListingScreen.kt │ │ └── viewmodel │ │ │ ├── HomeScreenViewModel.kt │ │ │ ├── ProfileViewModel.kt │ │ │ └── SearchListingViewModel.kt │ │ └── theme │ │ ├── Background.kt │ │ ├── Color.kt │ │ ├── Fonts.kt │ │ ├── Gradient.kt │ │ ├── Shape.kt │ │ ├── Theme.kt │ │ └── Typography.kt └── resources │ └── font │ ├── poppins_black.ttf │ ├── poppins_black_italic.ttf │ ├── poppins_bold.ttf │ ├── poppins_bold_italic.ttf │ ├── poppins_extra_bold.ttf │ ├── poppins_extra_bold_italic.ttf │ ├── poppins_extra_light.ttf │ ├── poppins_extra_light_italic.ttf │ ├── poppins_italic.ttf │ ├── poppins_light.ttf │ ├── poppins_light_italic.ttf │ ├── poppins_medium.ttf │ ├── poppins_medium_italic.ttf │ ├── poppins_regular.ttf │ ├── poppins_semi_bold.ttf │ ├── poppins_semi_bold_italic.ttf │ ├── poppins_thin.ttf │ └── poppins_thin_italic.ttf ├── desktopMain ├── drawable │ ├── bg_splash.xml │ ├── ethnic_friendship_pana.xml │ ├── ic_launcher_background.xml │ ├── mind_map.xml │ ├── mind_map_cuate.xml │ ├── mklogo.xml │ ├── no_data_cuate.xml │ ├── reminders_pana.xml │ └── world_amico.xml └── kotlin │ └── com │ └── multiplatformkickstarter │ └── app │ ├── feature │ └── debugmenu │ │ └── Debug.desktop.kt │ ├── localization │ └── Localization.desktop.kt │ └── platform │ └── Resources.kt ├── iosMain └── kotlin │ └── com │ └── multiplatformkickstarter │ └── app │ ├── feature │ └── debugmenu │ │ └── Debug.ios.kt │ ├── localization │ └── Localization.ios.kt │ ├── main.ios.kt │ └── platform │ └── Resources.kt ├── iosTest └── kotlin │ └── com │ └── multiplatformkickstarter │ └── app │ └── IosTest.kt └── jvmMain └── kotlin └── com └── multiplatformkickstarter └── app ├── feature └── debugmenu │ └── Debug.jvm.kt ├── localization └── Localization.jvm.kt └── platform └── Resources.jvm.kt /.fleet/run.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "type": "android-app", 5 | "name": "Android-app configuration", 6 | "module": "MultiplatformKickstarter.androidApp.main", 7 | "destination": "Pixel 7 API 33", 8 | }, 9 | { 10 | "type": "xcode-app", 11 | "name": "Xcode-app configuration", 12 | "buildTarget": { 13 | "project": "iosApp", 14 | "target": "iosApp", 15 | }, 16 | "configuration": "Debug", 17 | "destination": "iPhone 15 Pro | iOS 17.2", 18 | }, 19 | { 20 | "name": "desktopApp [Jvm]", 21 | "type": "jps-run", 22 | "workingDir": "$PROJECT_DIR$", 23 | "mainClass": "MainKt", 24 | "module": "MultiplatformKickstarter.desktopApp.jvmMain", 25 | "options": ["-Djava.library.path=$PROJECT_DIR$/desktopApp/build/classes/kotlin/jvm/main:$PROJECT_DIR$/shared/build/classes/kotlin/jvm/main:$PROJECT_DIR$/shared/build/processedResources/jvm/main", "-Dfile.encoding=UTF-8", "-Dsun.stdout.encoding=UTF-8", "-Dsun.stderr.encoding=UTF-8"] 26 | }, 27 | { 28 | "name": "Server", 29 | "type": "gradle", 30 | "workingDir": "$PROJECT_DIR$", 31 | "environment": { 32 | "JDBC_DATABASE_URL": "jdbc:postgresql:habits?user=postgres", 33 | "JWT_SECRET": "898748674728934843", 34 | "SECRET_KEY": "898748674728934843", 35 | "JDBC_DRIVER": "org.postgresql.Driver" 36 | }, 37 | "tasks": ["run"], 38 | "args": ["--info", "-p", "$PROJECT_DIR$/backend"], 39 | "initScripts": { 40 | "flmapper": "ext.mapPath = { path -> null }" 41 | } 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /.fleet/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "run.destination.stop.already.running": "Always" 3 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: ferranpons 4 | -------------------------------------------------------------------------------- /.github/workflows/basic.yml: -------------------------------------------------------------------------------- 1 | name: Basic CI 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v3 16 | - name: set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | java-version: '17' 20 | distribution: 'temurin' 21 | cache: gradle 22 | 23 | - name: Grant execute permission for gradlew 24 | run: chmod +x gradlew 25 | - name: Build with Gradle 26 | run: ./gradlew build 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea 4 | .DS_Store 5 | build 6 | captures 7 | .externalNativeBuild 8 | .cxx 9 | local.properties 10 | 11 | # Log/OS Files 12 | *.log 13 | 14 | # Android Studio generated files and folders 15 | captures/ 16 | .externalNativeBuild/ 17 | .cxx/ 18 | *.apk 19 | output.json 20 | 21 | # IntelliJ 22 | *.iml 23 | .idea/ 24 | misc.xml 25 | deploymentTargetDropDown.xml 26 | render.experimental.xml 27 | 28 | # Keystore files 29 | *.jks 30 | *.keystore 31 | 32 | # Google Services (e.g. APIs or Firebase) 33 | google-services.json 34 | 35 | # Android Profiling 36 | *.hprof 37 | xcuserdata 38 | 39 | *.klib 40 | .kotlin/metadata 41 | .kotlin/sessions -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/shelf 3 | /confluence/target 4 | /dependencies/repo 5 | /android.tests.dependencies 6 | /dependencies/android.tests.dependencies 7 | /dist 8 | /local 9 | /gh-pages 10 | /ideaSDK 11 | /clionSDK 12 | /android-studio/sdk 13 | out/ 14 | /tmp 15 | /intellij 16 | workspace.xml 17 | *.versionsBackup 18 | /idea/testData/debugger/tinyApp/classes* 19 | /jps-plugin/testData/kannotator 20 | /js/js.translator/testData/out/ 21 | /js/js.translator/testData/out-min/ 22 | /js/js.translator/testData/out-pir/ 23 | .gradle/ 24 | build/ 25 | !**/src/**/build 26 | !**/test/**/build 27 | *.iml 28 | !**/testData/**/*.iml 29 | .idea/remote-targets.xml 30 | .idea/libraries/Gradle*.xml 31 | .idea/libraries/Maven*.xml 32 | .idea/artifacts/PILL_*.xml 33 | .idea/artifacts/KotlinPlugin.xml 34 | .idea/modules 35 | .idea/runConfigurations/JPS_*.xml 36 | .idea/runConfigurations/PILL_*.xml 37 | .idea/runConfigurations/_FP_*.xml 38 | .idea/runConfigurations/_MT_*.xml 39 | .idea/libraries 40 | .idea/modules.xml 41 | .idea/gradle.xml 42 | .idea/compiler.xml 43 | .idea/inspectionProfiles/profiles_settings.xml 44 | .idea/.name 45 | .idea/artifacts/dist_auto_* 46 | .idea/artifacts/dist.xml 47 | .idea/artifacts/ideaPlugin.xml 48 | .idea/artifacts/kotlinc.xml 49 | .idea/artifacts/kotlin_compiler_jar.xml 50 | .idea/artifacts/kotlin_plugin_jar.xml 51 | .idea/artifacts/kotlin_jps_plugin_jar.xml 52 | .idea/artifacts/kotlin_daemon_client_jar.xml 53 | .idea/artifacts/kotlin_imports_dumper_compiler_plugin_jar.xml 54 | .idea/artifacts/kotlin_main_kts_jar.xml 55 | .idea/artifacts/kotlin_compiler_client_embeddable_jar.xml 56 | .idea/artifacts/kotlin_reflect_jar.xml 57 | .idea/artifacts/kotlin_stdlib_js_ir_* 58 | .idea/artifacts/kotlin_test_js_ir_* 59 | .idea/artifacts/kotlin_stdlib_wasm_* 60 | .idea/artifacts/kotlinx_atomicfu_runtime_* 61 | .idea/artifacts/kotlinx_cli_jvm_* 62 | .idea/jarRepositories.xml 63 | .idea/csv-plugin.xml 64 | .idea/libraries-with-intellij-classes.xml 65 | .idea/misc.xml 66 | .idea/ 67 | node_modules/ 68 | .rpt2_cache/ 69 | libraries/tools/kotlin-test-js-runner/lib/ 70 | local.properties 71 | buildSrcTmp/ 72 | distTmp/ 73 | outTmp/ 74 | /test.output 75 | /kotlin-native/dist 76 | kotlin-ide/ -------------------------------------------------------------------------------- /backend/.run/Adoptme [desktopRun].run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /backend/.run/Server.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 19 | 24 | 26 | true 27 | true 28 | false 29 | 30 | 31 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM gradle:7-jdk11 AS build 2 | COPY --chown=gradle:gradle . /home/gradle/src 3 | WORKDIR /home/gradle/src 4 | RUN gradle shadowJar --no-daemon 5 | 6 | FROM openjdk:11 7 | EXPOSE 8080:8080 8 | RUN mkdir /app 9 | COPY --from=build /home/gradle/src/build/libs/*.jar /app/ 10 | ENTRYPOINT ["java","-jar","/app/service-0.0.1-all.jar"] -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Multiplatform Kickstarter Service Template 2 | Basic Service template in ktor 3 | 4 | 5 | ## How to Run this service 6 | ``` 7 | ./gradlew run 8 | ``` 9 | 10 | ## How to run it using Docker 11 | ``` 12 | docker build -t multiplatformkickstarter . 13 | docker run -p 8080:8080 multiplatformkickstarter 14 | ``` 15 | 16 | To run MK Service container as a service on Linux with systemd: 17 | Create a service descriptor file /etc/systemd/system/docker.multiplatformkickstarter.service: 18 | 19 | ``` 20 | [Unit] 21 | Description=MK Service 22 | After=docker.service 23 | Requires=docker.service 24 | 25 | [Service] 26 | TimeoutStartSec=0 27 | Restart=always 28 | ExecStartPre=-/usr/bin/docker exec %n stop 29 | ExecStartPre=-/usr/bin/docker rm %n 30 | ExecStart=/usr/bin/docker run -p 8080:8080 multiplatformkickstarter 31 | 32 | [Install] 33 | WantedBy=default.target 34 | ``` 35 | 36 | 37 | Enable starting the service on system boot with the following command: 38 | ``` 39 | sudo systemctl enable docker.multiplatformkickstarter 40 | ``` 41 | 42 | 43 | You can also stop and start the service manually at any moment with the following commands, respectively: 44 | ``` 45 | sudo service docker.multiplatformkickstarter stop 46 | sudo service docker.multiplatformkickstarter start 47 | ``` 48 | -------------------------------------------------------------------------------- /backend/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | application 3 | kotlin("jvm") 4 | id("org.jetbrains.kotlin.plugin.serialization") 5 | alias(libs.plugins.shadow) 6 | } 7 | 8 | application { 9 | mainClass.set("com.multiplatformkickstarter.ApplicationKt") 10 | 11 | val isDevelopment: Boolean = project.ext.has("development") 12 | applicationDefaultJvmArgs = listOf("-Dio.ktor.development=$isDevelopment") 13 | } 14 | 15 | dependencies { 16 | implementation(project(":shared")) { 17 | exclude(group = "cafe.adriel.voyager") 18 | } 19 | implementation(libs.ktor.server.core) 20 | implementation(libs.ktor.server.auth) 21 | implementation(libs.ktor.server.auth.jwt) 22 | implementation(libs.ktor.server.locations) 23 | implementation(libs.ktor.server.host.common) 24 | implementation(libs.ktor.server.status.pages) 25 | implementation(libs.ktor.server.catching.headers) 26 | implementation(libs.ktor.server.compression) 27 | implementation(libs.ktor.server.conditional.headers) 28 | implementation(libs.ktor.server.cors) 29 | implementation(libs.ktor.server.call.logging) 30 | implementation(libs.ktor.server.metrics) 31 | implementation(libs.ktor.server.sessions) 32 | implementation(libs.ktor.server.html.builder.jvm) 33 | implementation(libs.ktor.server.content.negotiation) 34 | implementation(libs.ktor.server.serialization.gson) 35 | implementation(libs.ktor.server.serialization.kotlinx.json) 36 | implementation(libs.ktor.server.tomcat) 37 | implementation(libs.ktor.server.openapi) 38 | 39 | implementation(libs.kotlinx.datetime) 40 | implementation(libs.kotlinx.html.jvm) 41 | implementation(libs.logback.classic) 42 | 43 | implementation(libs.exposed.core) 44 | implementation(libs.exposed.dao) 45 | implementation(libs.exposed.jdbc) 46 | 47 | implementation(libs.postgresql) 48 | implementation(libs.hikariCP) 49 | implementation(libs.swagger.codegen) 50 | 51 | testImplementation(libs.ktor.server.tests.jvm) 52 | testImplementation(libs.kotlin.test.junit) 53 | } 54 | -------------------------------------------------------------------------------- /backend/docs/.swagger-codegen-ignore: -------------------------------------------------------------------------------- 1 | # Swagger Codegen Ignore 2 | # Generated by swagger-codegen https://github.com/swagger-api/swagger-codegen 3 | 4 | # Use this file to prevent files from being overwritten by the generator. 5 | # The patterns follow closely to .gitignore or .dockerignore. 6 | 7 | # As an example, the C# client generator defines ApiClient.cs. 8 | # You can make changes and tell Swagger Codgen to ignore just this file by uncommenting the following line: 9 | #ApiClient.cs 10 | 11 | # You can match any string of characters against a directory, file or extension with a single asterisk (*): 12 | #foo/*/qux 13 | # The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux 14 | 15 | # You can recursively match patterns against a directory, file or extension with a double asterisk (**): 16 | #foo/**/qux 17 | # This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux 18 | 19 | # You can also negate patterns with an exclamation (!). 20 | # For example, you can ignore all files in a docs folder with the file extension .md: 21 | #docs/*.md 22 | # Then explicitly reverse the ignore rule for a single file: 23 | #!docs/README.md 24 | -------------------------------------------------------------------------------- /backend/docs/.swagger-codegen/VERSION: -------------------------------------------------------------------------------- 1 | 3.0.41 -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/Application.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter 2 | 3 | import com.multiplatformkickstarter.plugins.configureAdministration 4 | import com.multiplatformkickstarter.plugins.configureHTTP 5 | import com.multiplatformkickstarter.plugins.configureMonitoring 6 | import com.multiplatformkickstarter.plugins.configureSerialization 7 | import com.multiplatformkickstarter.plugins.configureTemplating 8 | import io.ktor.server.engine.embeddedServer 9 | import io.ktor.server.tomcat.Tomcat 10 | 11 | fun main() { 12 | embeddedServer(Tomcat, port = 8080, host = "0.0.0.0") { 13 | configureSessions() 14 | configureGeneralRouting() 15 | configureAuthentication() 16 | configureHTTP() 17 | configureMonitoring() 18 | configureTemplating() 19 | configureSerialization() 20 | configureAdministration() 21 | }.start(wait = true) 22 | } 23 | 24 | const val API_VERSION = "/v1" 25 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/AuthenticationConfig.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(KtorExperimentalLocationsAPI::class) 2 | 3 | package com.multiplatformkickstarter 4 | 5 | import com.multiplatformkickstarter.auth.JWT_CONFIGURATION 6 | import com.multiplatformkickstarter.auth.JwtService 7 | import com.multiplatformkickstarter.auth.hash 8 | import com.multiplatformkickstarter.repository.DatabaseFactory 9 | import com.multiplatformkickstarter.repository.pets.PetsRepositoryImp 10 | import com.multiplatformkickstarter.repository.profile.ProfileRepositoryImpl 11 | import com.multiplatformkickstarter.repository.user.UserRepositoryImp 12 | import com.multiplatformkickstarter.routes.pets 13 | import com.multiplatformkickstarter.routes.profiles 14 | import com.multiplatformkickstarter.routes.users 15 | import io.ktor.server.application.Application 16 | import io.ktor.server.application.install 17 | import io.ktor.server.auth.Authentication 18 | import io.ktor.server.auth.jwt.jwt 19 | import io.ktor.server.locations.KtorExperimentalLocationsAPI 20 | import io.ktor.server.routing.routing 21 | 22 | fun Application.configureAuthentication() { 23 | DatabaseFactory.init() 24 | val userRepository = UserRepositoryImp() 25 | val petRepository = PetsRepositoryImp() 26 | val profileRepository = ProfileRepositoryImpl() 27 | val jwtService = JwtService() 28 | val hashFunction = { s: String -> hash(s) } 29 | 30 | install(Authentication) { 31 | jwt(JWT_CONFIGURATION) { 32 | verifier(jwtService.verifier) 33 | realm = "MyProjectName Server" 34 | validate { 35 | val payload = it.payload 36 | val claim = payload.getClaim("id") 37 | val claimString = claim.asInt() 38 | val user = userRepository.findUser(claimString) 39 | user 40 | } 41 | } 42 | } 43 | 44 | routing { 45 | users(userRepository, jwtService, hashFunction) 46 | pets(petRepository, userRepository) 47 | profiles(profileRepository, userRepository) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/GeneralRoutingConfig.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(KtorExperimentalLocationsAPI::class) 2 | 3 | package com.multiplatformkickstarter 4 | 5 | import io.ktor.http.HttpStatusCode 6 | import io.ktor.server.application.Application 7 | import io.ktor.server.application.call 8 | import io.ktor.server.application.install 9 | import io.ktor.server.http.content.staticResources 10 | import io.ktor.server.locations.KtorExperimentalLocationsAPI 11 | import io.ktor.server.locations.Locations 12 | import io.ktor.server.plugins.openapi.openAPI 13 | import io.ktor.server.plugins.statuspages.StatusPages 14 | import io.ktor.server.response.respond 15 | import io.ktor.server.response.respondText 16 | import io.ktor.server.routing.get 17 | import io.ktor.server.routing.routing 18 | import io.swagger.codegen.v3.generators.html.StaticHtmlCodegen 19 | 20 | fun Application.configureGeneralRouting() { 21 | install(Locations) { 22 | } 23 | 24 | routing { 25 | get("/") { 26 | call.respondText("Hello World!") 27 | } 28 | 29 | openAPI(path = "openapi", swaggerFile = "openapi/documentation.yaml") { 30 | codegen = StaticHtmlCodegen() 31 | } 32 | 33 | staticResources("/static", "static") 34 | 35 | this@configureGeneralRouting.install(StatusPages) { 36 | exception { call, _ -> 37 | call.respond(HttpStatusCode.Unauthorized) 38 | } 39 | exception { call, _ -> 40 | call.respond(HttpStatusCode.Forbidden) 41 | } 42 | } 43 | } 44 | } 45 | 46 | class AuthenticationException : RuntimeException() 47 | class AuthorizationException : RuntimeException() 48 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/SessionsConfig.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter 2 | 3 | import com.multiplatformkickstarter.auth.UserSession 4 | import io.ktor.server.application.Application 5 | import io.ktor.server.application.install 6 | import io.ktor.server.sessions.Sessions 7 | import io.ktor.server.sessions.directorySessionStorage 8 | import io.ktor.server.sessions.header 9 | import java.io.File 10 | 11 | fun Application.configureSessions() { 12 | install(Sessions) { 13 | header("user_session", directorySessionStorage(File("build/.sessions"))) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/auth/Auth.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.auth 2 | 3 | import io.ktor.util.hex 4 | import javax.crypto.Mac 5 | import javax.crypto.spec.SecretKeySpec 6 | 7 | val hashKey = hex(System.getenv("SECRET_KEY")) 8 | 9 | val hmacKey = SecretKeySpec(hashKey, "HmacSHA1") 10 | 11 | fun hash(password: String): String { 12 | val hmac = Mac.getInstance("HmacSHA1") 13 | hmac.init(hmacKey) 14 | return hex(hmac.doFinal(password.toByteArray(Charsets.UTF_8))) 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/auth/JwtService.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.auth 2 | 3 | import com.auth0.jwt.JWT 4 | import com.auth0.jwt.JWTVerifier 5 | import com.auth0.jwt.algorithms.Algorithm.HMAC512 6 | import com.multiplatformkickstarter.models.DatabaseUser 7 | import java.util.Date 8 | 9 | const val JWT_CONFIGURATION = "MyProjectNameJWT" 10 | 11 | class JwtService { 12 | 13 | private val issuer = "MyProjectNameServer" 14 | private val jwtSecret = System.getenv("JWT_SECRET") 15 | private val algorithm = HMAC512(jwtSecret) 16 | 17 | val verifier: JWTVerifier = JWT 18 | .require(algorithm) 19 | .withIssuer(issuer) 20 | .build() 21 | 22 | fun generateToken(databaseUser: DatabaseUser): String = JWT.create() 23 | .withSubject("Authentication") 24 | .withIssuer(issuer) 25 | .withClaim("id", databaseUser.userId) 26 | .withExpiresAt(expiresAt()) 27 | .sign(algorithm) 28 | 29 | @Suppress("MagicNumber") 30 | private fun expiresAt() = Date(System.currentTimeMillis() + 3_600_000 * 24) // 24 hours 31 | } 32 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/auth/UserSession.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.auth 2 | 3 | data class UserSession(val userId: Int) 4 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/models/DatabaseUser.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.models 2 | 3 | import io.ktor.server.auth.Principal 4 | import java.io.Serializable 5 | 6 | @Suppress("SerialVersionUIDInSerializableClass") 7 | data class DatabaseUser( 8 | val userId: Int, 9 | val email: String, 10 | val name: String, 11 | val passwordHash: String 12 | ) : Serializable, Principal 13 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/plugins/Administration.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.plugins 2 | 3 | import io.ktor.server.application.Application 4 | import io.ktor.server.application.install 5 | import io.ktor.server.engine.ShutDownUrl 6 | 7 | fun Application.configureAdministration() { 8 | install(ShutDownUrl.ApplicationCallPlugin) { 9 | // The URL that will be intercepted (you can also use the application.conf's ktor.deployment.shutdown.url key) 10 | shutDownUrl = "/ktor/application/shutdown" 11 | // A function that will be executed to get the exit code of the process 12 | exitCodeSupplier = { 0 } // ApplicationCall.() -> Int 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/plugins/HTTP.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.plugins 2 | 3 | import io.ktor.http.CacheControl 4 | import io.ktor.http.ContentType 5 | import io.ktor.http.HttpHeaders 6 | import io.ktor.http.HttpMethod 7 | import io.ktor.server.application.Application 8 | import io.ktor.server.application.install 9 | import io.ktor.server.http.content.CachingOptions 10 | import io.ktor.server.plugins.cachingheaders.CachingHeaders 11 | import io.ktor.server.plugins.compression.Compression 12 | import io.ktor.server.plugins.compression.deflate 13 | import io.ktor.server.plugins.compression.gzip 14 | import io.ktor.server.plugins.compression.minimumSize 15 | import io.ktor.server.plugins.conditionalheaders.ConditionalHeaders 16 | import io.ktor.server.plugins.cors.routing.CORS 17 | import java.time.ZoneId 18 | import java.time.ZonedDateTime 19 | 20 | private const val MINIMUM_SIZE = 1024L 21 | 22 | @Suppress("ForbiddenComment", "MagicNumber") 23 | fun Application.configureHTTP() { 24 | install(CachingHeaders) { 25 | options { _, outgoingContent -> 26 | when (outgoingContent.contentType?.withoutParameters()) { 27 | ContentType.Text.CSS -> CachingOptions( 28 | CacheControl.MaxAge(maxAgeSeconds = 24 * 60 * 60), 29 | ZonedDateTime.of(0, 0, 1, 0, 0, 0, 0, ZoneId.systemDefault()) 30 | ) 31 | else -> null 32 | } 33 | } 34 | } 35 | install(Compression) { 36 | gzip { 37 | priority = 1.0 38 | } 39 | deflate { 40 | priority = 10.0 41 | minimumSize(MINIMUM_SIZE) 42 | } 43 | } 44 | install(ConditionalHeaders) 45 | install(CORS) { 46 | allowMethod(HttpMethod.Options) 47 | allowMethod(HttpMethod.Put) 48 | allowMethod(HttpMethod.Delete) 49 | allowMethod(HttpMethod.Patch) 50 | allowHeader(HttpHeaders.Authorization) 51 | anyHost() // @TODO: Don't do this in production if possible. Try to limit it. 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/plugins/Monitoring.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.plugins 2 | 3 | import io.ktor.server.application.Application 4 | import io.ktor.server.application.install 5 | import io.ktor.server.plugins.callloging.CallLogging 6 | import io.ktor.server.request.path 7 | import org.slf4j.event.Level 8 | 9 | fun Application.configureMonitoring() { 10 | install(CallLogging) { 11 | level = Level.INFO 12 | filter { call -> call.request.path().startsWith("/") } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/plugins/Serialization.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.plugins 2 | 3 | import io.ktor.serialization.gson.gson 4 | import io.ktor.serialization.kotlinx.json.json 5 | import io.ktor.server.application.Application 6 | import io.ktor.server.application.call 7 | import io.ktor.server.application.install 8 | import io.ktor.server.plugins.contentnegotiation.ContentNegotiation 9 | import io.ktor.server.response.respond 10 | import io.ktor.server.routing.get 11 | import io.ktor.server.routing.routing 12 | 13 | fun Application.configureSerialization() { 14 | install(ContentNegotiation) { 15 | gson { 16 | } 17 | json() 18 | } 19 | 20 | routing { 21 | get("/json/gson") { 22 | call.respond(mapOf("hello" to "world")) 23 | } 24 | get("/json/kotlinx-serialization") { 25 | call.respond(mapOf("hello" to "world")) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/plugins/Templating.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.plugins 2 | 3 | import io.ktor.server.application.Application 4 | import io.ktor.server.application.call 5 | import io.ktor.server.html.respondHtml 6 | import io.ktor.server.routing.get 7 | import io.ktor.server.routing.routing 8 | import kotlinx.html.body 9 | import kotlinx.html.h1 10 | import kotlinx.html.li 11 | import kotlinx.html.ul 12 | 13 | @Suppress("MagicNumber") 14 | fun Application.configureTemplating() { 15 | routing { 16 | get("/html-dsl") { 17 | call.respondHtml { 18 | body { 19 | h1 { +"HTML" } 20 | ul { 21 | for (n in 1..10) { 22 | li { +"$n" } 23 | } 24 | } 25 | } 26 | } 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/DatabaseFactory.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository 2 | 3 | import com.multiplatformkickstarter.repository.pets.Pets 4 | import com.multiplatformkickstarter.repository.profile.Profiles 5 | import com.multiplatformkickstarter.repository.user.Users 6 | import com.zaxxer.hikari.HikariConfig 7 | import com.zaxxer.hikari.HikariDataSource 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.withContext 10 | import org.jetbrains.exposed.sql.Database 11 | import org.jetbrains.exposed.sql.SchemaUtils 12 | import org.jetbrains.exposed.sql.transactions.transaction 13 | 14 | private const val MAX_POOL_SIZE = 3 15 | 16 | object DatabaseFactory { 17 | fun init() { 18 | Database.connect(hikari()) 19 | 20 | transaction { 21 | SchemaUtils.create(Users) 22 | SchemaUtils.create(Pets) 23 | SchemaUtils.create(Profiles) 24 | } 25 | } 26 | 27 | private fun hikari(): HikariDataSource { 28 | val config = HikariConfig() 29 | config.driverClassName = System.getenv("JDBC_DRIVER") 30 | config.jdbcUrl = System.getenv("JDBC_DATABASE_URL") 31 | config.maximumPoolSize = MAX_POOL_SIZE 32 | config.isAutoCommit = false 33 | config.transactionIsolation = "TRANSACTION_REPEATABLE_READ" 34 | val user = System.getenv("DB_USER") 35 | if (user != null) { 36 | config.username = user 37 | } 38 | val password = System.getenv("DB_PASSWORD") 39 | if (password != null) { 40 | config.password = password 41 | } 42 | config.validate() 43 | return HikariDataSource(config) 44 | } 45 | 46 | suspend fun dbQuery(block: () -> T): T = 47 | withContext(Dispatchers.IO) { 48 | transaction { block() } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/pets/Pets.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.pets 2 | 3 | import com.multiplatformkickstarter.repository.user.Users 4 | import org.jetbrains.exposed.sql.Column 5 | import org.jetbrains.exposed.sql.Table 6 | 7 | @Suppress("MagicNumber") 8 | object Pets : Table() { 9 | val id: Column = integer("id").autoIncrement().uniqueIndex() 10 | val userId: Column = integer("userId").references(Users.userId) 11 | val title = varchar("title", 128) 12 | val description = varchar("description", 512) 13 | val images = varchar("images", 512) 14 | val category = integer("category") 15 | val location = varchar("location", 32) 16 | val published = varchar("published", 128) 17 | val modified = varchar("modified", 128) 18 | val breed = varchar("breed", 128) 19 | val age = varchar("age", 8) 20 | val gender = varchar("gender", 8) 21 | val size = varchar("size", 8) 22 | val color = varchar("color", 16) 23 | val status = varchar("status", 16) 24 | val shelterId = integer("shelterId") 25 | } 26 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/pets/PetsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.pets 2 | 3 | import com.multiplatformkickstarter.app.common.model.PetModel 4 | 5 | interface PetsRepository { 6 | suspend fun addPet( 7 | userId: Int, 8 | title: String, 9 | description: String, 10 | images: String, 11 | category: Int, 12 | location: String, 13 | published: String, 14 | breed: String, 15 | age: String, 16 | gender: String, 17 | size: String, 18 | color: String, 19 | status: String, 20 | shelterId: Int? 21 | ): PetModel? 22 | 23 | suspend fun getPet(petId: Int): PetModel 24 | 25 | suspend fun getPets(userId: Int): List 26 | 27 | suspend fun delete(petId: Int) 28 | 29 | suspend fun updatePet( 30 | petId: Int, 31 | title: String?, 32 | description: String?, 33 | images: String?, 34 | location: String?, 35 | modified: String?, 36 | breed: String?, 37 | age: String?, 38 | gender: String?, 39 | size: String?, 40 | color: String?, 41 | status: String?, 42 | shelterId: Int? 43 | ): PetModel? 44 | } 45 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/profile/ProfileRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.profile 2 | 3 | import com.multiplatformkickstarter.app.common.model.Profile 4 | 5 | interface ProfileRepository { 6 | 7 | suspend fun addProfile( 8 | userId: Int, 9 | name: String, 10 | description: String?, 11 | image: String?, 12 | location: String?, 13 | rating: Double? 14 | ): Profile? 15 | 16 | suspend fun getProfile(userId: Int): Profile? 17 | 18 | suspend fun updateProfile( 19 | userId: Int, 20 | name: String?, 21 | description: String?, 22 | image: String?, 23 | location: String?, 24 | rating: Double? 25 | ): Profile? 26 | } 27 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/profile/Profiles.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.profile 2 | 3 | import com.multiplatformkickstarter.repository.user.Users 4 | import org.jetbrains.exposed.sql.Column 5 | import org.jetbrains.exposed.sql.Table 6 | 7 | @Suppress("MagicNumber") 8 | object Profiles : Table() { 9 | val id: Column = integer("id").autoIncrement().uniqueIndex() 10 | val userId: Column = integer("userId").references(Users.userId) 11 | val name = varchar("name", 64) 12 | val description = varchar("description", 600) 13 | val image = varchar("image", 128) 14 | val location = varchar("location", 64) 15 | val rating = double("rating") 16 | } 17 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/user/UserRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.user 2 | 3 | import com.multiplatformkickstarter.models.DatabaseUser 4 | 5 | interface UserRepository { 6 | suspend fun addUser( 7 | email: String, 8 | name: String, 9 | passwordHash: String 10 | ): DatabaseUser? 11 | 12 | suspend fun findUser(userId: Int): DatabaseUser? 13 | suspend fun findUserByEmail(email: String): DatabaseUser? 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/user/UserRepositoryImp.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.user 2 | 3 | import com.multiplatformkickstarter.models.DatabaseUser 4 | import com.multiplatformkickstarter.repository.DatabaseFactory.dbQuery 5 | import org.jetbrains.exposed.sql.ResultRow 6 | import org.jetbrains.exposed.sql.insert 7 | import org.jetbrains.exposed.sql.select 8 | import org.jetbrains.exposed.sql.statements.InsertStatement 9 | 10 | class UserRepositoryImp : UserRepository { 11 | override suspend fun addUser( 12 | email: String, 13 | name: String, 14 | passwordHash: String 15 | ): DatabaseUser? { 16 | var statement: InsertStatement? = null // 1 17 | dbQuery { 18 | statement = Users.insert { user -> 19 | user[Users.email] = email 20 | user[Users.name] = name 21 | user[Users.passwordHash] = passwordHash 22 | } 23 | } 24 | 25 | return rowToUser(statement?.resultedValues?.get(0)) 26 | } 27 | 28 | override suspend fun findUser(userId: Int) = dbQuery { 29 | Users.select { Users.userId.eq(userId) } 30 | .map { rowToUser(it) }.singleOrNull() 31 | } 32 | 33 | override suspend fun findUserByEmail(email: String) = dbQuery { 34 | Users.select { Users.email.eq(email) } 35 | .map { rowToUser(it) }.singleOrNull() 36 | } 37 | 38 | private fun rowToUser(row: ResultRow?): DatabaseUser? { 39 | if (row == null) { 40 | return null 41 | } 42 | return DatabaseUser( 43 | userId = row[Users.userId], 44 | email = row[Users.email], 45 | name = row[Users.name], 46 | passwordHash = row[Users.passwordHash] 47 | ) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /backend/src/main/kotlin/com/multiplatformkickstarter/repository/user/Users.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.repository.user 2 | 3 | import org.jetbrains.exposed.sql.Column 4 | import org.jetbrains.exposed.sql.Table 5 | 6 | @Suppress("MagicNumber") 7 | object Users : Table() { 8 | val userId: Column = integer("id").autoIncrement().uniqueIndex() 9 | val email = varchar("email", 128).uniqueIndex() 10 | val name = varchar("name", 256) 11 | val passwordHash = varchar("password_hash", 64) 12 | } 13 | -------------------------------------------------------------------------------- /backend/src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /backend/src/main/resources/openapi/documentation.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | title: Sample API 4 | description: Optional multiline or single-line description in [CommonMark](http://commonmark.org/help/) or HTML. 5 | version: 0.1.9 6 | servers: 7 | - url: http://api.example.com/v1 8 | description: Optional server description, e.g. Main (production) server 9 | - url: http://staging-api.example.com 10 | description: Optional server description, e.g. Internal staging server for testing 11 | paths: 12 | /users: 13 | get: 14 | summary: Returns a list of users. 15 | description: Optional extended description in CommonMark or HTML. 16 | responses: 17 | '200': # status code 18 | description: A JSON array of user names 19 | content: 20 | application/json: 21 | schema: 22 | type: array 23 | items: 24 | type: string 25 | components: 26 | securitySchemes: 27 | BasicAuth: 28 | type: http 29 | scheme: basic 30 | security: 31 | - BasicAuth: [] -------------------------------------------------------------------------------- /backend/src/test/kotlin/com/multiplatformkickstarter/ApplicationTest.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter 2 | 3 | import io.ktor.client.request.get 4 | import io.ktor.client.statement.bodyAsText 5 | import io.ktor.http.HttpStatusCode 6 | import io.ktor.server.testing.testApplication 7 | import kotlin.test.Test 8 | import kotlin.test.assertEquals 9 | 10 | class ApplicationTest { 11 | @Test 12 | fun testRoot() = testApplication { 13 | application { 14 | configureGeneralRouting() 15 | } 16 | client.get("/").apply { 17 | assertEquals(HttpStatusCode.OK, status) 18 | assertEquals("Hello World!", bodyAsText()) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application).apply(false) 3 | alias(libs.plugins.android.library).apply(false) 4 | kotlin("android").version(libs.versions.kotlin).apply(false) 5 | kotlin("multiplatform").version(libs.versions.kotlin).apply(false) 6 | kotlin("plugin.serialization").version(libs.versions.kotlin).apply(false) 7 | alias(libs.plugins.compose.multiplatform).apply(false) 8 | alias(libs.plugins.compose.compiler).apply(false) 9 | alias(libs.plugins.ktlint).apply(false) 10 | alias(libs.plugins.detekt).apply(false) 11 | } 12 | 13 | subprojects { 14 | if (name != "desktopApp") { 15 | apply(plugin = "org.jlleitschuh.gradle.ktlint") 16 | apply(plugin = "io.gitlab.arturbosch.detekt") 17 | 18 | configure { 19 | debug.set(true) 20 | filter { 21 | exclude { element -> 22 | element.file.path.contains("generated") 23 | } 24 | } 25 | } 26 | 27 | 28 | configure { 29 | parallel = false 30 | config.setFrom("../config/detekt-config.yml") 31 | buildUponDefaultConfig = false 32 | } 33 | 34 | } 35 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 20 | 21 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 42 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/ic_launcher-playstore.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/multiplatformkickstarter/app/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.compose.foundation.isSystemInDarkTheme 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.DisposableEffect 9 | import androidx.compose.runtime.getValue 10 | import androidx.compose.runtime.mutableStateOf 11 | import com.google.accompanist.systemuicontroller.rememberSystemUiController 12 | import com.multiplatformkickstarter.app.MainApp 13 | import com.multiplatformkickstarter.app.common.model.theme.DarkThemeConfig 14 | import com.multiplatformkickstarter.app.ui.theme.MultiplatformKickstarterTheme 15 | 16 | class MainActivity : ComponentActivity() { 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | 20 | val uiState: MainActivityUiState by mutableStateOf(MainActivityUiState.Loading) 21 | 22 | setContent { 23 | val systemUiController = rememberSystemUiController() 24 | val darkTheme = shouldUseDarkTheme(uiState) 25 | 26 | // Update the dark content of the system bars to match the theme 27 | DisposableEffect(systemUiController, darkTheme) { 28 | systemUiController.systemBarsDarkContentEnabled = !darkTheme 29 | onDispose {} 30 | } 31 | 32 | MultiplatformKickstarterTheme(darkTheme = darkTheme) { 33 | MainApp() 34 | } 35 | } 36 | } 37 | } 38 | 39 | @Composable 40 | fun shouldUseDarkTheme( 41 | uiState: MainActivityUiState 42 | ): Boolean = when (uiState) { 43 | MainActivityUiState.Loading -> isSystemInDarkTheme() 44 | is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) { 45 | DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme() 46 | DarkThemeConfig.LIGHT -> false 47 | DarkThemeConfig.DARK -> true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/multiplatformkickstarter/app/android/MainActivityUiState.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.android 2 | 3 | import com.multiplatformkickstarter.app.common.model.UserData 4 | 5 | sealed interface MainActivityUiState { 6 | data object Loading : MainActivityUiState 7 | data class Success(val userData: UserData) : MainActivityUiState 8 | } 9 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/multiplatformkickstarter/app/android/MultiplatformKickstarterApplication.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.android 2 | 3 | import android.app.Application 4 | import com.multiplatformkickstarter.app.android.core.di.DependencyContainer 5 | 6 | class MultiplatformKickstarterApplication : Application() { 7 | 8 | override fun onCreate() { 9 | super.onCreate() 10 | initDependencyContainer() 11 | } 12 | 13 | private fun initDependencyContainer() { 14 | DependencyContainer.initialize(this) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/multiplatformkickstarter/app/android/SplashActivity.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.android 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.os.Build 6 | import android.os.Bundle 7 | import androidx.core.app.ComponentActivity 8 | import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen 9 | import androidx.lifecycle.lifecycleScope 10 | import kotlinx.coroutines.launch 11 | 12 | @SuppressLint("CustomSplashScreen", "RestrictedApi") 13 | class SplashActivity : ComponentActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 17 | val splashScreen = installSplashScreen() 18 | splashScreen.setKeepOnScreenCondition { true } 19 | } 20 | super.onCreate(savedInstanceState) 21 | lifecycleScope.launch { 22 | val intent = Intent(this@SplashActivity, MainActivity::class.java) 23 | startActivity(intent) 24 | finish() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/com/multiplatformkickstarter/app/android/core/di/DependencyContainer.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.android.core.di 2 | 3 | import android.content.Context 4 | import androidx.annotation.VisibleForTesting 5 | import com.google.firebase.ktx.BuildConfig 6 | import com.multiplatformkickstarter.app.di.appModule 7 | import org.koin.android.ext.koin.androidContext 8 | import org.koin.android.ext.koin.androidLogger 9 | import org.koin.core.context.startKoin 10 | import org.koin.core.logger.Level 11 | import org.koin.core.module.Module 12 | 13 | object DependencyContainer { 14 | 15 | @VisibleForTesting 16 | var testModules: MutableList = mutableListOf() 17 | 18 | private val internalModules = 19 | sequence { 20 | yieldAll(appModule()) 21 | yieldAll(testModules) 22 | } 23 | 24 | @JvmStatic 25 | fun initialize(context: Context) { 26 | startKoin { 27 | if (BuildConfig.DEBUG) { 28 | androidLogger(Level.ERROR) 29 | } 30 | androidContext(context) 31 | modules(internalModules.toList()) 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/drawable/mklogo.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_black.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_black_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_black_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_bold.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_bold_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_extra_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_extra_bold.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_extra_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_extra_bold_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_extra_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_extra_light.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_extra_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_extra_light_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_light.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_light_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_medium.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_medium_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_regular.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_semi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_semi_bold.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_semi_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_semi_bold_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_thin.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/font/poppins_thin_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/font/poppins_thin_italic.ttf -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-w1240dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 200dp 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values-w600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 48dp 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | #FF0EE37C 11 | #FF006D36 12 | #FF005227 13 | #FF003919 14 | #FF00210B 15 | 16 | #FFFCFCFC 17 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #891788 4 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/placeholder_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Buick Century 5 | Buick LaSabre 6 | Buick Roadmaster 7 | Buick Special Riviera 8 | Cadillac Couple De Ville 9 | Cadillac Eldorado 10 | Cadillac Fleetwood 11 | Cadillac Series 62 12 | Cadillac Seville 13 | Ford Fairlane 14 | Ford Galaxie 500 15 | Ford Mustang 16 | Ford Thunderbird 17 | GMC Le Mans 18 | Plymouth Fury 19 | Plymouth GTX 20 | Plymouth Roadrunner 21 | 22 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Multiplatform Kickstarter 3 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/strings_placeholder.xml: -------------------------------------------------------------------------------- 1 | 2 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt vehicula eros. 3 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt vehicula eros. 4 | 5 | John Doe 6 | Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec tincidunt vehicula eros. 7 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | 15 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/App.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.Composable 2 | import com.multiplatformkickstarter.app.MainApp 3 | import org.jetbrains.compose.ui.tooling.preview.Preview 4 | 5 | @Composable 6 | @Preview 7 | fun App() { 8 | MainApp() 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/src/desktopMain/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import com.multiplatformkickstarter.app.MainApp 5 | import com.multiplatformkickstarter.app.di.commonModule 6 | import com.multiplatformkickstarter.app.ui.theme.MultiplatformKickstarterTheme 7 | import org.koin.core.context.startKoin 8 | 9 | fun main() = application { 10 | startKoin { 11 | modules(commonModule) 12 | } 13 | Window(onCloseRequest = ::exitApplication) { 14 | MultiplatformKickstarterTheme { 15 | MainApp() 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/MainViewController.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import com.multiplatformkickstarter.app.MainApp 3 | import platform.UIKit.UIViewController 4 | 5 | fun homeScreenViewController(): UIViewController = ComposeUIViewController { 6 | MainApp() 7 | } 8 | 9 | /* ktlint-disable */ 10 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.ComposeViewport 3 | import kotlinx.browser.document 4 | 5 | @OptIn(ExperimentalComposeUiApi::class) 6 | fun main() { 7 | ComposeViewport(document.body!!) { 8 | App() 9 | } 10 | } -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | KotlinProject 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /composeApp/src/wasmJsMain/resources/styles.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width: 100%; 3 | height: 100%; 4 | margin: 0; 5 | padding: 0; 6 | overflow: hidden; 7 | } -------------------------------------------------------------------------------- /config/images/multiplatform-kickstarter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/config/images/multiplatform-kickstarter-logo.png -------------------------------------------------------------------------------- /config/images/multiplatform-kickstarter-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/config/images/multiplatform-kickstarter-screenshot.png -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle 2 | org.gradle.jvmargs=-Xms8g -Xmx8g -XX:MaxMetaspaceSize=1g -XX:+HeapDumpOnOutOfMemoryError -XX:+UseParallelGC -XX:-UseGCOverheadLimit 3 | kotlin.daemon.jvmargs=-Xms4g -Xmx4g -XX:+UseParallelGC 4 | org.gradle.caching=true 5 | kotlin.mpp.androidGradlePluginCompatibility.nowarn=true 6 | org.gradle.parallel=true 7 | 8 | org.gradle.configuration-cache=true 9 | 10 | # Kotlin 11 | kotlin.code.style=official 12 | 13 | # Android 14 | android.useAndroidX=true 15 | android.nonTransitiveRClass=true 16 | 17 | # Web 18 | kotlin.js.webpack.major.version=4 19 | 20 | # Desktop 21 | 22 | # MPP 23 | kotlin.mpp.enableCInteropCommonization=true 24 | kotlin.mpp.androidSourceSetLayoutVersion=2 25 | 26 | org.jetbrains.compose.experimental.jscanvas.enabled=true 27 | org.jetbrains.compose.experimental.macos.enabled=true 28 | org.jetbrains.compose.experimental.uikit.enabled=true 29 | kotlin.native.cachekind=none 30 | 31 | # Experimental 32 | #kotlin.experimental.tryK2=true 33 | #kapt.use.k2=true 34 | 35 | # Multiplatform Kickstarter config 36 | multiplatformkickstarter.version.major = 2 37 | multiplatformkickstarter.version.minor = 1 38 | multiplatformkickstarter.version.patch = 0 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /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 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/xcshareddata/xcschemes/iosApp.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 42 | 44 | 50 | 51 | 52 | 53 | 59 | 61 | 67 | 68 | 69 | 70 | 72 | 73 | 76 | 77 | 78 | -------------------------------------------------------------------------------- /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 | "idiom" : "iphone", 5 | "scale" : "2x", 6 | "size" : "20x20" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "scale" : "3x", 11 | "size" : "20x20" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "scale" : "2x", 16 | "size" : "29x29" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "scale" : "3x", 21 | "size" : "29x29" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "scale" : "2x", 26 | "size" : "40x40" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "40x40" 32 | }, 33 | { 34 | "idiom" : "iphone", 35 | "scale" : "2x", 36 | "size" : "60x60" 37 | }, 38 | { 39 | "idiom" : "iphone", 40 | "scale" : "3x", 41 | "size" : "60x60" 42 | }, 43 | { 44 | "idiom" : "ipad", 45 | "scale" : "1x", 46 | "size" : "20x20" 47 | }, 48 | { 49 | "idiom" : "ipad", 50 | "scale" : "2x", 51 | "size" : "20x20" 52 | }, 53 | { 54 | "idiom" : "ipad", 55 | "scale" : "1x", 56 | "size" : "29x29" 57 | }, 58 | { 59 | "idiom" : "ipad", 60 | "scale" : "2x", 61 | "size" : "29x29" 62 | }, 63 | { 64 | "idiom" : "ipad", 65 | "scale" : "1x", 66 | "size" : "40x40" 67 | }, 68 | { 69 | "idiom" : "ipad", 70 | "scale" : "2x", 71 | "size" : "40x40" 72 | }, 73 | { 74 | "idiom" : "ipad", 75 | "scale" : "1x", 76 | "size" : "76x76" 77 | }, 78 | { 79 | "idiom" : "ipad", 80 | "scale" : "2x", 81 | "size" : "76x76" 82 | }, 83 | { 84 | "idiom" : "ipad", 85 | "scale" : "2x", 86 | "size" : "83.5x83.5" 87 | }, 88 | { 89 | "idiom" : "ios-marketing", 90 | "scale" : "1x", 91 | "size" : "1024x1024" 92 | } 93 | ], 94 | "info" : { 95 | "author" : "xcode", 96 | "version" : 1 97 | } 98 | } -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/MKLogo.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "MKLogo.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "filename" : "MKLogo 2.png", 10 | "idiom" : "universal", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "filename" : "MKLogo 1.png", 15 | "idiom" : "universal", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "author" : "xcode", 21 | "version" : 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo 1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo 1.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo 2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo 2.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/iosApp/iosApp/Assets.xcassets/MKLogo.imageset/MKLogo.png -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/mind_map.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "mind_map.svg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | import SwiftUI 2 | import shared 3 | 4 | 5 | struct ContentView: UIViewControllerRepresentable { 6 | func makeUIViewController(context: Context) -> UIViewController { 7 | return Main_iosKt.homeScreenViewController() 8 | } 9 | 10 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) { 11 | } 12 | } 13 | 14 | struct ContentView_Previews: PreviewProvider { 15 | static var previews: some View { 16 | ContentView() 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | 1.0 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UIApplicationSceneManifest 26 | 27 | UIApplicationSupportsMultipleScenes 28 | 29 | 30 | UILaunchScreen 31 | 32 | UILaunchStoryboardName 33 | LaunchScreen.storyboard 34 | UIRequiredDeviceCapabilities 35 | 36 | armv7 37 | 38 | UISupportedInterfaceOrientations 39 | 40 | UIInterfaceOrientationPortrait 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UISupportedInterfaceOrientations~ipad 45 | 46 | UIInterfaceOrientationPortrait 47 | UIInterfaceOrientationPortraitUpsideDown 48 | UIInterfaceOrientationLandscapeLeft 49 | UIInterfaceOrientationLandscapeRight 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /iosApp/iosApp/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /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 | 4 | @main 5 | struct iOSApp: App { 6 | 7 | init() { 8 | HelperKt.doInitKoin() 9 | } 10 | 11 | var body: some Scene { 12 | WindowGroup { 13 | ContentView() 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | google() 4 | gradlePluginPortal() 5 | mavenCentral() 6 | } 7 | } 8 | 9 | dependencyResolutionManagement { 10 | repositories { 11 | google() 12 | mavenCentral() 13 | maven("https://androidx.dev/storage/compose-compiler/repository") 14 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 15 | maven("https://maven.pkg.jetbrains.space/public/p/ktor/eap") 16 | } 17 | 18 | versionCatalogs { 19 | create("libs") 20 | } 21 | } 22 | 23 | rootProject.name = "MultiplatformKickstarter" 24 | include(":composeApp") 25 | include(":shared") 26 | include(":backend") 27 | -------------------------------------------------------------------------------- /shared/src/androidMain/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /shared/src/androidMain/drawable/mklogo.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/Debug.android.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.debugmenu 2 | 3 | import co.touchlab.kermit.BuildConfig 4 | 5 | class AndroidDebug : Debug { 6 | override val isDebug: Boolean = BuildConfig.DEBUG 7 | } 8 | 9 | actual fun getDebug(): Debug = AndroidDebug() 10 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/multiplatformkickstarter/app/localization/Localization.android.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.localization 2 | 3 | import android.content.res.Configuration 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import java.util.Locale 7 | 8 | actual fun getCurrentLanguage(): AvailableLanguages { 9 | return when (Locale.getDefault().language) { 10 | AvailableLanguages.ES.name.lowercase() -> AvailableLanguages.ES 11 | AvailableLanguages.EN.name.lowercase() -> AvailableLanguages.EN 12 | AvailableLanguages.IT.name.lowercase() -> AvailableLanguages.IT 13 | AvailableLanguages.FR.name.lowercase() -> AvailableLanguages.FR 14 | AvailableLanguages.DE.name.lowercase() -> AvailableLanguages.DE 15 | else -> AvailableLanguages.EN 16 | } 17 | } 18 | 19 | @Composable 20 | actual fun SetLanguage(language: AvailableLanguages) { 21 | val context = LocalContext.current 22 | val locale = Locale(language.name.lowercase()) 23 | Locale.setDefault(locale) 24 | val config: Configuration = context.resources.configuration 25 | config.setLocale(locale) 26 | context.resources.updateConfiguration(config, context.resources.displayMetrics) 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/androidMain/kotlin/com/multiplatformkickstarter/app/platform/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import android.annotation.SuppressLint 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.platform.LocalContext 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | 10 | @SuppressLint("DiscouragedApi") 11 | @Composable 12 | actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { 13 | val context = LocalContext.current 14 | val id = context.resources.getIdentifier(res, "font", context.packageName) 15 | return Font(id, weight, style) 16 | } 17 | 18 | @SuppressLint("DiscouragedApi") 19 | @Composable 20 | fun getResourceId(name: String?, resourceFolder: String?): Int { 21 | val context = LocalContext.current 22 | return context.resources.getIdentifier(name, resourceFolder, context.packageName) 23 | } 24 | -------------------------------------------------------------------------------- /shared/src/androidMain/resources/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /shared/src/androidMain/resources/drawable/mklogo.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/drawable/mklogo.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_black.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_black_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_black_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_extra_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_extra_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_extra_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_extra_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_extra_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_extra_light.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_extra_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_extra_light_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_light.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_light_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_medium.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_medium_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_regular.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_semi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_semi_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_semi_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_semi_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_thin.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/composeResources/font/poppins_thin_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/composeResources/font/poppins_thin_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/MainApp.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.navigator.Navigator 5 | import cafe.adriel.voyager.transitions.ScaleTransition 6 | import com.multiplatformkickstarter.app.ui.screens.MainScreen 7 | 8 | @Composable 9 | fun MainApp() { 10 | Navigator(MainScreen()) { 11 | ScaleTransition(it) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/GeoLocation.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class GeoLocation(val latitude: Double, val longitude: Double) 7 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/PetCategory.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | enum class PetCategory(val id: Int) { 4 | DOGS(0), 5 | CATS(1), 6 | BIRDS(2), 7 | RABBITS(3), 8 | SMALL_AND_FURRY(4), 9 | HORSES(5), 10 | OTHER(6); 11 | 12 | companion object 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/PetModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | class PetModel( 7 | val id: Int, 8 | val title: String, 9 | val description: String, 10 | val images: List, 11 | val category: PetCategory, 12 | val location: GeoLocation, 13 | val published: String, 14 | val modified: String?, 15 | val breed: String, 16 | val age: PetAge, 17 | val gender: PetGender, 18 | val size: PetSize, 19 | val color: String, 20 | val status: PetStatus, 21 | val shelterId: Int?, 22 | val userId: Int 23 | ) 24 | 25 | enum class PetAge { 26 | BABY, YOUNG, ADULT, SENIOR; 27 | 28 | companion object 29 | } 30 | 31 | enum class PetGender { 32 | MALE, FEMALE; 33 | 34 | companion object 35 | } 36 | 37 | enum class PetSize { 38 | SMALL, MEDIUM, LARGE, XLARGE; 39 | 40 | companion object 41 | } 42 | 43 | enum class PetStatus { 44 | ADOPTABLE, FOUND; 45 | 46 | companion object 47 | } 48 | 49 | data class PetResponse(val id: Int) 50 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/Profile.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Profile( 7 | val id: Int, 8 | val userId: Int, 9 | val name: String, 10 | val description: String?, 11 | val image: String?, 12 | val location: GeoLocation?, 13 | val rating: Double? 14 | ) 15 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/SearchModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class SearchList(val list: List) 7 | 8 | @Serializable 9 | data class SearchModel( 10 | val text: String? = null, 11 | val petCategory: PetCategory? = null, 12 | val location: GeoLocation? = null 13 | ) 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/User.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class User( 7 | val id: Int, 8 | val name: String, 9 | val email: String 10 | ) 11 | 12 | data class AuthenticationResponse( 13 | val id: Int, 14 | val session: String, 15 | val token: String 16 | ) 17 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/UserData.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model 2 | 3 | import com.multiplatformkickstarter.app.common.model.theme.DarkThemeConfig 4 | import com.multiplatformkickstarter.app.common.model.theme.ThemeBrand 5 | 6 | data class UserData( 7 | val bookmarkedNewsResources: Set, 8 | val followedTopics: Set, 9 | val followedAuthors: Set, 10 | val themeBrand: ThemeBrand, 11 | val darkThemeConfig: DarkThemeConfig 12 | ) 13 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/theme/DarkThemeConfig.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model.theme 2 | 3 | enum class DarkThemeConfig { 4 | FOLLOW_SYSTEM, LIGHT, DARK 5 | } 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/model/theme/ThemeBrand.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.model.theme 2 | 3 | enum class ThemeBrand { 4 | DEFAULT, ANDROID 5 | } 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/common/usecases/UseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.common.usecases 2 | 3 | import kotlinx.coroutines.CoroutineDispatcher 4 | import kotlinx.coroutines.CoroutineScope 5 | import kotlinx.coroutines.Dispatchers 6 | import kotlinx.coroutines.withContext 7 | 8 | open class UseCase { 9 | 10 | /** 11 | * This function is responsible for running the Use Case's code block into a Background Thread 12 | * [Documentation](https://developer.android.com/kotlin/coroutines/coroutines-best-practices#main-safe) 13 | */ 14 | suspend fun execute( 15 | dispatcher: CoroutineDispatcher = Dispatchers.Default, 16 | code: suspend CoroutineScope.() -> R 17 | ): R = withContext(dispatcher) { code() } 18 | } 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/data/repositories/SessionRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.data.repositories 2 | 3 | import com.russhwolf.settings.Settings 4 | 5 | const val USER_ID_KEY = "LOGGED_IN_USER_ID_KEY" 6 | const val USER_EMAIL_KEY = "LOGGED_IN_USER_EMAIL_KEY" 7 | const val USER_SESSION_KEY = "USER_SESSION_KEY" 8 | const val USER_TOKEN_KEY = "USER_TOKEN_KEY" 9 | const val IS_LOGGED_IN_KEY = "IS_LOGGED_IN_KEY" 10 | 11 | class SessionRepository(private val settings: Settings) { 12 | 13 | fun getUserId(): Int { 14 | return settings.getInt(USER_ID_KEY, -1) 15 | } 16 | 17 | fun getEmail(): String { 18 | return settings.getString(USER_EMAIL_KEY, "") 19 | } 20 | 21 | fun getSession(): String { 22 | return settings.getString(USER_SESSION_KEY, "") 23 | } 24 | 25 | fun getToken(): String { 26 | return settings.getString(USER_TOKEN_KEY, "") 27 | } 28 | 29 | fun isLoggedIn(): Boolean { 30 | return settings.getBoolean(IS_LOGGED_IN_KEY, false) 31 | } 32 | 33 | fun initSession(id: Int, email: String, session: String, token: String) { 34 | settings.putInt(USER_ID_KEY, id) 35 | settings.putString(USER_EMAIL_KEY, email) 36 | settings.putString(USER_SESSION_KEY, session) 37 | settings.putString(USER_TOKEN_KEY, token) 38 | settings.putBoolean(IS_LOGGED_IN_KEY, true) 39 | } 40 | 41 | fun clear() = settings.clear() 42 | } 43 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/data/usecases/GetLastSearchUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.data.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.model.PetModel 4 | import com.multiplatformkickstarter.app.common.usecases.UseCase 5 | import com.multiplatformkickstarter.app.data.repositories.LastSearchAdsMockRepository 6 | import com.multiplatformkickstarter.app.feature.debugmenu.repositories.GlobalAppSettingsRepository 7 | import com.multiplatformkickstarter.app.feature.search.repositories.LastSearchesRepository 8 | import com.multiplatformkickstarter.app.feature.search.repositories.PetsFromSearchRepository 9 | 10 | class GetLastSearchUseCase( 11 | private val lastSearchesRepository: LastSearchesRepository, 12 | private val petsFromSearchRepository: PetsFromSearchRepository, 13 | private val lastSearchAdsMockRepository: LastSearchAdsMockRepository, 14 | private val globalAppSettingsRepository: GlobalAppSettingsRepository 15 | ) : UseCase() { 16 | 17 | suspend fun invoke(): Result> = runCatching { 18 | val searchesList = lastSearchesRepository.get() 19 | return@runCatching if (globalAppSettingsRepository.isMockedContentEnabled()) { 20 | lastSearchAdsMockRepository.getAds().getOrDefault(emptyList()) 21 | } else if (searchesList.list.isNotEmpty()) { 22 | petsFromSearchRepository.getPets(searchesList.list.last()).pets 23 | } else { 24 | emptyList() 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/data/usecases/GetNearMeAdsUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.data.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.model.PetModel 4 | import com.multiplatformkickstarter.app.common.model.SearchModel 5 | import com.multiplatformkickstarter.app.common.usecases.UseCase 6 | import com.multiplatformkickstarter.app.data.repositories.NearMeAdsMockRepository 7 | import com.multiplatformkickstarter.app.data.repositories.ProfileRepository 8 | import com.multiplatformkickstarter.app.feature.debugmenu.repositories.GlobalAppSettingsRepository 9 | import com.multiplatformkickstarter.app.feature.search.repositories.PetsFromSearchRepository 10 | 11 | class GetNearMeAdsUseCase( 12 | private val profileRepository: ProfileRepository, 13 | private val petsFromSearchRepository: PetsFromSearchRepository, 14 | private val nearMeAdsMockRepository: NearMeAdsMockRepository, 15 | private val globalAppSettingsRepository: GlobalAppSettingsRepository 16 | ) : UseCase() { 17 | 18 | suspend fun invoke(): Result> = runCatching { 19 | val location = profileRepository.getLocation() 20 | return@runCatching if (globalAppSettingsRepository.isMockedContentEnabled()) { 21 | nearMeAdsMockRepository.getAds().getOrDefault(emptyList()) 22 | } else if (location != null) { 23 | val searchModel = SearchModel(location = location) 24 | petsFromSearchRepository.getPets(searchModel).pets 25 | } else { 26 | emptyList() 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/data/usecases/GetSearchUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.data.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.model.PetCategory 4 | import com.multiplatformkickstarter.app.common.model.PetModel 5 | import com.multiplatformkickstarter.app.common.usecases.UseCase 6 | 7 | class GetSearchUseCase( 8 | private val nearMeAdsUseCase: GetNearMeAdsUseCase 9 | ) : UseCase() { 10 | 11 | suspend fun invoke(searchId: Int): Result> = runCatching { 12 | return@runCatching emptyList() 13 | } 14 | 15 | suspend fun invoke(petCategory: PetCategory): Result> = runCatching { 16 | return@runCatching nearMeAdsUseCase.invoke().getOrDefault, List>(emptyList()) 17 | .filter { it.category == petCategory } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/data/usecases/LoadProfileUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.data.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.usecases.UseCase 4 | import com.multiplatformkickstarter.app.data.repositories.ProfileRepository 5 | import com.multiplatformkickstarter.app.data.repositories.SessionRepository 6 | 7 | class LoadProfileUseCase( 8 | private val profileRepository: ProfileRepository, 9 | private val sessionRepository: SessionRepository 10 | ) : UseCase() { 11 | 12 | suspend fun invoke(userId: Int) { 13 | if (sessionRepository.isLoggedIn()) { 14 | val profile = profileRepository.getProfile(userId) 15 | profileRepository.initProfile(userId, profile.name, profile.description, profile.image, profile.location, profile.rating) 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/di/AppModule.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.di 2 | 3 | import com.multiplatformkickstarter.app.feature.loginsignup.di.logInSignUpModule 4 | import com.multiplatformkickstarter.app.feature.pets.di.petsModule 5 | 6 | fun appModule() = listOf( 7 | commonModule, 8 | logInSignUpModule, 9 | petsModule 10 | ) 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/extensions/KtorExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.extensions 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.call.body 5 | import io.ktor.client.plugins.ClientRequestException 6 | import io.ktor.client.plugins.ResponseException 7 | import io.ktor.client.plugins.ServerResponseException 8 | import io.ktor.client.request.HttpRequestBuilder 9 | import io.ktor.client.request.request 10 | import io.ktor.utils.io.errors.IOException 11 | import kotlinx.serialization.SerializationException 12 | 13 | suspend fun HttpClient.requestAndCatch( 14 | block: suspend HttpClient.() -> T, 15 | errorHandler: suspend ResponseException.() -> T 16 | ): T = runCatching { block() } 17 | .getOrElse { 18 | when (it) { 19 | is ResponseException -> it.errorHandler() 20 | else -> throw it 21 | } 22 | } 23 | 24 | suspend inline fun HttpClient.safeRequest( 25 | block: HttpRequestBuilder.() -> Unit 26 | ): ApiResponse = 27 | try { 28 | val response = request { block() } 29 | ApiResponse.Success(response.body()) 30 | } catch (e: ClientRequestException) { 31 | ApiResponse.Error.HttpError(e.response.status.value, e.errorBody()) 32 | } catch (e: ServerResponseException) { 33 | ApiResponse.Error.HttpError(e.response.status.value, e.errorBody()) 34 | } catch (e: IOException) { 35 | ApiResponse.Error.NetworkError 36 | } catch (e: SerializationException) { 37 | ApiResponse.Error.SerializationError 38 | } 39 | 40 | suspend inline fun ResponseException.errorBody(): E? = 41 | try { 42 | response.body() 43 | } catch (e: SerializationException) { 44 | null 45 | } 46 | 47 | sealed class ApiResponse { 48 | /** 49 | * Represents successful network responses (2xx). 50 | */ 51 | data class Success(val body: T) : ApiResponse() 52 | 53 | sealed class Error : ApiResponse() { 54 | /** 55 | * Represents server (50x) and client (40x) errors. 56 | */ 57 | data class HttpError(val code: Int, val errorBody: E?) : Error() 58 | 59 | /** 60 | * Represent IOExceptions and connectivity issues. 61 | */ 62 | data object NetworkError : Error() 63 | 64 | /** 65 | * Represent SerializationExceptions. 66 | */ 67 | data object SerializationError : Error() 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/Debug.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.debugmenu 2 | 3 | interface Debug { 4 | val isDebug: Boolean 5 | } 6 | 7 | expect fun getDebug(): Debug 8 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/viewmodel/DebugMenuViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.debugmenu.viewmodel 2 | 3 | import cafe.adriel.voyager.core.model.ScreenModel 4 | import com.multiplatformkickstarter.app.feature.debugmenu.repositories.GlobalAppSettingsRepository 5 | import com.multiplatformkickstarter.app.localization.AvailableLanguages 6 | import com.multiplatformkickstarter.app.platform.Environment 7 | import kotlinx.coroutines.channels.Channel 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.MutableStateFlow 10 | import kotlinx.coroutines.flow.asStateFlow 11 | import kotlinx.coroutines.flow.receiveAsFlow 12 | 13 | class DebugMenuViewModel( 14 | private val globalAppSettingsRepository: GlobalAppSettingsRepository 15 | ) : ScreenModel { 16 | private val _state = MutableStateFlow( 17 | DebugMenuState( 18 | currentEnvironment = globalAppSettingsRepository.getCurrentEnvironment(), 19 | currentLanguage = globalAppSettingsRepository.getCurrentLanguage(), 20 | isMockedContentChecked = globalAppSettingsRepository.isMockedContentEnabled(), 21 | isMockedUserChecked = globalAppSettingsRepository.isMockedUserEnabled() 22 | ) 23 | ) 24 | val state = _state.asStateFlow() 25 | private val _sideEffects = Channel() 26 | val sideEffects: Flow = _sideEffects.receiveAsFlow() 27 | 28 | fun onSelectedEnvironment(environment: Environment) { 29 | _state.value = _state.value.copy(currentEnvironment = environment) 30 | globalAppSettingsRepository.setSelectedEnvironment(environment) 31 | } 32 | 33 | fun onSelectedLanguage(language: AvailableLanguages) { 34 | _state.value = _state.value.copy(currentLanguage = language) 35 | globalAppSettingsRepository.setSelectedLanguage(language) 36 | } 37 | 38 | fun onMockedContentCheckChanged(checked: Boolean) { 39 | _state.value = _state.value.copy(isMockedContentChecked = checked) 40 | globalAppSettingsRepository.setMockedContentCheckStatus(checked) 41 | } 42 | 43 | fun onMockedUserCheckChanged(checked: Boolean) { 44 | _state.value = _state.value.copy(isMockedUserChecked = checked) 45 | globalAppSettingsRepository.setMockedUserCheckStatus(checked) 46 | } 47 | } 48 | 49 | data class DebugMenuState( 50 | val currentEnvironment: Environment, 51 | val currentLanguage: AvailableLanguages, 52 | val isMockedContentChecked: Boolean, 53 | val isMockedUserChecked: Boolean 54 | ) 55 | 56 | sealed class DebugMenuSideEffects { 57 | data object Initial : DebugMenuSideEffects() 58 | } 59 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/loginsignup/di/LogInSignUpModule.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.loginsignup.di 2 | 3 | import com.multiplatformkickstarter.app.data.repositories.AuthenticationRepository 4 | import com.multiplatformkickstarter.app.data.usecases.LoadProfileUseCase 5 | import com.multiplatformkickstarter.app.feature.loginsignup.usecases.EmailLogInUseCase 6 | import com.multiplatformkickstarter.app.feature.loginsignup.usecases.EmailSignUpUseCase 7 | import com.multiplatformkickstarter.app.feature.loginsignup.viewmodels.EmailLoginViewModel 8 | import com.multiplatformkickstarter.app.feature.loginsignup.viewmodels.EmailSignUpViewModel 9 | import org.koin.core.module.dsl.factoryOf 10 | import org.koin.dsl.module 11 | 12 | val logInSignUpModule = module { 13 | factory { 14 | EmailLoginViewModel(get(), get(), get(), get(), get()) 15 | } 16 | factory { 17 | EmailSignUpViewModel(get(), get(), get(), get(), get()) 18 | } 19 | 20 | factoryOf(::EmailLogInUseCase) 21 | factoryOf(::EmailSignUpUseCase) 22 | factory { LoadProfileUseCase(get(), get()) } 23 | factory { AuthenticationRepository(get()) } 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/loginsignup/usecases/EmailLogInUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.loginsignup.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.usecases.UseCase 4 | import com.multiplatformkickstarter.app.data.repositories.AuthenticationRepository 5 | import com.multiplatformkickstarter.app.data.repositories.SessionRepository 6 | 7 | class EmailLogInUseCase( 8 | private val authRepository: AuthenticationRepository, 9 | private val sessionRepository: SessionRepository 10 | ) : UseCase() { 11 | 12 | suspend fun invoke(email: String, password: String): Result = runCatching { 13 | val loginResponse = authRepository.login(email, password) 14 | sessionRepository.initSession(loginResponse.id, email, loginResponse.session, loginResponse.token) 15 | return@runCatching loginResponse.id 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/loginsignup/usecases/EmailSignUpUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.loginsignup.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.usecases.UseCase 4 | import com.multiplatformkickstarter.app.data.repositories.AuthenticationRepository 5 | import com.multiplatformkickstarter.app.data.repositories.SessionRepository 6 | 7 | class EmailSignUpUseCase( 8 | private val authRepository: AuthenticationRepository, 9 | private val sessionRepository: SessionRepository 10 | ) : UseCase() { 11 | 12 | suspend fun invoke(name: String, email: String, password: String): Result = runCatching { 13 | val loginResponse = authRepository.signUp(name, email, password) 14 | sessionRepository.initSession(loginResponse.id, email, loginResponse.session, loginResponse.token) 15 | return@runCatching loginResponse.id 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/pets/di/PetsModule.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.pets.di 2 | 3 | import cafe.adriel.voyager.navigator.Navigator 4 | import com.multiplatformkickstarter.app.feature.pets.viewmodels.MyPetsViewModel 5 | import org.koin.dsl.module 6 | 7 | val petsModule = module { 8 | factory { (navigator: Navigator) -> 9 | MyPetsViewModel(navigator) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/pets/viewmodels/MyPetsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.pets.viewmodels 2 | 3 | import cafe.adriel.voyager.core.model.ScreenModel 4 | import cafe.adriel.voyager.core.model.screenModelScope 5 | import cafe.adriel.voyager.navigator.Navigator 6 | import com.multiplatformkickstarter.app.common.model.PetModel 7 | import com.multiplatformkickstarter.app.feature.pets.MyPetsScreen 8 | import kotlinx.coroutines.channels.Channel 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | import kotlinx.coroutines.flow.receiveAsFlow 13 | import kotlinx.coroutines.launch 14 | 15 | class MyPetsViewModel( 16 | private val navigator: Navigator 17 | ) : ScreenModel { 18 | private val _state = MutableStateFlow(MyPetsState(emptyList())) 19 | val state = _state.asStateFlow() 20 | private val _sideEffects = Channel() 21 | val sideEffects: Flow = _sideEffects.receiveAsFlow() 22 | 23 | fun invoke() { 24 | screenModelScope.launch { 25 | } 26 | } 27 | 28 | fun onMyPetsClicked() { 29 | navigator.push(MyPetsScreen()) 30 | } 31 | } 32 | 33 | data class MyPetsState( 34 | val ads: List 35 | ) 36 | 37 | sealed class MyPetsSideEffects { 38 | data object Initial : MyPetsSideEffects() 39 | } 40 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/petupload/repositories/PetUploadPublishRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.petupload.repositories 2 | 3 | import com.multiplatformkickstarter.app.common.model.GeoLocation 4 | import com.multiplatformkickstarter.app.common.model.PetAge 5 | import com.multiplatformkickstarter.app.common.model.PetCategory 6 | import com.multiplatformkickstarter.app.common.model.PetGender 7 | import com.multiplatformkickstarter.app.common.model.PetResponse 8 | import com.multiplatformkickstarter.app.common.model.PetSize 9 | import com.multiplatformkickstarter.app.common.model.PetStatus 10 | import com.multiplatformkickstarter.app.data.repositories.BadRequestException 11 | import com.multiplatformkickstarter.app.data.repositories.UnauthorizedException 12 | import com.multiplatformkickstarter.app.extensions.requestAndCatch 13 | import com.multiplatformkickstarter.app.platform.ServerEnvironment 14 | import com.multiplatformkickstarter.app.platform.ServiceClient 15 | import io.ktor.client.request.forms.submitForm 16 | import io.ktor.http.HttpStatusCode 17 | import io.ktor.http.Parameters 18 | 19 | private const val PET_UPLOAD_PATH = "v1/pets/upload" 20 | class PetUploadPublishRepository(private val service: ServiceClient) { 21 | 22 | suspend fun upload( 23 | name: String, 24 | description: String, 25 | images: List, 26 | category: PetCategory, 27 | location: GeoLocation, 28 | breed: String, 29 | color: String, 30 | age: PetAge, 31 | gender: PetGender, 32 | size: PetSize, 33 | status: PetStatus 34 | ): PetResponse { 35 | return service.httpClient.requestAndCatch( 36 | { 37 | val response = this.submitForm( 38 | url = "${ServerEnvironment.PRODUCTION.url}/$PET_UPLOAD_PATH", 39 | formParameters = Parameters.build { 40 | append("name", name) 41 | append("description", description) 42 | append("images", images.toString()) 43 | append("category", category.name) 44 | append("location", location.toString()) 45 | append("breed", breed) 46 | append("color", color) 47 | append("age", age.name) 48 | append("gender", gender.name) 49 | append("size", size.name) 50 | append("status", status.name) 51 | } 52 | ) 53 | PetResponse( 54 | id = response.headers["pet_id"]?.toInt()!! 55 | ) 56 | }, 57 | { 58 | when (response.status) { 59 | HttpStatusCode.Unauthorized -> { 60 | throw UnauthorizedException() 61 | } 62 | HttpStatusCode.BadRequest -> { 63 | throw BadRequestException() 64 | } 65 | else -> throw this 66 | } 67 | } 68 | ) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/petupload/usecases/PetUploadUseCase.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.petupload.usecases 2 | 3 | import com.multiplatformkickstarter.app.common.model.GeoLocation 4 | import com.multiplatformkickstarter.app.common.model.PetAge 5 | import com.multiplatformkickstarter.app.common.model.PetCategory 6 | import com.multiplatformkickstarter.app.common.model.PetGender 7 | import com.multiplatformkickstarter.app.common.model.PetSize 8 | import com.multiplatformkickstarter.app.common.model.PetStatus 9 | import com.multiplatformkickstarter.app.common.usecases.UseCase 10 | import com.multiplatformkickstarter.app.feature.debugmenu.repositories.GlobalAppSettingsRepository 11 | import com.multiplatformkickstarter.app.feature.petupload.repositories.PetUploadPublishRepository 12 | import kotlin.random.Random 13 | 14 | class PetUploadUseCase( 15 | private val petUploadPublishRepository: PetUploadPublishRepository, 16 | private val globalAppSettingsRepository: GlobalAppSettingsRepository 17 | ) : UseCase() { 18 | 19 | suspend fun invoke( 20 | name: String, 21 | description: String, 22 | images: List, 23 | category: PetCategory, 24 | location: GeoLocation, 25 | breed: String, 26 | color: String, 27 | age: PetAge, 28 | gender: PetGender, 29 | size: PetSize, 30 | status: PetStatus 31 | ): Result = runCatching { 32 | if (!globalAppSettingsRepository.isMockedContentEnabled()) { 33 | val result = petUploadPublishRepository.upload( 34 | name, description, images, category, location, breed, color, age, gender, size, status 35 | ) 36 | return@runCatching result.id 37 | } else { 38 | return@runCatching Random(10000).nextInt() 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/profile/viewmodels/ProfileDetailViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.profile.viewmodels 2 | 3 | import cafe.adriel.voyager.core.model.ScreenModel 4 | import cafe.adriel.voyager.core.model.screenModelScope 5 | import cafe.adriel.voyager.navigator.Navigator 6 | import com.multiplatformkickstarter.app.common.model.GeoLocation 7 | import com.multiplatformkickstarter.app.common.model.PetModel 8 | import com.multiplatformkickstarter.app.data.repositories.ProfileRepository 9 | import com.multiplatformkickstarter.app.data.repositories.SessionRepository 10 | import com.multiplatformkickstarter.app.ui.screens.PetDetailScreen 11 | import kotlinx.coroutines.channels.Channel 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.receiveAsFlow 16 | import kotlinx.coroutines.launch 17 | 18 | class ProfileDetailViewModel( 19 | private val userId: Int, 20 | private var navigator: Navigator, 21 | private val profileRepository: ProfileRepository, 22 | private val sessionRepository: SessionRepository 23 | ) : ScreenModel { 24 | private val _state = MutableStateFlow(ProfileDetailState("", "", "", null, 0.0, emptyList())) 25 | val state = _state.asStateFlow() 26 | private val _sideEffects = Channel() 27 | val sideEffects: Flow = _sideEffects.receiveAsFlow() 28 | 29 | fun onStarted() { 30 | screenModelScope.launch { 31 | val profile = if (userId == sessionRepository.getUserId()) { 32 | profileRepository.getProfile() 33 | } else { 34 | profileRepository.getProfile(userId) 35 | } 36 | val pets = emptyList() 37 | _state.value = ProfileDetailState( 38 | name = profile.name, 39 | description = profile.description, 40 | image = profile.image, 41 | location = profile.location, 42 | rating = profile.rating, 43 | pets = pets 44 | ) 45 | } 46 | } 47 | 48 | fun onPetDetailClicked(id: Int) { 49 | navigator.push(PetDetailScreen(id)) 50 | } 51 | } 52 | 53 | data class ProfileDetailState( 54 | val name: String, 55 | val description: String?, 56 | val image: String?, 57 | val location: GeoLocation?, 58 | val rating: Double?, 59 | val pets: List 60 | ) 61 | 62 | sealed class ProfileDetailSideEffects { 63 | data object Initial : ProfileDetailSideEffects() 64 | } 65 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/search/repositories/LastSearchesRepository.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalSerializationApi::class, ExperimentalSettingsApi::class) 2 | 3 | package com.multiplatformkickstarter.app.feature.search.repositories 4 | 5 | import com.multiplatformkickstarter.app.common.model.SearchList 6 | import com.multiplatformkickstarter.app.common.model.SearchModel 7 | import com.russhwolf.settings.ExperimentalSettingsApi 8 | import com.russhwolf.settings.Settings 9 | import com.russhwolf.settings.serialization.decodeValue 10 | import com.russhwolf.settings.serialization.encodeValue 11 | import kotlinx.serialization.ExperimentalSerializationApi 12 | 13 | const val LAST_SEARCHES_KEY = "LAST_SEARCH_KEY" 14 | const val MAXIMUM_SAVED_SEARCHES = 10 15 | 16 | class LastSearchesRepository( 17 | private val settings: Settings 18 | ) { 19 | fun add(searchModel: SearchModel) { 20 | var lastSearches = settings.decodeValue(SearchList.serializer(), LAST_SEARCHES_KEY, SearchList(emptyList())) 21 | 22 | lastSearches = if (lastSearches.list.size < MAXIMUM_SAVED_SEARCHES) { 23 | lastSearches.copy(list = lastSearches.list.plus(searchModel)) 24 | } else { 25 | lastSearches.copy(list = lastSearches.list.subList(1, MAXIMUM_SAVED_SEARCHES - 1).plus(searchModel)) 26 | } 27 | 28 | settings.encodeValue(SearchList.serializer(), LAST_SEARCHES_KEY, lastSearches) 29 | } 30 | 31 | fun get(): SearchList { 32 | return settings.decodeValue(SearchList.serializer(), LAST_SEARCHES_KEY, SearchList(emptyList())) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/feature/search/repositories/PetsFromSearchRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.search.repositories 2 | 3 | import co.touchlab.kermit.Logger 4 | import com.multiplatformkickstarter.app.common.model.PetModel 5 | import com.multiplatformkickstarter.app.common.model.SearchModel 6 | import com.multiplatformkickstarter.app.extensions.requestAndCatch 7 | import com.multiplatformkickstarter.app.platform.ServerEnvironment 8 | import com.multiplatformkickstarter.app.platform.ServiceClient 9 | import io.ktor.client.call.body 10 | import io.ktor.client.request.get 11 | import io.ktor.http.HttpStatusCode 12 | 13 | private const val SEARCH_PATH = "v1/search" 14 | 15 | class PetsFromSearchRepository(private val service: ServiceClient) { 16 | 17 | suspend fun getPets(searchModel: SearchModel): GetPetsResponse { 18 | Logger.d(searchModel.text.toString()) 19 | return service.httpClient.requestAndCatch( 20 | { 21 | this.get("${ServerEnvironment.PRODUCTION.url}/$SEARCH_PATH/").body() 22 | }, 23 | { 24 | when (response.status) { 25 | // TODO: Return correct errors 26 | HttpStatusCode.BadRequest -> { throw this } 27 | HttpStatusCode.Conflict -> { throw this } 28 | else -> throw this 29 | } 30 | } 31 | ) 32 | } 33 | } 34 | 35 | data class GetPetsResponse(val pets: List) 36 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/navigation/FavoritesTab.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 6 | import cafe.adriel.voyager.navigator.tab.Tab 7 | import cafe.adriel.voyager.navigator.tab.TabOptions 8 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 9 | import com.multiplatformkickstarter.app.ui.icon.MultiplatformKickstarterIcons 10 | import com.multiplatformkickstarter.app.ui.screens.ProTemplateFeature 11 | 12 | internal object FavoritesTab : Tab { 13 | 14 | override val options: TabOptions 15 | @Composable 16 | get() { 17 | val icon = rememberVectorPainter(MultiplatformKickstarterIcons.Favorite) 18 | 19 | return remember { 20 | TabOptions( 21 | index = 1u, 22 | title = getCurrentLocalization().favorites, 23 | icon = icon 24 | ) 25 | } 26 | } 27 | 28 | @Composable 29 | override fun Content() { 30 | ProTemplateFeature() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/navigation/HomeTab.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 6 | import cafe.adriel.voyager.navigator.Navigator 7 | import cafe.adriel.voyager.navigator.tab.Tab 8 | import cafe.adriel.voyager.navigator.tab.TabOptions 9 | import cafe.adriel.voyager.transitions.ScaleTransition 10 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 11 | import com.multiplatformkickstarter.app.ui.icon.MultiplatformKickstarterIcons 12 | import com.multiplatformkickstarter.app.ui.screens.HomeTabScreen 13 | 14 | internal object HomeTab : Tab { 15 | 16 | override val options: TabOptions 17 | @Composable 18 | get() { 19 | val icon = rememberVectorPainter(MultiplatformKickstarterIcons.Home) 20 | 21 | return remember { 22 | TabOptions( 23 | index = 0u, 24 | title = getCurrentLocalization().home, 25 | icon = icon 26 | ) 27 | } 28 | } 29 | 30 | @Composable 31 | override fun Content() { 32 | Navigator(HomeTabScreen()) { 33 | ScaleTransition(it) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/navigation/InboxTab.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 6 | import cafe.adriel.voyager.navigator.tab.Tab 7 | import cafe.adriel.voyager.navigator.tab.TabOptions 8 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 9 | import com.multiplatformkickstarter.app.ui.icon.MultiplatformKickstarterIcons 10 | import com.multiplatformkickstarter.app.ui.screens.ProTemplateFeature 11 | 12 | internal object InboxTab : Tab { 13 | 14 | override val options: TabOptions 15 | @Composable 16 | get() { 17 | val icon = rememberVectorPainter(MultiplatformKickstarterIcons.Inbox) 18 | 19 | return remember { 20 | TabOptions( 21 | index = 3u, 22 | title = getCurrentLocalization().inbox, 23 | icon = icon 24 | ) 25 | } 26 | } 27 | 28 | @Composable 29 | override fun Content() { 30 | ProTemplateFeature() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/navigation/PetUploadTab.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 6 | import cafe.adriel.voyager.navigator.Navigator 7 | import cafe.adriel.voyager.navigator.tab.Tab 8 | import cafe.adriel.voyager.navigator.tab.TabOptions 9 | import cafe.adriel.voyager.transitions.ScaleTransition 10 | import com.multiplatformkickstarter.app.feature.petupload.PetUploadScreen 11 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 12 | import com.multiplatformkickstarter.app.ui.icon.MultiplatformKickstarterIcons 13 | 14 | internal object PetUploadTab : Tab { 15 | private val localization = getCurrentLocalization() 16 | 17 | override val options: TabOptions 18 | @Composable 19 | get() { 20 | val icon = rememberVectorPainter(MultiplatformKickstarterIcons.Create) 21 | 22 | return remember { 23 | TabOptions( 24 | index = 2u, 25 | title = localization.upload, 26 | icon = icon 27 | ) 28 | } 29 | } 30 | 31 | @Composable 32 | override fun Content() { 33 | Navigator(PetUploadScreen()) { 34 | ScaleTransition(it) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/navigation/ProfileTab.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.navigation 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 6 | import cafe.adriel.voyager.navigator.Navigator 7 | import cafe.adriel.voyager.navigator.tab.Tab 8 | import cafe.adriel.voyager.navigator.tab.TabOptions 9 | import cafe.adriel.voyager.transitions.ScaleTransition 10 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 11 | import com.multiplatformkickstarter.app.ui.icon.MultiplatformKickstarterIcons 12 | import com.multiplatformkickstarter.app.ui.screens.ProfileTabScreen 13 | 14 | internal object ProfileTab : Tab { 15 | 16 | override val options: TabOptions 17 | @Composable 18 | get() { 19 | val icon = rememberVectorPainter(MultiplatformKickstarterIcons.Person) 20 | 21 | return remember { 22 | TabOptions( 23 | index = 4u, 24 | title = getCurrentLocalization().profile, 25 | icon = icon 26 | ) 27 | } 28 | } 29 | 30 | @Composable 31 | override fun Content() { 32 | Navigator(ProfileTabScreen()) { 33 | ScaleTransition(it) 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/Environment.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | sealed class Environment(val name: String, val url: String) 4 | 5 | sealed class ServerEnvironment(name: String, endpoint: String) : Environment(name, endpoint) { 6 | data object PRODUCTION : Environment("PRODUCTION", "http://multiplatformkickstarter.com") 7 | data object PREPRODUCTION : Environment("PREPRODUCTION", "http://pre.multiplatformkickstarter.com") 8 | 9 | // This IP represents the localhost of your computer through the emulator 10 | data object LOCALHOST : Environment("LOCALHOST", "http://10.0.2.2:8080") 11 | 12 | companion object { 13 | val environments = listOf(PREPRODUCTION, PRODUCTION, LOCALHOST) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/Extensions.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import androidx.compose.animation.core.LinearEasing 4 | import androidx.compose.animation.core.RepeatMode 5 | import androidx.compose.animation.core.animateFloat 6 | import androidx.compose.animation.core.infiniteRepeatable 7 | import androidx.compose.animation.core.rememberInfiniteTransition 8 | import androidx.compose.animation.core.tween 9 | import androidx.compose.foundation.background 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.composed 12 | import androidx.compose.ui.geometry.Offset 13 | import androidx.compose.ui.graphics.Brush 14 | import androidx.compose.ui.graphics.Color 15 | 16 | private const val EMAIL_FORMAT_REGEX = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\$" 17 | 18 | fun String.isValidEmail(): Boolean { 19 | return this.matches(EMAIL_FORMAT_REGEX.toRegex()) 20 | } 21 | 22 | fun Modifier.shimmerLoadingAnimation( 23 | isLoadingCompleted: Boolean = true, 24 | isLightModeActive: Boolean = true, 25 | widthOfShadowBrush: Int = 500, 26 | angleOfAxisY: Float = 270f, 27 | durationMillis: Int = 1000 28 | ): Modifier { 29 | if (isLoadingCompleted) { 30 | return this 31 | } else { 32 | return composed { 33 | val shimmerColors = ShimmerAnimationData(isLightMode = isLightModeActive).getColours() 34 | 35 | val transition = rememberInfiniteTransition(label = "") 36 | 37 | val translateAnimation = transition.animateFloat( 38 | initialValue = 0f, 39 | targetValue = (durationMillis + widthOfShadowBrush).toFloat(), 40 | animationSpec = infiniteRepeatable( 41 | animation = tween( 42 | durationMillis = durationMillis, 43 | easing = LinearEasing 44 | ), 45 | repeatMode = RepeatMode.Restart 46 | ), 47 | label = "Shimmer loading animation" 48 | ) 49 | 50 | this.background( 51 | brush = Brush.linearGradient( 52 | colors = shimmerColors, 53 | start = Offset(x = translateAnimation.value - widthOfShadowBrush, y = 0.0f), 54 | end = Offset(x = translateAnimation.value, y = angleOfAxisY) 55 | ) 56 | ) 57 | } 58 | } 59 | } 60 | 61 | data class ShimmerAnimationData( 62 | private val isLightMode: Boolean 63 | ) { 64 | fun getColours(): List { 65 | return if (isLightMode) { 66 | val color = Color.White 67 | 68 | listOf( 69 | color.copy(alpha = 0.3f), 70 | color.copy(alpha = 0.5f), 71 | color.copy(alpha = 1.0f), 72 | color.copy(alpha = 0.5f), 73 | color.copy(alpha = 0.3f) 74 | ) 75 | } else { 76 | val color = Color.Black 77 | 78 | listOf( 79 | color.copy(alpha = 0.0f), 80 | color.copy(alpha = 0.3f), 81 | color.copy(alpha = 0.5f), 82 | color.copy(alpha = 0.3f), 83 | color.copy(alpha = 0.0f) 84 | ) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/Helper.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import com.multiplatformkickstarter.app.di.appModule 4 | import org.koin.core.context.startKoin 5 | 6 | fun initKoin() { 7 | startKoin { 8 | modules(appModule()) 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/MultiplatformKickstarterEventTracker.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import co.touchlab.kermit.Logger 4 | 5 | class MultiplatformKickstarterEventTracker : Tracker { 6 | override fun onEventTracked(event: TrackEvents) { 7 | Logger.d { "Event: ${event.name}" } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/Resources.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.text.font.Font 5 | import androidx.compose.ui.text.font.FontStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | 8 | @Composable 9 | expect fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font 10 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/RootNavigatorRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import cafe.adriel.voyager.navigator.Navigator 4 | import cafe.adriel.voyager.navigator.tab.TabNavigator 5 | 6 | data class RootNavigatorRepository(val navigator: Navigator, val tabNavigator: TabNavigator) 7 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/RootSnackbarHostStateRepository.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import androidx.compose.material3.SnackbarHostState 4 | 5 | data class RootSnackbarHostStateRepository(val snackbarHostState: SnackbarHostState) 6 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/ServiceClient.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 5 | import io.ktor.serialization.kotlinx.json.json 6 | 7 | object ServiceClient { 8 | val httpClient = HttpClient { 9 | install(ContentNegotiation) { 10 | json() 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/platform/TrackingComponent.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import kotlin.native.concurrent.ThreadLocal 4 | 5 | @ThreadLocal 6 | object TrackingComponent { 7 | 8 | private val EMPTY_TRACKER = EmptyTracker() 9 | 10 | private var tracker: Tracker = EMPTY_TRACKER 11 | 12 | fun setTracker(tracker: Tracker?) { 13 | if (tracker == null) { 14 | throw IllegalArgumentException("The LocationPickerTracker instance can't be null.") 15 | } 16 | TrackingComponent.tracker = tracker 17 | } 18 | 19 | fun getTracker(): Tracker { 20 | return tracker 21 | } 22 | 23 | fun reset() { 24 | tracker = EMPTY_TRACKER 25 | } 26 | 27 | class EmptyTracker : Tracker { 28 | override fun onEventTracked(event: TrackEvents) { } 29 | } 30 | } 31 | 32 | enum class TrackEvents(val eventName: String) { 33 | APP_STARTED("App Started"), 34 | LOGIN_SUCCESSFUL("Login Successful"), 35 | LOGIN_ERROR("Login Error"), 36 | SIGN_UP_SUCCESSFUL("Sign Up Successful"), 37 | SIGN_UP_ERROR("Sign Up Error"), 38 | PET_UPLOAD_SUCCESSFUL("Pet Upload Successful"), 39 | PET_UPLOAD_ERROR("Pet Upload Error"), 40 | REQUEST_ERROR("Request Error") 41 | } 42 | 43 | interface Tracker { 44 | fun onEventTracked(event: TrackEvents) 45 | } 46 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/components/ColoredSnackBar.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.components 2 | 3 | import androidx.compose.foundation.layout.padding 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Snackbar 6 | import androidx.compose.material3.SnackbarData 7 | import androidx.compose.material3.SnackbarDuration 8 | import androidx.compose.material3.SnackbarHost 9 | import androidx.compose.material3.SnackbarHostState 10 | import androidx.compose.material3.SnackbarResult 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.unit.dp 15 | import com.multiplatformkickstarter.app.ui.theme.Typography 16 | import com.multiplatformkickstarter.app.ui.theme.errorContainer 17 | import com.multiplatformkickstarter.app.ui.theme.infoContainer 18 | import com.multiplatformkickstarter.app.ui.theme.successContainer 19 | import com.multiplatformkickstarter.app.ui.theme.warningContainer 20 | 21 | private const val DELIMITER = "##" 22 | 23 | @Composable 24 | fun ColoredSnackBarHost( 25 | hostState: SnackbarHostState, 26 | action: @Composable (() -> Unit)? = null 27 | ) { 28 | SnackbarHost(hostState) { 29 | val message = it.getSnackbarMessage() 30 | val color = when (message.type) { 31 | SnackbarType.SUCCESS -> Pair(successContainer, MaterialTheme.colorScheme.onPrimary) 32 | SnackbarType.ERROR -> Pair(errorContainer, MaterialTheme.colorScheme.onError) 33 | SnackbarType.INFO -> Pair(infoContainer, MaterialTheme.colorScheme.onPrimary) 34 | SnackbarType.WARNING -> Pair(warningContainer, MaterialTheme.colorScheme.onPrimary) 35 | } 36 | 37 | Snackbar( 38 | modifier = Modifier.padding(8.dp), 39 | containerColor = color.first, 40 | contentColor = color.second, 41 | action = action 42 | ) { 43 | hostState.currentSnackbarData?.let { 44 | Text( 45 | text = message.message, 46 | style = Typography.get().bodyMedium 47 | ) 48 | } 49 | } 50 | } 51 | } 52 | 53 | suspend fun SnackbarHostState.showSnackbar( 54 | type: SnackbarType, 55 | message: String, 56 | actionLabel: String? = null, 57 | withDismissAction: Boolean = false, 58 | duration: SnackbarDuration = SnackbarDuration.Short 59 | ): SnackbarResult { 60 | return showSnackbar(SnackbarMessage(type, message).toString(), actionLabel, withDismissAction, duration) 61 | } 62 | 63 | private data class SnackbarMessage( 64 | val type: SnackbarType, 65 | val message: String 66 | ) { 67 | override fun toString(): String { 68 | return "$type$DELIMITER$message" 69 | } 70 | 71 | companion object { 72 | fun fromString(string: String): SnackbarMessage { 73 | val (type, message) = string.split(DELIMITER) 74 | 75 | return SnackbarMessage( 76 | type = SnackbarType.valueOf(type), 77 | message = message 78 | ) 79 | } 80 | } 81 | } 82 | 83 | private fun SnackbarData.getSnackbarMessage(): SnackbarMessage { 84 | return SnackbarMessage.fromString(visuals.message) 85 | } 86 | 87 | enum class SnackbarType { 88 | SUCCESS, ERROR, INFO, WARNING 89 | } 90 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/components/NoResultsLayout.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.components 2 | 3 | import androidx.compose.foundation.Image 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.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.padding 11 | import androidx.compose.foundation.layout.size 12 | import androidx.compose.foundation.shape.RoundedCornerShape 13 | import androidx.compose.material3.Button 14 | import androidx.compose.material3.ButtonDefaults 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.Text 17 | import androidx.compose.runtime.Composable 18 | import androidx.compose.ui.Alignment 19 | import androidx.compose.ui.Modifier 20 | import androidx.compose.ui.text.style.TextAlign 21 | import androidx.compose.ui.unit.dp 22 | import com.multiplatformkickstarter.app.localization.Localization 23 | import com.multiplatformkickstarter.app.resources.Res 24 | import com.multiplatformkickstarter.app.resources.no_data_cuate 25 | import com.multiplatformkickstarter.app.ui.theme.Typography 26 | import org.jetbrains.compose.resources.DrawableResource 27 | import org.jetbrains.compose.resources.vectorResource 28 | 29 | @Composable 30 | fun EmptyLayout( 31 | title: String? = null, 32 | description: String? = null, 33 | imageResource: DrawableResource? = null, 34 | actionLabel: String? = null, 35 | localization: Localization, 36 | action: () -> Unit 37 | ) { 38 | Column( 39 | modifier = Modifier.fillMaxSize(), 40 | verticalArrangement = Arrangement.Center, 41 | horizontalAlignment = Alignment.CenterHorizontally 42 | ) { 43 | Text( 44 | modifier = Modifier.padding(16.dp), 45 | text = title ?: localization.noResultsTitle, 46 | style = MaterialTheme.typography.titleLarge, 47 | textAlign = TextAlign.Center, 48 | color = MaterialTheme.colorScheme.primary 49 | ) 50 | Text( 51 | modifier = Modifier.padding(horizontal = 16.dp), 52 | text = description ?: localization.noResultsDesciption, 53 | style = MaterialTheme.typography.bodySmall, 54 | textAlign = TextAlign.Center, 55 | color = MaterialTheme.colorScheme.outline 56 | ) 57 | Image( 58 | imageVector = vectorResource(imageResource ?: Res.drawable.no_data_cuate), 59 | contentDescription = description, 60 | modifier = Modifier.size(260.dp) 61 | ) 62 | actionLabel?.let { 63 | Spacer(modifier = Modifier.size(10.dp)) 64 | Button( 65 | onClick = { 66 | action.invoke() 67 | }, 68 | colors = ButtonDefaults.buttonColors(), 69 | modifier = Modifier 70 | .fillMaxWidth() 71 | .size(50.dp) 72 | .padding(start = 16.dp, end = 16.dp), 73 | contentPadding = PaddingValues(0.dp), 74 | shape = RoundedCornerShape(10) 75 | ) { 76 | Text( 77 | text = it, 78 | style = Typography.get().titleMedium, 79 | modifier = Modifier.padding(8.dp) 80 | ) 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/components/RatingBar.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.components 2 | 3 | import androidx.compose.foundation.layout.Row 4 | import androidx.compose.material.icons.Icons 5 | import androidx.compose.material.icons.outlined.Star 6 | import androidx.compose.material3.Icon 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | 11 | @Composable 12 | fun RatingBar( 13 | modifier: Modifier = Modifier, 14 | rating: Double = 0.0, 15 | stars: Int = 5, 16 | starsColor: Color = Color.Yellow 17 | ) { 18 | val filledStars = kotlin.math.floor(rating).toInt() 19 | val unfilledStars = (stars - kotlin.math.ceil(rating)).toInt() 20 | val halfStar = !(rating.rem(1).equals(0.0)) 21 | 22 | Row(modifier = modifier) { 23 | repeat(filledStars) { 24 | Icon(imageVector = Icons.Outlined.Star, contentDescription = null, tint = starsColor) 25 | } 26 | 27 | if (halfStar) { 28 | Icon( 29 | imageVector = Icons.Outlined.Star, 30 | contentDescription = null, 31 | tint = starsColor 32 | ) 33 | } 34 | 35 | repeat(unfilledStars) { 36 | Icon( 37 | imageVector = Icons.Outlined.Star, 38 | contentDescription = null, 39 | tint = starsColor 40 | ) 41 | } 42 | } 43 | } 44 | 45 | /* 46 | @Preview 47 | @Composable 48 | fun RatingPreview() { 49 | RatingBar(rating = 2.5) 50 | } 51 | 52 | @Preview 53 | @Composable 54 | fun TenStarsRatingPreview() { 55 | RatingBar(stars = 10, rating = 8.5) 56 | } 57 | 58 | @Preview 59 | @Composable 60 | fun RatingPreviewFull() { 61 | RatingBar(rating = 5.0) 62 | } 63 | 64 | @Preview 65 | @Composable 66 | fun RatingPreviewWorst() { 67 | RatingBar(rating = 1.0) 68 | } 69 | 70 | @Preview 71 | @Composable 72 | fun RatingPreviewDisabled() { 73 | RatingBar(rating = 0.0, starsColor = Color.Gray) 74 | }*/ 75 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/components/Toast.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.foundation.layout.size 7 | import androidx.compose.foundation.shape.RoundedCornerShape 8 | import androidx.compose.material.Surface 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.LaunchedEffect 13 | import androidx.compose.runtime.MutableState 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.unit.dp 18 | import com.multiplatformkickstarter.app.ui.theme.Typography 19 | import kotlinx.coroutines.delay 20 | 21 | const val TOAST_DURATION = 3000L 22 | 23 | sealed interface ToastState { 24 | data object Hidden : ToastState 25 | class Shown( 26 | val message: String, 27 | val containerColor: Color? = null, 28 | val onContainerColor: Color? = null 29 | ) : ToastState 30 | } 31 | 32 | @Composable 33 | fun Toast( 34 | state: MutableState 35 | ) { 36 | val value = state.value 37 | if (value is ToastState.Shown) { 38 | Box( 39 | modifier = Modifier.fillMaxSize().padding(bottom = 100.dp), 40 | contentAlignment = Alignment.BottomCenter 41 | ) { 42 | Surface( 43 | modifier = Modifier.size(320.dp, 50.dp), 44 | color = value.containerColor ?: MaterialTheme.colorScheme.primaryContainer, 45 | shape = RoundedCornerShape(8.dp) 46 | ) { 47 | Box(contentAlignment = Alignment.Center) { 48 | Text( 49 | text = value.message, 50 | color = value.onContainerColor ?: MaterialTheme.colorScheme.onPrimaryContainer, 51 | style = Typography.get().bodyMedium 52 | ) 53 | } 54 | LaunchedEffect(value.message) { 55 | delay(TOAST_DURATION) 56 | state.value = ToastState.Hidden 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/components/TopAppBar.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.components 2 | 3 | import androidx.compose.material3.CenterAlignedTopAppBar 4 | import androidx.compose.material3.ExperimentalMaterial3Api 5 | import androidx.compose.material3.Icon 6 | import androidx.compose.material3.IconButton 7 | import androidx.compose.material3.MaterialTheme 8 | import androidx.compose.material3.Text 9 | import androidx.compose.material3.TopAppBarColors 10 | import androidx.compose.material3.TopAppBarDefaults 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.vector.ImageVector 14 | 15 | @OptIn(ExperimentalMaterial3Api::class) 16 | @Composable 17 | fun MultiplatformKickstarterTopAppBar( 18 | title: String, 19 | navigationIcon: ImageVector, 20 | navigationIconContentDescription: String?, 21 | actionIcon: ImageVector, 22 | actionIconContentDescription: String?, 23 | modifier: Modifier = Modifier, 24 | colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), 25 | onNavigationClick: () -> Unit = {}, 26 | onActionClick: () -> Unit = {} 27 | ) { 28 | CenterAlignedTopAppBar( 29 | title = { Text(text = title) }, 30 | navigationIcon = { 31 | IconButton(onClick = onNavigationClick) { 32 | Icon( 33 | imageVector = navigationIcon, 34 | contentDescription = navigationIconContentDescription, 35 | tint = MaterialTheme.colorScheme.onSurface 36 | ) 37 | } 38 | }, 39 | actions = { 40 | IconButton(onClick = onActionClick) { 41 | Icon( 42 | imageVector = actionIcon, 43 | contentDescription = actionIconContentDescription, 44 | tint = MaterialTheme.colorScheme.onSurface 45 | ) 46 | } 47 | }, 48 | colors = colors, 49 | modifier = modifier 50 | ) 51 | } 52 | 53 | /** 54 | * Top app bar with action, displayed on the right 55 | */ 56 | 57 | /* 58 | @OptIn(ExperimentalMaterial3Api::class) 59 | @Composable 60 | fun MyProjectNameTopAppBar( 61 | title: String, 62 | actionIcon: ImageVector, 63 | actionIconContentDescription: String?, 64 | modifier: Modifier = Modifier, 65 | colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(), 66 | onActionClick: () -> Unit = {} 67 | ) { 68 | CenterAlignedTopAppBar( 69 | title = { Text(text = title) }, 70 | actions = { 71 | /*IconButton(onClick = onActionClick) { 72 | Icon( 73 | imageVector = actionIcon, 74 | contentDescription = actionIconContentDescription, 75 | tint = MaterialTheme.colorScheme.onSurface 76 | ) 77 | }*/ 78 | }, 79 | colors = colors, 80 | modifier = modifier 81 | ) 82 | } 83 | */ 84 | 85 | /*@OptIn(ExperimentalMaterial3Api::class) 86 | @Preview("Top App Bar") 87 | @Composable 88 | fun MyProjectNameTopAppBarPreview() { 89 | MyProjectNameTopAppBar( 90 | titleRes = R.string.untitled, 91 | navigationIcon = Icons.Default.Search, 92 | navigationIconContentDescription = "Navigation icon", 93 | actionIcon = Icons.Default.MoreVert, 94 | actionIconContentDescription = "Action icon" 95 | ) 96 | }*/ 97 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/icon/MultiplatformKickstarterIcons.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.icon 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.automirrored.filled.ArrowBack 5 | import androidx.compose.material.icons.automirrored.rounded.ExitToApp 6 | import androidx.compose.material.icons.automirrored.rounded.Help 7 | import androidx.compose.material.icons.automirrored.rounded.KeyboardArrowRight 8 | import androidx.compose.material.icons.automirrored.rounded.TextSnippet 9 | import androidx.compose.material.icons.rounded.ArrowDropDown 10 | import androidx.compose.material.icons.rounded.BrokenImage 11 | import androidx.compose.material.icons.rounded.Check 12 | import androidx.compose.material.icons.rounded.Close 13 | import androidx.compose.material.icons.rounded.Create 14 | import androidx.compose.material.icons.rounded.Delete 15 | import androidx.compose.material.icons.rounded.Email 16 | import androidx.compose.material.icons.rounded.Face 17 | import androidx.compose.material.icons.rounded.Favorite 18 | import androidx.compose.material.icons.rounded.FilterList 19 | import androidx.compose.material.icons.rounded.Home 20 | import androidx.compose.material.icons.rounded.Info 21 | import androidx.compose.material.icons.rounded.Password 22 | import androidx.compose.material.icons.rounded.PersonAdd 23 | import androidx.compose.material.icons.rounded.Pets 24 | import androidx.compose.material.icons.rounded.PhotoCamera 25 | import androidx.compose.material.icons.rounded.Settings 26 | import androidx.compose.ui.graphics.vector.ImageVector 27 | 28 | /** 29 | * Now in Android icons. Material icons are [ImageVector]s, custom icons are drawable resource IDs. 30 | */ 31 | object MultiplatformKickstarterIcons { 32 | val ArrowBack = Icons.AutoMirrored.Filled.ArrowBack 33 | val ArrowDropDown = Icons.Rounded.ArrowDropDown 34 | val Bookmark = Icons.Rounded.Favorite 35 | val Check = Icons.Rounded.Check 36 | val Person = Icons.Rounded.Face 37 | val Settings = Icons.Rounded.Settings 38 | val Info = Icons.Rounded.Info 39 | val Home = Icons.Rounded.Home 40 | val Create = Icons.Rounded.Create 41 | val Inbox = Icons.Rounded.Email 42 | val Favorite = Icons.Rounded.Favorite 43 | val Exit = Icons.AutoMirrored.Rounded.ExitToApp 44 | val ArrowRight = Icons.AutoMirrored.Rounded.KeyboardArrowRight 45 | val Email = Icons.Rounded.Email 46 | val Password = Icons.Rounded.Password 47 | val Filter = Icons.Rounded.FilterList 48 | val Delete = Icons.Rounded.Delete 49 | val Camera = Icons.Rounded.PhotoCamera 50 | val Follow = Icons.Rounded.PersonAdd 51 | val Blog = Icons.AutoMirrored.Rounded.TextSnippet 52 | val Help = Icons.AutoMirrored.Rounded.Help 53 | val Pets = Icons.Rounded.Pets 54 | val BrokenImage = Icons.Rounded.BrokenImage 55 | val Close = Icons.Rounded.Close 56 | } 57 | 58 | /** 59 | * A sealed class to make dealing with [ImageVector] and [DrawableRes] icons easier. 60 | */ 61 | sealed class Icon { 62 | data class ImageVectorIcon(val imageVector: ImageVector) : Icon() 63 | data class DrawableResourceIcon(val id: Int) : Icon() 64 | } 65 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/screens/OnboardingScreen.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.screens 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.text.AnnotatedString 5 | import androidx.compose.ui.text.SpanStyle 6 | import androidx.compose.ui.text.buildAnnotatedString 7 | import androidx.compose.ui.text.font.FontWeight 8 | import androidx.compose.ui.text.withStyle 9 | import cafe.adriel.voyager.core.screen.Screen 10 | import cafe.adriel.voyager.navigator.LocalNavigator 11 | import cafe.adriel.voyager.navigator.currentOrThrow 12 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 13 | import com.multiplatformkickstarter.app.resources.Res 14 | import com.multiplatformkickstarter.app.resources.features_overview_cuate 15 | import com.multiplatformkickstarter.app.resources.mklogo 16 | import com.multiplatformkickstarter.app.resources.product_quality_amico 17 | import com.multiplatformkickstarter.app.ui.components.CarouselItem 18 | import com.multiplatformkickstarter.app.ui.components.OnboardingComponent 19 | import com.multiplatformkickstarter.app.ui.theme.MultiplatformKickstarterTheme 20 | 21 | class OnboardingScreen : Screen { 22 | 23 | @Composable 24 | override fun Content() { 25 | val localization = getCurrentLocalization() 26 | val navigator = LocalNavigator.currentOrThrow 27 | MultiplatformKickstarterTheme { 28 | val onboardingPromoTitle1 = buildAnnotatedString { 29 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 30 | append(localization.onboardingPromoTitle1) 31 | } 32 | } 33 | val onboardingPromoLine1 = buildAnnotatedString { 34 | append("Welcome to ") 35 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 36 | append("Multiplatform Kickstarter") 37 | } 38 | append(localization.onboardingPromoLine1) 39 | } 40 | 41 | val onboardingPromoTitle2 = buildAnnotatedString { 42 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 43 | append(localization.onboardingPromoTitle2) 44 | } 45 | } 46 | 47 | val onboardingPromoTitle3 = buildAnnotatedString { 48 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 49 | append(localization.onboardingPromoTitle3) 50 | } 51 | } 52 | 53 | val onboardingPromoLine3 = buildAnnotatedString { 54 | append(localization.onboardingPromoLine3) 55 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 56 | append("multiplatformkickstarter.com") 57 | } 58 | } 59 | 60 | val carouselItems: List = listOf( 61 | CarouselItem(Res.drawable.mklogo, onboardingPromoTitle1, onboardingPromoLine1, localization.next), 62 | CarouselItem(Res.drawable.features_overview_cuate, onboardingPromoTitle2, localization.onboardingPromoLine2.toAnnotatedString(), localization.next), 63 | CarouselItem(Res.drawable.product_quality_amico, onboardingPromoTitle3, onboardingPromoLine3, localization.close) { navigator.pop() } 64 | ) 65 | val onboardingComponent = OnboardingComponent(carouselItems) 66 | onboardingComponent.DrawCarousel() 67 | } 68 | } 69 | } 70 | 71 | private fun String.toAnnotatedString(): AnnotatedString { 72 | return buildAnnotatedString { 73 | append(this@toAnnotatedString) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/screens/ProTemplateFeatureScreen.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.screens 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxHeight 7 | import androidx.compose.foundation.layout.fillMaxSize 8 | import androidx.compose.foundation.layout.fillMaxWidth 9 | import androidx.compose.foundation.layout.padding 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.ui.Alignment 14 | import androidx.compose.ui.Modifier 15 | import androidx.compose.ui.layout.ContentScale 16 | import androidx.compose.ui.text.SpanStyle 17 | import androidx.compose.ui.text.buildAnnotatedString 18 | import androidx.compose.ui.text.font.FontWeight 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.text.withStyle 21 | import androidx.compose.ui.unit.dp 22 | import com.multiplatformkickstarter.app.localization.getCurrentLocalization 23 | import com.multiplatformkickstarter.app.resources.Res 24 | import com.multiplatformkickstarter.app.resources.mklogo 25 | import org.jetbrains.compose.resources.vectorResource 26 | 27 | @Composable 28 | fun ProTemplateFeature( 29 | modifier: Modifier = Modifier 30 | ) { 31 | val localization = getCurrentLocalization() 32 | Column( 33 | modifier = modifier.fillMaxSize(), 34 | verticalArrangement = Arrangement.Center, 35 | horizontalAlignment = Alignment.CenterHorizontally 36 | ) { 37 | Image( 38 | imageVector = vectorResource(Res.drawable.mklogo), 39 | "", 40 | modifier = Modifier 41 | .fillMaxHeight(0.4f) 42 | .fillMaxWidth() 43 | .padding(start = 24.dp, end = 24.dp), 44 | contentScale = ContentScale.FillWidth 45 | ) 46 | Text( 47 | modifier = Modifier.padding(16.dp), 48 | text = localization.proFeatureScreenTitle, 49 | style = MaterialTheme.typography.headlineMedium, 50 | textAlign = TextAlign.Center, 51 | color = MaterialTheme.colorScheme.primary 52 | ) 53 | val proFeaturesDescription = buildAnnotatedString { 54 | append(localization.proFeatureScreenDescription) 55 | withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { 56 | append("multiplatformkickstarter.com") 57 | } 58 | } 59 | Text( 60 | modifier = Modifier.padding(horizontal = 24.dp), 61 | text = proFeaturesDescription, 62 | style = MaterialTheme.typography.bodyMedium, 63 | textAlign = TextAlign.Center, 64 | color = MaterialTheme.colorScheme.outline 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/screens/viewmodel/ProfileViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.screens.viewmodel 2 | 3 | import cafe.adriel.voyager.core.model.ScreenModel 4 | import cafe.adriel.voyager.core.model.screenModelScope 5 | import cafe.adriel.voyager.navigator.Navigator 6 | import com.multiplatformkickstarter.app.common.model.GeoLocation 7 | import com.multiplatformkickstarter.app.data.repositories.ProfileRepository 8 | import com.multiplatformkickstarter.app.data.repositories.SessionRepository 9 | import com.multiplatformkickstarter.app.feature.loginsignup.LoginSignUpLandingScreen 10 | import com.multiplatformkickstarter.app.feature.pets.MyPetsScreen 11 | import com.multiplatformkickstarter.app.feature.profile.ProfileDetailScreen 12 | import com.multiplatformkickstarter.app.navigation.HomeTab 13 | import com.multiplatformkickstarter.app.platform.RootNavigatorRepository 14 | import kotlinx.coroutines.channels.Channel 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.MutableStateFlow 17 | import kotlinx.coroutines.flow.asStateFlow 18 | import kotlinx.coroutines.flow.receiveAsFlow 19 | import kotlinx.coroutines.launch 20 | 21 | class ProfileViewModel( 22 | private var navigator: Navigator, 23 | private val profileRepository: ProfileRepository, 24 | private val sessionRepository: SessionRepository, 25 | private val rootNavigatorRepository: RootNavigatorRepository 26 | ) : ScreenModel { 27 | private val _state = MutableStateFlow(ProfileState("", "", "", null, 0.0, false)) 28 | val state = _state.asStateFlow() 29 | private val _sideEffects = Channel() 30 | val sideEffects: Flow = _sideEffects.receiveAsFlow() 31 | 32 | init { 33 | if (!sessionRepository.isLoggedIn()) { 34 | rootNavigatorRepository.navigator.push(LoginSignUpLandingScreen()) 35 | } 36 | } 37 | 38 | fun onProfileDetailClicked() { 39 | rootNavigatorRepository.navigator.push(ProfileDetailScreen(sessionRepository.getUserId())) 40 | } 41 | 42 | fun onMyPetsClicked() { 43 | rootNavigatorRepository.navigator.push(MyPetsScreen()) 44 | } 45 | 46 | fun onProfileAccountSettingsClicked() { 47 | } 48 | 49 | fun onProfileSettingsClicked() { 50 | } 51 | 52 | fun onBlogClicked() { 53 | } 54 | 55 | fun onTermsAndConditionsClicked() { 56 | } 57 | 58 | fun onHelpClicked() { 59 | } 60 | 61 | fun onCloseSessionClicked() { 62 | // TODO: Logout on service 63 | sessionRepository.clear() 64 | rootNavigatorRepository.tabNavigator.current = HomeTab 65 | } 66 | 67 | fun onStarted(navigator: Navigator) { 68 | this.navigator = navigator 69 | screenModelScope.launch { 70 | _state.value = ProfileState( 71 | name = profileRepository.getName(), 72 | description = profileRepository.getDescription(), 73 | image = profileRepository.getImage(), 74 | location = profileRepository.getLocation(), 75 | rating = profileRepository.getRating(), 76 | isLoggedIn = sessionRepository.isLoggedIn() 77 | ) 78 | } 79 | } 80 | 81 | fun onSignUpLoginClicked() { 82 | rootNavigatorRepository.navigator.push(LoginSignUpLandingScreen()) 83 | } 84 | } 85 | 86 | data class ProfileState( 87 | val name: String, 88 | val description: String, 89 | val image: String, 90 | val location: GeoLocation?, 91 | val rating: Double, 92 | val isLoggedIn: Boolean 93 | ) 94 | 95 | sealed class ProfileSideEffects { 96 | data object Initial : ProfileSideEffects() 97 | data object OnSignedUp : ProfileSideEffects() 98 | data object OnSignUpError : ProfileSideEffects() 99 | } 100 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/screens/viewmodel/SearchListingViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.screens.viewmodel 2 | 3 | import cafe.adriel.voyager.core.model.ScreenModel 4 | import cafe.adriel.voyager.core.model.screenModelScope 5 | import cafe.adriel.voyager.navigator.Navigator 6 | import co.touchlab.kermit.Logger 7 | import com.multiplatformkickstarter.app.common.model.PetCategory 8 | import com.multiplatformkickstarter.app.common.model.PetModel 9 | import com.multiplatformkickstarter.app.data.usecases.GetSearchUseCase 10 | import com.multiplatformkickstarter.app.ui.screens.PetDetailScreen 11 | import kotlinx.coroutines.channels.Channel 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.MutableStateFlow 14 | import kotlinx.coroutines.flow.asStateFlow 15 | import kotlinx.coroutines.flow.receiveAsFlow 16 | import kotlinx.coroutines.launch 17 | 18 | class SearchListingViewModel( 19 | private val searchId: Int?, 20 | private val petCategory: PetCategory?, 21 | private val searchListingUserCase: GetSearchUseCase, 22 | private val navigator: Navigator 23 | ) : ScreenModel { 24 | private val _state = MutableStateFlow(SearchListingState(null, null, emptyList())) 25 | val state = _state.asStateFlow() 26 | private val _sideEffects = Channel() 27 | val sideEffects: Flow = _sideEffects.receiveAsFlow() 28 | 29 | fun onStarted() { 30 | screenModelScope.launch { 31 | if (searchId != null) { 32 | searchListingUserCase.invoke(searchId) 33 | .onSuccess { 34 | _state.value = _state.value.copy(pets = it) 35 | _sideEffects.trySend(SearchListingSideEffects.OnLoaded(it)) 36 | } 37 | .onFailure { 38 | _sideEffects.trySend(SearchListingSideEffects.OnLoadError) 39 | Logger.w(it) { "Error on loading search" } 40 | } 41 | } else if (petCategory != null) { 42 | searchListingUserCase.invoke(petCategory) 43 | .onSuccess { 44 | _state.value = _state.value.copy(pets = it) 45 | _sideEffects.trySend(SearchListingSideEffects.OnLoaded(it)) 46 | } 47 | .onFailure { 48 | _sideEffects.trySend(SearchListingSideEffects.OnLoadError) 49 | Logger.w(it) { "Error on loading search" } 50 | } 51 | } else { 52 | _sideEffects.trySend(SearchListingSideEffects.OnNoResults) 53 | } 54 | } 55 | } 56 | 57 | fun onPetClicked(id: Int) { 58 | navigator.push(PetDetailScreen(id)) 59 | } 60 | } 61 | 62 | data class SearchListingState( 63 | val searchId: Int? = null, 64 | val petCategory: PetCategory? = null, 65 | val pets: List 66 | ) 67 | 68 | sealed class SearchListingSideEffects { 69 | data object Initial : SearchListingSideEffects() 70 | data class OnLoaded(val pets: List) : SearchListingSideEffects() 71 | data object OnNoResults : SearchListingSideEffects() 72 | data object OnLoadError : SearchListingSideEffects() 73 | } 74 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val lightPrimary = Color(0xFF825500) 6 | val lightOnPrimary = Color(0xFFFFFFFF) 7 | val lightPrimaryContainer = Color(0xFFFFDDAE) 8 | val lightOnPrimaryContainer = Color(0xFF2A1800) 9 | val lightSecondary = Color(0xFF6F5B40) 10 | val lightOnSecondary = Color(0xFFFFFFFF) 11 | val lightSecondaryContainer = Color(0xFFFADEBC) 12 | val lightOnSecondaryContainer = Color(0xFF271904) 13 | val lightTertiary = Color(0xFF516440) 14 | val lightOnTertiary = Color(0xFFFFFFFF) 15 | val lightTertiaryContainer = Color(0xFFD3EABC) 16 | val lightOnTertiaryContainer = Color(0xFF102004) 17 | val lightError = Color(0xFFBA1B1B) 18 | val lightErrorContainer = Color(0xFFFFDAD4) 19 | val lightOnError = Color(0xFFFFFFFF) 20 | val lightOnErrorContainer = Color(0xFF410001) 21 | val lightBackground = Color(0xFFFCFCFC) 22 | val lightOnBackground = Color(0xFF1F1B16) 23 | val lightSurface = Color(0xFFFCFCFC) 24 | val lightOnSurface = Color(0xFF1F1B16) 25 | val lightSurfaceVariant = Color(0xFFF0E0CF) 26 | val lightOnSurfaceVariant = Color(0xFF4F4539) 27 | val lightOutline = Color(0xFF817567) 28 | val lightInverseOnSurface = Color(0xFFF9EFE6) 29 | val lightInverseSurface = Color(0xFF34302A) 30 | val lightPrimaryInverse = Color(0xFFFFB945) 31 | 32 | val darkPrimary = Color(0xFFFFB945) 33 | val darkOnPrimary = Color(0xFF452B00) 34 | val darkPrimaryContainer = Color(0xFF624000) 35 | val darkOnPrimaryContainer = Color(0xFFFFDDAE) 36 | val darkSecondary = Color(0xFFDDC3A2) 37 | val darkOnSecondary = Color(0xFF3E2E16) 38 | val darkSecondaryContainer = Color(0xFF56442B) 39 | val darkOnSecondaryContainer = Color(0xFFFADEBC) 40 | val darkTertiary = Color(0xFFB8CEA2) 41 | val darkOnTertiary = Color(0xFF243516) 42 | val darkTertiaryContainer = Color(0xFF3A4C2B) 43 | val darkOnTertiaryContainer = Color(0xFFD3EABC) 44 | val darkError = Color(0xFFFFB4A9) 45 | val darkErrorContainer = Color(0xFF930006) 46 | val darkOnError = Color(0xFF680003) 47 | val darkOnErrorContainer = Color(0xFFFFDAD4) 48 | val darkBackground = Color(0xFF1F1B16) 49 | val darkOnBackground = Color(0xFFEAE1D9) 50 | val darkSurface = Color(0xFF1F1B16) 51 | val darkOnSurface = Color(0xFFEAE1D9) 52 | val darkSurfaceVariant = Color(0xFF4F4539) 53 | val darkOnSurfaceVariant = Color(0xFFD3C4B4) 54 | val darkOutline = Color(0xFF9C8F80) 55 | val darkInverseOnSurface = Color(0xFF32281A) 56 | val darkInverseSurface = Color(0xFFEAE1D9) 57 | val darkPrimaryInverse = Color(0xFF624000) 58 | 59 | val successContainer = Color(0xFF017943) 60 | val errorContainer = Color(0xFFB21229) 61 | val warningContainer = Color(0xFFD97703) 62 | val infoContainer = Color(0xFF50595E) 63 | 64 | val scrimColor = Color(0x80000000) 65 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/theme/Fonts.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.text.font.FontFamily 5 | import androidx.compose.ui.text.font.FontStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | import com.multiplatformkickstarter.app.platform.font 8 | 9 | object Fonts { 10 | @Composable 11 | fun poppinsFamily() = FontFamily( 12 | font( 13 | "Poppins Light", 14 | "poppins_light", 15 | FontWeight.Light, 16 | FontStyle.Normal 17 | ), 18 | font( 19 | "Poppins Regular", 20 | "poppins_regular", 21 | FontWeight.Normal, 22 | FontStyle.Normal 23 | ), 24 | font( 25 | "Poppins Italic", 26 | "poppins_italic", 27 | FontWeight.Normal, 28 | FontStyle.Italic 29 | ), 30 | font( 31 | "Poppins Medium", 32 | "poppins_medium", 33 | FontWeight.Medium, 34 | FontStyle.Normal 35 | ), 36 | font( 37 | "Poppins Bold", 38 | "poppins_bold", 39 | FontWeight.Bold, 40 | FontStyle.Normal 41 | ) 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/theme/Gradient.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2022 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.multiplatformkickstarter.app.ui.theme 18 | 19 | import androidx.compose.runtime.Immutable 20 | import androidx.compose.runtime.staticCompositionLocalOf 21 | import androidx.compose.ui.graphics.Color 22 | 23 | /** 24 | * A class to model gradient color values for Now in Android. 25 | */ 26 | @Immutable 27 | data class GradientColors( 28 | val primary: Color = Color.Unspecified, 29 | val secondary: Color = Color.Unspecified, 30 | val tertiary: Color = Color.Unspecified, 31 | val neutral: Color = Color.Unspecified 32 | ) 33 | 34 | /** 35 | * A composition local for [GradientColors]. 36 | */ 37 | val LocalGradientColors = staticCompositionLocalOf { GradientColors() } 38 | -------------------------------------------------------------------------------- /shared/src/commonMain/kotlin/com/multiplatformkickstarter/app/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) 12 | -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_black.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_black.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_black_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_black_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_extra_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_extra_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_extra_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_extra_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_extra_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_extra_light.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_extra_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_extra_light_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_light.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_light_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_light_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_medium.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_medium_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_medium_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_regular.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_semi_bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_semi_bold.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_semi_bold_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_semi_bold_italic.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_thin.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_thin.ttf -------------------------------------------------------------------------------- /shared/src/commonMain/resources/font/poppins_thin_italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MultiplatformKickstarter/Adoptme/f84f0916c4044ef2df9c3c6153a742afdecae423/shared/src/commonMain/resources/font/poppins_thin_italic.ttf -------------------------------------------------------------------------------- /shared/src/desktopMain/drawable/bg_splash.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /shared/src/desktopMain/drawable/mklogo.xml: -------------------------------------------------------------------------------- 1 | 3 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/Debug.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.debugmenu 2 | 3 | class DesktopDebug : Debug { 4 | override val isDebug: Boolean 5 | get() = true 6 | } 7 | 8 | actual fun getDebug(): Debug = DesktopDebug() 9 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/multiplatformkickstarter/app/localization/Localization.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.localization 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | actual fun getCurrentLanguage(): AvailableLanguages = AvailableLanguages.EN 6 | 7 | @Composable 8 | actual fun SetLanguage(language: AvailableLanguages) { 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/desktopMain/kotlin/com/multiplatformkickstarter/app/platform/Resources.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package com.multiplatformkickstarter.app.platform 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | import kotlinx.coroutines.runBlocking 10 | import org.jetbrains.compose.resources.ExperimentalResourceApi 11 | import org.jetbrains.compose.resources.resource 12 | 13 | private val cache: MutableMap = mutableMapOf() 14 | 15 | @OptIn(ExperimentalResourceApi::class) 16 | @Composable 17 | actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { 18 | return cache.getOrPut(res) { 19 | val byteArray = runBlocking { 20 | resource("font/$res.ttf").readBytes() 21 | } 22 | androidx.compose.ui.text.platform.Font(res, byteArray, weight, style) 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/Debug.ios.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalNativeApi::class) 2 | 3 | package com.multiplatformkickstarter.app.feature.debugmenu 4 | 5 | import kotlin.experimental.ExperimentalNativeApi 6 | 7 | class IOSDebug : Debug { 8 | override val isDebug: Boolean 9 | get() = Platform.isDebugBinary 10 | } 11 | 12 | actual fun getDebug(): Debug = IOSDebug() 13 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/multiplatformkickstarter/app/localization/Localization.ios.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.localization 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | actual fun getCurrentLanguage(): AvailableLanguages = AvailableLanguages.EN 6 | 7 | @Composable 8 | actual fun SetLanguage(language: AvailableLanguages) { 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/multiplatformkickstarter/app/main.ios.kt: -------------------------------------------------------------------------------- 1 | /* ktlint-disable */ 2 | 3 | package com.multiplatformkickstarter.app 4 | 5 | 6 | import androidx.compose.ui.window.ComposeUIViewController 7 | import platform.UIKit.UIViewController 8 | 9 | fun homeScreenViewController(): UIViewController = ComposeUIViewController { 10 | MainApp() 11 | } 12 | 13 | /* ktlint-disable */ 14 | -------------------------------------------------------------------------------- /shared/src/iosMain/kotlin/com/multiplatformkickstarter/app/platform/Resources.kt: -------------------------------------------------------------------------------- 1 | @file:OptIn(ExperimentalResourceApi::class) 2 | 3 | package com.multiplatformkickstarter.app.platform 4 | 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.text.font.Font 7 | import androidx.compose.ui.text.font.FontStyle 8 | import androidx.compose.ui.text.font.FontWeight 9 | import kotlinx.coroutines.runBlocking 10 | import org.jetbrains.compose.resources.ExperimentalResourceApi 11 | import org.jetbrains.compose.resources.InternalResourceApi 12 | import org.jetbrains.compose.resources.readResourceBytes 13 | 14 | private val cache: MutableMap = mutableMapOf() 15 | 16 | @OptIn(InternalResourceApi::class) 17 | @Composable 18 | actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { 19 | return cache.getOrPut(res) { 20 | val byteArray = runBlocking { 21 | readResourceBytes("font/$res.ttf") 22 | } 23 | androidx.compose.ui.text.platform.Font(res, byteArray, weight, style) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /shared/src/iosTest/kotlin/com/multiplatformkickstarter/app/IosTest.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app 2 | 3 | import kotlin.test.Test 4 | 5 | class IosTest { 6 | 7 | @Test 8 | fun testExample() { 9 | // assertTrue(Greeting().greet().contains("iOS"), "Check iOS is mentioned") 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/com/multiplatformkickstarter/app/feature/debugmenu/Debug.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.feature.debugmenu 2 | 3 | class JvmDebug : Debug { 4 | override val isDebug: Boolean = true 5 | } 6 | 7 | actual fun getDebug(): Debug = JvmDebug() 8 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/com/multiplatformkickstarter/app/localization/Localization.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.localization 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | actual fun getCurrentLanguage(): AvailableLanguages = AvailableLanguages.EN 6 | 7 | @Composable 8 | actual fun SetLanguage(language: AvailableLanguages) { 9 | } 10 | -------------------------------------------------------------------------------- /shared/src/jvmMain/kotlin/com/multiplatformkickstarter/app/platform/Resources.jvm.kt: -------------------------------------------------------------------------------- 1 | package com.multiplatformkickstarter.app.platform 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.text.font.Font 5 | import androidx.compose.ui.text.font.FontStyle 6 | import androidx.compose.ui.text.font.FontWeight 7 | import kotlinx.coroutines.runBlocking 8 | import org.jetbrains.compose.resources.InternalResourceApi 9 | import org.jetbrains.compose.resources.readResourceBytes 10 | 11 | private val cache: MutableMap = mutableMapOf() 12 | 13 | @OptIn(InternalResourceApi::class) 14 | @Composable 15 | actual fun font(name: String, res: String, weight: FontWeight, style: FontStyle): Font { 16 | return cache.getOrPut(res) { 17 | val byteArray = runBlocking { 18 | readResourceBytes("font/$res.ttf") 19 | } 20 | androidx.compose.ui.text.platform.Font(res, byteArray, weight, style) 21 | } 22 | } 23 | --------------------------------------------------------------------------------