├── .github ├── FUNDING.yml └── dependabot.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.MD ├── assests └── screenshots │ ├── 1.png │ ├── 10.png │ ├── 11.png │ ├── 12.png │ ├── 13.png │ ├── 14.png │ ├── 15.png │ ├── 16.png │ ├── 2.png │ ├── 3.png │ ├── 4.png │ ├── 40.png │ ├── 41.png │ ├── 42.png │ ├── 43.png │ ├── 44.png │ ├── 45.png │ ├── 46.png │ ├── 47.png │ ├── 48.png │ ├── 49.png │ ├── 5.png │ ├── 50.png │ ├── 51.png │ ├── 52.png │ ├── 53.png │ ├── 54.png │ ├── 55.png │ ├── 56.png │ ├── 57.png │ ├── 58.png │ ├── 59.png │ ├── 6.png │ ├── 60.png │ ├── 61.png │ ├── 62.png │ ├── 7.png │ ├── 8.png │ ├── 9.png │ ├── poster.png │ └── poster_crypto.png ├── build.gradle.kts ├── composeApp ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── org │ │ └── company │ │ └── app │ │ ├── App.android.kt │ │ └── theme │ │ └── Theme.android.kt │ ├── commonMain │ ├── composeResources │ │ ├── drawable │ │ │ ├── ic_cyclone.xml │ │ │ ├── ic_dark_mode.xml │ │ │ ├── ic_light_mode.xml │ │ │ ├── ic_rotate_right.xml │ │ │ ├── thumbs_down.png │ │ │ └── thumbs_up.png │ │ ├── font │ │ │ ├── IndieFlower-Regular.ttf │ │ │ └── PoetsenOne-Regular.ttf │ │ └── values │ │ │ └── strings.xml │ └── kotlin │ │ └── org │ │ └── company │ │ └── app │ │ ├── App.kt │ │ ├── data │ │ ├── remote │ │ │ └── CryptoClient.kt │ │ └── repository │ │ │ └── CryptoApi.kt │ │ ├── di │ │ └── appModule.kt │ │ ├── domain │ │ ├── model │ │ │ ├── categories │ │ │ │ └── NewsCategoriesItem.kt │ │ │ ├── crypto │ │ │ │ ├── Data.kt │ │ │ │ ├── LatestListing.kt │ │ │ │ ├── Platform.kt │ │ │ │ ├── Quote.kt │ │ │ │ ├── Status.kt │ │ │ │ └── USD.kt │ │ │ └── news │ │ │ │ ├── Data.kt │ │ │ │ ├── NewsList.kt │ │ │ │ ├── RateLimit.kt │ │ │ │ └── SourceInfo.kt │ │ ├── repository │ │ │ └── Repository.kt │ │ └── usecase │ │ │ └── ResultState.kt │ │ ├── presentation │ │ ├── ui │ │ │ ├── components │ │ │ │ ├── ChartImage.kt │ │ │ │ ├── CryptoChart.kt │ │ │ │ ├── CryptoList.kt │ │ │ │ ├── CurrencyImage.kt │ │ │ │ ├── ErrorBox.kt │ │ │ │ ├── LoadingBox.kt │ │ │ │ ├── MarketRow.kt │ │ │ │ ├── NewsDetail.kt │ │ │ │ ├── PromotionalCard.kt │ │ │ │ └── SuggestionMessage.kt │ │ │ ├── navigation │ │ │ │ ├── rails │ │ │ │ │ ├── items │ │ │ │ │ │ └── NavigationItem.kt │ │ │ │ │ └── navbar │ │ │ │ │ │ └── NavigationSideBar.kt │ │ │ │ └── tab │ │ │ │ │ ├── analytics │ │ │ │ │ └── Analytics.kt │ │ │ │ │ ├── home │ │ │ │ │ └── Home.kt │ │ │ │ │ ├── news │ │ │ │ │ └── News.kt │ │ │ │ │ └── profile │ │ │ │ │ └── Profile.kt │ │ │ └── screens │ │ │ │ ├── analytics │ │ │ │ └── AnalyticScreen.kt │ │ │ │ ├── detail │ │ │ │ └── DetailScreen.kt │ │ │ │ ├── home │ │ │ │ ├── HomeContent.kt │ │ │ │ └── HomeScreen.kt │ │ │ │ ├── news │ │ │ │ └── NewsScreen.kt │ │ │ │ └── profile │ │ │ │ └── ProfileScreen.kt │ │ └── viewmodel │ │ │ └── MainViewModel.kt │ │ ├── theme │ │ ├── Color.kt │ │ └── Theme.kt │ │ └── utils │ │ ├── Constant.kt │ │ ├── formatMarketCap.kt │ │ └── formateTimeStamp.kt │ ├── commonTest │ └── kotlin │ │ └── org │ │ └── company │ │ └── app │ │ └── ComposeTest.kt │ ├── iosMain │ └── kotlin │ │ ├── main.kt │ │ └── org │ │ └── company │ │ └── app │ │ ├── App.ios.kt │ │ └── theme │ │ └── Theme.ios.kt │ ├── jsMain │ ├── kotlin │ │ ├── main.kt │ │ └── org │ │ │ └── company │ │ │ └── app │ │ │ ├── App.js.kt │ │ │ └── theme │ │ │ └── Theme.js.kt │ └── resources │ │ └── index.html │ └── jvmMain │ └── kotlin │ ├── main.kt │ └── org │ └── company │ └── app │ ├── App.jvm.kt │ └── theme │ └── Theme.jvm.kt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── iosApp ├── iosApp.xcodeproj │ ├── project.pbxproj │ └── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── iosApp │ ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ └── Contents.json │ └── Contents.json │ ├── Preview Content │ └── Preview Assets.xcassets │ │ └── Contents.json │ └── iosApp.swift └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: KhubaibKhan4 4 | patreon: MuhammadKhubaibImtiaz 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: muhammadkhubaibimtiaz 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: khubaibkhan 14 | custom: https://paypal.me/18.bscs 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | *.iml 3 | .gradle 4 | .idea 5 | .kotlin 6 | .DS_Store 7 | build 8 | */build 9 | captures 10 | .externalNativeBuild 11 | .cxx 12 | local.properties 13 | xcuserdata/ 14 | Pods/ 15 | *.jks 16 | *yarn.lock 17 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | Email: 18.bscs.803@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | # Crypto KMP 2 | ![Crypto KMP](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/poster_crypto.png) 3 | 4 | ## Overview 5 | Crypto-KMP is a Kotlin Multiplatform (KMP) project aimed at providing cryptocurrency-related functionalities across Android, iOS, Web, and Desktop platforms with shared code. This documentation provides an overview of the project structure, features, and usage instructions. 6 | 7 | ## 🚀 Future Plans 8 | - TV and Wear OS Versions 9 | - Real-time Tracking 10 | 11 | ## 🌟 Contributions 12 | If you wanna contribute, Please make sure to add new features & Then make a PR.Feel free to contribute to the project and stay tuned for more exciting updates! 13 | 14 | # Open To Work 15 | Do you wanna Convert your thoughts into Physicall & Successfull Project Then I'm here for you. I'm open to work, available for Freelance or Remote Work Opportunities. Feel free to reach me out on Email. 16 | 17 | ## 🤝 Connect with Me 18 | 19 | Let's chat about potential projects, job opportunities, or any other collaboration! Feel free to connect with me through the following channels: 20 | 21 | [![LinkedIn](https://img.shields.io/badge/LinkedIn-Connect-blue?style=for-the-badge&logo=linkedin)](https://www.linkedin.com/in/khubaibkhandev) 22 | [![Twitter](https://img.shields.io/badge/Twitter-Follow-blue?style=for-the-badge&logo=twitter)](https://twitter.com/codespacepro) 23 | [![Email](https://img.shields.io/badge/Email-Drop%20a%20Message-red?style=for-the-badge&logo=gmail)](mailto:18.bscs.803@gmail.com) 24 | 25 | ## 💰 You can help me by Donating 26 | [![BuyMeACoffee](https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black)](https://buymeacoffee.com/khubaibkhan) [![PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/18.bscs) [![Patreon](https://img.shields.io/badge/Patreon-F96854?style=for-the-badge&logo=patreon&logoColor=white)](https://patreon.com/MuhammadKhubaibImtiaz) [![Ko-Fi](https://img.shields.io/badge/Ko--fi-F16061?style=for-the-badge&logo=ko-fi&logoColor=white)](https://ko-fi.com/muhammadkhubaibimtiaz) 27 | 28 | # Tech Stack Highlights 29 | 30 | - **Kotlin Multiplatform**: `2.0.0` 31 | - **AGP (Android Gradle Plugin)**: `8.2.2` 32 | - **Compose**: `1.7.1` 33 | - `androidx-appcompat`: `1.6.1` 34 | - `androidx-activityCompose`: `1.9.3` 35 | - `ui-testing`: `1.7.6` 36 | - `compose-uitooling`: `1.6.7` 37 | - `composeImageLoader`: `1.7.1` 38 | - `composeIcons`: `1.1.0` 39 | - **Core Libraries**: `12.1.0` 40 | - **Kotlinx Libraries**: 41 | - `kotlinx-coroutines`: `1.9.0-RC` 42 | - `kotlinx-serialization`: `1.6.3` 43 | - `kotlinx-datetime`: `0.5.0` 44 | - **Networking**: 45 | - `coilNetwork`: `3.0.0-alpha01` 46 | - `ktor`: `2.3.12` 47 | - **Dependency Injection**: 48 | - `koin`: `3.6.0-Beta4` 49 | - **UI/UX**: 50 | - `compose-uitooling`: `1.6.3` 51 | - `composeIcons`: `1.1.0` 52 | - **Logging**: 53 | - `napier`: `2.7.1` 54 | - **Database/Storage**: 55 | - `sql-delight`: `2.0.2` 56 | - **JSON Parsing**: 57 | - `json`: `1.6.3` 58 | - **Build Configurations**: 59 | - `buildConfig`: `4.1.1` 60 | - **Cross-Platform Image Loading**: 61 | - `kamelImage`: `0.9.4` 62 | - **Size Measurement**: 63 | - `size`: `0.4.1` 64 | - **Voyager Navigation**: `1.1.0-Beta05` 65 | - **Other Utilities**: 66 | - `official-viewModel`: `2.8.4` 67 | 68 | ## Android Screen Shots 69 | 70 | | ![Screenshot 1](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/1.png) | ![Screenshot 2](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/2.png) | ![Screenshot 3](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/3.png) | 71 | | --- | --- | --- | 72 | | ![Screenshot 4](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/4.png) | ![Screenshot 5](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/5.png) | ![Screenshot 6](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/6.png) | 73 | | --- | --- | --- | 74 | | ![Screenshot 7](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/7.png) | ![Screenshot 8](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/8.png) | ![Screenshot 9](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/9.png) | 75 | | --- | --- | --- | 76 | | ![Screenshot 10](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/10.png) | ![Screenshot 11](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/11.png) | ![Screenshot 12](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/12.png) | 77 | | --- | --- | --- | 78 | ![Screenshot 13](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/13.png) | ![Screenshot 14](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/14.png) | ![Screenshot 15](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/15.png) | 79 | | --- | --- | --- | 80 | | ![Screenshot 16](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/16.png) | 81 | 82 | ## iOS Screen Shots 83 | 84 | | ![Screenshot 1](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/55.png) | ![Screenshot 2](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/56.png) | ![Screenshot 3](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/57.png) | 85 | | --- | --- | --- | 86 | | ![Screenshot 4](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/58.png) | ![Screenshot 5](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/59.png) | ![Screenshot 6](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/60.png) | 87 | | --- | --- | --- | 88 | | ![Screenshot 7](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/61.png) | ![Screenshot 8](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/62.png) | | 89 | | --- | --- | --- | 90 | 91 | ## Desktop Screen Shots 92 | 93 | | ![Screenshot 1](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/40.png) | 94 | | --- | 95 | | ![Screenshot 2](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/41.png) | 96 | | ![Screenshot 3](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/42.png) | 97 | | ![Screenshot 4](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/43.png) | 98 | | ![Screenshot 5](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/44.png) | 99 | | ![Screenshot 6](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/45.png) | 100 | | ![Screenshot 7](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/46.png) | 101 | | ![Screenshot 5](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/47.png) | 102 | | ![Screenshot 6](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/48.png) | 103 | | ![Screenshot 7](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/49.png) | 104 | | ![Screenshot 8](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/50.png) | 105 | | ![Screenshot 9](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/51.png) | 106 | | ![Screenshot 10](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/52.png) | 107 | | ![Screenshot 11](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/53.png) | 108 | | ![Screenshot 12](https://github.com/KhubaibKhan4/Crypto-KMP/blob/master/assests/screenshots/54.png) | 109 | 110 | ## Star History 111 | 112 | 113 | 114 | 115 | 116 | Star History Chart 117 | 118 | 119 | 120 | **Stargazers** 121 | 122 | [![Stargazers repo roster for @KhubaibKhan4/Crypto-KMP](http://reporoster.com/stars/dark/KhubaibKhan4/Crypto-KMP)](https://github.com/KhubaibKhan4/Crypto-KMP/stargazers) 123 | 124 | **Forkers** 125 | 126 | [![Forkers repo roster for @KhubaibKhan4/Crypto-KMP](http://reporoster.com/forks/dark/KhubaibKhan4/Crypto-KMP)](https://github.com/KhubaibKhan4/Crypto-KMP/network/members) 127 | 128 | ## Before running! 129 | - check your system with [KDoctor](https://github.com/Kotlin/kdoctor) 130 | - install JDK 17 or higher on your machine 131 | - add `local.properties` file to the project root and set a path to Android SDK there 132 | 133 | ### Android 134 | To run the application on android device/emulator: 135 | - open project in Android Studio and run imported android run configuration 136 | 137 | To build the application bundle: 138 | - run `./gradlew :composeApp:assembleDebug` 139 | - find `.apk` file in `composeApp/build/outputs/apk/debug/composeApp-debug.apk` 140 | Run android simulator UI tests: `./gradlew :composeApp:pixel5Check` 141 | 142 | ### Desktop 143 | Run the desktop application: `./gradlew :composeApp:run` 144 | Run desktop UI tests: `./gradlew :composeApp:jvmTest` 145 | 146 | ### iOS 147 | To run the application on iPhone device/simulator: 148 | - Open `iosApp/iosApp.xcproject` in Xcode and run standard configuration 149 | - Or use [Kotlin Multiplatform Mobile plugin](https://plugins.jetbrains.com/plugin/14936-kotlin-multiplatform-mobile) for Android Studio 150 | Run iOS simulator UI tests: `./gradlew :composeApp:iosSimulatorArm64Test` 151 | 152 | ### Experimental Browser (JS) 153 | Run the browser application: `./gradlew :composeApp:jsBrowserDevelopmentRun --continue` 154 | Run browser UI tests: `./gradlew :composeApp:jsBrowserTest` 155 | 156 | ## Community Resources 157 | Join our community to connect with other users and contributors: 158 | 159 | - **Documentation**: Find more information and resources in our [documentation](https://github.com/KhubaibKhan4/Youtube-Clone-KMP/wiki). 160 | - **Forums**: Ask questions and discuss ideas in our [community forums](https://github.com/KhubaibKhan4/Youtube-Clone-KMP/discussions). 161 | - **Chat**: Join our community chat on [Slack](https://join.slack.com/t/kotlinmultipl-gr51340/shared_invite/zt-2go24sz06-_lyxM2arRifMqwAPN2EzoA) to chat with other users and contributors. 162 | 163 | ## Acknowledgments 164 | We'd like to thank the following individuals and organizations for their contributions to the YouTube Clone project: 165 | 166 | - No Contributor Yet. 167 | - Checkout Other cool projects on my profile as well. 168 | -------------------------------------------------------------------------------- /assests/screenshots/1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/1.png -------------------------------------------------------------------------------- /assests/screenshots/10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/10.png -------------------------------------------------------------------------------- /assests/screenshots/11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/11.png -------------------------------------------------------------------------------- /assests/screenshots/12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/12.png -------------------------------------------------------------------------------- /assests/screenshots/13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/13.png -------------------------------------------------------------------------------- /assests/screenshots/14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/14.png -------------------------------------------------------------------------------- /assests/screenshots/15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/15.png -------------------------------------------------------------------------------- /assests/screenshots/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/16.png -------------------------------------------------------------------------------- /assests/screenshots/2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/2.png -------------------------------------------------------------------------------- /assests/screenshots/3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/3.png -------------------------------------------------------------------------------- /assests/screenshots/4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/4.png -------------------------------------------------------------------------------- /assests/screenshots/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/40.png -------------------------------------------------------------------------------- /assests/screenshots/41.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/41.png -------------------------------------------------------------------------------- /assests/screenshots/42.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/42.png -------------------------------------------------------------------------------- /assests/screenshots/43.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/43.png -------------------------------------------------------------------------------- /assests/screenshots/44.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/44.png -------------------------------------------------------------------------------- /assests/screenshots/45.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/45.png -------------------------------------------------------------------------------- /assests/screenshots/46.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/46.png -------------------------------------------------------------------------------- /assests/screenshots/47.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/47.png -------------------------------------------------------------------------------- /assests/screenshots/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/48.png -------------------------------------------------------------------------------- /assests/screenshots/49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/49.png -------------------------------------------------------------------------------- /assests/screenshots/5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/5.png -------------------------------------------------------------------------------- /assests/screenshots/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/50.png -------------------------------------------------------------------------------- /assests/screenshots/51.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/51.png -------------------------------------------------------------------------------- /assests/screenshots/52.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/52.png -------------------------------------------------------------------------------- /assests/screenshots/53.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/53.png -------------------------------------------------------------------------------- /assests/screenshots/54.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/54.png -------------------------------------------------------------------------------- /assests/screenshots/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/55.png -------------------------------------------------------------------------------- /assests/screenshots/56.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/56.png -------------------------------------------------------------------------------- /assests/screenshots/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/57.png -------------------------------------------------------------------------------- /assests/screenshots/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/58.png -------------------------------------------------------------------------------- /assests/screenshots/59.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/59.png -------------------------------------------------------------------------------- /assests/screenshots/6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/6.png -------------------------------------------------------------------------------- /assests/screenshots/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/60.png -------------------------------------------------------------------------------- /assests/screenshots/61.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/61.png -------------------------------------------------------------------------------- /assests/screenshots/62.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/62.png -------------------------------------------------------------------------------- /assests/screenshots/7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/7.png -------------------------------------------------------------------------------- /assests/screenshots/8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/8.png -------------------------------------------------------------------------------- /assests/screenshots/9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/9.png -------------------------------------------------------------------------------- /assests/screenshots/poster.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/poster.png -------------------------------------------------------------------------------- /assests/screenshots/poster_crypto.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/assests/screenshots/poster_crypto.png -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.multiplatform).apply(false) 3 | alias(libs.plugins.compose).apply(false) 4 | alias(libs.plugins.android.application).apply(false) 5 | alias(libs.plugins.buildConfig).apply(false) 6 | alias(libs.plugins.kotlinx.serialization).apply(false) 7 | alias(libs.plugins.sqlDelight).apply(false) 8 | alias(libs.plugins.compose.compiler).apply(false) 9 | } 10 | -------------------------------------------------------------------------------- /composeApp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.compose.ExperimentalComposeLibrary 2 | import org.jetbrains.compose.desktop.application.dsl.TargetFormat 3 | import com.android.build.api.dsl.ManagedVirtualDevice 4 | import org.jetbrains.kotlin.gradle.ExperimentalKotlinGradlePluginApi 5 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetTree 6 | 7 | plugins { 8 | alias(libs.plugins.multiplatform) 9 | alias(libs.plugins.compose) 10 | alias(libs.plugins.android.application) 11 | alias(libs.plugins.buildConfig) 12 | alias(libs.plugins.kotlinx.serialization) 13 | alias(libs.plugins.sqlDelight) 14 | alias(libs.plugins.compose.compiler) 15 | } 16 | 17 | kotlin { 18 | androidTarget { 19 | compilations.all { 20 | kotlinOptions { 21 | jvmTarget = "${JavaVersion.VERSION_1_8}" 22 | freeCompilerArgs += "-Xjdk-release=${JavaVersion.VERSION_1_8}" 23 | } 24 | } 25 | //https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html 26 | @OptIn(ExperimentalKotlinGradlePluginApi::class) 27 | instrumentedTestVariant { 28 | sourceSetTree.set(KotlinSourceSetTree.test) 29 | dependencies { 30 | debugImplementation(libs.androidx.testManifest) 31 | implementation(libs.androidx.junit4) 32 | } 33 | } 34 | } 35 | 36 | jvm() 37 | 38 | js { 39 | browser() 40 | binaries.executable() 41 | } 42 | 43 | listOf( 44 | iosX64(), 45 | iosArm64(), 46 | iosSimulatorArm64() 47 | ).forEach { 48 | it.binaries.framework { 49 | baseName = "ComposeApp" 50 | isStatic = true 51 | } 52 | } 53 | 54 | sourceSets { 55 | all { 56 | languageSettings { 57 | optIn("org.jetbrains.compose.resources.ExperimentalResourceApi") 58 | } 59 | } 60 | commonMain.dependencies { 61 | implementation(compose.runtime) 62 | implementation(compose.foundation) 63 | implementation(compose.material3) 64 | implementation(compose.material) 65 | implementation(compose.components.resources) 66 | implementation(compose.components.uiToolingPreview) 67 | implementation(compose.materialIconsExtended) 68 | implementation(libs.voyager.navigator) 69 | implementation(libs.tab.navigator) 70 | implementation(libs.napier) 71 | implementation(libs.kotlinx.coroutines.core) 72 | implementation(libs.lifecycle.viewmodel.compose) 73 | implementation(libs.ktor.core) 74 | implementation(libs.ktor.client.content.negociation) 75 | implementation(libs.ktor.client.serialization.json) 76 | implementation(libs.ktor.client.logging) 77 | implementation(libs.kotlinx.serialization.json) 78 | implementation(libs.kotlinx.datetime) 79 | implementation(libs.screen.size) 80 | implementation(project.dependencies.platform("io.insert-koin:koin-bom:4.0.0")) 81 | implementation(libs.insert.koin.koin.core) 82 | implementation(libs.koin.compose) 83 | implementation(libs.koin.annotations) 84 | implementation(libs.kamel.image) 85 | implementation(libs.kamel.image.default) 86 | implementation(libs.chart) 87 | implementation(libs.bignum) 88 | implementation(libs.ktor.core) 89 | implementation(libs.coil.compose.core) 90 | implementation(libs.coil.compose) 91 | implementation(libs.coil.mp) 92 | implementation(libs.coil.network.ktor) 93 | implementation(libs.alert.kmp) 94 | } 95 | 96 | commonTest.dependencies { 97 | implementation(kotlin("test")) 98 | @OptIn(ExperimentalComposeLibrary::class) 99 | implementation(compose.uiTest) 100 | implementation(libs.kotlinx.coroutines.test) 101 | } 102 | 103 | androidMain.dependencies { 104 | implementation(compose.uiTooling) 105 | implementation(libs.androidx.activityCompose) 106 | implementation(libs.kotlinx.coroutines.android) 107 | implementation(libs.ktor.client.okhttp) 108 | implementation(libs.sqlDelight.driver.android) 109 | implementation(project.dependencies.platform("io.insert-koin:koin-bom:4.0.0")) 110 | implementation(libs.io.insert.koin.koin.core) 111 | implementation(libs.koin.android) 112 | implementation(libs.koin.annotations) 113 | } 114 | 115 | jvmMain.dependencies { 116 | implementation(compose.desktop.currentOs) 117 | implementation(libs.kotlinx.coroutines.swing) 118 | implementation(libs.ktor.client.okhttp) 119 | implementation(libs.sqlDelight.driver.sqlite) 120 | } 121 | 122 | jsMain.dependencies { 123 | implementation(compose.html.core) 124 | implementation(libs.ktor.client.js) 125 | implementation(libs.sqlDelight.driver.js) 126 | } 127 | 128 | iosMain.dependencies { 129 | implementation(libs.ktor.client.darwin) 130 | implementation(libs.sqlDelight.driver.native) 131 | } 132 | 133 | } 134 | } 135 | 136 | android { 137 | namespace = "org.company.app" 138 | compileSdk = 34 139 | 140 | defaultConfig { 141 | minSdk = 24 142 | targetSdk = 34 143 | 144 | applicationId = "org.company.app.androidApp" 145 | versionCode = 1 146 | versionName = "1.0.0" 147 | 148 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 149 | } 150 | sourceSets["main"].apply { 151 | manifest.srcFile("src/androidMain/AndroidManifest.xml") 152 | res.srcDirs("src/androidMain/res") 153 | } 154 | //https://developer.android.com/studio/test/gradle-managed-devices 155 | @Suppress("UnstableApiUsage") 156 | testOptions { 157 | managedDevices.devices { 158 | maybeCreate("pixel5").apply { 159 | device = "Pixel 5" 160 | apiLevel = 34 161 | systemImageSource = "aosp" 162 | } 163 | } 164 | } 165 | compileOptions { 166 | sourceCompatibility = JavaVersion.VERSION_1_8 167 | targetCompatibility = JavaVersion.VERSION_1_8 168 | } 169 | buildFeatures { 170 | compose = true 171 | } 172 | composeOptions { 173 | kotlinCompilerExtensionVersion = "1.5.11" 174 | } 175 | } 176 | 177 | compose.desktop { 178 | application { 179 | mainClass = "MainKt" 180 | 181 | nativeDistributions { 182 | targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) 183 | packageName = "org.company.app.desktopApp" 184 | packageVersion = "1.0.0" 185 | } 186 | } 187 | } 188 | 189 | compose.experimental { 190 | web.application {} 191 | } 192 | 193 | buildConfig { 194 | // BuildConfig configuration here. 195 | // https://github.com/gmazzo/gradle-buildconfig-plugin#usage-in-kts 196 | } 197 | 198 | sqldelight { 199 | databases { 200 | create("MyDatabase") { 201 | // Database configuration here. 202 | // https://cashapp.github.io/sqldelight 203 | packageName.set("org.company.app.db") 204 | } 205 | } 206 | } 207 | task("testClasses"){} -------------------------------------------------------------------------------- /composeApp/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/org/company/app/App.android.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import android.app.Application 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import androidx.activity.ComponentActivity 8 | import androidx.activity.compose.setContent 9 | import androidx.activity.enableEdgeToEdge 10 | import org.company.app.di.appModule 11 | import org.koin.android.ext.koin.androidContext 12 | import org.koin.android.ext.koin.androidLogger 13 | import org.koin.core.context.startKoin 14 | 15 | class AndroidApp : Application() { 16 | companion object { 17 | lateinit var INSTANCE: AndroidApp 18 | } 19 | 20 | override fun onCreate() { 21 | super.onCreate() 22 | INSTANCE = this 23 | } 24 | } 25 | 26 | class AppActivity : ComponentActivity() { 27 | override fun onCreate(savedInstanceState: Bundle?) { 28 | super.onCreate(savedInstanceState) 29 | enableEdgeToEdge() 30 | setContent { App() } 31 | } 32 | } 33 | 34 | internal actual fun openUrl(url: String?) { 35 | val uri = url?.let { Uri.parse(it) } ?: return 36 | val intent = Intent().apply { 37 | action = Intent.ACTION_VIEW 38 | data = uri 39 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 40 | } 41 | AndroidApp.INSTANCE.startActivity(intent) 42 | } -------------------------------------------------------------------------------- /composeApp/src/androidMain/kotlin/org/company/app/theme/Theme.android.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import android.app.Activity 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.runtime.LaunchedEffect 6 | import androidx.compose.ui.platform.LocalView 7 | import androidx.core.view.WindowInsetsControllerCompat 8 | 9 | @Composable 10 | internal actual fun SystemAppearance(isDark: Boolean) { 11 | val view = LocalView.current 12 | LaunchedEffect(isDark) { 13 | val window = (view.context as Activity).window 14 | WindowInsetsControllerCompat(window, window.decorView).apply { 15 | isAppearanceLightStatusBars = isDark 16 | isAppearanceLightNavigationBars = isDark 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_cyclone.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_dark_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_light_mode.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/ic_rotate_right.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/thumbs_down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/composeApp/src/commonMain/composeResources/drawable/thumbs_down.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/drawable/thumbs_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/composeApp/src/commonMain/composeResources/drawable/thumbs_up.png -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/composeApp/src/commonMain/composeResources/font/IndieFlower-Regular.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/font/PoetsenOne-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/composeApp/src/commonMain/composeResources/font/PoetsenOne-Regular.ttf -------------------------------------------------------------------------------- /composeApp/src/commonMain/composeResources/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Cyclone 3 | Open github 4 | Run 5 | Stop 6 | Theme 7 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/App.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.interaction.MutableInteractionSource 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.RowScope 8 | import androidx.compose.foundation.layout.WindowInsets 9 | import androidx.compose.foundation.layout.fillMaxSize 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.ime 13 | import androidx.compose.foundation.layout.navigationBarsPadding 14 | import androidx.compose.foundation.layout.padding 15 | import androidx.compose.foundation.layout.windowInsetsPadding 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.material.BottomNavigation 18 | import androidx.compose.material.BottomNavigationItem 19 | import androidx.compose.material.icons.Icons 20 | import androidx.compose.material.icons.filled.Analytics 21 | import androidx.compose.material.icons.filled.Home 22 | import androidx.compose.material.icons.filled.Newspaper 23 | import androidx.compose.material.icons.filled.Person 24 | import androidx.compose.material.icons.filled.Wallet 25 | import androidx.compose.material.icons.outlined.Analytics 26 | import androidx.compose.material.icons.outlined.Home 27 | import androidx.compose.material.icons.outlined.Newspaper 28 | import androidx.compose.material.icons.outlined.Person 29 | import androidx.compose.material.icons.outlined.Wallet 30 | import androidx.compose.material3.Icon 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.NavigationBar 33 | import androidx.compose.material3.NavigationBarItem 34 | import androidx.compose.material3.NavigationBarItemColors 35 | import androidx.compose.material3.Scaffold 36 | import androidx.compose.material3.Text 37 | import androidx.compose.material3.contentColorFor 38 | import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi 39 | import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass 40 | import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass 41 | import androidx.compose.runtime.Composable 42 | import androidx.compose.runtime.getValue 43 | import androidx.compose.runtime.mutableStateOf 44 | import androidx.compose.runtime.saveable.rememberSaveable 45 | import androidx.compose.runtime.setValue 46 | import androidx.compose.ui.Modifier 47 | import androidx.compose.ui.draw.clip 48 | import androidx.compose.ui.graphics.Color 49 | import androidx.compose.ui.unit.dp 50 | import androidx.compose.ui.unit.sp 51 | import cafe.adriel.voyager.navigator.tab.CurrentTab 52 | import cafe.adriel.voyager.navigator.tab.LocalTabNavigator 53 | import cafe.adriel.voyager.navigator.tab.Tab 54 | import cafe.adriel.voyager.navigator.tab.TabNavigator 55 | import coil3.ImageLoader 56 | import coil3.PlatformContext 57 | import coil3.annotation.ExperimentalCoilApi 58 | import coil3.compose.setSingletonImageLoaderFactory 59 | import coil3.request.CachePolicy 60 | import coil3.request.crossfade 61 | import coil3.util.DebugLogger 62 | import org.company.app.di.appModule 63 | import org.company.app.presentation.ui.navigation.rails.items.NavigationItem 64 | import org.company.app.presentation.ui.navigation.rails.navbar.NavigationSideBar 65 | import org.company.app.presentation.ui.navigation.tab.analytics.Analytics 66 | import org.company.app.presentation.ui.navigation.tab.home.Home 67 | import org.company.app.presentation.ui.navigation.tab.news.News 68 | import org.company.app.presentation.ui.navigation.tab.profile.Profile 69 | import org.company.app.theme.AppTheme 70 | import org.company.app.theme.LocalThemeIsDark 71 | import org.koin.compose.KoinApplication 72 | import org.koin.core.KoinApplication 73 | 74 | @OptIn(ExperimentalCoilApi::class) 75 | @Composable 76 | internal fun App() = AppTheme { 77 | setSingletonImageLoaderFactory { context -> 78 | getAsyncImageLoader(context) 79 | } 80 | KoinApplication( 81 | application = { 82 | modules(appModule) 83 | } 84 | ){ 85 | AppContent() 86 | } 87 | } 88 | 89 | fun getAsyncImageLoader(context: PlatformContext) = 90 | ImageLoader.Builder(context) 91 | .crossfade(true) 92 | .logger(DebugLogger()) 93 | .memoryCachePolicy(CachePolicy.ENABLED) 94 | .build() 95 | 96 | @OptIn(ExperimentalMaterial3WindowSizeClassApi::class) 97 | @Composable 98 | fun AppContent() { 99 | val items = listOf( 100 | NavigationItem( 101 | title = "Home", 102 | selectedIcon = Icons.Default.Home, 103 | unselectedIcon = Icons.Outlined.Home, 104 | hasNews = false, 105 | ), 106 | NavigationItem( 107 | title = "Analytics", 108 | selectedIcon = Icons.Filled.Analytics, 109 | unselectedIcon = Icons.Outlined.Analytics, 110 | hasNews = true, 111 | ), 112 | NavigationItem( 113 | title = "News", 114 | selectedIcon = Icons.Filled.Newspaper, 115 | unselectedIcon = Icons.Outlined.Newspaper, 116 | hasNews = false, 117 | ), 118 | NavigationItem( 119 | title = "Profile", 120 | selectedIcon = Icons.Filled.Person, 121 | unselectedIcon = Icons.Outlined.Person, 122 | hasNews = false, 123 | ), 124 | ) 125 | val windowClass = calculateWindowSizeClass() 126 | val showNavigationRail = windowClass.widthSizeClass != WindowWidthSizeClass.Compact 127 | var selectedItemIndex by rememberSaveable { 128 | mutableStateOf(0) 129 | } 130 | 131 | TabNavigator(Home) { tabNavigator -> 132 | Scaffold(bottomBar = { 133 | if (!showNavigationRail) { 134 | NavigationBar( 135 | modifier = Modifier.fillMaxWidth().windowInsetsPadding(WindowInsets.ime), 136 | containerColor = MaterialTheme.colorScheme.background, 137 | contentColor = contentColorFor(Color.Red), 138 | tonalElevation = 8.dp 139 | ) { 140 | TabItem(Home) 141 | TabItem(Analytics) 142 | TabItem(News) 143 | TabItem(Profile) 144 | } 145 | } 146 | }) { 147 | Column( 148 | modifier = Modifier.fillMaxSize().padding( 149 | bottom = it.calculateBottomPadding(), 150 | start = if (showNavigationRail) 80.dp else 0.dp 151 | ) 152 | ) { 153 | CurrentTab() 154 | } 155 | } 156 | } 157 | if (showNavigationRail) { 158 | NavigationSideBar( 159 | items = items, 160 | selectedItemIndex = selectedItemIndex, 161 | onNavigate = { 162 | selectedItemIndex = it 163 | } 164 | ) 165 | 166 | Box( 167 | modifier = Modifier.fillMaxSize() 168 | .padding(start = 80.dp) 169 | ) { 170 | when (selectedItemIndex) { 171 | 0 -> { 172 | 173 | } 174 | 175 | 1 -> { 176 | Box( 177 | modifier = Modifier 178 | .fillMaxSize() 179 | .background(MaterialTheme.colorScheme.background) 180 | ) { 181 | TabNavigator(Analytics) 182 | } 183 | } 184 | 185 | 2 -> { 186 | Box( 187 | modifier = Modifier 188 | .fillMaxSize() 189 | .background(MaterialTheme.colorScheme.background) 190 | ) { 191 | TabNavigator(News) 192 | } 193 | } 194 | 195 | 3 -> { 196 | Box( 197 | modifier = Modifier 198 | .fillMaxSize() 199 | .background(MaterialTheme.colorScheme.background) 200 | ) { 201 | TabNavigator(Profile) 202 | } 203 | } 204 | } 205 | } 206 | 207 | } 208 | } 209 | 210 | @Composable 211 | fun RowScope.TabItem(tab: Tab) { 212 | val isDark by LocalThemeIsDark.current 213 | val tabNavigator = LocalTabNavigator.current 214 | NavigationBarItem( 215 | modifier = Modifier 216 | .height(58.dp).clip(RoundedCornerShape(16.dp)), 217 | selected = tabNavigator.current == tab, 218 | onClick = { 219 | tabNavigator.current = tab 220 | }, 221 | icon = { 222 | tab.options.icon?.let { painter -> 223 | Icon( 224 | painter, 225 | contentDescription = tab.options.title, 226 | tint = if (tabNavigator.current == tab) Color.Red else if (isDark) Color.White else Color.Black 227 | ) 228 | } 229 | }, 230 | label = { 231 | tab.options.title.let { title -> 232 | Text( 233 | title, 234 | fontSize = 12.sp, 235 | color = if (tabNavigator.current == tab) Color.Red else if (isDark) Color.White else Color.Black 236 | ) 237 | } 238 | }, 239 | enabled = true, 240 | alwaysShowLabel = true, 241 | interactionSource = MutableInteractionSource(), 242 | colors = NavigationBarItemColors( 243 | selectedTextColor = Color.Red, 244 | unselectedIconColor = Color.Black, 245 | selectedIconColor = Color.Red, 246 | unselectedTextColor = Color.Black, 247 | selectedIndicatorColor = Color.LightGray, 248 | disabledIconColor = Color.Transparent, 249 | disabledTextColor = Color.Transparent 250 | ) 251 | ) 252 | } 253 | 254 | internal expect fun openUrl(url: String?) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/data/remote/CryptoClient.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.data.remote 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.call.body 5 | import io.ktor.client.plugins.HttpTimeout 6 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 7 | import io.ktor.client.plugins.defaultRequest 8 | import io.ktor.client.plugins.logging.LogLevel 9 | import io.ktor.client.plugins.logging.Logger 10 | import io.ktor.client.plugins.logging.Logging 11 | import io.ktor.client.request.get 12 | import io.ktor.client.request.headers 13 | import io.ktor.serialization.kotlinx.json.json 14 | import kotlinx.serialization.json.Json 15 | import org.company.app.domain.model.categories.NewsCategoriesItem 16 | import org.company.app.domain.model.crypto.LatestListing 17 | import org.company.app.domain.model.news.NewsList 18 | import org.company.app.utils.Constant.API_KEY 19 | import org.company.app.utils.Constant.BASE_URL 20 | import org.company.app.utils.Constant.CRYPTO_URL 21 | import org.company.app.utils.Constant.TIME_OUT 22 | 23 | class CryptoClient(private val client: HttpClient) { 24 | suspend fun getLatestListing(): LatestListing { 25 | return client.get(BASE_URL + "cryptocurrency/listings/latest").body() 26 | } 27 | suspend fun getAllNews(): NewsList{ 28 | return client.get(CRYPTO_URL + "v2/news/?lang=EN").body() 29 | } 30 | suspend fun getNewsCategories(): List{ 31 | return client.get( CRYPTO_URL+"news/categories").body() 32 | 33 | } 34 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/data/repository/CryptoApi.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.data.repository 2 | 3 | import org.company.app.domain.model.categories.NewsCategoriesItem 4 | import org.company.app.domain.model.crypto.LatestListing 5 | import org.company.app.domain.model.news.NewsList 6 | 7 | interface CryptoApi { 8 | suspend fun getLatestListing(): LatestListing 9 | suspend fun getAllNews(): NewsList 10 | suspend fun getNewsCategories(): List 11 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/di/appModule.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.di 2 | 3 | import io.ktor.client.HttpClient 4 | import io.ktor.client.plugins.HttpTimeout 5 | import io.ktor.client.plugins.contentnegotiation.ContentNegotiation 6 | import io.ktor.client.plugins.defaultRequest 7 | import io.ktor.client.plugins.logging.LogLevel 8 | import io.ktor.client.plugins.logging.Logger 9 | import io.ktor.client.plugins.logging.Logging 10 | import io.ktor.client.request.headers 11 | import io.ktor.http.HttpHeaders 12 | import io.ktor.serialization.kotlinx.json.json 13 | import kotlinx.serialization.json.Json 14 | import org.company.app.data.remote.CryptoClient 15 | import org.company.app.domain.repository.Repository 16 | import org.company.app.presentation.viewmodel.MainViewModel 17 | import org.company.app.utils.Constant 18 | import org.koin.core.module.dsl.singleOf 19 | import org.koin.dsl.module 20 | 21 | val appModule = module { 22 | single { 23 | HttpClient { 24 | install(ContentNegotiation) { 25 | json( 26 | json = Json { 27 | isLenient = true 28 | ignoreUnknownKeys = true 29 | } 30 | ) 31 | } 32 | install(Logging) { 33 | level = LogLevel.ALL 34 | logger = object : Logger { 35 | override fun log(message: String) { 36 | println(message) 37 | } 38 | } 39 | filter { filter-> filter.url.host.contains("coinmarketcap.com") } 40 | sanitizeHeader { header-> header == HttpHeaders.Authorization } 41 | } 42 | install(HttpTimeout) { 43 | requestTimeoutMillis = Constant.TIME_OUT 44 | connectTimeoutMillis = Constant.TIME_OUT 45 | socketTimeoutMillis = Constant.TIME_OUT 46 | } 47 | defaultRequest { 48 | headers { 49 | append("X-CMC_PRO_API_KEY", Constant.API_KEY) 50 | } 51 | } 52 | } 53 | } 54 | single { CryptoClient(get()) } 55 | single { 56 | Repository(get()) 57 | } 58 | singleOf(::MainViewModel) 59 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/categories/NewsCategoriesItem.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.categories 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class NewsCategoriesItem( 9 | @SerialName("categoryName") 10 | val categoryName: String = "", 11 | @SerialName("excludedPhrases") 12 | val excludedPhrases: List? = null, 13 | @SerialName("includedPhrases") 14 | val includedPhrases: List? = null, 15 | @SerialName("wordsAssociatedWithCategory") 16 | val wordsAssociatedWithCategory: List? = null 17 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/Data.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Data( 9 | @SerialName("circulating_supply") 10 | val circulatingSupply: Double = 0.0, 11 | @SerialName("cmc_rank") 12 | val cmcRank: Int = 0, 13 | @SerialName("date_added") 14 | val dateAdded: String = "", 15 | @SerialName("id") 16 | val id: Int = 0, 17 | @SerialName("infinite_supply") 18 | val infiniteSupply: Boolean = false, 19 | @SerialName("last_updated") 20 | val lastUpdated: String = "", 21 | @SerialName("max_supply") 22 | val maxSupply: Double? = null, 23 | @SerialName("name") 24 | val name: String = "", 25 | @SerialName("num_market_pairs") 26 | val numMarketPairs: Int = 0, 27 | @SerialName("platform") 28 | val platform: Platform? = null, 29 | @SerialName("quote") 30 | val quote: Quote = Quote(), 31 | @SerialName("self_reported_circulating_supply") 32 | val selfReportedCirculatingSupply: Double? = null, 33 | @SerialName("self_reported_market_cap") 34 | val selfReportedMarketCap: Double? = null, 35 | @SerialName("slug") 36 | val slug: String = "", 37 | @SerialName("symbol") 38 | val symbol: String = "", 39 | @SerialName("tags") 40 | val tags: List = listOf(), 41 | @SerialName("total_supply") 42 | val totalSupply: Double = 0.0, 43 | @SerialName("tvl_ratio") 44 | val tvlRatio: Double? = null 45 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/LatestListing.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class LatestListing( 9 | @SerialName("data") 10 | val `data`: List = listOf(), 11 | @SerialName("status") 12 | val status: Status = Status() 13 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/Platform.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Platform( 9 | @SerialName("id") 10 | val id: Int = 0, 11 | @SerialName("name") 12 | val name: String = "", 13 | @SerialName("slug") 14 | val slug: String = "", 15 | @SerialName("symbol") 16 | val symbol: String = "", 17 | @SerialName("token_address") 18 | val tokenAddress: String = "" 19 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/Quote.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Quote( 9 | @SerialName("USD") 10 | val uSD: USD = USD() 11 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/Status.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Status( 9 | @SerialName("credit_count") 10 | val creditCount: Int = 0, 11 | @SerialName("elapsed") 12 | val elapsed: Int = 0, 13 | @SerialName("error_code") 14 | val errorCode: Int = 0, 15 | @SerialName("error_message") 16 | val errorMessage: String? = null, 17 | @SerialName("notice") 18 | val notice: String? = null, 19 | @SerialName("timestamp") 20 | val timestamp: String = "", 21 | @SerialName("total_count") 22 | val totalCount: Int = 0 23 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/crypto/USD.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.crypto 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class USD( 9 | @SerialName("fully_diluted_market_cap") 10 | val fullyDilutedMarketCap: Double = 0.0, 11 | @SerialName("last_updated") 12 | val lastUpdated: String = "", 13 | @SerialName("market_cap") 14 | val marketCap: Double = 0.0, 15 | @SerialName("market_cap_dominance") 16 | val marketCapDominance: Double = 0.0, 17 | @SerialName("percent_change_1h") 18 | val percentChange1h: Double = 0.0, 19 | @SerialName("percent_change_24h") 20 | val percentChange24h: Double = 0.0, 21 | @SerialName("percent_change_30d") 22 | val percentChange30d: Double = 0.0, 23 | @SerialName("percent_change_60d") 24 | val percentChange60d: Double = 0.0, 25 | @SerialName("percent_change_7d") 26 | val percentChange7d: Double = 0.0, 27 | @SerialName("percent_change_90d") 28 | val percentChange90d: Double = 0.0, 29 | @SerialName("price") 30 | val price: Double = 0.0, 31 | @SerialName("tvl") 32 | val tvl: Double? = null, 33 | @SerialName("volume_24h") 34 | val volume24h: Double = 0.0, 35 | @SerialName("volume_change_24h") 36 | val volumeChange24h: Double = 0.0 37 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/news/Data.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.news 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class Data( 9 | @SerialName("body") 10 | val body: String = "", 11 | @SerialName("categories") 12 | val categories: String = "", 13 | @SerialName("downvotes") 14 | val downvotes: String = "", 15 | @SerialName("guid") 16 | val guid: String = "", 17 | @SerialName("id") 18 | val id: String = "", 19 | @SerialName("imageurl") 20 | val imageurl: String = "", 21 | @SerialName("lang") 22 | val lang: String = "", 23 | @SerialName("published_on") 24 | val publishedOn: Int = 0, 25 | @SerialName("source") 26 | val source: String = "", 27 | @SerialName("source_info") 28 | val sourceInfo: SourceInfo = SourceInfo(), 29 | @SerialName("tags") 30 | val tags: String = "", 31 | @SerialName("title") 32 | val title: String = "", 33 | @SerialName("upvotes") 34 | val upvotes: String = "", 35 | @SerialName("url") 36 | val url: String = "" 37 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/news/NewsList.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.news 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class NewsList( 9 | @SerialName("Data") 10 | val `data`: List = listOf(), 11 | @SerialName("HasWarning") 12 | val hasWarning: Boolean = false, 13 | @SerialName("Message") 14 | val message: String = "", 15 | @SerialName("Promoted") 16 | val promoted: List = listOf(), 17 | @SerialName("RateLimit") 18 | val rateLimit: RateLimit = RateLimit(), 19 | @SerialName("Type") 20 | val type: Int = 0 21 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/news/RateLimit.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.news 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | class RateLimit -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/model/news/SourceInfo.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.model.news 2 | 3 | 4 | import kotlinx.serialization.SerialName 5 | import kotlinx.serialization.Serializable 6 | 7 | @Serializable 8 | data class SourceInfo( 9 | @SerialName("img") 10 | val img: String = "", 11 | @SerialName("lang") 12 | val lang: String = "", 13 | @SerialName("name") 14 | val name: String = "" 15 | ) -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/repository/Repository.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.repository 2 | 3 | import org.company.app.data.remote.CryptoClient 4 | import org.company.app.data.repository.CryptoApi 5 | import org.company.app.domain.model.categories.NewsCategoriesItem 6 | import org.company.app.domain.model.crypto.LatestListing 7 | import org.company.app.domain.model.news.NewsList 8 | 9 | class Repository( 10 | private val cryptoClient: CryptoClient 11 | ): CryptoApi { 12 | override suspend fun getLatestListing(): LatestListing { 13 | return cryptoClient.getLatestListing() 14 | } 15 | 16 | override suspend fun getAllNews(): NewsList { 17 | return cryptoClient.getAllNews() 18 | } 19 | 20 | override suspend fun getNewsCategories(): List { 21 | return cryptoClient.getNewsCategories() 22 | } 23 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/domain/usecase/ResultState.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.domain.usecase 2 | 3 | sealed class ResultState { 4 | object LOADING: ResultState() 5 | data class SUCCESS(val response: T): ResultState() 6 | data class ERROR(val message: String): ResultState() 7 | } 8 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/ChartImage.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.animation.core.LinearOutSlowInEasing 4 | import androidx.compose.animation.core.tween 5 | import androidx.compose.foundation.layout.BoxScope 6 | import androidx.compose.material3.LinearProgressIndicator 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.graphics.ColorFilter 11 | import androidx.compose.ui.graphics.painter.Painter 12 | import androidx.compose.ui.layout.ContentScale 13 | import io.kamel.core.Resource 14 | import io.kamel.image.KamelImage 15 | import io.kamel.image.asyncPainterResource 16 | 17 | @Composable 18 | fun ChartImage( 19 | id: Int, 20 | modifier: Modifier, 21 | tintColor: Color, 22 | ) { 23 | val image: Resource = 24 | asyncPainterResource("https://s3.coinmarketcap.com/generated/sparklines/web/7d/2781/$id.svg") 25 | KamelImage( 26 | { image }, contentDescription = null, 27 | modifier = modifier, 28 | contentScale = ContentScale.FillWidth, 29 | colorFilter = ColorFilter.tint(color = tintColor), 30 | onLoading = { 31 | LinearProgressIndicator( 32 | progress = { it }, 33 | ) 34 | }, animationSpec = tween( 35 | durationMillis = 300, 36 | delayMillis = 300, 37 | easing = LinearOutSlowInEasing 38 | ) 39 | ) 40 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/CryptoChart.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.Spacer 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.height 9 | import androidx.compose.material3.Checkbox 10 | import androidx.compose.material3.MaterialTheme 11 | import androidx.compose.material3.Text 12 | import androidx.compose.runtime.Composable 13 | import androidx.compose.runtime.getValue 14 | import androidx.compose.runtime.mutableStateOf 15 | import androidx.compose.runtime.remember 16 | import androidx.compose.runtime.setValue 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.graphics.Color 20 | import androidx.compose.ui.text.TextStyle 21 | import androidx.compose.ui.text.font.FontWeight 22 | import androidx.compose.ui.unit.dp 23 | import androidx.compose.ui.unit.sp 24 | import com.aay.compose.baseComponents.model.GridOrientation 25 | import com.aay.compose.baseComponents.model.LegendPosition 26 | import com.aay.compose.donutChart.PieChart 27 | import com.aay.compose.donutChart.model.PieChartData 28 | import com.aay.compose.lineChart.LineChart 29 | import com.aay.compose.lineChart.model.LineParameters 30 | import com.aay.compose.lineChart.model.LineType 31 | import org.company.app.domain.model.crypto.Data 32 | import kotlin.math.abs 33 | import kotlin.random.Random 34 | 35 | @Composable 36 | fun CryptoChart( 37 | dataList: Data, 38 | selectedPeriod: String, 39 | ) { 40 | val relevantData = when (selectedPeriod) { 41 | "1H" -> listOf( 42 | dataList.quote.uSD.percentChange1h, 43 | dataList.quote.uSD.percentChange24h, 44 | dataList.quote.uSD.percentChange7d, 45 | dataList.quote.uSD.percentChange30d, 46 | dataList.quote.uSD.percentChange60d, 47 | dataList.quote.uSD.percentChange90d, 48 | ) 49 | 50 | "1D" -> listOf( 51 | Random.nextDouble(0.0, 7.0), 52 | dataList.quote.uSD.percentChange24h, 53 | dataList.quote.uSD.percentChange7d, 54 | dataList.quote.uSD.percentChange30d, 55 | dataList.quote.uSD.percentChange60d, 56 | dataList.quote.uSD.percentChange90d, 57 | ) 58 | 59 | "1W" -> listOf( 60 | Random.nextDouble(0.0, 7.0), 61 | Random.nextDouble(0.0, 7.0), 62 | dataList.quote.uSD.percentChange7d, 63 | dataList.quote.uSD.percentChange30d, 64 | dataList.quote.uSD.percentChange60d, 65 | dataList.quote.uSD.percentChange90d, 66 | ) 67 | 68 | "1M" -> listOf( 69 | Random.nextDouble(0.0, 7.0), 70 | Random.nextDouble(0.0, 7.0), 71 | Random.nextDouble(0.0, 7.0), 72 | dataList.quote.uSD.percentChange30d, 73 | dataList.quote.uSD.percentChange60d, 74 | dataList.quote.uSD.percentChange90d, 75 | ) 76 | 77 | "3M" -> listOf( 78 | Random.nextDouble(0.0, 7.0), 79 | Random.nextDouble(0.0, 7.0), 80 | Random.nextDouble(0.0, 7.0), 81 | Random.nextDouble(0.0, 7.0), 82 | dataList.quote.uSD.percentChange60d, 83 | dataList.quote.uSD.percentChange90d, 84 | ) 85 | 86 | "6M" -> listOf( 87 | Random.nextDouble(0.0, 7.0), 88 | Random.nextDouble(0.0, 7.0), 89 | Random.nextDouble(0.0, 7.0), 90 | Random.nextDouble(0.0, 7.0), 91 | Random.nextDouble(0.0, 7.0), 92 | dataList.quote.uSD.percentChange90d, 93 | ) 94 | 95 | "1Y" -> listOf( 96 | Random.nextDouble(0.0, 7.0), 97 | Random.nextDouble(0.0, 7.0), 98 | Random.nextDouble(0.0, 7.0), 99 | Random.nextDouble(0.0, 7.0), 100 | Random.nextDouble(0.0, 7.0), 101 | dataList.quote.uSD.percentChange90d, 102 | ) 103 | 104 | else -> emptyList() 105 | } 106 | 107 | var isPieCharEnabled by remember { mutableStateOf(false) } 108 | val dataList1 = mutableListOf() 109 | dataList1.add(dataList.quote.uSD.percentChange1h) 110 | dataList1.add(dataList.quote.uSD.percentChange24h) 111 | dataList1.add(dataList.quote.uSD.percentChange7d) 112 | dataList1.add(dataList.quote.uSD.percentChange30d) 113 | dataList1.add(dataList.quote.uSD.percentChange60d) 114 | dataList1.add(dataList.quote.uSD.percentChange90d) 115 | println("DataList: $dataList1") 116 | val positiveDataList = relevantData.map { abs(it) } 117 | println("DataList: $positiveDataList") 118 | 119 | val testLineParameters: List = listOf( 120 | LineParameters( 121 | label = "Price", 122 | data = positiveDataList, 123 | lineColor = Color.Red, 124 | lineType = LineType.CURVED_LINE, 125 | lineShadow = true, 126 | ) 127 | ) 128 | 129 | Column( 130 | modifier = Modifier.fillMaxWidth(), 131 | horizontalAlignment = Alignment.CenterHorizontally, 132 | verticalArrangement = Arrangement.Center 133 | ) { 134 | if (!isPieCharEnabled) { 135 | LineChart( 136 | modifier = Modifier.fillMaxWidth() 137 | .height(270.dp), 138 | linesParameters = testLineParameters, 139 | isGrid = false, 140 | gridColor = Color.Blue, 141 | xAxisData = listOf( 142 | "2016", 143 | "2018", 144 | "2020", 145 | "2022", 146 | "2023", 147 | "2024" 148 | ), 149 | animateChart = true, 150 | showGridWithSpacer = true, 151 | legendPosition = LegendPosition.TOP, 152 | yAxisStyle = TextStyle( 153 | fontSize = 14.sp, 154 | color = Color.Gray, 155 | ), 156 | xAxisStyle = TextStyle( 157 | fontSize = 14.sp, 158 | color = Color.Gray, 159 | fontWeight = FontWeight.W400 160 | ), 161 | yAxisRange = 6, 162 | oneLineChart = false, 163 | gridOrientation = GridOrientation.VERTICAL 164 | ) 165 | } else { 166 | val testPieChartData: List = listOf( 167 | PieChartData( 168 | partName = "1H", 169 | data = 40.32, 170 | color = Color(0xFF22A699), 171 | ), 172 | PieChartData( 173 | partName = "24H", 174 | data = 65.02, 175 | color = Color(0xFFF2BE22), 176 | ), 177 | PieChartData( 178 | partName = "7D", 179 | data = 42.32, 180 | color = Color(0xFFF29727), 181 | ), 182 | PieChartData( 183 | partName = "1M", 184 | data = 15.32, 185 | color = Color(0xFFF24C3D), 186 | ), 187 | PieChartData( 188 | partName = "2M", 189 | data = 90.2, 190 | color = Color(0xFFF24C3D), 191 | ), 192 | PieChartData( 193 | partName = "3M", 194 | data = 55.4, 195 | color = Color(0xFFF24C3D), 196 | ), 197 | ) 198 | 199 | PieChart( 200 | modifier = Modifier.fillMaxWidth() 201 | .height(270.dp), 202 | pieChartData = testPieChartData, 203 | ratioLineColor = Color.LightGray, 204 | textRatioStyle = TextStyle(color = Color.Gray), 205 | ) 206 | } 207 | 208 | Spacer(modifier = Modifier.height(16.dp)) 209 | Row( 210 | modifier = Modifier.fillMaxWidth(), 211 | horizontalArrangement = Arrangement.Start, 212 | verticalAlignment = Alignment.CenterVertically 213 | ) { 214 | Checkbox( 215 | checked = isPieCharEnabled, 216 | onCheckedChange = { 217 | isPieCharEnabled = it 218 | } 219 | ) 220 | Text( 221 | text = dataList.symbol + " Pie Chart", 222 | fontSize = MaterialTheme.typography.titleMedium.fontSize, 223 | fontWeight = FontWeight.Bold 224 | ) 225 | } 226 | } 227 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/CryptoList.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.animation.AnimatedVisibility 4 | import androidx.compose.foundation.clickable 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 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.layout.width 13 | import androidx.compose.material.icons.Icons 14 | import androidx.compose.material.icons.filled.KeyboardArrowDown 15 | import androidx.compose.material3.HorizontalDivider 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.MaterialTheme 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.ui.Alignment 22 | import androidx.compose.ui.Modifier 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import cafe.adriel.voyager.navigator.LocalNavigator 28 | import org.company.app.domain.model.crypto.Data 29 | import org.company.app.presentation.ui.screens.detail.DetailScreen 30 | import org.company.app.theme.LocalThemeIsDark 31 | import kotlin.math.roundToInt 32 | 33 | @Composable 34 | fun CryptoList( 35 | dataList: List, 36 | coinsText: String, 37 | viewText: String, 38 | largeCapColor: Color = Color.Black, 39 | isCapIconEnabled: Boolean = false, 40 | ) { 41 | val isDark by LocalThemeIsDark.current 42 | val viewALlColor = 43 | if (viewText.contains("View All")) if (isDark) Color.White else Color.Black else largeCapColor 44 | val textColor = if (isDark) Color.White else Color.Black 45 | Column( 46 | modifier = Modifier.fillMaxWidth(), 47 | horizontalAlignment = Alignment.CenterHorizontally, 48 | verticalArrangement = Arrangement.Center 49 | ) { 50 | Row( 51 | modifier = Modifier.fillMaxWidth() 52 | .padding(start = 3.dp, end = 3.dp), 53 | verticalAlignment = Alignment.Top, 54 | horizontalArrangement = Arrangement.SpaceBetween 55 | ) { 56 | Text( 57 | text = coinsText, 58 | fontSize = 17.sp, 59 | fontWeight = FontWeight.Bold, 60 | color = textColor 61 | ) 62 | Row( 63 | verticalAlignment = Alignment.CenterVertically, 64 | horizontalArrangement = Arrangement.Center 65 | ) { 66 | Text( 67 | text = viewText, 68 | fontSize = 14.sp, 69 | fontWeight = FontWeight.Bold, 70 | color = viewALlColor 71 | ) 72 | AnimatedVisibility(isCapIconEnabled) { 73 | Icon( 74 | modifier = Modifier.size(25.dp), 75 | imageVector = Icons.Default.KeyboardArrowDown, 76 | contentDescription = null, 77 | tint = largeCapColor 78 | ) 79 | } 80 | } 81 | } 82 | Column( 83 | modifier = Modifier 84 | .fillMaxWidth() 85 | ) { 86 | dataList.forEach { data -> 87 | CryptoItem(data = data) 88 | HorizontalDivider() 89 | } 90 | } 91 | } 92 | } 93 | 94 | @Composable 95 | fun CryptoItem(data: Data) { 96 | val navigator = LocalNavigator.current 97 | val isDark by LocalThemeIsDark.current 98 | val textColor = if (isDark) Color.White else Color.Black 99 | val percentChange24h = data.quote.uSD.percentChange24h 100 | val textColor24h = if (percentChange24h > 0) Color.Green else Color.Red 101 | Column( 102 | modifier = Modifier 103 | .fillMaxWidth() 104 | .padding(8.dp) 105 | .clickable { 106 | navigator?.push(DetailScreen(data)) 107 | }, 108 | horizontalAlignment = Alignment.Start, 109 | verticalArrangement = Arrangement.Center 110 | ) { 111 | Row( 112 | modifier = Modifier.fillMaxWidth(), 113 | horizontalArrangement = Arrangement.Start, 114 | verticalAlignment = Alignment.CenterVertically 115 | ) { 116 | Text( 117 | text = data.cmcRank.toString(), 118 | fontSize = MaterialTheme.typography.labelMedium.fontSize, 119 | color = textColor 120 | ) 121 | Spacer(modifier = Modifier.width(50.dp)) 122 | Row( 123 | verticalAlignment = Alignment.CenterVertically 124 | ) { 125 | CurrencyImage( 126 | id = data.id, 127 | modifier = Modifier.size(35.dp) 128 | ) 129 | Spacer(modifier = Modifier.width(6.dp)) 130 | Column( 131 | horizontalAlignment = Alignment.Start, 132 | verticalArrangement = Arrangement.SpaceAround 133 | ) { 134 | Text( 135 | text = data.name, 136 | fontSize = 14.sp, 137 | fontWeight = FontWeight.Bold, 138 | color = textColor 139 | ) 140 | Text( 141 | text = data.symbol, 142 | fontSize = 11.sp, 143 | fontWeight = FontWeight.Bold, 144 | color = Color.Gray 145 | ) 146 | } 147 | } 148 | Spacer(modifier = Modifier.weight(1f)) 149 | ChartImage( 150 | id = data.id, 151 | modifier = Modifier.fillMaxWidth(0.40f), 152 | tintColor = textColor24h 153 | ) 154 | Spacer(modifier = Modifier.weight(1f)) 155 | Column( 156 | horizontalAlignment = Alignment.End, 157 | verticalArrangement = Arrangement.Center 158 | ) { 159 | Text( 160 | text = "$" + "${((data.quote.uSD.price * 100).roundToInt()) / 100.0}", 161 | fontSize = MaterialTheme.typography.titleMedium.fontSize, 162 | fontWeight = FontWeight.Bold, 163 | color = textColor 164 | ) 165 | Spacer(modifier = Modifier.weight(1f)) 166 | Text( 167 | text = "${percentChange24h.roundToInt()}%", 168 | color = textColor24h 169 | ) 170 | } 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/CurrencyImage.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxWidth 5 | import androidx.compose.material3.CircularProgressIndicator 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.painter.Painter 10 | import androidx.compose.ui.layout.ContentScale 11 | import coil3.compose.AsyncImage 12 | import io.kamel.core.Resource 13 | import io.kamel.image.KamelImage 14 | import io.kamel.image.asyncPainterResource 15 | 16 | @Composable 17 | fun CurrencyImage( 18 | id: Int, 19 | modifier: Modifier, 20 | ) { 21 | val image: Resource = asyncPainterResource("https://s2.coinmarketcap.com/static/img/coins/64x64/$id.png") 22 | KamelImage( 23 | { image }, contentDescription = null, 24 | modifier = modifier, 25 | contentScale = ContentScale.FillBounds, 26 | onLoading = { 27 | Box( 28 | modifier = Modifier.fillMaxWidth(), 29 | contentAlignment = Alignment.Center 30 | ) { 31 | CircularProgressIndicator(progress = {it}) 32 | } 33 | } 34 | ) 35 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/ErrorBox.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.text.selection.SelectionContainer 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.unit.sp 12 | 13 | @Composable 14 | fun ErrorBox(error: String) { 15 | Box( 16 | modifier = Modifier.fillMaxSize(), 17 | contentAlignment = Alignment.Center 18 | ) { 19 | SelectionContainer { 20 | Text( 21 | text = error, 22 | color = Color.Red, 23 | fontSize = 24.sp 24 | ) 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/LoadingBox.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.fillMaxSize 5 | import androidx.compose.foundation.layout.fillMaxWidth 6 | import androidx.compose.material3.CircularProgressIndicator 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | 12 | @Composable 13 | fun LoadingBox() { 14 | Box( 15 | modifier = Modifier.fillMaxSize(), 16 | contentAlignment = Alignment.Center 17 | ) { 18 | CircularProgressIndicator( 19 | color = Color.Blue 20 | ) 21 | } 22 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/MarketRow.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.layout.Arrangement 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.ui.Alignment 8 | import androidx.compose.ui.graphics.Color 9 | import androidx.compose.ui.text.font.FontWeight 10 | import androidx.compose.ui.unit.sp 11 | 12 | @Composable 13 | fun MarketDataRow( 14 | title: String, 15 | value: String, 16 | isDark: Boolean, 17 | ) { 18 | Column( 19 | horizontalAlignment = Alignment.CenterHorizontally, 20 | verticalArrangement = Arrangement.Center 21 | ) { 22 | Text( 23 | text = title, 24 | fontSize = 14.sp, 25 | color = Color.Gray 26 | ) 27 | Text( 28 | text = value, 29 | fontSize = 22.sp, 30 | color = if (isDark) Color.White else Color.Black, 31 | fontWeight = FontWeight.Bold 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/NewsDetail.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.rememberScrollState 15 | import androidx.compose.foundation.shape.CircleShape 16 | import androidx.compose.foundation.shape.RoundedCornerShape 17 | import androidx.compose.foundation.verticalScroll 18 | import androidx.compose.material.icons.Icons 19 | import androidx.compose.material.icons.filled.ArrowBackIosNew 20 | import androidx.compose.material3.Button 21 | import androidx.compose.material3.Divider 22 | import androidx.compose.material3.ExperimentalMaterial3Api 23 | import androidx.compose.material3.HorizontalDivider 24 | import androidx.compose.material3.Icon 25 | import androidx.compose.material3.MaterialTheme 26 | import androidx.compose.material3.Scaffold 27 | import androidx.compose.material3.Text 28 | import androidx.compose.material3.TopAppBar 29 | import androidx.compose.runtime.Composable 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.draw.clip 33 | import androidx.compose.ui.graphics.painter.Painter 34 | import androidx.compose.ui.layout.ContentScale 35 | import androidx.compose.ui.unit.dp 36 | import cafe.adriel.voyager.core.screen.Screen 37 | import cafe.adriel.voyager.navigator.LocalNavigator 38 | import coil3.compose.AsyncImage 39 | import io.kamel.core.Resource 40 | import io.kamel.image.KamelImage 41 | import io.kamel.image.asyncPainterResource 42 | import org.company.app.domain.model.news.Data 43 | import org.company.app.utils.formatTimestamp 44 | 45 | class NewsDetailScreen(private val news: Data) : Screen { 46 | @OptIn(ExperimentalMaterial3Api::class) 47 | @Composable 48 | override fun Content() { 49 | val navigator = LocalNavigator.current 50 | Scaffold( 51 | modifier = Modifier.fillMaxWidth(), 52 | topBar = { 53 | TopAppBar( 54 | title = {Text(text = news.title.take(15))}, 55 | navigationIcon = { 56 | Icon( 57 | imageVector = Icons.Default.ArrowBackIosNew, 58 | contentDescription = "Arrow Back", 59 | modifier = Modifier.clickable { 60 | navigator?.pop() 61 | } 62 | ) 63 | } 64 | ) 65 | } 66 | ){ 67 | Column( 68 | modifier = Modifier.fillMaxWidth() 69 | .padding(top = it.calculateTopPadding()) 70 | .verticalScroll(rememberScrollState()), 71 | horizontalAlignment = Alignment.CenterHorizontally, 72 | verticalArrangement = Arrangement.spacedBy(12.dp) 73 | ) { 74 | NewsDetailContent(news) 75 | } 76 | } 77 | } 78 | } 79 | 80 | @Composable 81 | fun NewsDetailContent(news: Data) { 82 | Column( 83 | modifier = Modifier 84 | .fillMaxSize() 85 | .padding(16.dp) 86 | ) { 87 | val image: Resource = asyncPainterResource(news.imageurl) 88 | KamelImage( 89 | { image }, contentDescription = null, 90 | modifier = Modifier 91 | .height(250.dp) 92 | .fillMaxWidth() 93 | .clip(RoundedCornerShape(16.dp)), 94 | contentScale = ContentScale.Crop 95 | ) 96 | Spacer(modifier = Modifier.height(16.dp)) 97 | Text( 98 | text = news.title, 99 | style = MaterialTheme.typography.headlineMedium, 100 | color = MaterialTheme.colorScheme.onBackground 101 | ) 102 | Spacer(modifier = Modifier.height(8.dp)) 103 | Row(verticalAlignment = Alignment.CenterVertically) { 104 | AsyncImage( 105 | model = news.sourceInfo.img, 106 | contentDescription = null, 107 | modifier = Modifier 108 | .size(24.dp) 109 | .clip(CircleShape) 110 | ) 111 | Spacer(modifier = Modifier.width(8.dp)) 112 | Text( 113 | text = news.sourceInfo.name, 114 | style = MaterialTheme.typography.labelMedium, 115 | color = MaterialTheme.colorScheme.primary 116 | ) 117 | } 118 | Spacer(modifier = Modifier.height(8.dp)) 119 | Text( 120 | text = "Published: ${formatTimestamp(news.publishedOn.toLong())}", 121 | style = MaterialTheme.typography.bodySmall, 122 | color = MaterialTheme.colorScheme.onSurfaceVariant 123 | ) 124 | Spacer(modifier = Modifier.height(16.dp)) 125 | HorizontalDivider() 126 | Spacer(modifier = Modifier.height(16.dp)) 127 | Text( 128 | text = news.body, 129 | style = MaterialTheme.typography.bodyLarge, 130 | color = MaterialTheme.colorScheme.onBackground 131 | ) 132 | Spacer(modifier = Modifier.height(16.dp)) 133 | HorizontalDivider() 134 | Spacer(modifier = Modifier.height(16.dp)) 135 | Text( 136 | text = "Tags: ${news.tags}", 137 | style = MaterialTheme.typography.bodyMedium, 138 | color = MaterialTheme.colorScheme.primary 139 | ) 140 | Spacer(modifier = Modifier.height(8.dp)) 141 | Text( 142 | text = "Categories: ${news.categories}", 143 | style = MaterialTheme.typography.bodyMedium, 144 | color = MaterialTheme.colorScheme.primary 145 | ) 146 | Spacer(modifier = Modifier.height(16.dp)) 147 | HorizontalDivider() 148 | Spacer(modifier = Modifier.height(16.dp)) 149 | Row( 150 | verticalAlignment = Alignment.CenterVertically, 151 | horizontalArrangement = Arrangement.SpaceBetween, 152 | modifier = Modifier.fillMaxWidth() 153 | ) { 154 | Text( 155 | text = "Upvotes: ${news.upvotes}", 156 | style = MaterialTheme.typography.bodySmall, 157 | color = MaterialTheme.colorScheme.onSurfaceVariant 158 | ) 159 | Text( 160 | text = "Downvotes: ${news.downvotes}", 161 | style = MaterialTheme.typography.bodySmall, 162 | color = MaterialTheme.colorScheme.onSurfaceVariant 163 | ) 164 | } 165 | Spacer(modifier = Modifier.height(16.dp)) 166 | Button( 167 | onClick = { /* Handle URL click */ }, 168 | modifier = Modifier.fillMaxWidth() 169 | ) { 170 | Text(text = "Read Full Article") 171 | } 172 | } 173 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/PromotionalCard.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import org.company.app.domain.model.news.Data 4 | import androidx.compose.animation.core.FastOutSlowInEasing 5 | import androidx.compose.animation.core.LinearOutSlowInEasing 6 | import androidx.compose.animation.core.animateDpAsState 7 | import androidx.compose.animation.core.tween 8 | import androidx.compose.foundation.ExperimentalFoundationApi 9 | import androidx.compose.foundation.horizontalScroll 10 | import androidx.compose.foundation.layout.Arrangement 11 | import androidx.compose.foundation.layout.Box 12 | import androidx.compose.foundation.layout.BoxScope 13 | import androidx.compose.foundation.layout.Column 14 | import androidx.compose.foundation.layout.Row 15 | import androidx.compose.foundation.layout.fillMaxSize 16 | import androidx.compose.foundation.layout.fillMaxWidth 17 | import androidx.compose.foundation.layout.height 18 | import androidx.compose.foundation.layout.padding 19 | import androidx.compose.foundation.layout.width 20 | import androidx.compose.foundation.pager.HorizontalPager 21 | import androidx.compose.foundation.pager.rememberPagerState 22 | import androidx.compose.foundation.rememberScrollState 23 | import androidx.compose.foundation.shape.CircleShape 24 | import androidx.compose.foundation.shape.RoundedCornerShape 25 | import androidx.compose.material.Card 26 | import androidx.compose.material.CircularProgressIndicator 27 | import androidx.compose.material3.MaterialTheme 28 | import androidx.compose.material3.Surface 29 | import androidx.compose.material3.Text 30 | import androidx.compose.runtime.Composable 31 | import androidx.compose.runtime.LaunchedEffect 32 | import androidx.compose.runtime.getValue 33 | import androidx.compose.runtime.mutableStateOf 34 | import androidx.compose.runtime.remember 35 | import androidx.compose.runtime.rememberCoroutineScope 36 | import androidx.compose.runtime.setValue 37 | import androidx.compose.ui.Alignment 38 | import androidx.compose.ui.Modifier 39 | import androidx.compose.ui.graphics.Color 40 | import androidx.compose.ui.graphics.painter.Painter 41 | import androidx.compose.ui.layout.ContentScale 42 | import androidx.compose.ui.text.TextStyle 43 | import androidx.compose.ui.text.style.TextAlign 44 | import androidx.compose.ui.unit.dp 45 | import androidx.compose.ui.unit.sp 46 | import io.kamel.core.Resource 47 | import io.kamel.image.KamelImage 48 | import io.kamel.image.asyncPainterResource 49 | import kotlinx.coroutines.delay 50 | import kotlinx.coroutines.launch 51 | import kotlinx.datetime.Clock 52 | 53 | @ExperimentalFoundationApi 54 | @Composable 55 | fun PromotionCardWithPager(promotions: List) { 56 | if (promotions.isNotEmpty()) { 57 | var currentPage by remember { mutableStateOf(0) } 58 | val scope = rememberCoroutineScope() 59 | val pagerState = rememberPagerState(pageCount = { promotions.size }) 60 | 61 | val currentTimeMillis = Clock.System.now().toEpochMilliseconds() 62 | val activePromotions = promotions.take(8) 63 | LaunchedEffect(currentPage) { 64 | delay(1000) 65 | while (true) { 66 | delay(2000) 67 | if (activePromotions.isNotEmpty()) { 68 | val nextPage = (currentPage + 1) % activePromotions.size 69 | scope.launch { 70 | currentPage = nextPage 71 | pagerState.animateScrollToPage(nextPage) 72 | } 73 | } 74 | } 75 | } 76 | 77 | Column( 78 | modifier = Modifier 79 | .fillMaxWidth() 80 | .height(250.dp) 81 | .padding(16.dp) 82 | ) { 83 | HorizontalPager( 84 | modifier = Modifier 85 | .fillMaxWidth() 86 | .weight(1f), 87 | state = pagerState 88 | ) { page -> 89 | Card( 90 | modifier = Modifier 91 | .fillMaxSize() 92 | .padding(8.dp), 93 | elevation = 8.dp, 94 | shape = RoundedCornerShape(16.dp) 95 | ) { 96 | if (activePromotions.isNotEmpty() && page in activePromotions.indices) { 97 | val image: Resource = 98 | asyncPainterResource(data = activePromotions[page].imageurl) 99 | KamelImage( 100 | { image }, contentDescription = null, 101 | modifier = Modifier.fillMaxWidth(), 102 | contentScale = ContentScale.Crop, 103 | onLoading = { 104 | CircularProgressIndicator(progress = it) 105 | }, animationSpec = tween( 106 | durationMillis = 100, 107 | delayMillis = 100, 108 | easing = FastOutSlowInEasing 109 | ) 110 | ) 111 | } else { 112 | Box( 113 | modifier = Modifier.fillMaxSize(), 114 | contentAlignment = Alignment.Center 115 | ) { 116 | Text( 117 | text = "No promotions available", 118 | style = TextStyle(color = Color.Gray, fontSize = 16.sp), 119 | textAlign = TextAlign.Center 120 | ) 121 | } 122 | } 123 | } 124 | } 125 | 126 | DotsIndicator( 127 | modifier = Modifier 128 | .padding(vertical = 8.dp) 129 | .align(Alignment.CenterHorizontally), 130 | pageCount = activePromotions.size, 131 | currentPage = currentPage, 132 | onPageSelected = { page -> 133 | scope.launch { 134 | currentPage = page 135 | pagerState.scrollToPage(page) 136 | } 137 | } 138 | ) 139 | } 140 | } 141 | } 142 | 143 | @Composable 144 | fun DotsIndicator( 145 | modifier: Modifier = Modifier, 146 | pageCount: Int, 147 | currentPage: Int, 148 | onPageSelected: (Int) -> Unit, 149 | ) { 150 | Box(modifier = modifier) { 151 | Row( 152 | modifier = Modifier.horizontalScroll(rememberScrollState()), 153 | horizontalArrangement = Arrangement.spacedBy(8.dp), 154 | verticalAlignment = Alignment.CenterVertically 155 | ) { 156 | repeat(pageCount) { index -> 157 | Dot( 158 | isSelected = index == currentPage, 159 | onClick = { 160 | onPageSelected(index) 161 | } 162 | ) 163 | } 164 | } 165 | } 166 | } 167 | 168 | @Composable 169 | fun Dot( 170 | isSelected: Boolean, 171 | onClick: () -> Unit, 172 | ) { 173 | val width by animateDpAsState( 174 | targetValue = if (isSelected) 16.dp else 6.dp, 175 | animationSpec = tween( 176 | durationMillis = 300, 177 | easing = LinearOutSlowInEasing 178 | ) 179 | ) 180 | val shape = if (isSelected) CircleShape else RoundedCornerShape(50) 181 | val color = if (isSelected) Color.Blue else Color.LightGray 182 | 183 | Surface( 184 | modifier = Modifier.width(width) 185 | .height(6.dp), 186 | shape = shape, 187 | color = color, 188 | onClick = onClick 189 | ) {} 190 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/components/SuggestionMessage.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.components 2 | 3 | import androidx.compose.foundation.Image 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.Arrangement 6 | import androidx.compose.foundation.layout.Box 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.layout.size 13 | import androidx.compose.foundation.layout.width 14 | import androidx.compose.foundation.shape.CircleShape 15 | import androidx.compose.foundation.shape.RoundedCornerShape 16 | import androidx.compose.material3.Icon 17 | import androidx.compose.material3.Text 18 | import androidx.compose.runtime.Composable 19 | import androidx.compose.runtime.getValue 20 | import androidx.compose.ui.Alignment 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.draw.shadow 23 | import androidx.compose.ui.graphics.Color 24 | import androidx.compose.ui.text.font.FontWeight 25 | import androidx.compose.ui.unit.dp 26 | import androidx.compose.ui.unit.sp 27 | import crypto_kmp.composeapp.generated.resources.Res 28 | import crypto_kmp.composeapp.generated.resources.thumbs_down 29 | import crypto_kmp.composeapp.generated.resources.thumbs_up 30 | import org.company.app.theme.LocalThemeIsDark 31 | import org.jetbrains.compose.resources.DrawableResource 32 | import org.jetbrains.compose.resources.painterResource 33 | 34 | @Composable 35 | fun SuggestionMessage() { 36 | val isDark by LocalThemeIsDark.current 37 | Column( 38 | modifier = Modifier.fillMaxWidth() 39 | .padding(all = 10.dp) 40 | .shadow(4.dp, shape = RoundedCornerShape(12.dp)) 41 | .background( 42 | color = if (isDark) Color.DarkGray else Color.White, 43 | shape = RoundedCornerShape(12.dp) 44 | ), 45 | horizontalAlignment = Alignment.CenterHorizontally, 46 | verticalArrangement = Arrangement.Center 47 | ) { 48 | Column( 49 | modifier = Modifier.fillMaxWidth().padding(8.dp), 50 | horizontalAlignment = Alignment.CenterHorizontally, 51 | verticalArrangement = Arrangement.Center 52 | ) { 53 | Row( 54 | verticalAlignment = Alignment.CenterVertically, 55 | horizontalArrangement = Arrangement.Start 56 | ) { 57 | Column( 58 | horizontalAlignment = Alignment.Start, 59 | verticalArrangement = Arrangement.Center 60 | ) { 61 | Text( 62 | text = "How do you feel about the Crypto\n market today?", 63 | color = if (isDark) Color.White else Color.Black, 64 | fontSize = 14.sp, 65 | fontWeight = FontWeight.SemiBold 66 | ) 67 | Text( 68 | text = "Vote to see results", 69 | color = Color.LightGray, 70 | fontSize = 12.sp, 71 | ) 72 | } 73 | Spacer(modifier = Modifier.weight(1f)) 74 | Row( 75 | horizontalArrangement = Arrangement.Center, 76 | verticalAlignment = Alignment.CenterVertically 77 | ) { 78 | ThumbsIcon( 79 | icon = Res.drawable.thumbs_down, 80 | ) 81 | Spacer(modifier = Modifier.width(8.dp)) 82 | ThumbsIcon( 83 | icon = Res.drawable.thumbs_up, 84 | ) 85 | 86 | } 87 | } 88 | 89 | } 90 | } 91 | } 92 | 93 | @Composable 94 | fun ThumbsIcon( 95 | icon: DrawableResource, 96 | contentDes: String = "Thumbs", 97 | ) { 98 | Box( 99 | modifier = Modifier.size(40.dp) 100 | .background(color = Color.Gray, shape = CircleShape), 101 | contentAlignment = Alignment.Center 102 | ) { 103 | Image( 104 | modifier = Modifier.fillMaxWidth() 105 | .padding(6.dp), 106 | painter = painterResource(icon), 107 | contentDescription = contentDes, 108 | ) 109 | } 110 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/rails/items/NavigationItem.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.rails.items 2 | 3 | import androidx.compose.ui.graphics.vector.ImageVector 4 | 5 | data class NavigationItem( 6 | val title: String, 7 | val unselectedIcon: ImageVector, 8 | val selectedIcon: ImageVector, 9 | val hasNews: Boolean, 10 | val badgeCount: Int? = null, 11 | ) 12 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/rails/navbar/NavigationSideBar.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.rails.navbar 2 | 3 | import androidx.compose.animation.AnimatedVisibility 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.height 8 | import androidx.compose.foundation.shape.RoundedCornerShape 9 | import androidx.compose.material.icons.Icons 10 | import androidx.compose.material.icons.filled.Menu 11 | import androidx.compose.material3.Badge 12 | import androidx.compose.material3.BadgedBox 13 | import androidx.compose.material3.Icon 14 | import androidx.compose.material3.IconButton 15 | import androidx.compose.material3.MaterialTheme 16 | import androidx.compose.material3.NavigationRail 17 | import androidx.compose.material3.NavigationRailItem 18 | import androidx.compose.material3.Text 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.runtime.mutableStateOf 22 | import androidx.compose.runtime.remember 23 | import androidx.compose.runtime.setValue 24 | import androidx.compose.ui.Alignment 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.draw.clip 27 | import androidx.compose.ui.unit.dp 28 | import org.company.app.presentation.ui.navigation.rails.items.NavigationItem 29 | 30 | @Composable 31 | fun NavigationSideBar( 32 | items: List, selectedItemIndex: Int, onNavigate: (Int) -> Unit, 33 | ) { 34 | var isTitleVisible by remember { mutableStateOf(false) } 35 | NavigationRail( 36 | header = { 37 | IconButton(onClick = { 38 | isTitleVisible = !isTitleVisible 39 | }) { 40 | Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu") 41 | } 42 | }, 43 | modifier = Modifier.height(400.dp) 44 | .clip(RoundedCornerShape(40.dp)) 45 | ) { 46 | Column( 47 | modifier = Modifier.fillMaxHeight(), 48 | verticalArrangement = Arrangement.spacedBy(12.dp, Alignment.CenterVertically) 49 | ) { 50 | items.forEachIndexed { index, item -> 51 | NavigationRailItem( 52 | selected = selectedItemIndex == index, 53 | onClick = { 54 | onNavigate(index) 55 | }, 56 | icon = { 57 | NavigationIcon( 58 | item = item, selected = selectedItemIndex == index 59 | ) 60 | }, 61 | label = { 62 | AnimatedVisibility(isTitleVisible) { 63 | Text( 64 | text = item.title, 65 | fontSize = MaterialTheme.typography.bodySmall.fontSize 66 | ) 67 | } 68 | } 69 | ) 70 | } 71 | } 72 | } 73 | } 74 | 75 | @Composable 76 | fun NavigationIcon( 77 | item: NavigationItem, selected: Boolean, 78 | ) { 79 | BadgedBox(badge = { 80 | if (item.badgeCount != null) { 81 | Badge { 82 | Text(text = item.badgeCount.toString()) 83 | } 84 | } else if (item.hasNews) { 85 | Badge() 86 | } 87 | }) { 88 | Icon( 89 | imageVector = if (selected) item.selectedIcon else item.unselectedIcon, 90 | contentDescription = item.title 91 | ) 92 | } 93 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/tab/analytics/Analytics.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.tab.analytics 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Analytics 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 10 | import cafe.adriel.voyager.navigator.Navigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import org.company.app.presentation.ui.screens.analytics.AnalyticScreen 14 | 15 | object Analytics : Tab { 16 | @Composable 17 | override fun Content() { 18 | Navigator(AnalyticScreen()) 19 | } 20 | 21 | override val options: TabOptions 22 | @Composable 23 | get() { 24 | val icon = rememberVectorPainter(Icons.Default.Analytics) 25 | val title by remember { mutableStateOf("Analytics") } 26 | val index: UShort = 1u 27 | return TabOptions(index, title, icon) 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/tab/home/Home.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.tab.home 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Home 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 10 | import cafe.adriel.voyager.navigator.Navigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import org.company.app.presentation.ui.screens.home.HomeScreen 14 | 15 | object Home : Tab { 16 | @Composable 17 | override fun Content() { 18 | Navigator(HomeScreen()) 19 | } 20 | 21 | override val options: TabOptions 22 | @Composable 23 | get() { 24 | val icon = rememberVectorPainter(Icons.Default.Home) 25 | val title by remember { mutableStateOf("Home") } 26 | val index: UShort = 0u 27 | return TabOptions(index, title, icon) 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/tab/news/News.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.tab.news 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Newspaper 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 10 | import cafe.adriel.voyager.navigator.Navigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import org.company.app.presentation.ui.screens.news.NewsScreen 14 | 15 | object News : Tab { 16 | @Composable 17 | override fun Content() { 18 | Navigator(NewsScreen()) 19 | } 20 | 21 | override val options: TabOptions 22 | @Composable 23 | get() { 24 | val icon = rememberVectorPainter(Icons.Default.Newspaper) 25 | val title by remember { mutableStateOf("News") } 26 | val index: UShort = 2u 27 | return TabOptions(index, title, icon) 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/navigation/tab/profile/Profile.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.navigation.tab.profile 2 | 3 | import androidx.compose.material.icons.Icons 4 | import androidx.compose.material.icons.filled.Person 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.ui.graphics.vector.rememberVectorPainter 10 | import cafe.adriel.voyager.navigator.Navigator 11 | import cafe.adriel.voyager.navigator.tab.Tab 12 | import cafe.adriel.voyager.navigator.tab.TabOptions 13 | import org.company.app.presentation.ui.screens.profile.ProfileScreen 14 | 15 | object Profile : Tab { 16 | @Composable 17 | override fun Content() { 18 | Navigator(ProfileScreen()) 19 | } 20 | 21 | override val options: TabOptions 22 | @Composable 23 | get() { 24 | val icon = rememberVectorPainter(Icons.Default.Person) 25 | val title by remember { mutableStateOf("Profile") } 26 | val index: UShort = 3u 27 | return TabOptions(index, title, icon) 28 | } 29 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/screens/detail/DetailScreen.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.screens.detail 2 | 3 | import Notification 4 | import NotificationType 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.layout.Arrangement 7 | import androidx.compose.foundation.layout.Column 8 | import androidx.compose.foundation.layout.Row 9 | import androidx.compose.foundation.layout.Spacer 10 | import androidx.compose.foundation.layout.WindowInsets 11 | import androidx.compose.foundation.layout.fillMaxWidth 12 | import androidx.compose.foundation.layout.height 13 | import androidx.compose.foundation.layout.padding 14 | import androidx.compose.foundation.layout.size 15 | import androidx.compose.foundation.layout.statusBars 16 | import androidx.compose.foundation.layout.width 17 | import androidx.compose.foundation.layout.windowInsetsPadding 18 | import androidx.compose.foundation.rememberScrollState 19 | import androidx.compose.foundation.shape.RoundedCornerShape 20 | import androidx.compose.foundation.verticalScroll 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.outlined.ArrowBackIosNew 23 | import androidx.compose.material.icons.outlined.Notifications 24 | import androidx.compose.material.icons.outlined.StarOutline 25 | import androidx.compose.material3.ButtonDefaults 26 | import androidx.compose.material3.Card 27 | import androidx.compose.material3.CenterAlignedTopAppBar 28 | import androidx.compose.material3.ExperimentalMaterial3Api 29 | import androidx.compose.material3.HorizontalDivider 30 | import androidx.compose.material3.Icon 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.Scaffold 33 | import androidx.compose.material3.Tab 34 | import androidx.compose.material3.TabRow 35 | import androidx.compose.material3.TabRowDefaults 36 | import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset 37 | import androidx.compose.material3.Text 38 | import androidx.compose.material3.TextButton 39 | import androidx.compose.runtime.Composable 40 | import androidx.compose.runtime.getValue 41 | import androidx.compose.runtime.mutableStateOf 42 | import androidx.compose.runtime.remember 43 | import androidx.compose.runtime.setValue 44 | import androidx.compose.ui.Alignment 45 | import androidx.compose.ui.Modifier 46 | import androidx.compose.ui.graphics.Color 47 | import androidx.compose.ui.text.SpanStyle 48 | import androidx.compose.ui.text.buildAnnotatedString 49 | import androidx.compose.ui.text.font.FontWeight 50 | import androidx.compose.ui.text.style.TextAlign 51 | import androidx.compose.ui.text.style.TextOverflow 52 | import androidx.compose.ui.text.withStyle 53 | import androidx.compose.ui.unit.dp 54 | import androidx.compose.ui.unit.sp 55 | import cafe.adriel.voyager.core.screen.Screen 56 | import cafe.adriel.voyager.navigator.LocalNavigator 57 | import createNotification 58 | import org.company.app.domain.model.crypto.Data 59 | import org.company.app.presentation.ui.components.CryptoChart 60 | import org.company.app.presentation.ui.components.CurrencyImage 61 | import org.company.app.presentation.ui.components.MarketDataRow 62 | import org.company.app.theme.LocalThemeIsDark 63 | import org.company.app.utils.formatMarketCap 64 | import kotlin.math.roundToInt 65 | 66 | class DetailScreen( 67 | private val data: Data, 68 | ) : Screen { 69 | @Composable 70 | override fun Content() { 71 | DetailContent(data) 72 | } 73 | } 74 | 75 | @OptIn(ExperimentalMaterial3Api::class) 76 | @Composable 77 | fun DetailContent(data: Data) { 78 | val notification = createNotification(NotificationType.TOP) 79 | var selectedPeriod by remember { mutableStateOf("1H") } 80 | val capList = remember { mutableListOf("1H", "1D", "1W", "1M", "3M", "6M", "1Y") } 81 | val isDark by LocalThemeIsDark.current 82 | val navigator = LocalNavigator.current 83 | val textColor = if (isDark) Color.White else Color.Black 84 | val percentChange24h = data.quote.uSD.percentChange24h 85 | val capMarket = data.quote.uSD.percentChange30d 86 | val textColor24h = if (percentChange24h > 0) Color.Green else Color.Red 87 | val textColor1h = if (capMarket > 0) Color.Green else Color.Red 88 | Scaffold( 89 | topBar = { 90 | CenterAlignedTopAppBar( 91 | title = { 92 | Row( 93 | modifier = Modifier.fillMaxWidth(), 94 | horizontalArrangement = Arrangement.Center, 95 | verticalAlignment = Alignment.CenterVertically 96 | ) { 97 | CurrencyImage( 98 | id = data.id, 99 | modifier = Modifier.size(30.dp) 100 | ) 101 | Spacer(modifier = Modifier.width(6.dp)) 102 | Text( 103 | "${data.name}", 104 | textAlign = TextAlign.Center, 105 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 106 | maxLines = 1, 107 | overflow = TextOverflow.Ellipsis 108 | ) 109 | } 110 | }, 111 | navigationIcon = { 112 | Icon( 113 | imageVector = Icons.Outlined.ArrowBackIosNew, 114 | contentDescription = "Menu Icon", 115 | modifier = Modifier.clickable { 116 | navigator?.pop() 117 | } 118 | ) 119 | }, 120 | actions = { 121 | Icon( 122 | imageVector = Icons.Outlined.Notifications, 123 | contentDescription = "Notifications Icon", 124 | modifier = Modifier.clickable { 125 | notification.show( 126 | message ="CMC Rank:"+data.cmcRank.toString() +"Name:"+data.name + "Currency:" +data.quote.uSD, 127 | title = data.name, 128 | duration = NotificationDuration.SHORT 129 | ) 130 | } 131 | ) 132 | Icon( 133 | imageVector = Icons.Outlined.StarOutline, 134 | contentDescription = "Favourite Icon" 135 | ) 136 | }, 137 | modifier = Modifier.fillMaxWidth() 138 | .windowInsetsPadding(WindowInsets.statusBars) 139 | ) 140 | } 141 | ) { paddingValues-> 142 | Column( 143 | modifier = Modifier 144 | .fillMaxWidth() 145 | .padding(top = paddingValues.calculateTopPadding()) 146 | .verticalScroll(rememberScrollState()), 147 | horizontalAlignment = Alignment.Start, 148 | verticalArrangement = Arrangement.Center 149 | ) { 150 | Row( 151 | modifier = Modifier.fillMaxWidth() 152 | .padding(8.dp), 153 | horizontalArrangement = Arrangement.Start, 154 | verticalAlignment = Alignment.CenterVertically 155 | ) { 156 | CurrencyImage( 157 | id = data.id, 158 | modifier = Modifier.size(30.dp) 159 | ) 160 | Spacer(modifier = Modifier.width(6.dp)) 161 | Text( 162 | text = data.name, 163 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 164 | fontWeight = FontWeight.Bold, 165 | ) 166 | } 167 | Spacer(modifier = Modifier.height(16.dp)) 168 | Row( 169 | modifier = Modifier.fillMaxWidth() 170 | .padding(8.dp), 171 | horizontalArrangement = Arrangement.SpaceBetween, 172 | verticalAlignment = Alignment.CenterVertically 173 | ) { 174 | val priceString = buildAnnotatedString { 175 | withStyle( 176 | SpanStyle( 177 | color = Color.Gray, 178 | fontSize = 20.sp, 179 | fontWeight = FontWeight.Bold 180 | ) 181 | ) { 182 | append("$ ") 183 | } 184 | withStyle( 185 | SpanStyle( 186 | color = textColor, 187 | fontSize = 24.sp, 188 | fontWeight = FontWeight.Bold 189 | ) 190 | ) { 191 | append("$" + "${((data.quote.uSD.price * 100).roundToInt()) / 100.0}") 192 | } 193 | withStyle( 194 | SpanStyle( 195 | color = Color.Gray, 196 | fontSize = 20.sp, 197 | fontWeight = FontWeight.Bold 198 | ) 199 | ) { 200 | append(" USD") 201 | } 202 | 203 | } 204 | Text( 205 | text = priceString 206 | ) 207 | Text( 208 | text = "${percentChange24h.roundToInt()}%", 209 | color = textColor24h, 210 | fontSize = 20.sp 211 | ) 212 | } 213 | Row( 214 | modifier = Modifier.fillMaxWidth() 215 | .padding(8.dp), 216 | horizontalArrangement = Arrangement.Center, 217 | verticalAlignment = Alignment.CenterVertically 218 | ) { 219 | CurrencyImage( 220 | id = data.id, 221 | modifier = Modifier.size(20.dp) 222 | ) 223 | Spacer(modifier = Modifier.width(6.dp)) 224 | val latestCap = buildAnnotatedString { 225 | withStyle( 226 | SpanStyle( 227 | color = Color.Gray, 228 | fontSize = 16.sp, 229 | fontWeight = FontWeight.Bold 230 | ) 231 | ) { 232 | append("${data.quote.uSD.fullyDilutedMarketCap}") 233 | } 234 | withStyle( 235 | SpanStyle( 236 | color = Color.Gray, 237 | fontSize = 16.sp, 238 | fontWeight = FontWeight.Bold 239 | ) 240 | ) { 241 | append(" ${data.symbol}") 242 | } 243 | 244 | } 245 | Text( 246 | text = latestCap 247 | ) 248 | Spacer(modifier = Modifier.weight(1f)) 249 | Text( 250 | text = "${capMarket.roundToInt()}%", 251 | color = textColor1h, 252 | fontSize = 16.sp 253 | ) 254 | } 255 | Spacer(modifier = Modifier.height(16.dp)) 256 | TabRow( 257 | selectedTabIndex = capList.indexOf(selectedPeriod), 258 | containerColor = if (isDark) MaterialTheme.colorScheme.surface else Color.White, 259 | contentColor = if (isDark) Color.White else Color.Black, 260 | indicator = { tabPositions -> 261 | TabRowDefaults.Indicator( 262 | modifier = Modifier.tabIndicatorOffset( 263 | tabPositions[capList.indexOf( 264 | selectedPeriod 265 | )] 266 | ) 267 | ) 268 | }, 269 | modifier = Modifier.fillMaxWidth() 270 | ) { 271 | capList.forEachIndexed { index, period -> 272 | Tab( 273 | selected = selectedPeriod == period, 274 | onClick = { selectedPeriod = period }, 275 | text = { Text(text = period) }, 276 | ) 277 | } 278 | } 279 | Column( 280 | modifier = Modifier.fillMaxWidth() 281 | .height(330.dp), 282 | horizontalAlignment = Alignment.CenterHorizontally, 283 | verticalArrangement = Arrangement.Center 284 | ) { 285 | when (selectedPeriod) { 286 | "1H" -> { 287 | CryptoChart(data, "1H") 288 | } 289 | 290 | "1D" -> { 291 | CryptoChart(data, "1D") 292 | 293 | } 294 | 295 | "1W" -> { 296 | CryptoChart(data, "1W") 297 | } 298 | 299 | "1M" -> { 300 | CryptoChart(data, "1M") 301 | } 302 | 303 | "3M" -> { 304 | CryptoChart(data, "3M") 305 | } 306 | 307 | "6M" -> { 308 | CryptoChart(data, "6M") 309 | } 310 | 311 | "1Y" -> { 312 | CryptoChart(data, "1Y") 313 | } 314 | 315 | } 316 | } 317 | Spacer(modifier = Modifier.height(8.dp)) 318 | MarketData(data, isDark) 319 | Spacer(modifier = Modifier.height(24.dp)) 320 | BuyContent(data) 321 | } 322 | 323 | } 324 | } 325 | 326 | @Composable 327 | fun BuyContent( 328 | data: Data, 329 | ) { 330 | Card( 331 | modifier = Modifier.fillMaxWidth() 332 | .height(140.dp), 333 | shape = RoundedCornerShape( 334 | bottomStart = 12.dp, bottomEnd = 12.dp 335 | ) 336 | ) { 337 | Column( 338 | modifier = Modifier.fillMaxWidth() 339 | .padding(12.dp), 340 | horizontalAlignment = Alignment.Start, 341 | verticalArrangement = Arrangement.Center 342 | ) { 343 | Spacer(modifier = Modifier.weight(1f)) 344 | 345 | Row( 346 | modifier = Modifier.fillMaxWidth(), 347 | verticalAlignment = Alignment.CenterVertically, 348 | horizontalArrangement = Arrangement.Start 349 | ) { 350 | CurrencyImage( 351 | id = data.id, 352 | modifier = Modifier.size(30.dp) 353 | ) 354 | Spacer(modifier = Modifier.width(10.dp)) 355 | Text( 356 | text = data.name, 357 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 358 | fontWeight = FontWeight.Bold, 359 | ) 360 | Spacer(modifier = Modifier.weight(1f)) 361 | Text( 362 | text = "0 ${data.symbol}", 363 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 364 | fontWeight = FontWeight.Bold, 365 | ) 366 | } 367 | Spacer(modifier = Modifier.weight(1f)) 368 | TextButton( 369 | onClick = { 370 | 371 | }, 372 | modifier = Modifier.fillMaxWidth(), 373 | colors = ButtonDefaults.buttonColors( 374 | containerColor = Color(0xFF9234eb), 375 | contentColor = Color.White 376 | ), 377 | shape = RoundedCornerShape(8.dp), 378 | ) { 379 | Text( 380 | "Buy ${data.symbol}", 381 | fontSize = MaterialTheme.typography.titleMedium.fontSize 382 | ) 383 | } 384 | } 385 | } 386 | } 387 | 388 | @Composable 389 | fun MarketData( 390 | data: Data, 391 | isDark: Boolean, 392 | ) { 393 | Column( 394 | modifier = Modifier.fillMaxWidth(), 395 | verticalArrangement = Arrangement.Center, 396 | horizontalAlignment = Alignment.Start 397 | ) { 398 | Text( 399 | text = "MARKET DATA", 400 | fontSize = MaterialTheme.typography.titleMedium.fontSize, 401 | fontWeight = FontWeight.Bold, 402 | color = if (isDark) Color.White else Color.Black, 403 | modifier = Modifier.padding(start = 8.dp) 404 | ) 405 | Spacer(modifier = Modifier.height(16.dp)) 406 | HorizontalDivider(color = Color.LightGray) 407 | Spacer(modifier = Modifier.height(6.dp)) 408 | Row( 409 | modifier = Modifier.fillMaxWidth(), 410 | horizontalArrangement = Arrangement.SpaceAround, 411 | verticalAlignment = Alignment.CenterVertically 412 | ) { 413 | MarketDataRow("MARKET CAP", formatMarketCap(data.quote.uSD.marketCap), isDark) 414 | MarketDataRow("24H VOLUME", formatMarketCap(data.quote.uSD.volume24h), isDark) 415 | MarketDataRow("RANK", "#${data.cmcRank}", isDark) 416 | } 417 | } 418 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/screens/home/HomeContent.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.screens.home 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Box 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.Row 8 | import androidx.compose.foundation.layout.Spacer 9 | import androidx.compose.foundation.layout.WindowInsets 10 | import androidx.compose.foundation.layout.fillMaxWidth 11 | import androidx.compose.foundation.layout.height 12 | import androidx.compose.foundation.layout.padding 13 | import androidx.compose.foundation.layout.safeDrawing 14 | import androidx.compose.foundation.layout.statusBars 15 | import androidx.compose.foundation.layout.windowInsetsPadding 16 | import androidx.compose.foundation.rememberScrollState 17 | import androidx.compose.foundation.shape.RoundedCornerShape 18 | import androidx.compose.foundation.verticalScroll 19 | import androidx.compose.material.ExperimentalMaterialApi 20 | import androidx.compose.material.TopAppBar 21 | import androidx.compose.material.icons.Icons 22 | import androidx.compose.material.icons.filled.Search 23 | import androidx.compose.material.icons.outlined.Notifications 24 | import androidx.compose.material.icons.outlined.WbSunny 25 | import androidx.compose.material.pullrefresh.PullRefreshIndicator 26 | import androidx.compose.material.pullrefresh.pullRefresh 27 | import androidx.compose.material.pullrefresh.rememberPullRefreshState 28 | import androidx.compose.material3.CenterAlignedTopAppBar 29 | import androidx.compose.material3.ExperimentalMaterial3Api 30 | import androidx.compose.material3.Icon 31 | import androidx.compose.material3.MaterialTheme 32 | import androidx.compose.material3.Scaffold 33 | import androidx.compose.material3.Text 34 | import androidx.compose.material3.TextField 35 | import androidx.compose.material3.TextFieldDefaults 36 | import androidx.compose.runtime.Composable 37 | import androidx.compose.runtime.LaunchedEffect 38 | import androidx.compose.runtime.collectAsState 39 | import androidx.compose.runtime.getValue 40 | import androidx.compose.runtime.mutableStateOf 41 | import androidx.compose.runtime.remember 42 | import androidx.compose.runtime.rememberCoroutineScope 43 | import androidx.compose.runtime.setValue 44 | import androidx.compose.ui.Alignment 45 | import androidx.compose.ui.Modifier 46 | import androidx.compose.ui.graphics.Color 47 | import androidx.compose.ui.text.font.FontWeight 48 | import androidx.compose.ui.text.style.TextAlign 49 | import androidx.compose.ui.unit.dp 50 | import cafe.adriel.voyager.navigator.LocalNavigator 51 | import kotlinx.coroutines.delay 52 | import kotlinx.coroutines.launch 53 | import org.company.app.domain.model.crypto.Data 54 | import org.company.app.domain.model.crypto.LatestListing 55 | import org.company.app.domain.usecase.ResultState 56 | import org.company.app.presentation.ui.components.CryptoList 57 | import org.company.app.presentation.ui.components.ErrorBox 58 | import org.company.app.presentation.ui.components.LoadingBox 59 | import org.company.app.presentation.ui.components.SuggestionMessage 60 | import org.company.app.presentation.viewmodel.MainViewModel 61 | import org.company.app.theme.LocalThemeIsDark 62 | import org.koin.compose.koinInject 63 | 64 | @OptIn(ExperimentalMaterialApi::class) 65 | @Composable 66 | fun HomeContent( 67 | viewModel: MainViewModel = koinInject(), 68 | ) { 69 | var isDark by LocalThemeIsDark.current 70 | var listingData by remember { mutableStateOf(null) } 71 | var queryText by remember { mutableStateOf("") } 72 | val refreshScope = rememberCoroutineScope() 73 | var refreshing by remember { mutableStateOf(false) } 74 | fun refresh() { 75 | refreshScope.launch { 76 | viewModel.getLatestListing() 77 | delay(1500) 78 | refreshing = false 79 | } 80 | } 81 | 82 | val refreshState = rememberPullRefreshState(refreshing, ::refresh) 83 | LaunchedEffect(Unit) { 84 | viewModel.getLatestListing() 85 | } 86 | val latestState by viewModel.latestListing.collectAsState() 87 | when (latestState) { 88 | is ResultState.ERROR -> { 89 | val error = (latestState as ResultState.ERROR).message 90 | ErrorBox(error) 91 | } 92 | 93 | is ResultState.LOADING -> { 94 | LoadingBox() 95 | } 96 | 97 | is ResultState.SUCCESS -> { 98 | val data = (latestState as ResultState.SUCCESS).response 99 | listingData = data 100 | } 101 | } 102 | Box( 103 | modifier = Modifier 104 | .fillMaxWidth() 105 | .pullRefresh(state = refreshState), 106 | contentAlignment = Alignment.Center 107 | ) { 108 | Column( 109 | modifier = Modifier 110 | .fillMaxWidth() 111 | .windowInsetsPadding(WindowInsets.statusBars) 112 | .verticalScroll(rememberScrollState()), 113 | horizontalAlignment = Alignment.CenterHorizontally, 114 | verticalArrangement = Arrangement.Center 115 | ) { 116 | Row( 117 | modifier = Modifier.fillMaxWidth() 118 | .padding(3.dp), 119 | horizontalArrangement = Arrangement.SpaceBetween, 120 | verticalAlignment = Alignment.CenterVertically 121 | ) { 122 | 123 | Text( 124 | "Crypto Coins", 125 | textAlign = TextAlign.Center, 126 | fontSize = MaterialTheme.typography.titleLarge.fontSize, 127 | fontWeight = FontWeight.Bold 128 | ) 129 | 130 | Spacer(modifier = Modifier.weight(1f)) 131 | 132 | Icon( 133 | imageVector = Icons.Outlined.WbSunny, 134 | contentDescription = "Menu Icon", 135 | modifier = Modifier.clickable { 136 | isDark = !isDark 137 | } 138 | ) 139 | 140 | Icon( 141 | imageVector = Icons.Outlined.Notifications, 142 | contentDescription = "Menu Icon" 143 | ) 144 | } 145 | TextField( 146 | value = queryText, 147 | onValueChange = { 148 | queryText = it 149 | }, 150 | placeholder = { Text(text = "Search Coins") }, 151 | leadingIcon = { 152 | Icon( 153 | imageVector = Icons.Default.Search, 154 | contentDescription = null, 155 | modifier = Modifier.padding(horizontal = 12.dp) 156 | ) 157 | }, 158 | modifier = Modifier 159 | .fillMaxWidth() 160 | .padding(10.dp), 161 | colors = TextFieldDefaults.colors( 162 | focusedTextColor = if (isDark) Color.White else Color.Black, 163 | unfocusedTextColor = if (isDark) Color.White else Color.LightGray, 164 | focusedIndicatorColor = Color.Transparent, 165 | unfocusedIndicatorColor = Color.Transparent 166 | ), 167 | shape = RoundedCornerShape(40.dp) 168 | ) 169 | listingData?.data?.let { dataList -> 170 | if (queryText.isEmpty()) { 171 | if (dataList.isNotEmpty()) { 172 | val combinedFilteredList = mutableListOf() 173 | if (dataList.size > 0) { 174 | combinedFilteredList.add(dataList[0]) 175 | } 176 | if (dataList.size > 12) { 177 | combinedFilteredList.add(dataList[12]) 178 | } 179 | 180 | CryptoList( 181 | dataList = combinedFilteredList, 182 | coinsText = "Favourite", 183 | viewText = "Large Cap", 184 | largeCapColor = Color(0xFFc127d9), 185 | isCapIconEnabled = true 186 | ) 187 | Spacer(modifier = Modifier.height(8.dp)) 188 | SuggestionMessage() 189 | Spacer(modifier = Modifier.height(8.dp)) 190 | CryptoList( 191 | dataList = dataList, 192 | coinsText = "All Coins", 193 | viewText = "View All" 194 | ) 195 | } else { 196 | Text(text = "No data available.") 197 | } 198 | } else { 199 | val filteredList = 200 | dataList.filter { it.name.contains(queryText, ignoreCase = true) } 201 | if (filteredList.isNotEmpty()) { 202 | CryptoList( 203 | dataList = filteredList, 204 | coinsText = "Search Results", 205 | viewText = "View All" 206 | ) 207 | } else { 208 | Text(text = "No matching results.") 209 | } 210 | } 211 | 212 | } 213 | 214 | } 215 | PullRefreshIndicator( 216 | refreshing = refreshing, 217 | state = refreshState, 218 | modifier = Modifier 219 | .align(Alignment.TopCenter) 220 | ) 221 | } 222 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/screens/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.screens.home 2 | 3 | import androidx.compose.runtime.Composable 4 | import cafe.adriel.voyager.core.screen.Screen 5 | 6 | class HomeScreen : Screen { 7 | @Composable 8 | override fun Content() { 9 | HomeContent() 10 | } 11 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/screens/news/NewsScreen.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.screens.news 2 | 3 | import androidx.compose.foundation.ExperimentalFoundationApi 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.clickable 6 | import androidx.compose.foundation.horizontalScroll 7 | import androidx.compose.foundation.layout.Arrangement 8 | import androidx.compose.foundation.layout.Box 9 | import androidx.compose.foundation.layout.Column 10 | import androidx.compose.foundation.layout.PaddingValues 11 | import androidx.compose.foundation.layout.Row 12 | import androidx.compose.foundation.layout.Spacer 13 | import androidx.compose.foundation.layout.WindowInsets 14 | import androidx.compose.foundation.layout.fillMaxSize 15 | import androidx.compose.foundation.layout.fillMaxWidth 16 | import androidx.compose.foundation.layout.height 17 | import androidx.compose.foundation.layout.padding 18 | import androidx.compose.foundation.layout.size 19 | import androidx.compose.foundation.layout.statusBars 20 | import androidx.compose.foundation.layout.width 21 | import androidx.compose.foundation.layout.windowInsetsPadding 22 | import androidx.compose.foundation.lazy.LazyColumn 23 | import androidx.compose.foundation.lazy.LazyRow 24 | import androidx.compose.foundation.lazy.grid.GridCells 25 | import androidx.compose.foundation.lazy.grid.LazyVerticalGrid 26 | import androidx.compose.foundation.lazy.grid.items 27 | import androidx.compose.foundation.lazy.items 28 | import androidx.compose.foundation.pager.HorizontalPager 29 | import androidx.compose.foundation.pager.PageSize 30 | import androidx.compose.foundation.pager.PagerState 31 | import androidx.compose.foundation.pager.rememberPagerState 32 | import androidx.compose.foundation.rememberScrollState 33 | import androidx.compose.foundation.shape.CircleShape 34 | import androidx.compose.foundation.shape.RoundedCornerShape 35 | import androidx.compose.material3.Card 36 | import androidx.compose.material3.CardDefaults 37 | import androidx.compose.material3.Divider 38 | import androidx.compose.material3.MaterialTheme 39 | import androidx.compose.material3.Surface 40 | import androidx.compose.material3.Text 41 | import androidx.compose.runtime.Composable 42 | import androidx.compose.runtime.LaunchedEffect 43 | import androidx.compose.runtime.collectAsState 44 | import androidx.compose.runtime.getValue 45 | import androidx.compose.runtime.mutableStateOf 46 | import androidx.compose.runtime.remember 47 | import androidx.compose.runtime.setValue 48 | import androidx.compose.ui.Alignment 49 | import androidx.compose.ui.Modifier 50 | import androidx.compose.ui.draw.clip 51 | import androidx.compose.ui.graphics.Brush 52 | import androidx.compose.ui.graphics.Color 53 | import androidx.compose.ui.graphics.painter.Painter 54 | import androidx.compose.ui.layout.ContentScale 55 | import androidx.compose.ui.text.style.TextOverflow 56 | import androidx.compose.ui.unit.Dp 57 | import androidx.compose.ui.unit.dp 58 | import cafe.adriel.voyager.core.screen.Screen 59 | import cafe.adriel.voyager.navigator.LocalNavigator 60 | import coil3.compose.AsyncImage 61 | import io.kamel.core.Resource 62 | import io.kamel.image.KamelImage 63 | import io.kamel.image.asyncPainterResource 64 | import kotlinx.coroutines.delay 65 | import org.company.app.domain.model.news.Data 66 | import org.company.app.domain.model.news.NewsList 67 | import org.company.app.domain.usecase.ResultState 68 | import org.company.app.presentation.ui.components.ErrorBox 69 | import org.company.app.presentation.ui.components.LoadingBox 70 | import org.company.app.presentation.ui.components.NewsDetailScreen 71 | import org.company.app.presentation.ui.components.PromotionCardWithPager 72 | import org.company.app.presentation.viewmodel.MainViewModel 73 | import org.company.app.utils.formatTimestamp 74 | import org.koin.compose.koinInject 75 | import kotlin.math.min 76 | 77 | class NewsScreen : Screen { 78 | @Composable 79 | override fun Content() { 80 | NewsContent() 81 | } 82 | } 83 | 84 | @Composable 85 | fun NewsContent( 86 | viewModel: MainViewModel = koinInject(), 87 | ) { 88 | var newsList by remember { mutableStateOf(null) } 89 | 90 | LaunchedEffect(Unit) { 91 | viewModel.getAllNews() 92 | } 93 | 94 | val newsState by viewModel.allNews.collectAsState() 95 | when (newsState) { 96 | is ResultState.ERROR -> { 97 | val error = (newsState as ResultState.ERROR).message 98 | ErrorBox(error) 99 | } 100 | 101 | ResultState.LOADING -> { 102 | LoadingBox() 103 | } 104 | 105 | is ResultState.SUCCESS -> { 106 | val response = (newsState as ResultState.SUCCESS).response 107 | newsList = response 108 | } 109 | } 110 | 111 | LazyColumn( 112 | modifier = Modifier.fillMaxSize() 113 | .windowInsetsPadding(WindowInsets.statusBars) 114 | ) { 115 | item { 116 | HeaderSection(newsList?.data) 117 | } 118 | item { 119 | Spacer(modifier = Modifier.height(16.dp)) 120 | } 121 | item { 122 | LazyVerticalGrid( 123 | columns = GridCells.Adaptive(300.dp), 124 | horizontalArrangement = Arrangement.spacedBy(8.dp), 125 | verticalArrangement = Arrangement.spacedBy(8.dp), 126 | modifier = Modifier.fillMaxWidth() 127 | .height(900.dp) 128 | ) { 129 | newsList?.data?.let { listData -> 130 | items(listData) { 131 | NewsItemView(it) 132 | } 133 | } 134 | } 135 | } 136 | } 137 | } 138 | 139 | @OptIn(ExperimentalFoundationApi::class) 140 | @Composable 141 | fun HeaderSection(newsList: List?, modifier: Modifier = Modifier) { 142 | Column(modifier = modifier.padding(horizontal = 16.dp)) { 143 | Spacer(modifier = Modifier.height(16.dp)) 144 | 145 | if (!newsList.isNullOrEmpty()) { 146 | val itemsToDisplay = min(8, newsList.size) 147 | val shuffledNewsList = remember { newsList.shuffled() } 148 | 149 | if (itemsToDisplay > 0) { 150 | Box(modifier = Modifier.fillMaxWidth()) { 151 | PromotionCardWithPager(shuffledNewsList) 152 | } 153 | } 154 | } 155 | 156 | Spacer(modifier = Modifier.height(16.dp)) 157 | 158 | QuickFilters() 159 | } 160 | } 161 | 162 | @Composable 163 | fun QuickFilters( 164 | viewModel: MainViewModel = koinInject() 165 | ) { 166 | LaunchedEffect(Unit) { 167 | viewModel.getNewsCategories() 168 | } 169 | val newsCategories by viewModel.newsCategories.collectAsState() 170 | 171 | when (newsCategories) { 172 | is ResultState.ERROR -> { 173 | val error = (newsCategories as ResultState.ERROR).message 174 | ErrorBox(error) 175 | } 176 | 177 | ResultState.LOADING -> { 178 | // LoadingBox() 179 | } 180 | 181 | is ResultState.SUCCESS -> { 182 | val response = (newsCategories as ResultState.SUCCESS).response 183 | LazyRow( 184 | horizontalArrangement = Arrangement.spacedBy(8.dp), 185 | contentPadding = PaddingValues(horizontal = 16.dp, vertical = 8.dp) 186 | ) { 187 | items(response) { filter -> 188 | Chip(text = filter.categoryName) 189 | } 190 | } 191 | } 192 | } 193 | 194 | } 195 | 196 | 197 | @Composable 198 | fun Chip(text: String) { 199 | Surface( 200 | shape = RoundedCornerShape(16.dp), 201 | color = MaterialTheme.colorScheme.primary, 202 | contentColor = Color.White, 203 | modifier = Modifier.clickable { } 204 | ) { 205 | Text( 206 | text = text, 207 | modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp) 208 | ) 209 | } 210 | } 211 | 212 | @Composable 213 | fun NewsItemView(news: Data) { 214 | val navigator = LocalNavigator.current 215 | Card( 216 | modifier = Modifier 217 | .fillMaxWidth() 218 | .padding(8.dp) 219 | .clickable { navigator?.push(NewsDetailScreen(news)) }, 220 | shape = RoundedCornerShape(16.dp), 221 | elevation = CardDefaults.cardElevation(8.dp), 222 | colors = CardDefaults.cardColors( 223 | containerColor = MaterialTheme.colorScheme.surface 224 | ) 225 | ) { 226 | Column(modifier = Modifier.padding(16.dp)) { 227 | val image: Resource = asyncPainterResource(news.imageurl) 228 | Box( 229 | modifier = Modifier 230 | .height(200.dp) 231 | .fillMaxWidth() 232 | .clip(RoundedCornerShape(16.dp)) 233 | .background( 234 | brush = Brush.verticalGradient( 235 | colors = listOf( 236 | Color.Transparent, 237 | Color.Black.copy(alpha = 0.7f) 238 | ), 239 | startY = 100f 240 | ) 241 | ) 242 | ) { 243 | KamelImage( 244 | { image }, contentDescription = null, 245 | modifier = Modifier.matchParentSize(), 246 | contentScale = ContentScale.Crop 247 | ) 248 | Text( 249 | text = news.title, 250 | style = MaterialTheme.typography.headlineSmall.copy(color = Color.White), 251 | modifier = Modifier 252 | .align(Alignment.BottomStart) 253 | .padding(8.dp) 254 | ) 255 | } 256 | Spacer(modifier = Modifier.height(8.dp)) 257 | Text( 258 | text = news.body, 259 | style = MaterialTheme.typography.bodyMedium, 260 | maxLines = 3, 261 | overflow = TextOverflow.Ellipsis, 262 | color = MaterialTheme.colorScheme.onSurface 263 | ) 264 | Spacer(modifier = Modifier.height(8.dp)) 265 | Row( 266 | verticalAlignment = Alignment.CenterVertically, 267 | horizontalArrangement = Arrangement.SpaceBetween 268 | ) { 269 | Row(verticalAlignment = Alignment.CenterVertically) { 270 | AsyncImage( 271 | model = news.sourceInfo.img, 272 | contentDescription = null, 273 | modifier = Modifier 274 | .size(24.dp) 275 | .clip(CircleShape) 276 | ) 277 | Spacer(modifier = Modifier.width(8.dp)) 278 | Text( 279 | text = news.sourceInfo.name, 280 | style = MaterialTheme.typography.labelMedium, 281 | color = MaterialTheme.colorScheme.primary 282 | ) 283 | } 284 | Text( 285 | text = " Published: ${formatTimestamp(news.publishedOn.toLong())}", 286 | style = MaterialTheme.typography.bodySmall, 287 | color = MaterialTheme.colorScheme.onSurfaceVariant 288 | ) 289 | } 290 | } 291 | } 292 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/ui/screens/profile/ProfileScreen.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.ui.screens.profile 2 | 3 | import androidx.compose.material3.Text 4 | import androidx.compose.runtime.Composable 5 | import cafe.adriel.voyager.core.screen.Screen 6 | 7 | class ProfileScreen : Screen { 8 | @Composable 9 | override fun Content() { 10 | ProfileContent() 11 | } 12 | } 13 | 14 | @Composable 15 | fun ProfileContent() { 16 | Text("Profile Content") 17 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/presentation/viewmodel/MainViewModel.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.presentation.viewmodel 2 | 3 | import androidx.lifecycle.ViewModel 4 | import androidx.lifecycle.viewModelScope 5 | import kotlinx.coroutines.flow.MutableStateFlow 6 | import kotlinx.coroutines.flow.StateFlow 7 | import kotlinx.coroutines.flow.asStateFlow 8 | import kotlinx.coroutines.launch 9 | import org.company.app.domain.model.categories.NewsCategoriesItem 10 | import org.company.app.domain.model.crypto.LatestListing 11 | import org.company.app.domain.model.news.NewsList 12 | import org.company.app.domain.repository.Repository 13 | import org.company.app.domain.usecase.ResultState 14 | 15 | class MainViewModel(private val repository: Repository) : ViewModel() { 16 | private val _latestListing = MutableStateFlow>(ResultState.LOADING) 17 | var latestListing: StateFlow> = _latestListing.asStateFlow() 18 | 19 | private val _allNews = MutableStateFlow>(ResultState.LOADING) 20 | var allNews: StateFlow> = _allNews.asStateFlow() 21 | 22 | private val _newsCategories = MutableStateFlow>>(ResultState.LOADING) 23 | var newsCategories: StateFlow>> = _newsCategories.asStateFlow() 24 | 25 | fun getLatestListing() { 26 | viewModelScope.launch { 27 | _latestListing.value = ResultState.LOADING 28 | try { 29 | val response = repository.getLatestListing() 30 | _latestListing.value = ResultState.SUCCESS(response) 31 | } catch (e: Exception) { 32 | _latestListing.value = ResultState.ERROR(e.message.toString()) 33 | } 34 | } 35 | } 36 | 37 | fun getAllNews(){ 38 | viewModelScope.launch { 39 | _allNews.value = ResultState.LOADING 40 | try { 41 | val response = repository.getAllNews() 42 | _allNews.value = ResultState.SUCCESS(response) 43 | }catch (e:Exception){ 44 | _allNews.value = ResultState.ERROR(e.message.toString()) 45 | } 46 | } 47 | } 48 | fun getNewsCategories(){ 49 | viewModelScope.launch { 50 | _newsCategories.value = ResultState.LOADING 51 | try { 52 | val response = repository.getNewsCategories() 53 | _newsCategories.value = ResultState.SUCCESS(response) 54 | }catch (e:Exception){ 55 | _newsCategories.value = ResultState.ERROR(e.message.toString()) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | //generated by https://m3.material.io/theme-builder#/custom 6 | //Color palette was taken here: https://colorhunt.co/palettes/popular 7 | 8 | internal val md_theme_light_primary = Color(0xFF00687A) 9 | internal val md_theme_light_onPrimary = Color(0xFFFFFFFF) 10 | internal val md_theme_light_primaryContainer = Color(0xFFABEDFF) 11 | internal val md_theme_light_onPrimaryContainer = Color(0xFF001F26) 12 | internal val md_theme_light_secondary = Color(0xFF00696E) 13 | internal val md_theme_light_onSecondary = Color(0xFFFFFFFF) 14 | internal val md_theme_light_secondaryContainer = Color(0xFF6FF6FE) 15 | internal val md_theme_light_onSecondaryContainer = Color(0xFF002022) 16 | internal val md_theme_light_tertiary = Color(0xFF904D00) 17 | internal val md_theme_light_onTertiary = Color(0xFFFFFFFF) 18 | internal val md_theme_light_tertiaryContainer = Color(0xFFFFDCC2) 19 | internal val md_theme_light_onTertiaryContainer = Color(0xFF2E1500) 20 | internal val md_theme_light_error = Color(0xFFBA1A1A) 21 | internal val md_theme_light_errorContainer = Color(0xFFFFDAD6) 22 | internal val md_theme_light_onError = Color(0xFFFFFFFF) 23 | internal val md_theme_light_onErrorContainer = Color(0xFF410002) 24 | internal val md_theme_light_background = Color(0xFFFFFBFF) 25 | internal val md_theme_light_onBackground = Color(0xFF221B00) 26 | internal val md_theme_light_surface = Color(0xFFFFFBFF) 27 | internal val md_theme_light_onSurface = Color(0xFF221B00) 28 | internal val md_theme_light_surfaceVariant = Color(0xFFDBE4E7) 29 | internal val md_theme_light_onSurfaceVariant = Color(0xFF3F484B) 30 | internal val md_theme_light_outline = Color(0xFF70797B) 31 | internal val md_theme_light_inverseOnSurface = Color(0xFFFFF0C0) 32 | internal val md_theme_light_inverseSurface = Color(0xFF3A3000) 33 | internal val md_theme_light_inversePrimary = Color(0xFF55D6F4) 34 | internal val md_theme_light_shadow = Color(0xFF000000) 35 | internal val md_theme_light_surfaceTint = Color(0xFF00687A) 36 | internal val md_theme_light_outlineVariant = Color(0xFFBFC8CB) 37 | internal val md_theme_light_scrim = Color(0xFF000000) 38 | 39 | internal val md_theme_dark_primary = Color(0xFF55D6F4) 40 | internal val md_theme_dark_onPrimary = Color(0xFF003640) 41 | internal val md_theme_dark_primaryContainer = Color(0xFF004E5C) 42 | internal val md_theme_dark_onPrimaryContainer = Color(0xFFABEDFF) 43 | internal val md_theme_dark_secondary = Color(0xFF4CD9E2) 44 | internal val md_theme_dark_onSecondary = Color(0xFF00373A) 45 | internal val md_theme_dark_secondaryContainer = Color(0xFF004F53) 46 | internal val md_theme_dark_onSecondaryContainer = Color(0xFF6FF6FE) 47 | internal val md_theme_dark_tertiary = Color(0xFFFFB77C) 48 | internal val md_theme_dark_onTertiary = Color(0xFF4D2700) 49 | internal val md_theme_dark_tertiaryContainer = Color(0xFF6D3900) 50 | internal val md_theme_dark_onTertiaryContainer = Color(0xFFFFDCC2) 51 | internal val md_theme_dark_error = Color(0xFFFFB4AB) 52 | internal val md_theme_dark_errorContainer = Color(0xFF93000A) 53 | internal val md_theme_dark_onError = Color(0xFF690005) 54 | internal val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6) 55 | internal val md_theme_dark_background = Color(0xFF221B00) 56 | internal val md_theme_dark_onBackground = Color(0xFFFFE264) 57 | internal val md_theme_dark_surface = Color(0xFF221B00) 58 | internal val md_theme_dark_onSurface = Color(0xFFFFE264) 59 | internal val md_theme_dark_surfaceVariant = Color(0xFF3F484B) 60 | internal val md_theme_dark_onSurfaceVariant = Color(0xFFBFC8CB) 61 | internal val md_theme_dark_outline = Color(0xFF899295) 62 | internal val md_theme_dark_inverseOnSurface = Color(0xFF221B00) 63 | internal val md_theme_dark_inverseSurface = Color(0xFFFFE264) 64 | internal val md_theme_dark_inversePrimary = Color(0xFF00687A) 65 | internal val md_theme_dark_shadow = Color(0xFF000000) 66 | internal val md_theme_dark_surfaceTint = Color(0xFF55D6F4) 67 | internal val md_theme_dark_outlineVariant = Color(0xFF3F484B) 68 | internal val md_theme_dark_scrim = Color(0xFF000000) 69 | 70 | 71 | internal val seed = Color(0xFF2C3639) 72 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material3.MaterialTheme 5 | import androidx.compose.material3.Surface 6 | import androidx.compose.material3.darkColorScheme 7 | import androidx.compose.material3.lightColorScheme 8 | import androidx.compose.runtime.* 9 | 10 | private val LightColorScheme = lightColorScheme( 11 | primary = md_theme_light_primary, 12 | onPrimary = md_theme_light_onPrimary, 13 | primaryContainer = md_theme_light_primaryContainer, 14 | onPrimaryContainer = md_theme_light_onPrimaryContainer, 15 | secondary = md_theme_light_secondary, 16 | onSecondary = md_theme_light_onSecondary, 17 | secondaryContainer = md_theme_light_secondaryContainer, 18 | onSecondaryContainer = md_theme_light_onSecondaryContainer, 19 | tertiary = md_theme_light_tertiary, 20 | onTertiary = md_theme_light_onTertiary, 21 | tertiaryContainer = md_theme_light_tertiaryContainer, 22 | onTertiaryContainer = md_theme_light_onTertiaryContainer, 23 | error = md_theme_light_error, 24 | errorContainer = md_theme_light_errorContainer, 25 | onError = md_theme_light_onError, 26 | onErrorContainer = md_theme_light_onErrorContainer, 27 | background = md_theme_light_background, 28 | onBackground = md_theme_light_onBackground, 29 | surface = md_theme_light_surface, 30 | onSurface = md_theme_light_onSurface, 31 | surfaceVariant = md_theme_light_surfaceVariant, 32 | onSurfaceVariant = md_theme_light_onSurfaceVariant, 33 | outline = md_theme_light_outline, 34 | inverseOnSurface = md_theme_light_inverseOnSurface, 35 | inverseSurface = md_theme_light_inverseSurface, 36 | inversePrimary = md_theme_light_inversePrimary, 37 | surfaceTint = md_theme_light_surfaceTint, 38 | outlineVariant = md_theme_light_outlineVariant, 39 | scrim = md_theme_light_scrim, 40 | ) 41 | 42 | private val DarkColorScheme = darkColorScheme( 43 | primary = md_theme_dark_primary, 44 | onPrimary = md_theme_dark_onPrimary, 45 | primaryContainer = md_theme_dark_primaryContainer, 46 | onPrimaryContainer = md_theme_dark_onPrimaryContainer, 47 | secondary = md_theme_dark_secondary, 48 | onSecondary = md_theme_dark_onSecondary, 49 | secondaryContainer = md_theme_dark_secondaryContainer, 50 | onSecondaryContainer = md_theme_dark_onSecondaryContainer, 51 | tertiary = md_theme_dark_tertiary, 52 | onTertiary = md_theme_dark_onTertiary, 53 | tertiaryContainer = md_theme_dark_tertiaryContainer, 54 | onTertiaryContainer = md_theme_dark_onTertiaryContainer, 55 | error = md_theme_dark_error, 56 | errorContainer = md_theme_dark_errorContainer, 57 | onError = md_theme_dark_onError, 58 | onErrorContainer = md_theme_dark_onErrorContainer, 59 | background = md_theme_dark_background, 60 | onBackground = md_theme_dark_onBackground, 61 | surface = md_theme_dark_surface, 62 | onSurface = md_theme_dark_onSurface, 63 | surfaceVariant = md_theme_dark_surfaceVariant, 64 | onSurfaceVariant = md_theme_dark_onSurfaceVariant, 65 | outline = md_theme_dark_outline, 66 | inverseOnSurface = md_theme_dark_inverseOnSurface, 67 | inverseSurface = md_theme_dark_inverseSurface, 68 | inversePrimary = md_theme_dark_inversePrimary, 69 | surfaceTint = md_theme_dark_surfaceTint, 70 | outlineVariant = md_theme_dark_outlineVariant, 71 | scrim = md_theme_dark_scrim, 72 | ) 73 | 74 | internal val LocalThemeIsDark = compositionLocalOf { mutableStateOf(true) } 75 | 76 | @Composable 77 | internal fun AppTheme( 78 | content: @Composable() () -> Unit 79 | ) { 80 | val systemIsDark = isSystemInDarkTheme() 81 | val isDarkState = remember { mutableStateOf(systemIsDark) } 82 | CompositionLocalProvider( 83 | LocalThemeIsDark provides isDarkState 84 | ) { 85 | val isDark by isDarkState 86 | SystemAppearance(!isDark) 87 | MaterialTheme( 88 | colorScheme = if (isDark) DarkColorScheme else LightColorScheme, 89 | content = { Surface(content = content) } 90 | ) 91 | } 92 | } 93 | 94 | @Composable 95 | internal expect fun SystemAppearance(isDark: Boolean) 96 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/utils/Constant.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.utils 2 | 3 | object Constant { 4 | const val BASE_URL = "https://pro-api.coinmarketcap.com/v1/" 5 | const val CRYPTO_URL = "https://min-api.cryptocompare.com/data/" 6 | const val API_KEY = "55d74250-258c-44bb-ba06-57a19719ffa0" 7 | const val TIME_OUT: Long = 150000 8 | } -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/utils/formatMarketCap.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.utils 2 | 3 | import com.ionspin.kotlin.bignum.decimal.toBigDecimal 4 | 5 | fun formatMarketCap(marketCap: Double): String { 6 | val suffixes = listOf("", "K", "M", "B", "T") 7 | var value = marketCap.toBigDecimal() 8 | var index = 0 9 | 10 | while (value >= 1000.toBigDecimal() && index < suffixes.size - 1) { 11 | value /= 1000.toBigDecimal() 12 | index++ 13 | } 14 | return "${value.toPlainString().take(5)}${suffixes[index]}" 15 | } 16 | -------------------------------------------------------------------------------- /composeApp/src/commonMain/kotlin/org/company/app/utils/formateTimeStamp.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.utils 2 | 3 | import kotlinx.datetime.Instant 4 | import kotlinx.datetime.TimeZone 5 | import kotlinx.datetime.toLocalDateTime 6 | 7 | fun formatTimestamp(timestamp: Long): String { 8 | val instant = Instant.fromEpochSeconds(timestamp) 9 | val localDateTime = instant.toLocalDateTime(TimeZone.currentSystemDefault()) 10 | 11 | val day = localDateTime.date.dayOfMonth.toString().padStart(2, '0') 12 | val month = localDateTime.date.month.name.lowercase().replaceFirstChar { it.uppercase() } 13 | val year = localDateTime.date.year.toString() 14 | 15 | return "$day $month $year" 16 | } 17 | 18 | -------------------------------------------------------------------------------- /composeApp/src/commonTest/kotlin/org/company/app/ComposeTest.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import androidx.compose.foundation.layout.Column 4 | import androidx.compose.material3.Button 5 | import androidx.compose.material3.Text 6 | import androidx.compose.runtime.getValue 7 | import androidx.compose.runtime.mutableStateOf 8 | import androidx.compose.runtime.remember 9 | import androidx.compose.runtime.setValue 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.testTag 12 | import androidx.compose.ui.test.ExperimentalTestApi 13 | import androidx.compose.ui.test.assertTextEquals 14 | import androidx.compose.ui.test.onNodeWithTag 15 | import androidx.compose.ui.test.performClick 16 | import androidx.compose.ui.test.runComposeUiTest 17 | import kotlin.test.Test 18 | 19 | @OptIn(ExperimentalTestApi::class) 20 | class ComposeTest { 21 | 22 | @Test 23 | fun simpleCheck() = runComposeUiTest { 24 | setContent { 25 | var txt by remember { mutableStateOf("Go") } 26 | Column { 27 | Text( 28 | text = txt, 29 | modifier = Modifier.testTag("t_text") 30 | ) 31 | Button( 32 | onClick = { txt += "." }, 33 | modifier = Modifier.testTag("t_button") 34 | ) { 35 | Text("click me") 36 | } 37 | } 38 | } 39 | 40 | onNodeWithTag("t_button").apply { 41 | repeat(3) { performClick() } 42 | } 43 | onNodeWithTag("t_text").assertTextEquals("Go...") 44 | } 45 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.window.ComposeUIViewController 2 | import org.company.app.App 3 | import platform.UIKit.UIViewController 4 | 5 | fun MainViewController(): UIViewController = ComposeUIViewController(configure = { enforceStrictPlistSanityCheck = false }) { App() } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/org/company/app/App.ios.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import platform.Foundation.NSURL 4 | import platform.UIKit.UIApplication 5 | 6 | internal actual fun openUrl(url: String?) { 7 | val nsUrl = url?.let { NSURL.URLWithString(it) } ?: return 8 | UIApplication.sharedApplication.openURL(nsUrl) 9 | } -------------------------------------------------------------------------------- /composeApp/src/iosMain/kotlin/org/company/app/theme/Theme.ios.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import platform.UIKit.UIApplication 6 | import platform.UIKit.UIStatusBarStyleDarkContent 7 | import platform.UIKit.UIStatusBarStyleLightContent 8 | import platform.UIKit.setStatusBarStyle 9 | 10 | @Composable 11 | internal actual fun SystemAppearance(isDark: Boolean) { 12 | LaunchedEffect(isDark) { 13 | UIApplication.sharedApplication.setStatusBarStyle( 14 | if (isDark) UIStatusBarStyleDarkContent else UIStatusBarStyleLightContent 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.ExperimentalComposeUiApi 2 | import androidx.compose.ui.window.CanvasBasedWindow 3 | import org.company.app.App 4 | import org.company.app.di.appModule 5 | import org.jetbrains.skiko.wasm.onWasmReady 6 | import org.koin.core.context.startKoin 7 | 8 | @OptIn(ExperimentalComposeUiApi::class) 9 | fun main() { 10 | onWasmReady { 11 | CanvasBasedWindow("Crypto-KMP") { 12 | App() 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/org/company/app/App.js.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import kotlinx.browser.window 4 | 5 | internal actual fun openUrl(url: String?) { 6 | url?.let { window.open(it) } 7 | } -------------------------------------------------------------------------------- /composeApp/src/jsMain/kotlin/org/company/app/theme/Theme.js.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /composeApp/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Crypto-KMP 6 | 7 | 8 | 9 |
10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.ui.unit.dp 2 | import androidx.compose.ui.window.Window 3 | import androidx.compose.ui.window.application 4 | import androidx.compose.ui.window.rememberWindowState 5 | import org.company.app.App 6 | import java.awt.Dimension 7 | 8 | fun main() = application { 9 | Window( 10 | title = "Crypto-KMP", 11 | state = rememberWindowState(width = 1280.dp, height = 720.dp), 12 | onCloseRequest = ::exitApplication, 13 | ) { 14 | window.minimumSize = Dimension(800, 600) 15 | App() 16 | } 17 | } -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/org/company/app/App.jvm.kt: -------------------------------------------------------------------------------- 1 | package org.company.app 2 | 3 | import java.awt.Desktop 4 | import java.net.URI 5 | 6 | internal actual fun openUrl(url: String?) { 7 | val uri = url?.let { URI.create(it) } ?: return 8 | Desktop.getDesktop().browse(uri) 9 | } -------------------------------------------------------------------------------- /composeApp/src/jvmMain/kotlin/org/company/app/theme/Theme.jvm.kt: -------------------------------------------------------------------------------- 1 | package org.company.app.theme 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal actual fun SystemAppearance(isDark: Boolean) { 7 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx4G -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx4G" 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | org.gradle.daemon=true 6 | org.gradle.parallel=true 7 | 8 | #Kotlin 9 | kotlin.code.style=official 10 | kotlin.js.compiler=ir 11 | 12 | #Android 13 | android.useAndroidX=true 14 | android.nonTransitiveRClass=true 15 | 16 | #Compose 17 | org.jetbrains.compose.experimental.uikit.enabled=true 18 | org.jetbrains.compose.experimental.jscanvas.enabled=true 19 | org.jetbrains.compose.experimental.wasm.enabled=true 20 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | 3 | alertKmp = "2.0.0" 4 | bignum = "0.3.9" 5 | chart = "Beta-0.0.5" 6 | kamelImage = "1.0.1" 7 | koinAnnotations = "1.3.1" 8 | kotlin = "2.0.21" 9 | compose = "1.7.1" 10 | agp = "8.2.2" 11 | androidx-activityCompose = "1.9.3" 12 | androidx-uiTest = "1.7.6" 13 | lifecycleViewmodelCompose = "2.8.4" 14 | voyager = "1.1.0-beta03" 15 | napier = "2.7.1" 16 | buildConfig = "5.3.5" 17 | kotlinx-coroutines = "1.9.0" 18 | ktor = "3.0.1" 19 | kotlinx-serialization = "1.7.3" 20 | kotlinx-datetime = "0.6.1" 21 | sqlDelight = "2.0.2" 22 | size = "0.5.0" 23 | coil3 = "3.0.0-alpha08" 24 | 25 | 26 | [libraries] 27 | 28 | alert-kmp = { module = "io.github.khubaibkhan4:alert-kmp", version.ref = "alertKmp" } 29 | androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" } 30 | androidx-testManifest = { module = "androidx.compose.ui:ui-test-manifest", version.ref = "androidx-uiTest" } 31 | androidx-junit4 = { module = "androidx.compose.ui:ui-test-junit4", version.ref = "androidx-uiTest" } 32 | bignum = { module = "com.ionspin.kotlin:bignum", version.ref = "bignum" } 33 | chart = { module = "io.github.thechance101:chart", version.ref = "chart" } 34 | insert-koin-koin-core = { module = "io.insert-koin:koin-core" } 35 | io-insert-koin-koin-core = { module = "io.insert-koin:koin-core" } 36 | kamel-image = { module = "media.kamel:kamel-image", version.ref = "kamelImage" } 37 | kamel-image-default = { module = "media.kamel:kamel-image-default", version.ref = "kamelImage" } 38 | koin-android = { module = "io.insert-koin:koin-android" } 39 | koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koinAnnotations" } 40 | koin-compose = { module = "io.insert-koin:koin-compose" } 41 | lifecycle-viewmodel-compose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } 42 | voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" } 43 | tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" } 44 | napier = { module = "io.github.aakira:napier", version.ref = "napier" } 45 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 46 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } 47 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 48 | kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } 49 | ktor-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } 50 | ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } 51 | ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" } 52 | ktor-client-js = { module = "io.ktor:ktor-client-js", version.ref = "ktor" } 53 | ktor-client-content-negociation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } 54 | ktor-client-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } 55 | ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" } 56 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } 57 | kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinx-datetime" } 58 | screen-size = { module = "dev.chrisbanes.material3:material3-window-size-class-multiplatform", version.ref = "size" } 59 | sqlDelight-driver-sqlite = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqlDelight" } 60 | sqlDelight-driver-android = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } 61 | sqlDelight-driver-native = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } 62 | sqlDelight-driver-js = { module = "app.cash.sqldelight:web-worker-driver", version.ref = "sqlDelight" } 63 | coil-compose = { module = "io.coil-kt.coil3:coil-compose", version.ref = "coil3" } 64 | coil-compose-core = { module = "io.coil-kt.coil3:coil-compose-core", version.ref = "coil3" } 65 | coil-network-ktor = { module = "io.coil-kt.coil3:coil-network-ktor", version.ref = "coil3" } 66 | coil-mp = { module = "io.coil-kt.coil3:coil", version.ref = "coil3" } 67 | 68 | [plugins] 69 | 70 | multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 71 | compose = { id = "org.jetbrains.compose", version.ref = "compose" } 72 | android-application = { id = "com.android.application", version.ref = "agp" } 73 | buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "buildConfig" } 74 | kotlinx-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 75 | sqlDelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" } 76 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 77 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KhubaibKhan4/Crypto-KMP/1298614b35728f6345fa0743e181f0979f0b6fd8/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | 2 | #!/bin/sh 3 | 4 | # 5 | # Copyright © 2015-2021 the original authors. 6 | # 7 | # Licensed under the Apache License, Version 2.0 (the "License"); 8 | # you may not use this file except in compliance with the License. 9 | # You may obtain a copy of the License at 10 | # 11 | # https://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, software 14 | # distributed under the License is distributed on an "AS IS" BASIS, 15 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 16 | # See the License for the specific language governing permissions and 17 | # limitations under the License. 18 | # 19 | 20 | ############################################################################## 21 | # 22 | # Gradle start up script for POSIX generated by Gradle. 23 | # 24 | # Important for running: 25 | # 26 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 27 | # noncompliant, but you have some other compliant shell such as ksh or 28 | # bash, then to run this script, type that shell name before the whole 29 | # command line, like: 30 | # 31 | # ksh Gradle 32 | # 33 | # Busybox and similar reduced shells will NOT work, because this script 34 | # requires all of these POSIX shell features: 35 | # * functions; 36 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 37 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 38 | # * compound commands having a testable exit status, especially «case»; 39 | # * various built-in commands including «command», «set», and «ulimit». 40 | # 41 | # Important for patching: 42 | # 43 | # (2) This script targets any POSIX shell, so it avoids extensions provided 44 | # by Bash, Ksh, etc; in particular arrays are avoided. 45 | # 46 | # The "traditional" practice of packing multiple parameters into a 47 | # space-separated string is a well documented source of bugs and security 48 | # problems, so this is (mostly) avoided, by progressively accumulating 49 | # options in "$@", and eventually passing that to Java. 50 | # 51 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 52 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 53 | # see the in-line comments for details. 54 | # 55 | # There are tweaks for specific operating systems such as AIX, CygWin, 56 | # Darwin, MinGW, and NonStop. 57 | # 58 | # (3) This script is generated from the Groovy template 59 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 60 | # within the Gradle project. 61 | # 62 | # You can find Gradle at https://github.com/gradle/gradle/. 63 | # 64 | ############################################################################## 65 | 66 | # Attempt to set APP_HOME 67 | 68 | # Resolve links: $0 may be a link 69 | app_path=$0 70 | 71 | # Need this for daisy-chained symlinks. 72 | while 73 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 74 | [ -h "$app_path" ] 75 | do 76 | ls=$( ls -ld "$app_path" ) 77 | link=${ls#*' -> '} 78 | case $link in #( 79 | /*) app_path=$link ;; #( 80 | *) app_path=$APP_HOME$link ;; 81 | esac 82 | done 83 | 84 | # This is normally unused 85 | # shellcheck disable=SC2034 86 | APP_BASE_NAME=${0##*/} 87 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 88 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 89 | 90 | # Use the maximum available, or set MAX_FD != -1 to use that value. 91 | MAX_FD=maximum 92 | 93 | warn () { 94 | echo "$*" 95 | } >&2 96 | 97 | die () { 98 | echo 99 | echo "$*" 100 | echo 101 | exit 1 102 | } >&2 103 | 104 | # OS specific support (must be 'true' or 'false'). 105 | cygwin=false 106 | msys=false 107 | darwin=false 108 | nonstop=false 109 | case "$( uname )" in #( 110 | CYGWIN* ) cygwin=true ;; #( 111 | Darwin* ) darwin=true ;; #( 112 | MSYS* | MINGW* ) msys=true ;; #( 113 | NONSTOP* ) nonstop=true ;; 114 | esac 115 | 116 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 175 | 176 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 177 | 178 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 179 | for arg do 180 | if 181 | case $arg in #( 182 | -*) false ;; # don't mess with options #( 183 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 184 | [ -e "$t" ] ;; #( 185 | *) false ;; 186 | esac 187 | then 188 | arg=$( cygpath --path --ignore --mixed "$arg" ) 189 | fi 190 | # Roll the args list around exactly as many times as the number of 191 | # args, so each arg winds up back in the position where it started, but 192 | # possibly modified. 193 | # 194 | # NB: a `for` loop captures its iteration list before it begins, so 195 | # changing the positional parameters here affects neither the number of 196 | # iterations, nor the values presented in `arg`. 197 | shift # remove old arg 198 | set -- "$@" "$arg" # push replacement arg 199 | done 200 | fi 201 | 202 | 203 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 204 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 205 | 206 | # Collect all arguments for the java command: 207 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 208 | # and any embedded shellness will be escaped. 209 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 210 | # treated as '${Hostname}' itself on the command line. 211 | 212 | set -- \ 213 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 214 | -classpath "$CLASSPATH" \ 215 | org.gradle.wrapper.GradleWrapperMain \ 216 | "$@" 217 | 218 | # Stop when "xargs" is not available. 219 | if ! command -v xargs >/dev/null 2>&1 220 | then 221 | die "xargs is not available" 222 | fi 223 | 224 | # Use "xargs" to parse quoted args. 225 | # 226 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 227 | # 228 | # In Bash we could simply go: 229 | # 230 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 231 | # set -- "${ARGS[@]}" "$@" 232 | # 233 | # but POSIX shell has neither arrays nor command substitution, so instead we 234 | # post-process each arg (as a line of input to sed) to backslash-escape any 235 | # character that might be a shell metacharacter, then use eval to reverse 236 | # that process (while maintaining the separation between arguments), and wrap 237 | # the whole thing up as a single "set" statement. 238 | # 239 | # This will of course break if any of these variables contains a newline or 240 | # an unmatched quote. 241 | # 242 | 243 | eval "set -- $( 244 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 245 | xargs -n1 | 246 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 247 | tr '\n' ' ' 248 | )" '"$@"' 249 | 250 | exec "$JAVACMD" "$@" 251 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | 2 | @rem 3 | @rem Copyright 2015 the original author or authors. 4 | @rem 5 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 6 | @rem you may not use this file except in compliance with the License. 7 | @rem You may obtain a copy of the License at 8 | @rem 9 | @rem https://www.apache.org/licenses/LICENSE-2.0 10 | @rem 11 | @rem Unless required by applicable law or agreed to in writing, software 12 | @rem distributed under the License is distributed on an "AS IS" BASIS, 13 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | @rem See the License for the specific language governing permissions and 15 | @rem limitations under the License. 16 | @rem 17 | 18 | @if "%DEBUG%"=="" @echo off 19 | @rem ########################################################################## 20 | @rem 21 | @rem Gradle startup script for Windows 22 | @rem 23 | @rem ########################################################################## 24 | 25 | @rem Set local scope for the variables with windows NT shell 26 | if "%OS%"=="Windows_NT" setlocal 27 | 28 | set DIRNAME=%~dp0 29 | if "%DIRNAME%"=="" set DIRNAME=. 30 | @rem This is normally unused 31 | set APP_BASE_NAME=%~n0 32 | set APP_HOME=%DIRNAME% 33 | 34 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 35 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 36 | 37 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 38 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 39 | 40 | @rem Find java.exe 41 | if defined JAVA_HOME goto findJavaFromJavaHome 42 | 43 | set JAVA_EXE=java.exe 44 | %JAVA_EXE% -version >NUL 2>&1 45 | if %ERRORLEVEL% equ 0 goto execute 46 | 47 | echo. 1>&2 48 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 49 | echo. 1>&2 50 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 51 | echo location of your Java installation. 1>&2 52 | 53 | goto fail 54 | 55 | :findJavaFromJavaHome 56 | set JAVA_HOME=%JAVA_HOME:"=% 57 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 58 | 59 | if exist "%JAVA_EXE%" goto execute 60 | 61 | echo. 1>&2 62 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 63 | echo. 1>&2 64 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 65 | echo location of your Java installation. 1>&2 66 | 67 | goto fail 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93A953A29CC810C00F8E227 /* iosApp.swift */; }; 11 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A953E29CC810D00F8E227 /* Assets.xcassets */; }; 12 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A93A954129CC810D00F8E227 /* Preview Assets.xcassets */; }; 13 | /* End PBXBuildFile section */ 14 | 15 | /* Begin PBXFileReference section */ 16 | A93A953729CC810C00F8E227 /* Crypto-KMP.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Crypto-KMP.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 17 | A93A953A29CC810C00F8E227 /* iosApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosApp.swift; sourceTree = ""; }; 18 | A93A953E29CC810D00F8E227 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 19 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 20 | /* End PBXFileReference section */ 21 | 22 | /* Begin PBXFrameworksBuildPhase section */ 23 | A93A953429CC810C00F8E227 /* Frameworks */ = { 24 | isa = PBXFrameworksBuildPhase; 25 | buildActionMask = 2147483647; 26 | files = ( 27 | ); 28 | runOnlyForDeploymentPostprocessing = 0; 29 | }; 30 | /* End PBXFrameworksBuildPhase section */ 31 | 32 | /* Begin PBXGroup section */ 33 | A93A952E29CC810C00F8E227 = { 34 | isa = PBXGroup; 35 | children = ( 36 | A93A953929CC810C00F8E227 /* iosApp */, 37 | A93A953829CC810C00F8E227 /* Products */, 38 | C4127409AE3703430489E7BC /* Frameworks */, 39 | ); 40 | sourceTree = ""; 41 | }; 42 | A93A953829CC810C00F8E227 /* Products */ = { 43 | isa = PBXGroup; 44 | children = ( 45 | A93A953729CC810C00F8E227 /* Crypto-KMP.app */, 46 | ); 47 | name = Products; 48 | sourceTree = ""; 49 | }; 50 | A93A953929CC810C00F8E227 /* iosApp */ = { 51 | isa = PBXGroup; 52 | children = ( 53 | A93A953A29CC810C00F8E227 /* iosApp.swift */, 54 | A93A953E29CC810D00F8E227 /* Assets.xcassets */, 55 | A93A954029CC810D00F8E227 /* Preview Content */, 56 | ); 57 | path = iosApp; 58 | sourceTree = ""; 59 | }; 60 | A93A954029CC810D00F8E227 /* Preview Content */ = { 61 | isa = PBXGroup; 62 | children = ( 63 | A93A954129CC810D00F8E227 /* Preview Assets.xcassets */, 64 | ); 65 | path = "Preview Content"; 66 | sourceTree = ""; 67 | }; 68 | C4127409AE3703430489E7BC /* Frameworks */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | /* End PBXGroup section */ 76 | 77 | /* Begin PBXNativeTarget section */ 78 | A93A953629CC810C00F8E227 /* iosApp */ = { 79 | isa = PBXNativeTarget; 80 | buildConfigurationList = A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */; 81 | buildPhases = ( 82 | A9D80A052AAB5CDE006C8738 /* ShellScript */, 83 | A93A953329CC810C00F8E227 /* Sources */, 84 | A93A953429CC810C00F8E227 /* Frameworks */, 85 | A93A953529CC810C00F8E227 /* Resources */, 86 | ); 87 | buildRules = ( 88 | ); 89 | dependencies = ( 90 | ); 91 | name = iosApp; 92 | productName = iosApp; 93 | productReference = A93A953729CC810C00F8E227 /* Crypto-KMP.app */; 94 | productType = "com.apple.product-type.application"; 95 | }; 96 | /* End PBXNativeTarget section */ 97 | 98 | /* Begin PBXProject section */ 99 | A93A952F29CC810C00F8E227 /* Project object */ = { 100 | isa = PBXProject; 101 | attributes = { 102 | LastSwiftUpdateCheck = 1420; 103 | LastUpgradeCheck = 1420; 104 | TargetAttributes = { 105 | A93A953629CC810C00F8E227 = { 106 | CreatedOnToolsVersion = 14.2; 107 | }; 108 | }; 109 | }; 110 | buildConfigurationList = A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */; 111 | compatibilityVersion = "Xcode 14.0"; 112 | developmentRegion = en; 113 | hasScannedForEncodings = 0; 114 | knownRegions = ( 115 | en, 116 | Base, 117 | ); 118 | mainGroup = A93A952E29CC810C00F8E227; 119 | productRefGroup = A93A953829CC810C00F8E227 /* Products */; 120 | projectDirPath = ""; 121 | projectRoot = ""; 122 | targets = ( 123 | A93A953629CC810C00F8E227 /* iosApp */, 124 | ); 125 | }; 126 | /* End PBXProject section */ 127 | 128 | /* Begin PBXResourcesBuildPhase section */ 129 | A93A953529CC810C00F8E227 /* Resources */ = { 130 | isa = PBXResourcesBuildPhase; 131 | buildActionMask = 2147483647; 132 | files = ( 133 | A93A954229CC810D00F8E227 /* Preview Assets.xcassets in Resources */, 134 | A93A953F29CC810D00F8E227 /* Assets.xcassets in Resources */, 135 | ); 136 | runOnlyForDeploymentPostprocessing = 0; 137 | }; 138 | /* End PBXResourcesBuildPhase section */ 139 | 140 | /* Begin PBXShellScriptBuildPhase section */ 141 | A9D80A052AAB5CDE006C8738 /* ShellScript */ = { 142 | isa = PBXShellScriptBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | ); 146 | inputFileListPaths = ( 147 | ); 148 | inputPaths = ( 149 | ); 150 | outputFileListPaths = ( 151 | ); 152 | outputPaths = ( 153 | ); 154 | runOnlyForDeploymentPostprocessing = 0; 155 | shellPath = /bin/sh; 156 | shellScript = "cd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; 157 | }; 158 | /* End PBXShellScriptBuildPhase section */ 159 | 160 | /* Begin PBXSourcesBuildPhase section */ 161 | A93A953329CC810C00F8E227 /* Sources */ = { 162 | isa = PBXSourcesBuildPhase; 163 | buildActionMask = 2147483647; 164 | files = ( 165 | A93A953B29CC810C00F8E227 /* iosApp.swift in Sources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXSourcesBuildPhase section */ 170 | 171 | /* Begin XCBuildConfiguration section */ 172 | A93A954329CC810D00F8E227 /* Debug */ = { 173 | isa = XCBuildConfiguration; 174 | buildSettings = { 175 | ALWAYS_SEARCH_USER_PATHS = NO; 176 | CLANG_ANALYZER_NONNULL = YES; 177 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 178 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 179 | CLANG_ENABLE_MODULES = YES; 180 | CLANG_ENABLE_OBJC_ARC = YES; 181 | CLANG_ENABLE_OBJC_WEAK = YES; 182 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 183 | CLANG_WARN_BOOL_CONVERSION = YES; 184 | CLANG_WARN_COMMA = YES; 185 | CLANG_WARN_CONSTANT_CONVERSION = YES; 186 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 187 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 188 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 189 | CLANG_WARN_EMPTY_BODY = YES; 190 | CLANG_WARN_ENUM_CONVERSION = YES; 191 | CLANG_WARN_INFINITE_RECURSION = YES; 192 | CLANG_WARN_INT_CONVERSION = YES; 193 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 194 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 195 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 196 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 197 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 198 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 199 | CLANG_WARN_STRICT_PROTOTYPES = YES; 200 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 201 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 202 | CLANG_WARN_UNREACHABLE_CODE = YES; 203 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 204 | COPY_PHASE_STRIP = NO; 205 | DEBUG_INFORMATION_FORMAT = dwarf; 206 | ENABLE_STRICT_OBJC_MSGSEND = YES; 207 | ENABLE_TESTABILITY = YES; 208 | GCC_C_LANGUAGE_STANDARD = gnu11; 209 | GCC_DYNAMIC_NO_PIC = NO; 210 | GCC_NO_COMMON_BLOCKS = YES; 211 | GCC_OPTIMIZATION_LEVEL = 0; 212 | GCC_PREPROCESSOR_DEFINITIONS = ( 213 | "DEBUG=1", 214 | "$(inherited)", 215 | ); 216 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 217 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 218 | GCC_WARN_UNDECLARED_SELECTOR = YES; 219 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 220 | GCC_WARN_UNUSED_FUNCTION = YES; 221 | GCC_WARN_UNUSED_VARIABLE = YES; 222 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 223 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 224 | MTL_FAST_MATH = YES; 225 | ONLY_ACTIVE_ARCH = YES; 226 | SDKROOT = iphoneos; 227 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 228 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 229 | }; 230 | name = Debug; 231 | }; 232 | A93A954429CC810D00F8E227 /* Release */ = { 233 | isa = XCBuildConfiguration; 234 | buildSettings = { 235 | ALWAYS_SEARCH_USER_PATHS = NO; 236 | CLANG_ANALYZER_NONNULL = YES; 237 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 238 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 239 | CLANG_ENABLE_MODULES = YES; 240 | CLANG_ENABLE_OBJC_ARC = YES; 241 | CLANG_ENABLE_OBJC_WEAK = YES; 242 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 243 | CLANG_WARN_BOOL_CONVERSION = YES; 244 | CLANG_WARN_COMMA = YES; 245 | CLANG_WARN_CONSTANT_CONVERSION = YES; 246 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 247 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 248 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 249 | CLANG_WARN_EMPTY_BODY = YES; 250 | CLANG_WARN_ENUM_CONVERSION = YES; 251 | CLANG_WARN_INFINITE_RECURSION = YES; 252 | CLANG_WARN_INT_CONVERSION = YES; 253 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 254 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 255 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 257 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 258 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 259 | CLANG_WARN_STRICT_PROTOTYPES = YES; 260 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 261 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | COPY_PHASE_STRIP = NO; 265 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 266 | ENABLE_NS_ASSERTIONS = NO; 267 | ENABLE_STRICT_OBJC_MSGSEND = YES; 268 | GCC_C_LANGUAGE_STANDARD = gnu11; 269 | GCC_NO_COMMON_BLOCKS = YES; 270 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 271 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 272 | GCC_WARN_UNDECLARED_SELECTOR = YES; 273 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 274 | GCC_WARN_UNUSED_FUNCTION = YES; 275 | GCC_WARN_UNUSED_VARIABLE = YES; 276 | IPHONEOS_DEPLOYMENT_TARGET = 16.2; 277 | MTL_ENABLE_DEBUG_INFO = NO; 278 | MTL_FAST_MATH = YES; 279 | SDKROOT = iphoneos; 280 | SWIFT_COMPILATION_MODE = wholemodule; 281 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Release; 285 | }; 286 | A93A954629CC810D00F8E227 /* Debug */ = { 287 | isa = XCBuildConfiguration; 288 | buildSettings = { 289 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 290 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 291 | CODE_SIGN_STYLE = Automatic; 292 | CURRENT_PROJECT_VERSION = 1; 293 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 294 | ENABLE_PREVIEWS = YES; 295 | FRAMEWORK_SEARCH_PATHS = ( 296 | "${inherited}", 297 | "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 298 | ); 299 | GENERATE_INFOPLIST_FILE = YES; 300 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 301 | LD_RUNPATH_SEARCH_PATHS = ( 302 | "$(inherited)", 303 | "@executable_path/Frameworks", 304 | ); 305 | MARKETING_VERSION = 1.0; 306 | OTHER_LDFLAGS = ( 307 | "${inherited}", 308 | "-framework", 309 | ComposeApp, 310 | ); 311 | PRODUCT_BUNDLE_IDENTIFIER = org.company.app.iosApp; 312 | PRODUCT_NAME = "Crypto-KMP"; 313 | SWIFT_EMIT_LOC_STRINGS = YES; 314 | SWIFT_VERSION = 5.0; 315 | TARGETED_DEVICE_FAMILY = "1,2"; 316 | }; 317 | name = Debug; 318 | }; 319 | A93A954729CC810D00F8E227 /* Release */ = { 320 | isa = XCBuildConfiguration; 321 | buildSettings = { 322 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 323 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 324 | CODE_SIGN_STYLE = Automatic; 325 | CURRENT_PROJECT_VERSION = 1; 326 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 327 | ENABLE_PREVIEWS = YES; 328 | FRAMEWORK_SEARCH_PATHS = ( 329 | "${inherited}", 330 | "$(SRCROOT)/../composeApp/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)", 331 | ); 332 | GENERATE_INFOPLIST_FILE = YES; 333 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 334 | LD_RUNPATH_SEARCH_PATHS = ( 335 | "$(inherited)", 336 | "@executable_path/Frameworks", 337 | ); 338 | MARKETING_VERSION = 1.0; 339 | OTHER_LDFLAGS = ( 340 | "${inherited}", 341 | "-framework", 342 | ComposeApp, 343 | ); 344 | PRODUCT_BUNDLE_IDENTIFIER = org.company.app.iosApp; 345 | PRODUCT_NAME = "Crypto-KMP"; 346 | SWIFT_EMIT_LOC_STRINGS = YES; 347 | SWIFT_VERSION = 5.0; 348 | TARGETED_DEVICE_FAMILY = "1,2"; 349 | }; 350 | name = Release; 351 | }; 352 | /* End XCBuildConfiguration section */ 353 | 354 | /* Begin XCConfigurationList section */ 355 | A93A953229CC810C00F8E227 /* Build configuration list for PBXProject "iosApp" */ = { 356 | isa = XCConfigurationList; 357 | buildConfigurations = ( 358 | A93A954329CC810D00F8E227 /* Debug */, 359 | A93A954429CC810D00F8E227 /* Release */, 360 | ); 361 | defaultConfigurationIsVisible = 0; 362 | defaultConfigurationName = Release; 363 | }; 364 | A93A954529CC810D00F8E227 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 365 | isa = XCConfigurationList; 366 | buildConfigurations = ( 367 | A93A954629CC810D00F8E227 /* Debug */, 368 | A93A954729CC810D00F8E227 /* Release */, 369 | ); 370 | defaultConfigurationIsVisible = 0; 371 | defaultConfigurationName = Release; 372 | }; 373 | /* End XCConfigurationList section */ 374 | }; 375 | rootObject = A93A952F29CC810C00F8E227 /* Project object */; 376 | } 377 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | } 12 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /iosApp/iosApp/iosApp.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import ComposeApp 3 | 4 | @main 5 | class AppDelegate: UIResponder, UIApplicationDelegate { 6 | var window: UIWindow? 7 | 8 | func application( 9 | _ application: UIApplication, 10 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 11 | ) -> Bool { 12 | window = UIWindow(frame: UIScreen.main.bounds) 13 | if let window = window { 14 | window.rootViewController = MainKt.MainViewController() 15 | window.makeKeyAndVisible() 16 | } 17 | return true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "Crypto-KMP" 2 | include(":composeApp") 3 | 4 | pluginManagement { 5 | repositories { 6 | google() 7 | gradlePluginPortal() 8 | mavenCentral() 9 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 10 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") 11 | maven( "https://androidx.dev/storage/compose-compiler/repository") 12 | maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") 13 | } 14 | } 15 | 16 | dependencyResolutionManagement { 17 | repositories { 18 | google() 19 | mavenCentral() 20 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 21 | maven("https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental") 22 | maven( "https://androidx.dev/storage/compose-compiler/repository") 23 | maven("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev") 24 | } 25 | } 26 | --------------------------------------------------------------------------------