[
  {
    "path": ".github/workflows/code-analysis.yml",
    "content": "name: Code Analysis\non:\n  pull_request:\n    branches:\n      - develop\njobs:\n  code-analysis:\n    runs-on: macos-latest\n    environment: Dev\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Download & Copy local.properties\n        run: |\n          curl -o local.properties \"${{ secrets.LOCAL_PROPERTIES_URL }}\"\n          mv local.properties ./local.properties\n      - name: Set up JDK\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'oracle'\n          java-version: '17'\n      - name: Run Ktlint\n        run: ./gradlew ktlintCheck\n        continue-on-error: true\n      - name: Run Detekt\n        run: ./gradlew detekt\n        continue-on-error: true\n\n\n"
  },
  {
    "path": ".github/workflows/update-dependencies-action.yml",
    "content": "name: Update Dependencies\non:\n  schedule:\n    - cron: '00 08 * * 4'\njobs:\n  version-update:\n    runs-on: macos-latest\n    environment: Dev\n    steps:\n      - name: Checkout code\n        uses: actions/checkout@v4\n      - name: Download & Copy local.properties\n        run: |\n          curl -o local.properties \"${{ secrets.LOCAL_PROPERTIES_URL }}\"\n          mv local.properties ./local.properties\n      - name: Set up JDK\n        uses: actions/setup-java@v4\n        with:\n          distribution: 'oracle'\n          java-version: '17'\n\n      - name: Version Update\n        run: |\n          ./gradlew versionCatalogUpdate\n\n      - name: Open Pull Request\n        uses: peter-evans/create-pull-request@v6\n        with:\n          token: ${{ secrets.GITHUB_TOKEN }}\n          title: \"Update Version Catalog\"\n          body: \"Automated version update using GitHub Actions.\"\n          commit-message: \"Update version catalog\"\n"
  },
  {
    "path": ".gitignore",
    "content": "*.iml\n.gradle\n/local.properties\n/.idea/caches\n/.idea/libraries\n/.idea/modules.xml\n/.idea/workspace.xml\n/.idea/navEditor.xml\n/.idea/assetWizardSettings.xml\n.DS_Store\n/build\n/captures\n.externalNativeBuild\n.cxx\nlocal.properties\n/.idea/\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing to Weather App\n\n👍🎉 First off, thank you for taking the time to contribute! 🎉👍\n\nThe following is a set of guidelines for contributing to Weather App. These are mostly guidelines, not rules. Use your best judgment, and feel free to propose changes to this document in a pull request.\n\n## How Can I Contribute?\n\n### Reporting Bugs\n\nBefore submitting a bug report, please check if the issue has already been reported by searching through the [existing issues](https://github.com/odaridavid/weather-app/issues). If you can't find an existing issue addressing the problem, feel free to open a new one. When you report a bug, please include as much detail as possible, including:\n\n- Steps to reproduce the bug\n- Expected behavior\n- Actual behavior\n- Screenshots or code snippets, if applicable\n- Your Android Version.\n\n### Suggesting Enhancements\n\nIf you have an idea for an enhancement or new feature, we'd love to hear about it! You can submit your suggestion by opening a new issue. Please provide a clear description of the enhancement you're proposing and any relevant context or examples that might help us understand your idea better.\n\n### Contributing Code\n\nWe welcome contributions in the form of code improvements, bug fixes, or new features. To contribute code:\n\n1. Fork the repository to your GitHub account.\n2. Create a new branch for your feature or bug fix.\n3. Make your changes and commit them with clear, descriptive messages.\n4. Push your changes to your fork.\n5. Submit a pull request to the main repository, explaining the changes you've made and why they're valuable.\n\n### Code Style\n\nWhen contributing code, please adhere to the existing code style and formatting conventions used in the project. If you're unsure about the style or formatting, feel free to ask for guidance in your pull request.\n\n### Code Review\n\nAll contributions will go through a code review process before being merged. During the review, feedback may be provided to help improve the quality of the code. Please be receptive to feedback and be prepared to make any necessary changes to your code.\n\n### Testing\n\nIf you're adding new features or fixing bugs, please ensure that appropriate tests are included to cover the changes you've made. This helps maintain the overall quality and stability of the application.\n\n\n## Questions\n\nIf you have any questions about contributing or anything else related to the project, feel free to reach out to [maintainer's email or contact details](davidkibzodari@gmail.com).\n\nOnce again, thank you for contributing to Weather App! Your support and contributions are greatly appreciated. 🌤️🌈\n"
  },
  {
    "path": "LICENSE",
    "content": "                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright [yyyy] [name of copyright owner]\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "### Weather App\n\n[![Build Status](https://app.bitrise.io/app/80f9b4627fc90757/status.svg?token=3KnRQl0WRfDT5UTzPDiRgA&branch=develop)](https://app.bitrise.io/app/80f9b4627fc90757)\n[![codecov](https://codecov.io/gh/odaridavid/WeatherApp/branch/develop/graph/badge.svg?token=eZcGjGhF83)](https://codecov.io/gh/odaridavid/WeatherApp)\n\n*Summary*\n\nA simple weather app that gets your location and displays the forecast for the current day and a few\ndays after that.\n\n*API :* [OpenWeatherMap](https://openweathermap.org/api)\n\nReason for choosing mentioned API :\n\n- 1000 free api calls per day, good for a small project.\n- Ability to specify different units in requests and receive a formatted response based on the unit\n  i.e imperial/metric etc.\n- Api response can also be modified to include the amount of needed data all with one call ,\n  contains icons for different conditions and has multilingual support if adopting for a wider\n  audience is ever needed.\n- They have a large user base and handle millions of requests, if the app were ever to scale,\n  there's confidence on the api providing high availability.\n- Has capabilities for alerts for severe weather conditions\n\nMore info on how to make an api call [here](https://openweathermap.org/api/one-call-3#multi).\n\n# Pre-requisite 📝\n\nIn your `local.properties` you will need to add your Open Weather API key and copy the urls in.\n\n```properties\nOPEN_WEATHER_API_KEY=YOUR KEY\nOPEN_WEATHER_BASE_URL=https://api.openweathermap.org\nOPEN_WEATHER_ICONS_URL=https://openweathermap.org/img/wn/\n```\n\nCheck for one under  [`Api Keys`](https://home.openweathermap.org/api_keys)\n\n*Environment*\n\n- Built on A.S Hedgehog+\n- JDK 17\n\n# Design/Architectural decisions 📐\n\nThe project makes use of common android patterns in modern android codebases.\n\n**Project Structure**\n\nThe folders are split into 4 boundaries:\n<details>\n  <summary>Core</summary> \n Contains the models/data classes that are independent of any framework specific dependencies and represent the business logic. \n   In a Clean Arch world you can consider these as your domain classes and interfaces.\n</details>\n\n<details>\n  <summary>Data</summary>\n  Contains data sources , local or remote, this is where the implementation for such is kept. All\n  data related actions and formatting happens in this layer as well.\n  It may contain framework related dependencies to orchestrate and create instances of data stores\n  like a database or shared preference etc.\n  One common pattern used in this area is the repository pattern, which mediates data sources and\n  acts as a source of truth to the consumer.\n</details>\n\n<details>\n  <summary>DI</summary>\n  This acts as the glue between the core ,data and UI.The UI relies on the core models and\n  interfaces which are implemented in data.\n</details>\n\n<details>\n  <summary>UI</summary>\n  Contains the presentation layer of the app, the screen components and viewmodels. Framework\n  specific dependencies are best suited for this layer.\n  In this layer MVI is also used, it looks similar to MVVM but the difference is the actions from a\n  screen a.k.a intents e.g ```HomeScreenIntent``` are predefined and are finite,making the\n  the screen state a bit more predictable and it's easier to scan through what actions are possible\n  from a given screen.\n\nThe screen state e.g ```HomeScreenViewState``` is also modelled as a class with immutable\nproperties and makes state management way easier by reducing the state whenever their is a new\nupdate received.\nSome design patterns that can be seen here are the Observer pattern when consuming the flow ->\nstate flows in the composables and provides a reactive app.\n</details>\n\n![Add flow diagram here](/docs/MVI.png)\n\n<details>\n  <summary>Testing</summary>\n\nThe data layer is unit tested by mocking out external dependencies and the ui layer on the\nviewmodels, an integration test is written that makes use of fake,so as to mimic the real scenario\nas much as possible over using mocks, which would also turn it to a unit test.\n</details>\n\n# Other Stuff 📦\n\n*Code style*\n\nFor now there is no strict adherence to a code style, but the project is formatted using the default\nandroid studio formatter.\nYou can run `./gradlew detekt` to check for any code smells and `./gradlew ktlint` to check for any\nlinting issues.\nAlternatively for ktlint\nthe [IDE plugin](https://pinterest.github.io/ktlint/latest/install/setup/#recommended-setup) might\nbe a much better option :)\nOr setting up\na [pre-commit/pre-push hook](https://pinterest.github.io/ktlint/latest/install/cli/#git-hooks) to\nrun the checks before a commit is made or pushed.\n\n*CI/CD*\n\nThe project is built on Bitrise and the workflow is maintained from its dashboard.Github actions are\nresponsible for\ndependency updates and running code quality checks ,defined in the `.github/workflows` folder.\n\n*Design System*\n\nUnder the `designsystem` package ,it follows a tiered approach to styling the app i.e\n<details>\n<summary>Atoms (Smallest Components)</summary>\nTypography:\nDefine font styles, sizes, and weights for headers, paragraphs, and other text elements.\n\nColor Palette:\nEstablish a color palette with primary, secondary, and accent colors. Specify their usage in\ndifferent contexts.\n\nIcons:\nDesign a set of basic icons that represent common actions or concepts. Ensure consistency in style\nand sizing.\n\nButtons:\nCreate button styles with variations for primary, secondary, and tertiary actions. Include states\nlike hover and disabled.\n\nInput Fields:\nDesign consistent styles for text inputs, checkboxes, radio buttons, and other form elements.\n</details>\n<details>\n<summary>Molecules (Simple Components)</summary>\nForm Elements:\nCombine atoms to create complete form components. Ensure consistency in spacing and alignment.\n\nCards:\nCombine text, images, and buttons to create card components. Define variations for different use\ncases.\n\nBadges:\nAssemble icons and text to create badge components for notifications or status indicators.\n\nAvatars:\nDesign avatar components for user profiles, incorporating images or initials.\n</details>\n<details>\n<summary>Organisms (Complex Components)</summary>\nNavigation Bars:\nCreate a consistent navigation bar design that includes menus, icons, and navigation elements.\n\nHeaders and Footers:\nDefine headers and footers with appropriate spacing, logos, and navigation links.\n\nLists:\nAssemble atoms and molecules to create list components, incorporating variations like simple lists,\ndetailed lists, and nested lists.\n\nModals:\nDesign modal components for overlays or pop-ups, ensuring consistency in styles and behavior.\n</details>\n<details>\n<summary>Templates (Page-Level Structures)</summary>\nPage Layouts:\nEstablish consistent layouts for different types of pages (e.g., home page, product page, settings \npage).\n\nGrid Systems:\nDefine grid systems that ensure alignment and consistency across various screen sizes.\n</details>\n\n*Performance*\n\nThe app is monitored using Firebase Performance and Crashlytics,for performance it's using the\ndefault\ntraces but can be extended as the app grows to monitor specific parts of the app that might be slow.\nLeakCanary is also used to monitor for any memory leaks that might occur in debug mode.\n\n*Build Times*\n\nThe current CI build time , factoring in the project size, the number of tests etc.\n\n| Task                                    | Avg Time |\n|-----------------------------------------|----------|\n| Build -  Bitrise                        | 4m 30s   |\n| Code Analysis    - Github Actions       | 6m 30s   |\n| Update Dependencies    - Github Actions | 8m 30s   |\n\n# Technologies 🔨\n\n**Language :** [Kotlin](https://github.com/JetBrains/kotlin)\n\n**Libraries :**\n<details>\n  <summary>UI</summary> \n  <a href=\"https://developer.android.com/jetpack/compose\">Compose</a><br>\n  <a href=\"https://coil-kt.github.io/coil/compose/\">Coil</a><br>\n  <a href=\"https://developer.android.com/guide/playcore/in-app-updates\">InAppUpdate</a>\n</details>\n\n<details>\n  <summary>Data</summary> \n  <a href=\"https://square.github.io/retrofit/\">Retrofit</a><br>\n  <a href=\"https://square.github.io/okhttp/\">OkHTTP</a><br>\n  <a href=\"https://kotlinlang.org/docs/serialization.html\">kotlinx.serialization</a><br>\n  <a href=\"https://developer.android.com/topic/libraries/architecture/datastore\">Preference Data Store</a>\n</details>\n\n<details>\n  <summary>Testing</summary> \n  <a href=\"https://junit.org/junit4/\">JUnit</a><br>\n  <a href=\"https://mockk.io/\">Mockk</a><br>\n  <a href=\"https://truth.dev/\">Truth</a><br>\n  <a href=\"https://github.com/cashapp/turbine\">Turbine</a>\n</details>\n\n<details>\n  <summary>Tooling/Project setup</summary>\n  <a href=\"https://github.com/google/secrets-gradle-plugin\">Gradle secrets plugin</a><br>\n  <a href=\"https://developer.android.com/training/dependency-injection/hilt-android\">Hilt (DI)</a><br>\n  <a href=\"https://firebase.google.com/docs\">Firebase - Crashlytics, Performance</a><br>\n  <a href=\"https://www.bitrise.io/\">Bitrise</a><br>\n  <a href=\"https://about.codecov.io/\">Codecov</a><br>\n  <a href=\"https://github.com/detekt/detekt\">Detekt</a><br>\n  <a href=\"https://ktlint.github.io/\">Ktlint</a><br>\n  <a href=\"https://square.github.io/leakcanary/\">LeakCanary</a><br>\n  <a href=\"https://github.com/mikepenz/AboutLibraries\">About Libraries</a><br>\n  <a href=\"https://kotlinlang.org/docs/multiplatform.html\">KMM</a>\n</details>\n\n# LICENSE\n\n```\n   Copyright 2023 David Odari\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n   \n```\n\n# Screenshots 📱\n\n|                               Light Theme                                |                               Dark Theme                                |\n|:------------------------------------------------------------------------:|:-----------------------------------------------------------------------:|\n|       <img src=\"/docs/screenshots/(Light)Main.png\" width=\"250px\">        |       <img src=\"/docs/screenshots/(Dark)Main.png\" width=\"250px\">        |\n|     <img src=\"/docs/screenshots/(Light)Settings.png\" width=\"250px\">      |     <img src=\"/docs/screenshots/(Dark)Settings.png\" width=\"250px\">      |\n| <img src=\"/docs/screenshots/(Light)Settings-Exclude.png\" width=\"250px\">  | <img src=\"/docs/screenshots/(Dark)Settings-Exclude.png\" width=\"250px\">  |\n|       <img src=\"/docs/screenshots/(Light)About.png\" width=\"250px\">       |       <img src=\"/docs/screenshots/(Dark)About.png\" width=\"250px\">       |\n|       <img src=\"/docs/screenshots/(Light)Error.png\" width=\"250px\">       |       <img src=\"/docs/screenshots/(Dark)Error.png\" width=\"250px\">       |\n| <img src=\"/docs/screenshots/(Light)Settings-Language.png\" width=\"250px\"> | <img src=\"/docs/screenshots/(Dark)Settings-Language.png\" width=\"250px\"> |\n|   <img src=\"/docs/screenshots/(Light)Settings-Time.png\" width=\"250px\">   |   <img src=\"/docs/screenshots/(Dark)Settings-Time.png\" width=\"250px\">   |\n|         <img src=\"/docs/screenshots/Excluded.png\" width=\"250px\">         |                                    -                                    |\n\n![](https://media.giphy.com/media/hWvk9iUU4uBBeyBq0k/giphy.gif)\n\n\n"
  },
  {
    "path": "app/.gitignore",
    "content": "/build"
  },
  {
    "path": "app/build.gradle.kts",
    "content": "import com.google.firebase.perf.plugin.FirebasePerfExtension\n\nplugins {\n    alias(libs.plugins.com.android.application)\n    alias(libs.plugins.org.jetbrains.kotlin.android)\n    alias(libs.plugins.mapsplatform.secrets.gradle.plugin)\n    kotlin(\"kapt\")\n    alias(libs.plugins.dagger.hilt.android)\n    alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization)\n    id(\"com.google.gms.google-services\")\n    id(\"com.google.firebase.crashlytics\")\n    id(\"io.gitlab.arturbosch.detekt\")\n    jacoco\n    alias(libs.plugins.firebase.perf.plugin)\n    alias(libs.plugins.about.lib.plugin)\n    alias(libs.plugins.compose.compiler)\n}\n\njacoco {\n    toolVersion = \"0.8.11\"\n}\n\nproject.afterEvaluate {\n    setupAndroidReporting()\n}\n\nfun setupAndroidReporting() {\n    val buildTypes = listOf(\"debug\")\n\n    buildTypes.forEach { buildTypeName ->\n        val sourceName = buildTypeName\n        val testTaskName = \"test${sourceName.capitalize()}UnitTest\"\n        println(\"Task -> $testTaskName\")\n\n        tasks.register<JacocoReport>(\"${testTaskName}Coverage\") {\n            dependsOn(tasks.findByName(testTaskName))\n\n            group = \"Reporting\"\n            description =\n                \"Generate Jacoco coverage reports on the ${sourceName.capitalize()} build.\"\n\n            reports {\n                xml.required.set(true)\n                csv.required.set(false)\n                html.required.set(true)\n            }\n\n            val fileFilter = listOf(\n                // android\n                \"**/R.class\",\n                \"**/R$*.class\",\n                \"**/BuildConfig.*\",\n                \"**/Manifest*.*\",\n                \"**/*Test*.*\",\n                \"android/**/*.*\",\n                // kotlin\n                \"**/*MapperImpl*.*\",\n                \"**/*\\$ViewInjector*.*\",\n                \"**/*\\$ViewBinder*.*\",\n                \"**/BuildConfig.*\",\n                \"**/*Component*.*\",\n                \"**/*BR*.*\",\n                \"**/Manifest*.*\",\n                \"**/*\\$Lambda$*.*\",\n                \"**/*Companion*.*\",\n                \"**/*Module*.*\",\n                \"**/*Dagger*.*\",\n                \"**/*Hilt*.*\",\n                \"**/*MembersInjector*.*\",\n                \"**/*_MembersInjector.class\",\n                \"**/*_Factory*.*\",\n                \"**/*_Provide*Factory*.*\",\n                // sealed and data classes\n                \"**/*\\$Result.*\",\n                \"**/*\\$Result$*.*\",\n                // adapters generated by moshi\n                \"**/*JsonAdapter.*\",\n                \"**/*Activity*\",\n                \"**/di/**\",\n                \"**/hilt*/**\",\n                // TODO Remove once UI and instrumented tests are added\n                \"**/entrypoint/**\",\n                \"**/designsystem/**\",\n                \"**/*Screen*.*\",\n                \"**/*NavGraph*.*\",\n                \"**/*Destinations*.*\",\n                \"**/common/**\",\n                \"**/*Extensions*.*\",\n            )\n\n            val javaTree = fileTree(\"${project.buildDir}/intermediates/javac/$sourceName/classes\") {\n                exclude(fileFilter)\n            }\n            val kotlinTree = fileTree(\"${project.buildDir}/tmp/kotlin-classes/$sourceName\") {\n                exclude(fileFilter)\n            }\n            classDirectories.setFrom(files(javaTree, kotlinTree))\n\n            executionData.setFrom(files(\"${project.buildDir}/jacoco/$testTaskName.exec\"))\n            val coverageSourceDirs = listOf(\n                \"${project.projectDir}/src/main/java\",\n                \"${project.projectDir}/src/$buildTypeName/java\"\n            )\n\n            sourceDirectories.setFrom(files(coverageSourceDirs))\n            additionalSourceDirs.setFrom(files(coverageSourceDirs))\n        }\n    }\n}\n\nandroid {\n    namespace = \"com.github.odaridavid.weatherapp\"\n    compileSdk = 35\n\n    defaultConfig {\n        applicationId = \"com.github.odaridavid.weatherapp\"\n        minSdk = 23\n        targetSdk = 35\n        versionCode = 1\n        versionName = \"1.0\"\n\n        testInstrumentationRunner = \"androidx.test.runner.AndroidJUnitRunner\"\n        vectorDrawables {\n            useSupportLibrary = true\n        }\n    }\n\n    buildTypes {\n        debug {\n            applicationIdSuffix = \".debug\"\n            configure<FirebasePerfExtension> {\n                // Set this flag to 'false' to disable @AddTrace annotation processing and\n                // automatic monitoring of HTTP/S network requests\n                // for a specific build variant at compile time.\n                // Breaks jacoco reporting if true see https://github.com/firebase/firebase-android-sdk/issues/3948\n                setInstrumentationEnabled(false)\n            }\n            enableAndroidTestCoverage = true\n        }\n\n        release {\n            isMinifyEnabled = false\n            proguardFiles(\n                getDefaultProguardFile(\"proguard-android-optimize.txt\"),\n                \"proguard-rules.pro\"\n            )\n        }\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_17\n        targetCompatibility = JavaVersion.VERSION_17\n    }\n    kotlinOptions {\n        jvmTarget = JavaVersion.VERSION_17.toString()\n        freeCompilerArgs = freeCompilerArgs + \"-opt-in=kotlin.RequiresOptIn\" + listOf(\n            \"-P\",\n            // See https://github.com/androidx/androidx/blob/androidx-main/compose/compiler/design/compiler-metrics.md\n            \"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir}/reports/kotlin-compile/compose\"\n        )\n    }\n    buildFeatures {\n        compose = true\n        buildConfig = true\n    }\n    packaging {\n        resources {\n            excludes += \"/META-INF/{AL2.0,LGPL2.1}\"\n        }\n    }\n}\n\ndependencies {\n\n    implementation(project(\":shared\"))\n    // Jetpack Core\n    implementation(libs.bundles.androidx)\n    implementation(platform(libs.compose.bom))\n    implementation(libs.bundles.compose)\n\n    // Google Play Services\n    implementation(libs.playservices.location)\n\n    // Data & Async\n    implementation(libs.retrofit)\n    implementation(platform(libs.okhttp.bom))\n    implementation(libs.okhttp)\n    implementation(libs.okhttp.logging.interceptor)\n    implementation(libs.kotlinx.coroutines.android)\n    implementation(libs.kotlinx.serialization.converter)\n    implementation(libs.kotlinx.serialization)\n    implementation(libs.coil)\n\n    // DI\n    implementation(libs.hilt.android)\n    kapt(libs.hilt.compiler)\n\n    // Firebase\n    implementation(platform(libs.firebase.bom))\n    implementation(libs.bundles.firebase)\n\n    // Test\n    testImplementation(libs.junit)\n    testImplementation(libs.turbine)\n    testImplementation(libs.mock.android)\n    testImplementation(libs.mock.agent)\n    testImplementation(libs.truth)\n    testImplementation(libs.coroutines.test)\n    debugImplementation(libs.compose.ui.tooling)\n    debugImplementation(libs.compose.ui.test.manifest)\n    // Android Test\n    androidTestImplementation(libs.bundles.android.test)\n    androidTestImplementation(libs.coroutines.test)\n    androidTestImplementation(libs.turbine)\n\n    // Chucker\n    debugImplementation(libs.chucker.debug)\n    releaseImplementation(libs.chucker.release)\n\n    // Memory Leak Detection\n    debugImplementation(libs.leakcanary)\n\n    // In-app update\n    implementation(libs.bundles.google.play)\n\n    // About\n    implementation(libs.about.lib.core)\n    implementation(libs.about.lib.compose.ui)\n}\n\nkapt {\n    correctErrorTypes = true\n}\n"
  },
  {
    "path": "app/google-services.json",
    "content": "{\n  \"project_info\": {\n    \"project_number\": \"610111461146\",\n    \"project_id\": \"weatherapp-e7fb4\",\n    \"storage_bucket\": \"weatherapp-e7fb4.appspot.com\"\n  },\n  \"client\": [\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:610111461146:android:93477e9051250424edbea3\",\n        \"android_client_info\": {\n          \"package_name\": \"com.github.odaridavid.weatherapp\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyAZXonDiplhz8zSQJbC1JT0XiqVmtHbKy0\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    },\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:610111461146:android:1b766bcb5b1f3602edbea3\",\n        \"android_client_info\": {\n          \"package_name\": \"com.github.odaridavid.weatherapp.debug\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyAZXonDiplhz8zSQJbC1JT0XiqVmtHbKy0\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    },\n    {\n      \"client_info\": {\n        \"mobilesdk_app_id\": \"1:610111461146:android:c4a8151c04739b83edbea3\",\n        \"android_client_info\": {\n          \"package_name\": \"dev.davidodari.weatherupdates\"\n        }\n      },\n      \"oauth_client\": [\n        {\n          \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n          \"client_type\": 3\n        }\n      ],\n      \"api_key\": [\n        {\n          \"current_key\": \"AIzaSyAZXonDiplhz8zSQJbC1JT0XiqVmtHbKy0\"\n        }\n      ],\n      \"services\": {\n        \"appinvite_service\": {\n          \"other_platform_oauth_client\": [\n            {\n              \"client_id\": \"610111461146-na7se6v73h0i76v52d4g53snq48p9vi2.apps.googleusercontent.com\",\n              \"client_type\": 3\n            }\n          ]\n        }\n      }\n    }\n  ],\n  \"configuration_version\": \"1\"\n}"
  },
  {
    "path": "app/proguard-rules.pro",
    "content": "# Add project specific ProGuard rules here.\n# You can control the set of applied configuration files using the\n# proguardFiles setting in build.gradle.kts.\n#\n# For more details, see\n#   http://developer.android.com/guide/developing/tools/proguard.html\n\n# If your project uses WebView with JS, uncomment the following\n# and specify the fully qualified class name to the JavaScript interface\n# class:\n#-keepclassmembers class fqcn.of.javascript.interface.for.webview {\n#   public *;\n#}\n\n# Uncomment this to preserve the line number information for\n# debugging stack traces.\n#-keepattributes SourceFile,LineNumberTable\n\n# If you keep the line number information, uncomment this to\n# hide the original source file name.\n#-renamesourcefileattribute SourceFile\n"
  },
  {
    "path": "app/src/androidTest/kotlin/com/github/odaridavid/weatherapp/SettingsRepositoryTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport android.content.Context\nimport androidx.test.core.app.ApplicationProvider\nimport androidx.test.filters.SmallTest\nimport app.cash.turbine.test\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Before\nimport org.junit.Test\n\n@SmallTest\nclass SettingsRepositoryTest {\n\n    // TODO instrumentation test coverage\n    private lateinit var settingsRepository: SettingsRepository\n\n    @Before\n    fun setup() {\n        val context = ApplicationProvider.getApplicationContext<Context>()\n        settingsRepository = DefaultSettingsRepository(context)\n    }\n\n    @Test\n    fun when_we_update_language_then_we_get_the_updated_language() = runTest {\n        settingsRepository.setLanguage(SupportedLanguage.FRENCH)\n        val language = settingsRepository.getLanguage()\n        language.test {\n            awaitItem().also { language ->\n                assert(language == SupportedLanguage.FRENCH)\n            }\n        }\n    }\n\n    @Test\n    fun when_we_update_units_then_we_get_the_updated_units() = runTest {\n        settingsRepository.setUnits(Units.IMPERIAL)\n        val units = settingsRepository.getUnits()\n        units.test {\n            awaitItem().also { units ->\n                assert(units == Units.IMPERIAL)\n            }\n        }\n    }\n\n    @Test\n    fun when_we_update_time_format_then_we_get_the_updated_time_format() = runTest {\n        settingsRepository.setFormat(TimeFormat.TWENTY_FOUR_HOUR)\n        val timeFormat = settingsRepository.getFormat()\n        timeFormat.test {\n            awaitItem().also { timeFormat ->\n                assert(timeFormat == TimeFormat.TWENTY_FOUR_HOUR)\n            }\n        }\n    }\n\n    @Test\n    fun when_we_update_excluded_data_then_we_get_the_updated_excluded_data() = runTest {\n        val excludedData = listOf(ExcludedData.MINUTELY, ExcludedData.ALERTS)\n        settingsRepository.setExcludedData(excludedData)\n        val data = settingsRepository.getExcludedData()\n        data.test {\n            awaitItem().also { data ->\n                assert(data == \"minutely,alerts\")\n            }\n        }\n    }\n\n    @Test\n    fun when_we_update_default_location_then_we_get_the_updated_default_location() = runTest {\n        val defaultLocation = DefaultLocation(23.23, 34.12)\n        settingsRepository.setDefaultLocation(defaultLocation)\n        val location = settingsRepository.getDefaultLocation()\n        location.test {\n            awaitItem().also { location ->\n                assert(location == defaultLocation)\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/debug/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#CD9914\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/debug/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#18879A\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "app/src/debug/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/debug/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\"/>\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\"/>\n</adaptive-icon>"
  },
  {
    "path": "app/src/main/AndroidManifest.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<manifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\">\n\n    <uses-permission android:name=\"android.permission.INTERNET\" />\n    <uses-permission android:name=\"android.permission.ACCESS_COARSE_LOCATION\" />\n\n    <application\n        android:name=\".WeatherApp\"\n        android:allowBackup=\"true\"\n        android:dataExtractionRules=\"@xml/data_extraction_rules\"\n        android:fullBackupContent=\"@xml/backup_rules\"\n        android:icon=\"@mipmap/ic_launcher\"\n        android:label=\"@string/app_name\"\n        android:roundIcon=\"@mipmap/ic_launcher_round\"\n        android:supportsRtl=\"true\"\n        android:theme=\"@style/Theme.WeatherApp\"\n        tools:targetApi=\"31\">\n        <activity\n            android:name=\".ui.MainActivity\"\n            android:exported=\"true\"\n            android:theme=\"@style/Theme.WeatherApp\"\n            android:windowSoftInputMode=\"adjustResize\">\n            <intent-filter>\n                <action android:name=\"android.intent.action.MAIN\" />\n\n                <category android:name=\"android.intent.category.LAUNCHER\" />\n            </intent-filter>\n\n            <meta-data\n                android:name=\"android.app.lib_name\"\n                android:value=\"\" />\n        </activity>\n        <!--        TODO Add new activity launcher with different icon for debug stuff-->\n    </application>\n\n</manifest>\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/MainViewModel.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass MainViewModel @Inject constructor(\n    private val settingsRepository: SettingsRepository,\n    private val logger: Logger\n) : ViewModel() {\n\n    private val _state = MutableStateFlow(MainViewState())\n    val state: StateFlow<MainViewState> = _state.asStateFlow()\n\n    private val _hasAppUpdate = MutableStateFlow(false)\n    val hasAppUpdate: StateFlow<Boolean> = _hasAppUpdate.asStateFlow()\n\n    fun processIntent(mainViewIntent: MainViewIntent) {\n        when (mainViewIntent) {\n            is MainViewIntent.GrantPermission -> {\n                setState { copy(isPermissionGranted = mainViewIntent.isGranted) }\n            }\n\n            is MainViewIntent.CheckLocationSettings -> {\n                setState { copy(isLocationSettingEnabled = mainViewIntent.isEnabled) }\n            }\n\n            is MainViewIntent.ReceiveLocation -> {\n                val defaultLocation = DefaultLocation(\n                    longitude = mainViewIntent.longitude,\n                    latitude = mainViewIntent.latitude\n                )\n                viewModelScope.launch {\n                    settingsRepository.setDefaultLocation(defaultLocation)\n                }\n                setState { copy(defaultLocation = defaultLocation) }\n            }\n\n            is MainViewIntent.LogException -> {\n                logger.logException(mainViewIntent.throwable)\n            }\n\n            is MainViewIntent.UpdateApp -> {\n                viewModelScope.launch {\n                    _hasAppUpdate.emit(true)\n                }\n            }\n        }\n    }\n\n    private fun setState(stateReducer: MainViewState.() -> MainViewState) {\n        viewModelScope.launch {\n            _state.emit(stateReducer(state.value))\n        }\n    }\n}\n\ndata class MainViewState(\n    val isPermissionGranted: Boolean = false,\n    val isLocationSettingEnabled: Boolean = false,\n    val defaultLocation: DefaultLocation? = DefaultLocation(longitude = 0.0, latitude = 0.0)\n)\n\nsealed class MainViewIntent {\n\n    data class GrantPermission(val isGranted: Boolean) : MainViewIntent()\n\n    data class CheckLocationSettings(val isEnabled: Boolean) : MainViewIntent()\n\n    data class ReceiveLocation(val latitude: Double, val longitude: Double) : MainViewIntent()\n\n    data class LogException(val throwable: Throwable) : MainViewIntent()\n\n    data object UpdateApp : MainViewIntent()\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/WeatherApp.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport android.app.Application\nimport dagger.hilt.android.HiltAndroidApp\n\n@HiltAndroidApp\nclass WeatherApp : Application()\n// TODO Add global error handling\n// TODO Timber logging.\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/common/AndroidExtensions.kt",
    "content": "package com.github.odaridavid.weatherapp.common\n\nimport android.Manifest\nimport android.app.Activity\nimport android.content.Context\nimport android.content.pm.PackageManager\nimport android.location.Address\nimport android.location.Geocoder\nimport android.os.Build\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.core.content.ContextCompat\nimport com.github.odaridavid.weatherapp.designsystem.organism.PermissionRationaleDialog\nimport java.util.Locale\n\nprivate const val NO_OF_ADDRESSES = 1\nfun Context.getCityName(longitude: Double, latitude: Double, onAddressReceived: (Address) -> Unit) {\n    val geoCoder = Geocoder(this, Locale.getDefault())\n\n    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {\n        geoCoder.getFromLocation(latitude, longitude, NO_OF_ADDRESSES) { addresses ->\n            if (addresses.isNotEmpty()) {\n                 onAddressReceived(addresses[0])\n            }\n        }\n    } else {\n        try {\n            val addresses = geoCoder.getFromLocation(latitude, longitude, NO_OF_ADDRESSES)\n            if (addresses?.isNotEmpty() == true) {\n                  onAddressReceived(addresses[0])\n            }\n        } catch (e: Exception) {\n            e.printStackTrace()\n        }\n    }\n}\n\n@Composable\nfun Activity.OnPermissionDenied(\n    activityPermissionResult: ActivityResultLauncher<String>,\n) {\n    val showWeatherUI = remember { mutableStateOf(false) }\n    if (shouldShowRequestPermissionRationale(Manifest.permission.ACCESS_COARSE_LOCATION)) {\n        val isDialogShown = remember { mutableStateOf(true) }\n        if (isDialogShown.value) {\n            PermissionRationaleDialog(\n                isDialogShown,\n                activityPermissionResult,\n                showWeatherUI\n            )\n        }\n    } else {\n        activityPermissionResult.launch(Manifest.permission.ACCESS_COARSE_LOCATION)\n    }\n}\n\n@Composable\nfun Context.CheckForPermissions(\n    onPermissionGranted: @Composable () -> Unit,\n    onPermissionDenied: @Composable () -> Unit\n) {\n    when (ContextCompat.checkSelfPermission(\n        this,\n        Manifest.permission.ACCESS_COARSE_LOCATION\n    )) {\n        PackageManager.PERMISSION_GRANTED -> {\n            onPermissionGranted()\n        }\n        PackageManager.PERMISSION_DENIED -> {\n            onPermissionDenied()\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/common/LocationRequest.kt",
    "content": "package com.github.odaridavid.weatherapp.common\n\nimport android.app.Activity\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.IntentSenderRequest\nimport com.google.android.gms.common.api.ApiException\nimport com.google.android.gms.common.api.ResolvableApiException\nimport com.google.android.gms.location.*\nimport com.google.android.gms.tasks.Task\n\nfun createLocationRequest(\n    activity: Activity,\n    locationRequestLauncher: ActivityResultLauncher<IntentSenderRequest>,\n    onLocationRequestSuccessful: () -> Unit\n) {\n    val locationRequest = LocationRequest.Builder(30_000L)\n        .setPriority(Priority.PRIORITY_BALANCED_POWER_ACCURACY)\n        .build()\n\n    val locationSettingsRequest = LocationSettingsRequest.Builder()\n        .addLocationRequest(locationRequest)\n\n    val task: Task<LocationSettingsResponse> =\n        LocationServices.getSettingsClient(activity)\n            .checkLocationSettings(locationSettingsRequest.build())\n\n    task.addOnCompleteListener { response ->\n        try {\n            val result = response.getResult(ApiException::class.java)\n            val hasLocationAccess = result.locationSettingsStates?.isLocationUsable == true\n            if (hasLocationAccess) {\n                onLocationRequestSuccessful()\n            }\n        } catch (exception: ApiException) {\n            when (exception.statusCode) {\n                LocationSettingsStatusCodes.RESOLUTION_REQUIRED -> {\n                    val resolvable = exception as ResolvableApiException\n                    val intentSender =\n                        IntentSenderRequest.Builder(resolvable.resolution).build()\n                    locationRequestLauncher.launch(intentSender)\n                }\n                LocationSettingsStatusCodes.SETTINGS_CHANGE_UNAVAILABLE -> {\n                    // Do nothing, location settings can't be changed\n                }\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/Extensions.kt",
    "content": "package com.github.odaridavid.weatherapp.data\n\nimport android.content.Context\nimport androidx.datastore.core.DataStore\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.preferencesDataStore\n\nval Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = \"settings\")\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/settings/DefaultSettingsRepository.kt",
    "content": "package com.github.odaridavid.weatherapp.data.settings\n\nimport android.content.Context\nimport androidx.datastore.preferences.core.Preferences\nimport androidx.datastore.preferences.core.edit\nimport androidx.datastore.preferences.core.stringPreferencesKey\nimport com.github.odaridavid.weatherapp.BuildConfig\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.data.dataStore\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.map\nimport javax.inject.Inject\n\nclass DefaultSettingsRepository @Inject constructor(\n    @ApplicationContext private val context: Context\n) : SettingsRepository {\n\n    private val prefLanguage by lazy { stringPreferencesKey(KEY_LANGUAGE) }\n    private val prefUnits by lazy { stringPreferencesKey(KEY_UNITS) }\n    private val prefTimeFormat by lazy { stringPreferencesKey(KEY_TIME_FORMAT) }\n    private val prefLatLng by lazy { stringPreferencesKey(KEY_LAT_LNG) }\n    private val prefExcludedData by lazy { stringPreferencesKey(KEY_EXCLUDED_DATA) }\n\n    override suspend fun setLanguage(language: SupportedLanguage) {\n        set(key = prefLanguage, value = language.name)\n    }\n\n    override suspend fun getLanguage(): Flow<SupportedLanguage> {\n        return get(key = prefLanguage, default = SupportedLanguage.ENGLISH.name).map {\n            SupportedLanguage.valueOf(it)\n        }\n    }\n\n    override suspend fun setUnits(units: Units) {\n        set(key = prefUnits, value = units.name)\n    }\n\n    override suspend fun getUnits(): Flow<Units> {\n        return get(key = prefUnits, default = Units.METRIC.name).map {\n            Units.valueOf(it)\n        }\n    }\n\n    override fun getAppVersion(): String =\n        \"Version : ${BuildConfig.VERSION_NAME}-${BuildConfig.BUILD_TYPE}\"\n\n    override suspend fun setDefaultLocation(defaultLocation: DefaultLocation) {\n        set(key = prefLatLng, value = \"${defaultLocation.latitude}/${defaultLocation.longitude}\")\n    }\n\n    override suspend fun getDefaultLocation(): Flow<DefaultLocation> {\n        return get(\n            key = prefLatLng,\n            default = \"$DEFAULT_LATITUDE/$DEFAULT_LONGITUDE\"\n        ).map { latlng ->\n            val latLngList = latlng.split(\"/\").map { it.toDouble() }\n            DefaultLocation(latitude = latLngList[0], longitude = latLngList[1])\n        }\n    }\n\n    override suspend fun getFormat(): Flow<TimeFormat> {\n        return get(key = prefTimeFormat, default = TimeFormat.TWENTY_FOUR_HOUR.name).map {\n            TimeFormat.valueOf(it)\n        }\n    }\n\n    override suspend fun setFormat(format: TimeFormat) {\n        set(key = prefTimeFormat, value = format.name)\n    }\n\n    // TODO Refactor to flow of list of excluded data\n    override suspend fun getExcludedData(): Flow<String> = get(\n        key = prefExcludedData,\n        default = \"${ExcludedData.MINUTELY.value},${ExcludedData.ALERTS.value}\"\n    )\n\n    override suspend fun setExcludedData(excludedData: List<ExcludedData>) {\n        val formattedData = excludedData.joinToString(separator = \",\") { it.value }\n        set(key = prefExcludedData, value = formattedData)\n    }\n\n    private suspend fun <T> set(key: Preferences.Key<T>, value: T) {\n        context.dataStore.edit { settings ->\n            settings[key] = value\n        }\n    }\n\n    private fun <T> get(key: Preferences.Key<T>, default: T): Flow<T> {\n        return context.dataStore.data.map { settings ->\n            settings[key] ?: default\n        }\n    }\n\n    companion object {\n        // Düsseldorf\n        const val DEFAULT_LONGITUDE = 6.773456\n        const val DEFAULT_LATITUDE = 51.227741\n\n        const val KEY_LANGUAGE = \"language\"\n        const val KEY_UNITS = \"units\"\n        const val KEY_LAT_LNG = \"lat_lng\"\n        const val KEY_TIME_FORMAT = \"time_formats\"\n        const val KEY_EXCLUDED_DATA = \"excluded_data\"\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/DefaultWeatherRepository.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather\n\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.api.WeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.remote.RemoteWeatherDataSource\nimport com.github.odaridavid.weatherapp.data.weather.remote.mapThrowableToErrorType\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.Result.Error\nimport com.github.odaridavid.weatherapp.model.Weather\nimport kotlinx.coroutines.flow.first\nimport javax.inject.Inject\n\nclass DefaultWeatherRepository @Inject constructor(\n    private val remoteWeatherDataSource: RemoteWeatherDataSource,\n    private val logger: Logger,\n    private val settingsRepository: SettingsRepository,\n) : WeatherRepository {\n\n    override suspend fun fetchWeatherData(\n        defaultLocation: DefaultLocation,\n        language: String,\n        units: String,\n    ): Result<Weather> =\n        try {\n            val format = settingsRepository.getFormat().first().value\n\n            val excludedData = settingsRepository\n                .getExcludedData()\n                .first()\n                .replace(ExcludedData.NONE.value, \"\")\n\n            remoteWeatherDataSource.fetchWeatherData(\n                defaultLocation = defaultLocation,\n                units = units,\n                language = language,\n                format = format,\n                excludedData = excludedData\n            )\n        } catch (throwable: Throwable) {\n            val errorType = mapThrowableToErrorType(throwable)\n            logger.logException(throwable)\n            Error(errorType)\n        }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/FirebaseLogger.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather\n\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.google.firebase.crashlytics.ktx.crashlytics\nimport com.google.firebase.ktx.Firebase\nimport javax.inject.Inject\n\nclass FirebaseLogger @Inject constructor() : Logger {\n    override fun logException(throwable: Throwable) {\n        Firebase.crashlytics.recordException(throwable)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/remote/DefaultRemoteWeatherDataSource.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather.remote\n\nimport com.github.odaridavid.weatherapp.BuildConfig\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.Weather\nimport javax.inject.Inject\n\nclass DefaultRemoteWeatherDataSource @Inject constructor(\n    private val openWeatherService: OpenWeatherService,\n) : RemoteWeatherDataSource {\n\n    override suspend fun fetchWeatherData(\n        defaultLocation: DefaultLocation,\n        language: String,\n        units: String,\n        format: String,\n        excludedData: String,\n    ): Result<Weather> =\n        try {\n\n            val response = openWeatherService.getWeatherData(\n                longitude = defaultLocation.longitude,\n                latitude = defaultLocation.latitude,\n                excludedInfo = excludedData,\n                units = units,\n                language = language,\n                appid = BuildConfig.OPEN_WEATHER_API_KEY,\n            )\n\n            if (response.isSuccessful && response.body() != null) {\n                val weatherData = response.body()!!.toCoreModel(unit = units, format = format)\n                Result.Success(data = weatherData)\n            } else {\n                val throwable = mapResponseCodeToThrowable(response.code())\n                throw throwable\n            }\n        } catch (e: Exception) {\n            throw e\n        }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/remote/Mappers.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather.remote\n\nimport com.github.odaridavid.weatherapp.BuildConfig\nimport com.github.odaridavid.weatherapp.model.ClientException\nimport com.github.odaridavid.weatherapp.model.CurrentWeather\nimport com.github.odaridavid.weatherapp.model.DailyWeather\nimport com.github.odaridavid.weatherapp.model.ErrorType\nimport com.github.odaridavid.weatherapp.model.GenericException\nimport com.github.odaridavid.weatherapp.model.HourlyWeather\nimport com.github.odaridavid.weatherapp.model.ServerException\nimport com.github.odaridavid.weatherapp.model.Temperature\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport com.github.odaridavid.weatherapp.model.Weather\nimport com.github.odaridavid.weatherapp.model.WeatherInfo\nimport java.io.IOException\nimport java.net.HttpURLConnection\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\nimport kotlin.math.roundToInt\n\nfun WeatherResponse.toCoreModel(unit: String, format: String): Weather = Weather(\n    current = current?.toCoreModel(unit = unit),\n    daily = daily?.map { it.toCoreModel(unit = unit) },\n    hourly = hourly?.map { it.toCoreModel(unit = unit, format = format) }\n)\n\nfun CurrentWeatherResponse.toCoreModel(unit: String): CurrentWeather =\n    CurrentWeather(\n        temperature = formatTemperatureValue(temperature, unit),\n        feelsLike = formatTemperatureValue(feelsLike, unit),\n        weather = weather.map { it.toCoreModel() }\n    )\n\nfun DailyWeatherResponse.toCoreModel(unit: String): DailyWeather =\n    DailyWeather(\n        forecastedTime = getDate(forecastedTime, \"EEEE dd/M\"),\n        temperature = temperature.toCoreModel(unit = unit),\n        weather = weather.map { it.toCoreModel() }\n    )\n\nfun HourlyWeatherResponse.toCoreModel(unit: String, format: String): HourlyWeather {\n    val formatPattern = when (format) {\n        TimeFormat.TWELVE_HOUR.value -> \"h:mm a\"\n        TimeFormat.TWENTY_FOUR_HOUR.value -> \"HH:SS\"\n        else -> \"HH:SS\"\n    }\n    return HourlyWeather(\n        forecastedTime = getDate(forecastedTime, formatPattern),\n        temperature = formatTemperatureValue(temperature, unit),\n        weather = weather.map { it.toCoreModel() }\n    )\n}\n\nfun WeatherInfoResponse.toCoreModel(): WeatherInfo =\n    WeatherInfo(\n        id = id,\n        main = main,\n        description = description,\n        icon = \"${BuildConfig.OPEN_WEATHER_ICONS_URL}$icon@2x.png\"\n    )\n\nfun TemperatureResponse.toCoreModel(unit: String): Temperature =\n    Temperature(\n        min = formatTemperatureValue(min, unit),\n        max = formatTemperatureValue(max, unit)\n    )\n\nprivate fun formatTemperatureValue(temperature: Float, unit: String): String =\n    \"${temperature.roundToInt()}${getUnitSymbols(unit = unit)}\"\n\nprivate fun getUnitSymbols(unit: String) = when (unit) {\n    Units.METRIC.value -> Units.METRIC.tempLabel\n    Units.IMPERIAL.value -> Units.IMPERIAL.tempLabel\n    Units.STANDARD.value -> Units.STANDARD.tempLabel\n    else -> \"N/A\"\n}\n\nprivate fun getDate(utcInMillis: Long, formatPattern: String): String {\n    // TODO use locale from supported languages\n    val sdf = SimpleDateFormat(formatPattern, Locale.ENGLISH)\n    val dateFormat = Date(utcInMillis * 1000)\n    return sdf.format(dateFormat)\n}\n\nfun mapResponseCodeToThrowable(code: Int): Throwable = when (code) {\n    HttpURLConnection.HTTP_BAD_REQUEST -> ClientException(\"Bad request : $code: Check request parameters\")\n    HttpURLConnection.HTTP_UNAUTHORIZED -> ClientException(\"Unauthorized access : $code: Check API Token\")\n    HttpURLConnection.HTTP_NOT_FOUND -> ClientException(\"Resource not found : $code: Check parameters\")\n    TOO_MANY_REQUESTS -> ClientException(\"Too many requests : $code: Rate limit exceeded\")\n    in CLIENT_ERRORS -> ClientException(\"Client error : $code\")\n    in SERVER_ERRORS -> ServerException(\"Server error : $code\")\n    else -> GenericException(\"Generic error : $code\")\n}\n\nfun mapThrowableToErrorType(throwable: Throwable): ErrorType {\n    val errorType = when (throwable) {\n        is IOException -> ErrorType.IO_CONNECTION\n        is ClientException -> ErrorType.CLIENT\n        is ServerException -> ErrorType.SERVER\n        else -> ErrorType.GENERIC\n    }\n    return errorType\n}\n\nprivate val SERVER_ERRORS = 500..600\nprivate val CLIENT_ERRORS = 400..499\nprivate const val TOO_MANY_REQUESTS = 429\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/remote/OpenWeatherService.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather.remote\n\nimport retrofit2.Response\nimport retrofit2.http.GET\nimport retrofit2.http.Query\n\ninterface OpenWeatherService {\n\n    @GET(\"/data/3.0/onecall\")\n    suspend fun getWeatherData(\n        @Query(\"lat\") latitude: Double,\n        @Query(\"lon\") longitude: Double,\n        @Query(\"appid\") appid: String,\n        @Query(\"exclude\") excludedInfo: String,\n        @Query(\"units\") units: String,\n        @Query(\"lang\") language: String\n    ): Response<WeatherResponse>\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/remote/RemoteWeatherDataSource.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather.remote\n\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.Weather\n\ninterface RemoteWeatherDataSource {\n\n    suspend fun fetchWeatherData(\n        defaultLocation: DefaultLocation,\n        language: String,\n        units: String,\n        format: String,\n        excludedData: String\n    ): Result<Weather>\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/data/weather/remote/WeatherResponse.kt",
    "content": "package com.github.odaridavid.weatherapp.data.weather.remote\n\nimport kotlinx.serialization.SerialName\nimport kotlinx.serialization.Serializable\n\n@Serializable\ndata class WeatherResponse(\n    @SerialName(\"current\") val current: CurrentWeatherResponse? = null,\n    @SerialName(\"hourly\") val hourly: List<HourlyWeatherResponse>? = null,\n    @SerialName(\"daily\") val daily: List<DailyWeatherResponse>? = null,\n)\n\n@Serializable\ndata class CurrentWeatherResponse(\n    @SerialName(\"temp\") val temperature: Float,\n    @SerialName(\"feels_like\") val feelsLike: Float,\n    @SerialName(\"weather\") val weather: List<WeatherInfoResponse>\n)\n\n@Serializable\ndata class HourlyWeatherResponse(\n    @SerialName(\"dt\") val forecastedTime: Long,\n    @SerialName(\"temp\") val temperature: Float,\n    @SerialName(\"weather\") val weather: List<WeatherInfoResponse>\n)\n\n@Serializable\ndata class DailyWeatherResponse(\n    @SerialName(\"dt\") val forecastedTime: Long,\n    @SerialName(\"temp\") val temperature: TemperatureResponse,\n    @SerialName(\"weather\") val weather: List<WeatherInfoResponse>\n)\n\n@Serializable\ndata class WeatherInfoResponse(\n    val id: Int,\n    val main: String,\n    val description: String,\n    val icon: String\n)\n\n@Serializable\ndata class TemperatureResponse(\n    @SerialName(\"min\") val min: Float,\n    @SerialName(\"max\") val max: Float,\n)\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/Theme.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem\n\nimport androidx.compose.foundation.isSystemInDarkTheme\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport com.github.odaridavid.weatherapp.designsystem.atom.DarkColorPalette\nimport com.github.odaridavid.weatherapp.designsystem.atom.Dimensions\nimport com.github.odaridavid.weatherapp.designsystem.atom.LightColorPalette\nimport com.github.odaridavid.weatherapp.designsystem.atom.LocalDimens\nimport com.github.odaridavid.weatherapp.designsystem.atom.LocalWeight\nimport com.github.odaridavid.weatherapp.designsystem.atom.Weight\nimport com.github.odaridavid.weatherapp.designsystem.atom.shapes\nimport com.github.odaridavid.weatherapp.designsystem.atom.typography\n\n// TODO Debug menu or app to preview design system components\n@Composable\nfun WeatherAppTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) {\n    val colors = if (darkTheme) {\n        DarkColorPalette\n    } else {\n        LightColorPalette\n    }\n\n    MaterialTheme(\n        colorScheme = colors,\n        typography = typography,\n        shapes = shapes,\n        content = content\n    )\n}\n\nobject WeatherAppTheme {\n    val dimens: Dimensions\n        @Composable\n        get() = LocalDimens.current\n\n    val weight: Weight\n        @Composable\n        get() = LocalWeight.current\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/atom/Color.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.atom\n\nimport androidx.compose.material3.darkColorScheme\nimport androidx.compose.material3.lightColorScheme\nimport androidx.compose.ui.graphics.Color\n\n// TODO Expand color range\nval pink50 = Color(0xffffE9F7)\nval pink200 = Color(0xffff7597)\nval pinkA200 = Color(0xffff3886)\nval pink500 = Color(0xffff0266)\nval pink600 = Color(0xffd8004d)\nval black = Color(0xff24191c)\nval darkBlue = Color(0xff1976D2)\nval blue500 = Color(0xff448aff)\nval blue200 = Color(0xff92Aeff)\nval lightBlue = Color(0xffbbdefb)\nval grey = Color(0xff757575)\nval lightGrey = Color(0xffBDBDBD)\nval white = Color(0xffffffff)\n\nval LightColorPalette = lightColorScheme(\n    primary = pink500,\n    secondary = blue500,\n    onPrimary = pink50,\n    onSecondary = black,\n    primaryContainer = pink200,\n    onPrimaryContainer = black,\n    secondaryContainer = blue200,\n    onSecondaryContainer = black,\n    surface = white\n)\n\nval DarkColorPalette = darkColorScheme(\n    primary = blue500,\n    secondary = pinkA200,\n    surface = black,\n    onPrimary = black,\n    onSecondary = white,\n    primaryContainer = blue200,\n    onPrimaryContainer = black,\n    secondaryContainer = pink200,\n    onSecondaryContainer = black\n)\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/atom/Dimensions.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.atom\n\nimport androidx.compose.runtime.staticCompositionLocalOf\nimport androidx.compose.ui.unit.dp\n\nobject Dimensions {\n    val none = 0.dp\n    val extraSmall = 4.dp\n    val small = 8.dp\n    val medium = 16.dp\n    val large = 24.dp\n    val extraLarge = 32.dp\n}\n\nobject Weight {\n    const val NONE = 0f\n    const val HALF = 0.5f\n    const val FULL = 1f\n}\n\nval LocalDimens = staticCompositionLocalOf { Dimensions }\nval LocalWeight = staticCompositionLocalOf { Weight }\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/atom/Shape.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.atom\n\nimport androidx.compose.foundation.shape.RoundedCornerShape\nimport androidx.compose.material3.Shapes\nimport androidx.compose.ui.unit.dp\n\nval shapes = Shapes(\n    extraSmall = RoundedCornerShape(4.dp),\n    small = RoundedCornerShape(8.dp),\n    medium = RoundedCornerShape(size = 0f),\n    large = RoundedCornerShape(\n        topStart = 16.dp,\n        topEnd = 0.dp,\n        bottomEnd = 0.dp,\n        bottomStart = 16.dp\n    )\n)\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/atom/Type.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.atom\n\nimport androidx.compose.material3.Typography\nimport androidx.compose.ui.text.TextStyle\nimport androidx.compose.ui.text.font.Font\nimport androidx.compose.ui.text.font.FontFamily\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.LineHeightStyle\nimport androidx.compose.ui.text.style.LineHeightStyle.Alignment\nimport androidx.compose.ui.text.style.LineHeightStyle.Trim\nimport androidx.compose.ui.unit.sp\nimport com.github.odaridavid.weatherapp.R\n\nprivate val fonts = FontFamily(\n    Font(R.font.rubik_regular),\n    Font(R.font.rubik_medium, FontWeight.W500),\n    Font(R.font.rubik_bold, FontWeight.Bold)\n)\n\n// typography adapted from NIA Design System, to be improved and tweaked later.\nval typography = typographyFromDefaults(\n    displayLarge = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Bold,\n        fontSize = 64.sp,\n        lineHeight = 64.sp,\n        letterSpacing = (-0.25).sp,\n    ),\n    displayMedium = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Bold,\n        fontSize = 48.sp,\n        lineHeight = 52.sp,\n        letterSpacing = 0.sp,\n    ),\n    displaySmall = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 36.sp,\n        lineHeight = 44.sp,\n        letterSpacing = 0.sp,\n    ),\n    headlineLarge = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 32.sp,\n        lineHeight = 40.sp,\n        letterSpacing = 0.sp,\n    ),\n    headlineMedium = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 28.sp,\n        lineHeight = 36.sp,\n        letterSpacing = 0.sp,\n    ),\n    headlineSmall = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 24.sp,\n        lineHeight = 32.sp,\n        letterSpacing = 0.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Bottom,\n            trim = Trim.None,\n        ),\n    ),\n    titleLarge = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Bold,\n        fontSize = 22.sp,\n        lineHeight = 28.sp,\n        letterSpacing = 0.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Bottom,\n            trim = Trim.LastLineBottom,\n        ),\n    ),\n    titleMedium = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Bold,\n        fontSize = 18.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.1.sp,\n    ),\n    titleSmall = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Medium,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.1.sp,\n    ),\n    // Default text style\n    bodyLarge = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 16.sp,\n        lineHeight = 24.sp,\n        letterSpacing = 0.5.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Center,\n            trim = Trim.None,\n        ),\n    ),\n    bodyMedium = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.25.sp,\n    ),\n    bodySmall = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Normal,\n        fontSize = 12.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.4.sp,\n    ),\n    labelLarge = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Medium,\n        fontSize = 14.sp,\n        lineHeight = 20.sp,\n        letterSpacing = 0.1.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Center,\n            trim = Trim.LastLineBottom,\n        ),\n    ),\n    labelMedium = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Medium,\n        fontSize = 12.sp,\n        lineHeight = 16.sp,\n        letterSpacing = 0.5.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Center,\n            trim = Trim.LastLineBottom,\n        ),\n    ),\n    labelSmall = TextStyle(\n        fontFamily = fonts,\n        fontWeight = FontWeight.Medium,\n        fontSize = 10.sp,\n        lineHeight = 14.sp,\n        letterSpacing = 0.sp,\n        lineHeightStyle = LineHeightStyle(\n            alignment = Alignment.Center,\n            trim = Trim.LastLineBottom,\n        ),\n    ),\n)\n\nfun typographyFromDefaults(\n    displayLarge: TextStyle?,\n    displayMedium: TextStyle?,\n    displaySmall: TextStyle?,\n    headlineLarge: TextStyle?,\n    headlineMedium: TextStyle?,\n    headlineSmall: TextStyle?,\n    titleLarge: TextStyle?,\n    titleMedium: TextStyle?,\n    titleSmall: TextStyle?,\n    bodyLarge: TextStyle?,\n    bodyMedium: TextStyle?,\n    bodySmall: TextStyle?,\n    labelLarge: TextStyle?,\n    labelMedium: TextStyle?,\n    labelSmall: TextStyle?,\n): Typography {\n    val defaults = Typography()\n    return Typography(\n        displayLarge = defaults.displayLarge.merge(displayLarge),\n        displayMedium = defaults.displayMedium.merge(displayMedium),\n        displaySmall = defaults.displaySmall.merge(displaySmall),\n        headlineLarge = defaults.headlineLarge.merge(headlineLarge),\n        headlineMedium = defaults.headlineMedium.merge(headlineMedium),\n        headlineSmall = defaults.headlineSmall.merge(headlineSmall),\n        titleLarge = defaults.titleLarge.merge(titleLarge),\n        titleMedium = defaults.titleMedium.merge(titleMedium),\n        titleSmall = defaults.titleSmall.merge(titleSmall),\n        bodyLarge = defaults.bodyLarge.merge(bodyLarge),\n        bodyMedium = defaults.bodyMedium.merge(bodyMedium),\n        bodySmall = defaults.bodySmall.merge(bodySmall),\n        labelLarge = defaults.labelLarge.merge(labelLarge),\n        labelMedium = defaults.labelMedium.merge(labelMedium),\n        labelSmall = defaults.labelSmall.merge(labelSmall)\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/molecule/Buttons.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.molecule\n\nimport androidx.compose.foundation.background\nimport androidx.compose.material3.Button\nimport androidx.compose.material3.ButtonDefaults\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\n\n@Composable\nfun PositiveButton(\n    text: String,\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit\n) {\n    Button(\n        modifier = modifier.background(\n            color = MaterialTheme.colorScheme.primaryContainer,\n            shape = MaterialTheme.shapes.small\n        ),\n        onClick = {\n            onClick()\n        },\n        colors = ButtonDefaults.buttonColors(\n            containerColor = MaterialTheme.colorScheme.primaryContainer,\n            contentColor = MaterialTheme.colorScheme.onPrimaryContainer\n        )\n    ) {\n        LargeLabel(text = text)\n    }\n}\n\n@Composable\nfun NegativeButton(\n    text: String,\n    modifier: Modifier = Modifier,\n    onClick: () -> Unit\n) {\n    Button(\n        modifier = modifier.background(\n            color = MaterialTheme.colorScheme.secondaryContainer,\n            shape = MaterialTheme.shapes.small\n        ),\n        onClick = {\n            onClick()\n        },\n        colors = ButtonDefaults.buttonColors(\n            containerColor = MaterialTheme.colorScheme.secondaryContainer,\n            contentColor = MaterialTheme.colorScheme.onSecondaryContainer\n        )\n    ) {\n        LargeLabel(text = text)\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/molecule/Image.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.molecule\n\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.defaultMinSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.painter.Painter\nimport androidx.compose.ui.unit.dp\nimport coil.compose.AsyncImage\n\n// todo check with 48\nprivate val ICON_SIZE = 40.dp\n\n@Composable\nfun RemoteImage(\n    url: String,\n    contentDescription: String,\n    modifier: Modifier = Modifier\n) {\n    AsyncImage(\n        model = url,\n        contentDescription = contentDescription,\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun ActionIcon(\n    painter: Painter,\n    contentDescription: String,\n    modifier: Modifier = Modifier,\n    onClicked: () -> Unit\n) {\n    Image(\n        painter = painter,\n        contentDescription = contentDescription,\n        modifier = modifier\n            .defaultMinSize(ICON_SIZE)\n            .clickable { onClicked() }\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/molecule/Text.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.molecule\n\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Text\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.graphics.Color\nimport androidx.compose.ui.text.font.FontWeight\nimport androidx.compose.ui.text.style.TextAlign\n\n@Composable\nfun LargeDisplay(text: String, modifier: Modifier = Modifier) {\n    Text(\n        text = text,\n        modifier = modifier,\n        style = MaterialTheme.typography.displayLarge\n    )\n}\n\n@Composable\nfun MediumDisplay(text: String, modifier: Modifier = Modifier) {\n    Text(\n        text = text,\n        modifier = modifier,\n        style = MaterialTheme.typography.displayMedium\n    )\n}\n\n@Composable\nfun SmallDisplay(text: String, modifier: Modifier = Modifier) {\n    Text(\n        text = text,\n        modifier = modifier,\n        style = MaterialTheme.typography.displaySmall\n    )\n}\n\n@Composable\nfun LargeLabel(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    fontWeight: FontWeight = FontWeight.Medium\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.labelLarge.copy(fontWeight = fontWeight),\n        modifier = modifier,\n        color = color\n    )\n}\n\n@Composable\nfun MediumLabel(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    textAlign: TextAlign = TextAlign.Start\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.labelMedium,\n        modifier = modifier,\n        color = color,\n        textAlign = textAlign\n    )\n}\n\n@Composable\nfun MediumHeadline(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    textAlign: TextAlign? = null,\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.headlineMedium.copy(fontWeight = FontWeight.Bold),\n        color = color,\n        modifier = modifier,\n        textAlign = textAlign,\n    )\n}\n\n@Composable\nfun SmallHeadline(\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.headlineSmall.copy(fontWeight = FontWeight.Bold),\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun LargeBody(\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodyLarge,\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun SmallBody(\n    text: String,\n    modifier: Modifier = Modifier,\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodySmall,\n        modifier = modifier,\n    )\n}\n\n@Composable\nfun MediumBody(\n    text: String,\n    modifier: Modifier = Modifier,\n    color: Color = Color.Unspecified,\n    textAlign: TextAlign? = null,\n) {\n    Text(\n        text = text,\n        style = MaterialTheme.typography.bodyMedium,\n        color = color,\n        modifier = modifier,\n        textAlign = textAlign\n    )\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/organism/BottomSheets.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.organism\n\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.Checkbox\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.ModalBottomSheet\nimport androidx.compose.material3.RadioButton\nimport androidx.compose.material3.SheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.runtime.mutableStateListOf\nimport androidx.compose.runtime.mutableStateOf\nimport androidx.compose.runtime.remember\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.runtime.snapshots.SnapshotStateList\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumBody\nimport com.github.odaridavid.weatherapp.designsystem.molecule.PositiveButton\nimport com.github.odaridavid.weatherapp.designsystem.molecule.SmallHeadline\nimport kotlinx.coroutines.CoroutineScope\nimport kotlinx.coroutines.launch\n\n// todo reuse some logic for single select and multi select\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun MultiSelectBottomSheet(\n    title: String,\n    items: List<BottomSheetItem>,\n    selectedItems: List<BottomSheetItem>,\n    sheetState: SheetState,\n    onSaveState: (List<BottomSheetItem>) -> Unit,\n    onDismiss: (() -> Unit)? = null,\n) {\n    val scope = rememberCoroutineScope()\n    ModalBottomSheet(\n        onDismissRequest = {\n            scope.launch {\n                sheetState.hide()\n            }\n            if (onDismiss != null) {\n                onDismiss()\n            }\n        },\n        sheetState = sheetState,\n//        windowInsets = WindowInsets.navigationBars,\n    ) {\n        Column(\n            modifier = Modifier\n                .fillMaxWidth()\n        ) {\n            SmallHeadline(\n                text = title,\n                modifier = Modifier.padding(\n                    vertical = WeatherAppTheme.dimens.small,\n                    horizontal = WeatherAppTheme.dimens.medium\n                )\n            )\n            val selectedItemsState =\n                remember { mutableStateListOf(*selectedItems.toTypedArray()) }\n\n            LazyColumn() {\n                items(items) { item ->\n                    Row {\n                        Checkbox(\n                            checked = selectedItemsState.count { it.id == item.id } > 0,\n                            onCheckedChange = { isSelected ->\n                                if (isSelected) {\n                                    if (item.id == NONE_ID) selectedItemsState.clear()\n                                    // TODO use removeIf once min sdk is 24\n                                    else selectedItemsState.removeAll { it.id == NONE_ID }\n\n                                    selectedItemsState.add(item.copy(isSelected = true))\n                                } else {\n                                    selectedItemsState.removeAll { it.id == item.id }\n                                }\n                            }\n                        )\n                        MediumBody(\n                            text = item.name,\n                            modifier = Modifier.padding(\n                                horizontal = WeatherAppTheme.dimens.small,\n                                vertical = WeatherAppTheme.dimens.medium\n                            )\n                        )\n                    }\n                }\n            }\n            MultiSelectSaveButtonSection(\n                sheetState = sheetState,\n                coroutineScope = scope,\n                onSaveState = onSaveState,\n                selectedItemsState = selectedItemsState,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SingleSelectBottomSheet(\n    title: String,\n    items: List<BottomSheetItem>,\n    selectedItem: BottomSheetItem,\n    sheetState: SheetState,\n    onSaveState: (BottomSheetItem) -> Unit,\n    onDismiss: (() -> Unit)? = null,\n) {\n    val scope = rememberCoroutineScope()\n    ModalBottomSheet(\n        onDismissRequest = {\n            scope.launch {\n                sheetState.hide()\n            }\n            if (onDismiss != null) {\n                onDismiss()\n            }\n        },\n        sheetState = sheetState,\n//        windowInsets = WindowInsets.navigationBars,\n    ) {\n        Column(\n            modifier = Modifier.fillMaxWidth()\n        ) {\n            SmallHeadline(\n                text = title,\n                modifier = Modifier.padding(\n                    vertical = WeatherAppTheme.dimens.small,\n                    horizontal = WeatherAppTheme.dimens.medium\n                )\n            )\n\n            val selectedItemsState = remember { mutableStateOf(selectedItem) }\n            LazyColumn(Modifier.weight(1f, false)) {\n                items(items) { item ->\n                    Row {\n                        RadioButton(\n                            selected = selectedItemsState.value.id == item.id,\n                            onClick = {\n                                selectedItemsState.value = item.copy(isSelected = true)\n                            }\n                        )\n                        MediumBody(\n                            text = item.name,\n                            modifier = Modifier.padding(\n                                horizontal = WeatherAppTheme.dimens.small,\n                                vertical = WeatherAppTheme.dimens.medium\n                            )\n                        )\n                    }\n                }\n            }\n            SingleSelectSaveButtonSection(\n                sheetState = sheetState,\n                coroutineScope = scope,\n                onSaveState = onSaveState,\n                selectedItemState = selectedItemsState,\n            )\n        }\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SingleSelectSaveButtonSection(\n    sheetState: SheetState,\n    coroutineScope: CoroutineScope,\n    onSaveState: (BottomSheetItem) -> Unit,\n    selectedItemState: MutableState<BottomSheetItem>,\n) {\n    SaveButton(\n        sheetState = sheetState,\n        coroutineScope = coroutineScope,\n    ) {\n        onSaveState(selectedItemState.value)\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun MultiSelectSaveButtonSection(\n    sheetState: SheetState,\n    coroutineScope: CoroutineScope,\n    onSaveState: (List<BottomSheetItem>) -> Unit,\n    selectedItemsState: SnapshotStateList<BottomSheetItem>,\n) {\n    SaveButton(\n        sheetState = sheetState,\n        coroutineScope = coroutineScope,\n    ) {\n        onSaveState(selectedItemsState)\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SaveButton(\n    sheetState: SheetState,\n    coroutineScope: CoroutineScope,\n    onSaveState: () -> Unit,\n) {\n    Box(\n        contentAlignment = Alignment.BottomEnd,\n        modifier = Modifier\n            .fillMaxWidth()\n    ) {\n        PositiveButton(\n            text = stringResource(id = R.string.settings_save),\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(WeatherAppTheme.dimens.medium)\n\n        ) {\n            coroutineScope.launch {\n                sheetState.hide()\n            }\n            onSaveState()\n        }\n    }\n}\n\ndata class BottomSheetItem(\n    val name: String,\n    val id: Int,\n    val isSelected: Boolean = false,\n)\n\nconst val NONE_ID = -1\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/organism/Dialogs.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.organism\n\nimport android.Manifest\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.compose.foundation.background\nimport androidx.compose.foundation.layout.Box\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.BasicAlertDialog\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.MutableState\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.window.Dialog\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.LargeLabel\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumBody\nimport com.github.odaridavid.weatherapp.designsystem.molecule.NegativeButton\nimport com.github.odaridavid.weatherapp.designsystem.molecule.PositiveButton\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun PermissionRationaleDialog(\n    isDialogShown: MutableState<Boolean>,\n    activityPermissionResult: ActivityResultLauncher<String>,\n    showWeatherUI: MutableState<Boolean>\n) {\n    BasicAlertDialog(\n        onDismissRequest = { isDialogShown.value = false },\n        modifier = Modifier\n            .background(MaterialTheme.colorScheme.surface)\n    ) {\n        Column {\n            LargeLabel(\n                text = stringResource(R.string.location_rationale_title),\n                modifier = Modifier.padding(\n                    horizontal = WeatherAppTheme.dimens.medium,\n                    vertical = WeatherAppTheme.dimens.small\n                )\n            )\n\n            MediumBody(\n                text = stringResource(R.string.location_rationale_description),\n                modifier = Modifier.padding(WeatherAppTheme.dimens.medium)\n            )\n\n            Row(modifier = Modifier.padding(WeatherAppTheme.dimens.medium)) {\n                PositiveButton(\n                    text = stringResource(R.string.location_rationale_button_grant),\n                    onClick = {\n                        isDialogShown.value = false\n                        activityPermissionResult.launch(Manifest.permission.ACCESS_COARSE_LOCATION)\n                    }\n                )\n                Spacer(modifier = Modifier.weight(1f))\n                NegativeButton(\n                    text = stringResource(R.string.location_rationale_button_deny),\n                    onClick = {\n                        isDialogShown.value = false\n                        showWeatherUI.value = false\n                    }\n                )\n            }\n        }\n    }\n}\n\n@Composable\nfun UpdateDialog(\n    onDismiss: () -> Unit,\n    onConfirm: () -> Unit,\n) {\n    Dialog(onDismissRequest = { onDismiss() }) {\n        Box {\n            MediumBody(text = stringResource(R.string.update_available))\n            PositiveButton(\n                text = stringResource(R.string.install_update),\n                onClick = { onConfirm() }\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/organism/NavBars.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.organism\n\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.ActionIcon\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumHeadline\n\n// TODO Create a navbar factory when more screens are added\n@Composable\nfun HomeTopBar(cityName: String, onSettingClicked: () -> Unit) {\n    Row(\n        modifier = Modifier\n            .padding(WeatherAppTheme.dimens.medium)\n            .fillMaxWidth()\n    ) {\n        MediumHeadline(text = cityName)\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n        ActionIcon(\n            painter = painterResource(id = R.drawable.ic_settings),\n            contentDescription = stringResource(R.string.home_content_description_setting_icon),\n            modifier = Modifier.padding(WeatherAppTheme.dimens.small),\n            onClicked = { onSettingClicked() }\n        )\n    }\n}\n\n@Composable\nfun TopNavigationBar(onBackButtonClicked: () -> Unit, title: String) {\n    Row(modifier = Modifier.padding(WeatherAppTheme.dimens.medium)) {\n        ActionIcon(\n            painter = painterResource(id = R.drawable.ic_arrow_back),\n            contentDescription = stringResource(R.string.back_button_content_description_icon),\n            modifier = Modifier.padding(WeatherAppTheme.dimens.small),\n            onClicked = { onBackButtonClicked() }\n        )\n\n        MediumHeadline(\n            text = title,\n            modifier = Modifier.align(Alignment.CenterVertically),\n            textAlign = TextAlign.Center,\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/organism/Row.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.organism\n\nimport androidx.annotation.DrawableRes\nimport androidx.compose.foundation.Image\nimport androidx.compose.foundation.clickable\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.painterResource\nimport androidx.compose.ui.text.font.FontWeight\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.LargeBody\nimport com.github.odaridavid.weatherapp.designsystem.molecule.LargeLabel\n\n@Composable\nfun SettingOptionRow(\n    optionLabel: String,\n    optionValue: String? = null,\n    @DrawableRes optionIcon: Int,\n    optionIconContentDescription: String,\n    modifier: Modifier = Modifier,\n    onOptionClicked: () -> Unit\n) {\n    Row(\n        modifier = modifier\n            .fillMaxWidth()\n            .clickable { onOptionClicked() }\n            .padding(WeatherAppTheme.dimens.medium)\n    ) {\n        Image(\n            painter = painterResource(id = optionIcon),\n            contentDescription = optionIconContentDescription,\n            modifier = Modifier.padding(WeatherAppTheme.dimens.small)\n        )\n\n        LargeLabel(\n            text = optionLabel,\n            modifier = Modifier.padding(WeatherAppTheme.dimens.small),\n            fontWeight = FontWeight.Bold\n        )\n\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n\n        optionValue?.let {\n            LargeBody(\n                text = it,\n                modifier = Modifier.padding(WeatherAppTheme.dimens.extraSmall)\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/organism/TextWidgets.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.organism\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.LargeDisplay\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumBody\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumLabel\nimport com.github.odaridavid.weatherapp.designsystem.molecule.PositiveButton\n\n// TODO These components still feel meh. They could be better or maybe they are too specfic\n@Composable\nfun Temperature(text: String) {\n    MediumBody(\n        text = text,\n        modifier = Modifier.padding(WeatherAppTheme.dimens.extraSmall)\n    )\n}\n\n@Composable\nfun ForecastedTime(text: String, modifier: Modifier = Modifier) {\n    MediumBody(\n        text = text,\n        modifier = modifier.padding(WeatherAppTheme.dimens.extraSmall)\n    )\n}\n\n@Composable\nfun VersionInfoText(versionInfo: String, modifier: Modifier) {\n    MediumLabel(\n        text = versionInfo,\n        modifier = modifier\n            .fillMaxWidth()\n            .padding(WeatherAppTheme.dimens.medium),\n        textAlign = TextAlign.Center\n    )\n}\n\n@Composable\nfun TemperatureHeadline(temperature: String, modifier: Modifier = Modifier) {\n    LargeDisplay(\n        text = temperature,\n        modifier = modifier\n            .padding(horizontal = WeatherAppTheme.dimens.medium)\n            .padding(vertical = WeatherAppTheme.dimens.small)\n    )\n}\n\n@Composable\nfun ActionErrorMessage(\n    @StringRes errorMessageId: Int,\n    modifier: Modifier,\n    onTryAgainClicked: () -> Unit,\n) {\n    Column(modifier = Modifier.fillMaxWidth()) {\n        MediumBody(\n            text = stringResource(id = errorMessageId),\n            textAlign = TextAlign.Center,\n            modifier = modifier.align(Alignment.CenterHorizontally),\n        )\n        PositiveButton(\n            text = stringResource(id = R.string.home_error_try_again),\n            onClick = { onTryAgainClicked() },\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.medium)\n                .align(Alignment.CenterHorizontally)\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/templates/ErrorScreen.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.templates\n\nimport androidx.compose.foundation.layout.ColumnScope\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.organism.ActionErrorMessage\n\n@Composable\nfun ColumnScope.ErrorScreen(errorMsgId: Int, onTryAgainClicked: () -> Unit) {\n    Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.HALF))\n    ActionErrorMessage(\n        errorMessageId = errorMsgId,\n        modifier = Modifier.padding(WeatherAppTheme.dimens.medium)\n    ) {\n        onTryAgainClicked()\n    }\n    Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.HALF))\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/templates/InfoScreens.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.templates\n\nimport androidx.annotation.StringRes\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumBody\n\n@Composable\nfun InfoScreen(@StringRes message: Int) {\n    Column(\n        modifier = Modifier\n            .fillMaxWidth()\n            .padding(WeatherAppTheme.dimens.medium)\n    ) {\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.HALF))\n        MediumBody(\n            text = stringResource(message),\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.medium)\n                .align(Alignment.CenterHorizontally)\n        )\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.HALF))\n    }\n}\n\n@Composable\nfun RequiresPermissionsScreen() {\n    InfoScreen(message = R.string.location_no_permission_screen_description)\n}\n\n@Composable\nfun EnableLocationSettingScreen() {\n    InfoScreen(message = R.string.location_settings_not_enabled)\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/designsystem/templates/ProgressScreens.kt",
    "content": "package com.github.odaridavid.weatherapp.designsystem.templates\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.material3.CircularProgressIndicator\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\n\n@Composable\nfun LoadingScreen() {\n    Column(modifier = Modifier.fillMaxSize()) {\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n        CircularProgressIndicator(\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.medium)\n                .align(Alignment.CenterHorizontally)\n        )\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/di/ClientModule.kt",
    "content": "package com.github.odaridavid.weatherapp.di\n\nimport android.content.Context\nimport com.chuckerteam.chucker.api.ChuckerCollector\nimport com.chuckerteam.chucker.api.ChuckerInterceptor\nimport com.github.odaridavid.weatherapp.BuildConfig\nimport com.github.odaridavid.weatherapp.data.weather.remote.OpenWeatherService\nimport com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory\nimport dagger.Module\nimport dagger.Provides\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport dagger.hilt.components.SingletonComponent\nimport kotlinx.serialization.ExperimentalSerializationApi\nimport kotlinx.serialization.json.Json\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.OkHttpClient\nimport okhttp3.logging.HttpLoggingInterceptor\nimport retrofit2.Retrofit\nimport java.util.concurrent.TimeUnit\nimport javax.inject.Singleton\n\n@Module\n@InstallIn(SingletonComponent::class)\nobject ClientModule {\n\n    @OptIn(ExperimentalSerializationApi::class)\n    @Provides\n    @Singleton\n    fun provideRetrofit(\n        @ApplicationContext context: Context,\n    ): Retrofit {\n        val okHttpClient = provideOkhttpClient(context = context)\n        val json = providesJson()\n        val contentType = \"application/json\".toMediaType()\n        return Retrofit.Builder()\n            .baseUrl(BuildConfig.OPEN_WEATHER_BASE_URL)\n            .client(okHttpClient)\n            .addConverterFactory(json.asConverterFactory(contentType))\n            .build()\n    }\n\n    @Provides\n    @Singleton\n    fun provideOpenWeatherService(retrofit: Retrofit): OpenWeatherService =\n        retrofit.create(OpenWeatherService::class.java)\n\n    private fun providesJson(): Json = Json { ignoreUnknownKeys = true }\n\n    private fun provideOkhttpClient(\n        context: Context\n    ): OkHttpClient {\n        val loggingInterceptor = provideLoggingInterceptor()\n        val chuckerInterceptor = provideChuckerInterceptor(context)\n        return OkHttpClient.Builder()\n            .connectTimeout(60L, TimeUnit.SECONDS)\n            .addInterceptor(loggingInterceptor)\n            .addInterceptor(chuckerInterceptor)\n            .build()\n    }\n\n    private fun provideLoggingInterceptor(): HttpLoggingInterceptor {\n        val level = if (BuildConfig.DEBUG) {\n            HttpLoggingInterceptor.Level.BODY\n        } else HttpLoggingInterceptor.Level.NONE\n        return HttpLoggingInterceptor().also {\n            it.level = level\n        }\n    }\n\n    private fun provideChuckerInterceptor(\n        context: Context\n    ): ChuckerInterceptor =\n        ChuckerInterceptor.Builder(context = context)\n            .collector(ChuckerCollector(context = context))\n            .maxContentLength(length = 250000L)\n            .redactHeaders(headerNames = emptySet())\n            .alwaysReadResponseBody(enable = false)\n            .build()\n\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/di/RepositoryModule.kt",
    "content": "package com.github.odaridavid.weatherapp.di\n\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.api.WeatherRepository\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository\nimport com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.FirebaseLogger\nimport com.github.odaridavid.weatherapp.data.weather.remote.DefaultRemoteWeatherDataSource\nimport com.github.odaridavid.weatherapp.data.weather.remote.RemoteWeatherDataSource\nimport dagger.Binds\nimport dagger.Module\nimport dagger.hilt.InstallIn\nimport dagger.hilt.android.components.ViewModelComponent\n\n@Module\n@InstallIn(ViewModelComponent::class)\ninterface RepositoryModule {\n\n    @Binds\n    fun bindWeatherRepository(weatherRepository: DefaultWeatherRepository): WeatherRepository\n\n    @Binds\n    fun bindSettingsRepository(settingsRepository: DefaultSettingsRepository): SettingsRepository\n\n    @Binds\n    fun bindFirebaseLogger(logger: FirebaseLogger): Logger\n\n    @Binds\n    fun bindRemoteWeatherDataSource(remoteWeatherDataSource: DefaultRemoteWeatherDataSource): RemoteWeatherDataSource\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/AppNavGraph.kt",
    "content": "package com.github.odaridavid.weatherapp.ui\n\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.hilt.navigation.compose.hiltViewModel\nimport androidx.navigation.NavHostController\nimport androidx.navigation.compose.NavHost\nimport androidx.navigation.compose.composable\nimport com.github.odaridavid.weatherapp.ui.about.AboutScreen\nimport com.github.odaridavid.weatherapp.ui.home.HomeScreen\nimport com.github.odaridavid.weatherapp.ui.home.HomeScreenIntent\nimport com.github.odaridavid.weatherapp.ui.home.HomeViewModel\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsScreen\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsScreenIntent\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsScreenViewState\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsViewModel\n\n@Composable\nfun WeatherAppScreensConfig(\n    navController: NavHostController\n) {\n    NavHost(navController = navController, startDestination = Destinations.HOME.route) {\n        composable(Destinations.HOME.route) {\n            val homeViewModel = hiltViewModel<HomeViewModel>()\n            val state = homeViewModel\n                .state\n                .collectAsState()\n                .value\n\n            HomeScreen(\n                state = state,\n                onSettingClicked = { navController.navigate(Destinations.SETTINGS.route) },\n                onTryAgainClicked = { homeViewModel.processIntent(HomeScreenIntent.LoadWeatherData) },\n                onCityNameReceived = { cityName ->\n                    homeViewModel.processIntent(HomeScreenIntent.DisplayCityName(cityName = cityName))\n                }\n            )\n        }\n        composable(Destinations.SETTINGS.route) {\n            val settingsViewModel = hiltViewModel<SettingsViewModel>()\n            val state = settingsViewModel\n                .state\n                .collectAsState(initial = SettingsScreenViewState())\n                .value\n\n            settingsViewModel.processIntent(SettingsScreenIntent.LoadSettingScreenData)\n\n            SettingsScreen(\n                state = state,\n                onBackButtonClicked = { navController.navigateUp() },\n                onLanguageChanged = { selectedLanguage ->\n                    settingsViewModel.processIntent(\n                        SettingsScreenIntent.ChangeLanguage(\n                            selectedLanguage\n                        )\n                    )\n                },\n                onUnitChanged = { selectedUnit ->\n                    settingsViewModel.processIntent(SettingsScreenIntent.ChangeUnits(selectedUnit))\n                },\n                onTimeFormatChanged = { selectedFormat ->\n                    settingsViewModel.processIntent(\n                        SettingsScreenIntent.ChangeTimeFormat(selectedFormat)\n                    )\n                },\n                onAboutClicked = {\n                    navController.navigate(Destinations.ABOUT.route)\n                },\n                onExcludedDataChanged = { excludeData ->\n                    settingsViewModel.processIntent(\n                        SettingsScreenIntent.ChangeExcludedData(excludeData)\n                    )\n                }\n            )\n        }\n        composable(Destinations.ABOUT.route) {\n            AboutScreen {\n                navController.navigateUp()\n            }\n        }\n    }\n}\n\nenum class Destinations(val route: String) {\n    HOME(\"home\"),\n    SETTINGS(\"settings\"),\n    ABOUT(\"about\")\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/MainActivity.kt",
    "content": "package com.github.odaridavid.weatherapp.ui\n\nimport android.annotation.SuppressLint\nimport android.location.Location\nimport android.os.Bundle\nimport android.util.Log\nimport androidx.activity.ComponentActivity\nimport androidx.activity.compose.setContent\nimport androidx.activity.enableEdgeToEdge\nimport androidx.activity.result.contract.ActivityResultContracts\nimport androidx.activity.viewModels\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.safeDrawingPadding\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.material3.Surface\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.collectAsState\nimport androidx.compose.ui.Modifier\nimport androidx.navigation.compose.rememberNavController\nimport com.github.odaridavid.weatherapp.MainViewIntent\nimport com.github.odaridavid.weatherapp.MainViewModel\nimport com.github.odaridavid.weatherapp.MainViewState\nimport com.github.odaridavid.weatherapp.common.CheckForPermissions\nimport com.github.odaridavid.weatherapp.common.OnPermissionDenied\nimport com.github.odaridavid.weatherapp.common.createLocationRequest\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.organism.UpdateDialog\nimport com.github.odaridavid.weatherapp.designsystem.templates.EnableLocationSettingScreen\nimport com.github.odaridavid.weatherapp.designsystem.templates.LoadingScreen\nimport com.github.odaridavid.weatherapp.designsystem.templates.RequiresPermissionsScreen\nimport com.github.odaridavid.weatherapp.ui.update.UpdateManager\nimport com.google.android.gms.location.FusedLocationProviderClient\nimport com.google.android.gms.location.LocationServices\nimport dagger.hilt.android.AndroidEntryPoint\nimport javax.inject.Inject\n\n@AndroidEntryPoint\nclass MainActivity : ComponentActivity() {\n\n    private val mainViewModel: MainViewModel by viewModels()\n\n    @Inject\n    lateinit var updateManager: UpdateManager\n\n    private val locationRequestLauncher =\n        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->\n            if (result.resultCode == RESULT_OK) {\n                mainViewModel.processIntent(MainViewIntent.CheckLocationSettings(isEnabled = true))\n            } else {\n                mainViewModel.processIntent(MainViewIntent.CheckLocationSettings(isEnabled = false))\n            }\n        }\n    private val permissionRequestLauncher =\n        registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->\n            mainViewModel.processIntent(MainViewIntent.GrantPermission(isGranted = isGranted))\n        }\n\n    private val updateRequestLauncher =\n        registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()) { result ->\n            if (result.resultCode == RESULT_OK) {\n                // TODO Trigger a UI event ,is this even necessary since we already have a listener?\n                Log.d(\"MainActivity\", \"Update successful\")\n            } else {\n                Log.e(\"MainActivity\", \"Update failed\")\n            }\n        }\n\n    private lateinit var fusedLocationProviderClient: FusedLocationProviderClient\n    override fun onCreate(savedInstanceState: Bundle?) {\n        super.onCreate(savedInstanceState)\n\n        enableEdgeToEdge()\n\n        updateManager.checkForUpdates(\n            activityResultLauncher = updateRequestLauncher,\n            onUpdateDownloaded = {\n                mainViewModel.processIntent(MainViewIntent.UpdateApp)\n            },\n            onUpdateFailure = { exception ->\n                mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception))\n            }\n        )\n\n        createLocationRequest(\n            activity = this@MainActivity,\n            locationRequestLauncher = locationRequestLauncher\n        ) {\n            mainViewModel.processIntent(MainViewIntent.CheckLocationSettings(isEnabled = true))\n        }\n\n        fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)\n\n        setContent {\n\n            WeatherAppTheme {\n                Surface(\n                    modifier = Modifier.fillMaxSize().safeDrawingPadding(),\n                    color = MaterialTheme.colorScheme.background\n                ) {\n                    val state = mainViewModel.state.collectAsState().value\n\n                    // TODO test this with internal testing track\n                    mainViewModel.hasAppUpdate.collectAsState().value.let { hasAppUpdate ->\n                        if (hasAppUpdate) {\n                            UpdateDialog(\n                                onDismiss = {\n                                    // TODO dismiss it\n                                },\n                                onConfirm = {\n                                    updateManager.completeUpdate()\n                                }\n                            )\n                        }\n                    }\n\n                    CheckForPermissions(\n                        onPermissionGranted = {\n                            mainViewModel.processIntent(MainViewIntent.GrantPermission(isGranted = true))\n                        },\n                        onPermissionDenied = {\n                            OnPermissionDenied(activityPermissionResult = permissionRequestLauncher)\n                        }\n                    )\n\n                    InitMainScreen(state)\n                }\n            }\n        }\n    }\n\n    @SuppressLint(\"MissingPermission\")\n    @Composable\n    private fun InitMainScreen(state: MainViewState) {\n        when {\n            state.isLocationSettingEnabled && state.isPermissionGranted -> {\n                fusedLocationProviderClient.lastLocation\n                    .addOnSuccessListener { location: Location? ->\n                        location?.run {\n                            mainViewModel.processIntent(\n                                MainViewIntent.ReceiveLocation(\n                                    longitude = location.longitude,\n                                    latitude = location.latitude\n                                )\n                            )\n                        }\n                    }.addOnFailureListener { exception ->\n                        mainViewModel.processIntent(MainViewIntent.LogException(throwable = exception))\n                    }\n                WeatherAppScreensConfig(navController = rememberNavController())\n            }\n\n            state.isLocationSettingEnabled && !state.isPermissionGranted -> {\n                RequiresPermissionsScreen()\n            }\n\n            !state.isLocationSettingEnabled && !state.isPermissionGranted -> {\n                EnableLocationSettingScreen()\n            }\n\n            else -> LoadingScreen()\n        }\n    }\n\n    override fun onDestroy() {\n        super.onDestroy()\n        updateManager.unregisterListeners()\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/about/AboutScreen.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.about\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.organism.TopNavigationBar\nimport com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer\n\n@Composable\nfun AboutScreen(\n    onBackButtonClicked: () -> Unit,\n) {\n    Column {\n        TopNavigationBar(\n            onBackButtonClicked = onBackButtonClicked,\n            title = stringResource(R.string.about_screen_title),\n        )\n\n        LibrariesContainer(\n            Modifier.fillMaxSize()\n        )\n    }\n}"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreen.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.home\n\nimport androidx.compose.foundation.ExperimentalFoundationApi\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Row\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.foundation.layout.fillMaxSize\nimport androidx.compose.foundation.layout.fillMaxWidth\nimport androidx.compose.foundation.layout.padding\nimport androidx.compose.foundation.lazy.LazyColumn\nimport androidx.compose.foundation.lazy.LazyRow\nimport androidx.compose.foundation.lazy.items\nimport androidx.compose.material3.MaterialTheme\nimport androidx.compose.runtime.Composable\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.platform.LocalContext\nimport androidx.compose.ui.res.stringResource\nimport androidx.compose.ui.text.style.TextAlign\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.common.getCityName\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.molecule.LargeLabel\nimport com.github.odaridavid.weatherapp.designsystem.molecule.MediumBody\nimport com.github.odaridavid.weatherapp.designsystem.molecule.RemoteImage\nimport com.github.odaridavid.weatherapp.designsystem.organism.ForecastedTime\nimport com.github.odaridavid.weatherapp.designsystem.organism.HomeTopBar\nimport com.github.odaridavid.weatherapp.designsystem.organism.Temperature\nimport com.github.odaridavid.weatherapp.designsystem.organism.TemperatureHeadline\nimport com.github.odaridavid.weatherapp.designsystem.templates.ErrorScreen\nimport com.github.odaridavid.weatherapp.designsystem.templates.LoadingScreen\nimport com.github.odaridavid.weatherapp.model.CurrentWeather\nimport com.github.odaridavid.weatherapp.model.DailyWeather\nimport com.github.odaridavid.weatherapp.model.HourlyWeather\n\n@Composable\nfun HomeScreen(\n    state: HomeScreenViewState,\n    onSettingClicked: () -> Unit,\n    onTryAgainClicked: () -> Unit,\n    onCityNameReceived: (String) -> Unit\n) {\n    Column(modifier = Modifier.fillMaxSize()) {\n\n        LocalContext.current.getCityName(\n            latitude = state.defaultLocation.latitude,\n            longitude = state.defaultLocation.longitude\n        ) { address ->\n            val cityName = address.locality\n            onCityNameReceived(cityName)\n        }\n\n        HomeTopBar(cityName = state.locationName, onSettingClicked)\n\n        if (state.isLoading) {\n            LoadingScreen()\n        }\n\n        if (state.errorMessageId != null) {\n            ErrorScreen(state.errorMessageId, onTryAgainClicked)\n        } else {\n            state.weather?.current?.let { currentWeather ->\n                CurrentWeatherWidget(currentWeather = currentWeather)\n            } ?: run {\n                EmptySectionWidget(\n                    label = stringResource(id = R.string.home_title_currently),\n                    weatherType = stringResource(id = R.string.home_weather_type_currently)\n                )\n            }\n\n            state.weather?.hourly?.let { hourlyWeather ->\n                HourlyWeatherWidget(hourlyWeatherList = hourlyWeather)\n            } ?: run {\n                EmptySectionWidget(\n                    label = stringResource(id = R.string.home_today_forecast_title),\n                    weatherType = stringResource(id = R.string.home_weather_type_hourly)\n                )\n            }\n\n            state.weather?.daily?.let { dailyWeather ->\n                DailyWeatherWidget(dailyWeatherList = dailyWeather)\n            } ?: run {\n                EmptySectionWidget(\n                    label = stringResource(id = R.string.home_weekly_forecast_title),\n                    weatherType = stringResource(id = R.string.home_weather_type_daily)\n                )\n            }\n        }\n    }\n}\n\n@Composable\nprivate fun EmptySectionWidget(label: String, weatherType: String) {\n    Column {\n        LargeLabel(\n            text = label,\n            modifier = Modifier.padding(\n                horizontal = WeatherAppTheme.dimens.medium,\n                vertical = WeatherAppTheme.dimens.small\n            )\n        )\n        MediumBody(\n            text = stringResource(R.string.home_enable_weather_in_settings, weatherType),\n            modifier = Modifier\n                .fillMaxWidth()\n                .padding(WeatherAppTheme.dimens.medium),\n            textAlign = TextAlign.Center\n        )\n    }\n}\n\n@Composable\nprivate fun CurrentWeatherWidget(currentWeather: CurrentWeather) {\n    Column {\n        LargeLabel(\n            text = stringResource(id = R.string.home_title_currently),\n            modifier = Modifier.padding(\n                horizontal = WeatherAppTheme.dimens.medium,\n                vertical = WeatherAppTheme.dimens.small\n            )\n        )\n\n        TemperatureHeadline(temperature = currentWeather.temperature)\n\n        LargeLabel(\n            text = stringResource(\n                id = R.string.home_feels_like_description,\n                currentWeather.feelsLike\n            ),\n            color = MaterialTheme.colorScheme.secondary,\n            modifier = Modifier.padding(\n                horizontal = WeatherAppTheme.dimens.medium,\n                vertical = WeatherAppTheme.dimens.small\n            )\n        )\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun HourlyWeatherWidget(hourlyWeatherList: List<HourlyWeather>) {\n    LargeLabel(\n        text = stringResource(id = R.string.home_today_forecast_title),\n        modifier = Modifier.padding(\n            horizontal = WeatherAppTheme.dimens.medium,\n            vertical = WeatherAppTheme.dimens.small\n        )\n    )\n\n    LazyRow(modifier = Modifier.padding(WeatherAppTheme.dimens.medium)) {\n        items(hourlyWeatherList) { hourlyWeather ->\n            HourlyWeatherRow(\n                hourlyWeather = hourlyWeather,\n                modifier = Modifier.animateItemPlacement()\n            )\n        }\n    }\n}\n\n@Composable\nprivate fun HourlyWeatherRow(hourlyWeather: HourlyWeather, modifier: Modifier) {\n    Row(modifier = modifier) {\n        RemoteImage(\n            url = hourlyWeather.weather.first().icon,\n            contentDescription = hourlyWeather.weather.first().description,\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.extraSmall)\n                .align(Alignment.CenterVertically),\n        )\n        Column(\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.extraSmall)\n                .align(Alignment.CenterVertically)\n        ) {\n            Temperature(text = hourlyWeather.temperature)\n            ForecastedTime(text = hourlyWeather.forecastedTime)\n        }\n    }\n}\n\n@OptIn(ExperimentalFoundationApi::class)\n@Composable\nprivate fun DailyWeatherWidget(dailyWeatherList: List<DailyWeather>) {\n    LargeLabel(\n        text = stringResource(id = R.string.home_weekly_forecast_title),\n        modifier = Modifier.padding(\n            horizontal = WeatherAppTheme.dimens.medium,\n            vertical = WeatherAppTheme.dimens.small\n        )\n    )\n\n    LazyColumn(modifier = Modifier.padding(WeatherAppTheme.dimens.medium)) {\n        items(dailyWeatherList) { dailyWeather ->\n            DailyWeatherRow(dailyWeather = dailyWeather, modifier = Modifier.animateItemPlacement())\n        }\n    }\n}\n\n@Composable\nprivate fun DailyWeatherRow(dailyWeather: DailyWeather, modifier: Modifier) {\n    Row(\n        modifier = modifier\n            .padding(WeatherAppTheme.dimens.small)\n            .fillMaxWidth()\n    ) {\n        RemoteImage(\n            url = dailyWeather.weather.first().icon,\n            contentDescription = dailyWeather.weather.first().description,\n            modifier = Modifier\n                .padding(WeatherAppTheme.dimens.extraSmall)\n                .align(Alignment.CenterVertically),\n        )\n        ForecastedTime(\n            text = dailyWeather.forecastedTime,\n            modifier = Modifier\n                .align(Alignment.CenterVertically)\n        )\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n        Column(modifier = Modifier.align(Alignment.CenterVertically)) {\n            Temperature(\n                text = stringResource(\n                    id = R.string.home_max_temp,\n                    dailyWeather.temperature.max\n                )\n            )\n            Temperature(\n                text = stringResource(\n                    id = R.string.home_min_temp,\n                    dailyWeather.temperature.min\n                )\n            )\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeScreenIntent.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.home\n\nsealed class HomeScreenIntent {\n    data object LoadWeatherData : HomeScreenIntent()\n\n    data class DisplayCityName(val cityName: String) : HomeScreenIntent()\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/home/HomeViewModel.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.home\n\nimport androidx.annotation.StringRes\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.api.WeatherRepository\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.Units\nimport com.github.odaridavid.weatherapp.model.Weather\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass HomeViewModel @Inject constructor(\n    private val weatherRepository: WeatherRepository,\n    private val settingsRepository: SettingsRepository\n) : ViewModel() {\n\n    private val _state = MutableStateFlow(HomeScreenViewState(isLoading = true))\n    val state: StateFlow<HomeScreenViewState> = _state.asStateFlow()\n\n    init {\n        viewModelScope.launch {\n            combine(\n                settingsRepository.getLanguage(),\n                settingsRepository.getUnits(),\n                settingsRepository.getDefaultLocation()\n            ) { language, units, defaultLocation ->\n                Triple(language, units, defaultLocation)\n            }.collect { (language, units, defaultLocation) ->\n                setState {\n                    copy(\n                        language = language,\n                        units = units,\n                        defaultLocation = defaultLocation\n                    )\n                }.also { processIntent(HomeScreenIntent.LoadWeatherData) }\n            }\n        }\n    }\n\n    fun processIntent(homeScreenIntent: HomeScreenIntent) {\n        when (homeScreenIntent) {\n            is HomeScreenIntent.LoadWeatherData -> {\n                viewModelScope.launch {\n                    val result = weatherRepository.fetchWeatherData(\n                        language = state.value.language.languageValue,\n                        defaultLocation = state.value.defaultLocation,\n                        units = state.value.units.value\n                    )\n                    processResult(result)\n                }\n            }\n\n            is HomeScreenIntent.DisplayCityName -> {\n                setState { copy(locationName = homeScreenIntent.cityName) }\n            }\n        }\n    }\n\n    private fun processResult(result: Result<Weather>) {\n        when (result) {\n            is Result.Success -> {\n                val weatherData = result.data\n                setState {\n                    copy(\n                        weather = weatherData,\n                        isLoading = false,\n                        errorMessageId = null\n                    )\n                }\n            }\n\n            is Result.Error -> {\n                setState {\n                    copy(\n                        isLoading = false,\n                        errorMessageId = result.errorType.toResourceId()\n                    )\n                }\n            }\n        }\n    }\n\n    private fun setState(stateReducer: HomeScreenViewState.() -> HomeScreenViewState) {\n        viewModelScope.launch {\n            _state.emit(stateReducer(state.value))\n        }\n    }\n}\n\ndata class HomeScreenViewState(\n    val units: Units = Units.METRIC,\n    val defaultLocation: DefaultLocation = DefaultLocation(0.0, 0.0),\n    val locationName: String = \"-\",\n    val language: SupportedLanguage = SupportedLanguage.ENGLISH,\n    val weather: Weather? = null,\n    val isLoading: Boolean = false,\n    @StringRes val errorMessageId: Int? = null\n)\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/home/Mappers.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.home\n\nimport androidx.annotation.StringRes\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.model.ErrorType\n\n@StringRes\nfun ErrorType.toResourceId(): Int = when (this) {\n    ErrorType.SERVER -> R.string.error_server\n    ErrorType.GENERIC -> R.string.error_generic\n    ErrorType.IO_CONNECTION -> R.string.error_connection\n    ErrorType.CLIENT -> R.string.error_client\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/settings/SettingsScreen.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.settings\n\nimport androidx.compose.foundation.layout.Column\nimport androidx.compose.foundation.layout.Spacer\nimport androidx.compose.material3.ExperimentalMaterial3Api\nimport androidx.compose.material3.SheetState\nimport androidx.compose.material3.rememberModalBottomSheetState\nimport androidx.compose.runtime.Composable\nimport androidx.compose.runtime.rememberCoroutineScope\nimport androidx.compose.ui.Alignment\nimport androidx.compose.ui.Modifier\nimport androidx.compose.ui.res.stringResource\nimport com.github.odaridavid.weatherapp.R\nimport com.github.odaridavid.weatherapp.designsystem.WeatherAppTheme\nimport com.github.odaridavid.weatherapp.designsystem.organism.MultiSelectBottomSheet\nimport com.github.odaridavid.weatherapp.designsystem.organism.SettingOptionRow\nimport com.github.odaridavid.weatherapp.designsystem.organism.SingleSelectBottomSheet\nimport com.github.odaridavid.weatherapp.designsystem.organism.TopNavigationBar\nimport com.github.odaridavid.weatherapp.designsystem.organism.VersionInfoText\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport kotlinx.coroutines.launch\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nfun SettingsScreen(\n    state: SettingsScreenViewState,\n    onBackButtonClicked: () -> Unit,\n    onLanguageChanged: (SupportedLanguage) -> Unit,\n    onUnitChanged: (Units) -> Unit,\n    onTimeFormatChanged: (TimeFormat) -> Unit,\n    onAboutClicked: () -> Unit,\n    onExcludedDataChanged: (List<ExcludedData>) -> Unit,\n) {\n    Column {\n        TopNavigationBar(\n            onBackButtonClicked = onBackButtonClicked,\n            title = stringResource(R.string.settings_screen_title),\n        )\n\n        val scope = rememberCoroutineScope()\n        val languageSheetState = rememberModalBottomSheetState(\n            skipPartiallyExpanded = true\n        )\n        SettingOptionRow(\n            optionLabel = stringResource(R.string.settings_language_label),\n            optionValue = state.selectedLanguage.languageName,\n            optionIcon = R.drawable.ic_language,\n            optionIconContentDescription = stringResource(R.string.settings_content_description_lang_icon)\n        ) {\n            scope.launch {\n                languageSheetState.show()\n            }\n        }\n\n        val unitsSheetState = rememberModalBottomSheetState()\n        SettingOptionRow(\n            optionLabel = stringResource(R.string.settings_unit_label),\n            optionValue = state.selectedUnit.value,\n            optionIcon = R.drawable.ic_units,\n            optionIconContentDescription = stringResource(R.string.settings_content_description_unit_icon)\n        ) {\n            scope.launch {\n                unitsSheetState.show()\n            }\n        }\n\n        val timeFormatSheetState = rememberModalBottomSheetState()\n        SettingOptionRow(\n            optionLabel = stringResource(R.string.settings_time_format),\n            optionValue = state.selectedTimeFormat.value,\n            optionIcon = R.drawable.ic_time_24,\n            optionIconContentDescription = stringResource(R.string.settings_content_description_time_icon)\n        ) {\n            scope.launch {\n                timeFormatSheetState.show()\n            }\n        }\n\n        val excludeSheetState = rememberModalBottomSheetState(\n            skipPartiallyExpanded = true\n        )\n        SettingOptionRow(\n            optionLabel = stringResource(id = R.string.settings_exclude_label),\n            optionIcon = R.drawable.ic_exclude_24,\n            optionValue = state.selectedExcludedDataDisplayValue,\n            optionIconContentDescription = stringResource(R.string.settings_content_description_exclude_icon),\n        ) {\n            scope.launch {\n                excludeSheetState.show()\n            }\n        }\n\n        SettingOptionRow(\n            optionLabel = stringResource(R.string.settings_about),\n            optionIcon = R.drawable.ic_info_24,\n            optionIconContentDescription = stringResource(R.string.settings_content_description_about_icon)\n        ) {\n            onAboutClicked()\n        }\n\n        SetupBottomSheets(\n            state = state,\n            onLanguageChanged = onLanguageChanged,\n            onUnitChanged = onUnitChanged,\n            onTimeFormatChanged = onTimeFormatChanged,\n            onExcludedDataChanged = onExcludedDataChanged,\n            languageSheetState = languageSheetState,\n            unitsSheetState = unitsSheetState,\n            timeFormatSheetState = timeFormatSheetState,\n            excludeSheetState = excludeSheetState,\n        )\n\n        Spacer(modifier = Modifier.weight(WeatherAppTheme.weight.FULL))\n\n        VersionInfoText(\n            versionInfo = state.versionInfo,\n            modifier = Modifier.align(Alignment.CenterHorizontally),\n        )\n    }\n}\n\n@OptIn(ExperimentalMaterial3Api::class)\n@Composable\nprivate fun SetupBottomSheets(\n    state: SettingsScreenViewState,\n    onLanguageChanged: (SupportedLanguage) -> Unit,\n    onUnitChanged: (Units) -> Unit,\n    onTimeFormatChanged: (TimeFormat) -> Unit,\n    onExcludedDataChanged: (List<ExcludedData>) -> Unit,\n    languageSheetState: SheetState,\n    unitsSheetState: SheetState,\n    timeFormatSheetState: SheetState,\n    excludeSheetState: SheetState\n) {\n\n    if (languageSheetState.isVisible) {\n        SingleSelectBottomSheet(\n            title = stringResource(id = R.string.settings_language_label),\n            sheetState = languageSheetState,\n            selectedItem = state.selectedLanguage.toBottomSheetModel(isSelected = true),\n            items = state.availableLanguages.map { it.toBottomSheetModel(isSelected = false) },\n            onSaveState = { bottomSheet ->\n                onLanguageChanged(bottomSheet.toSupportedLanguage())\n            }\n        )\n    }\n\n    if (unitsSheetState.isVisible) {\n        SingleSelectBottomSheet(\n            title = stringResource(id = R.string.settings_unit_label),\n            sheetState = unitsSheetState,\n            selectedItem = state.selectedUnit.toBottomSheetModel(isSelected = true),\n            items = state.availableUnits.map { it.toBottomSheetModel(isSelected = false) },\n            onSaveState = { bottomSheet ->\n                onUnitChanged(bottomSheet.toUnits())\n            }\n        )\n    }\n    if (timeFormatSheetState.isVisible) {\n        SingleSelectBottomSheet(\n            title = stringResource(id = R.string.settings_time_format),\n            sheetState = timeFormatSheetState,\n            selectedItem = state.selectedTimeFormat.toBottomSheetModel(isSelected = true),\n            items = state.availableFormats.map { it.toBottomSheetModel(isSelected = false) },\n            onSaveState = { bottomSheet ->\n                onTimeFormatChanged(bottomSheet.toTimeFormat())\n            }\n        )\n    }\n    if (excludeSheetState.isVisible) {\n        MultiSelectBottomSheet(\n            title = stringResource(id = R.string.settings_exclude_label),\n            sheetState = excludeSheetState,\n            selectedItems = state.selectedExcludedData.map { it.toBottomSheetModel(isSelected = true) },\n            items = state.excludedData.map { it.toBottomSheetModel(isSelected = false) },\n            onSaveState = { bottomSheet ->\n                onExcludedDataChanged(bottomSheet.map { it.toExcludedData() })\n            }\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/settings/SettingsScreenIntent.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.settings\n\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\n\nsealed class SettingsScreenIntent {\n\n    data object LoadSettingScreenData : SettingsScreenIntent()\n\n    data class ChangeLanguage(val selectedLanguage: SupportedLanguage) : SettingsScreenIntent()\n\n    data class ChangeUnits(val selectedUnits: Units) : SettingsScreenIntent()\n\n    data class ChangeTimeFormat(val selectedTimeFormat: TimeFormat) : SettingsScreenIntent()\n\n    data class ChangeExcludedData(val selectedExcludedData: List<ExcludedData>) :\n        SettingsScreenIntent()\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/settings/SettingsViewModel.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.settings\n\nimport androidx.lifecycle.ViewModel\nimport androidx.lifecycle.viewModelScope\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport dagger.hilt.android.lifecycle.HiltViewModel\nimport kotlinx.coroutines.flow.MutableStateFlow\nimport kotlinx.coroutines.flow.StateFlow\nimport kotlinx.coroutines.flow.asStateFlow\nimport kotlinx.coroutines.flow.combine\nimport kotlinx.coroutines.launch\nimport javax.inject.Inject\n\n@HiltViewModel\nclass SettingsViewModel @Inject constructor(\n    private val settingsRepository: SettingsRepository,\n    private val logger: Logger,\n) : ViewModel() {\n\n    private val _state = MutableStateFlow(SettingsScreenViewState())\n    val state: StateFlow<SettingsScreenViewState> = _state.asStateFlow()\n\n    fun processIntent(settingsScreenIntent: SettingsScreenIntent) {\n        when (settingsScreenIntent) {\n            SettingsScreenIntent.LoadSettingScreenData -> {\n                viewModelScope.launch {\n                    combine(\n                        settingsRepository.getLanguage(),\n                        settingsRepository.getUnits(),\n                        settingsRepository.getFormat(),\n                        settingsRepository.getExcludedData()\n                    ) { language, units, format, excludedData ->\n                        SettingsScreenViewState(\n                            selectedLanguage = language,\n                            selectedUnit = units,\n                            selectedTimeFormat = format,\n                            selectedExcludedData = mapStringToExcludedData(excludedData),\n                            selectedExcludedDataDisplayValue = excludedData,\n                            versionInfo = settingsRepository.getAppVersion(),\n                            availableLanguages = SupportedLanguage.entries,\n                            availableUnits = Units.entries,\n                            availableFormats = TimeFormat.entries,\n                            excludedData = ExcludedData.entries,\n                        )\n                    }.collect { state ->\n                        setState { state }\n                    }\n                }\n            }\n\n            is SettingsScreenIntent.ChangeLanguage -> {\n                viewModelScope.launch {\n                    settingsRepository.setLanguage(settingsScreenIntent.selectedLanguage)\n                    setState { copy(selectedLanguage = settingsScreenIntent.selectedLanguage) }\n                }\n            }\n\n            is SettingsScreenIntent.ChangeUnits -> {\n                viewModelScope.launch {\n                    settingsRepository.setUnits(settingsScreenIntent.selectedUnits)\n                    setState { copy(selectedUnit = settingsScreenIntent.selectedUnits) }\n                }\n            }\n\n            is SettingsScreenIntent.ChangeTimeFormat -> {\n                viewModelScope.launch {\n                    val format = settingsScreenIntent.selectedTimeFormat\n                    settingsRepository.setFormat(format)\n                    setState { copy(selectedTimeFormat = format) }\n                }\n            }\n\n            is SettingsScreenIntent.ChangeExcludedData -> {\n                viewModelScope.launch {\n                    settingsRepository.setExcludedData(settingsScreenIntent.selectedExcludedData)\n                    setState {\n                        copy(\n                            selectedExcludedData = settingsScreenIntent.selectedExcludedData,\n                            selectedExcludedDataDisplayValue = mapExcludedDataToDisplayValue(\n                                settingsScreenIntent.selectedExcludedData\n                            ),\n                        )\n                    }\n                }\n            }\n        }\n    }\n\n    private fun setState(stateReducer: SettingsScreenViewState.() -> SettingsScreenViewState) {\n        viewModelScope.launch {\n            _state.emit(stateReducer(state.value))\n        }\n    }\n\n    private fun mapExcludedDataToDisplayValue(excludedData: List<ExcludedData>): String =\n        excludedData.joinToString(separator = \",\") { it.value.trim() }\n\n    private fun mapStringToExcludedData(excludedData: String): List<ExcludedData> {\n        return excludedData.split(\",\").map {\n            when (it.trim()) {\n                ExcludedData.CURRENT.value -> ExcludedData.CURRENT\n                ExcludedData.HOURLY.value -> ExcludedData.HOURLY\n                ExcludedData.DAILY.value -> ExcludedData.DAILY\n                ExcludedData.MINUTELY.value -> ExcludedData.MINUTELY\n                ExcludedData.ALERTS.value -> ExcludedData.ALERTS\n                ExcludedData.NONE.value -> ExcludedData.NONE\n                else -> {\n                    logger.logException(IllegalArgumentException(\"Invalid excluded data\"))\n                    ExcludedData.NONE\n                }\n            }\n        }\n    }\n}\n\ndata class SettingsScreenViewState(\n    val selectedUnit: Units = Units.METRIC,\n    val selectedLanguage: SupportedLanguage = SupportedLanguage.ENGLISH,\n    val selectedTimeFormat: TimeFormat = TimeFormat.TWENTY_FOUR_HOUR,\n    val selectedExcludedData: List<ExcludedData> = emptyList(),\n    val selectedExcludedDataDisplayValue: String = \"\",\n    val availableLanguages: List<SupportedLanguage> = emptyList(),\n    val availableUnits: List<Units> = emptyList(),\n    val availableFormats: List<TimeFormat> = emptyList(),\n    val excludedData: List<ExcludedData> = emptyList(),\n    val versionInfo: String = \"\",\n    val error: Throwable? = null\n)\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/settings/UIMapper.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.settings\n\nimport com.github.odaridavid.weatherapp.designsystem.organism.BottomSheetItem\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\n\n// TODO Fix repetition of mapping\nfun ExcludedData.toBottomSheetModel(isSelected: Boolean = false): BottomSheetItem {\n    return BottomSheetItem(\n        id = this.id,\n        name = this.name,\n        isSelected = isSelected\n    )\n}\n\nfun BottomSheetItem.toExcludedData(): ExcludedData {\n    return ExcludedData.entries.first { it.id == this.id }\n}\n\nfun TimeFormat.toBottomSheetModel(isSelected: Boolean = false): BottomSheetItem {\n    return BottomSheetItem(\n        id = this.ordinal,\n        name = this.value,\n        isSelected = isSelected\n    )\n}\n\nfun BottomSheetItem.toTimeFormat(): TimeFormat {\n    return TimeFormat.entries.first { it.ordinal == this.id }\n}\n\nfun Units.toBottomSheetModel(isSelected: Boolean = false): BottomSheetItem {\n    return BottomSheetItem(\n        id = this.ordinal,\n        name = this.value,\n        isSelected = isSelected\n    )\n}\n\nfun BottomSheetItem.toUnits(): Units {\n    return Units.entries.first { it.ordinal == this.id }\n}\n\nfun SupportedLanguage.toBottomSheetModel(isSelected: Boolean = false): BottomSheetItem {\n    return BottomSheetItem(\n        id = this.ordinal,\n        name = this.languageName,\n        isSelected = isSelected\n    )\n}\n\nfun BottomSheetItem.toSupportedLanguage(): SupportedLanguage {\n    return SupportedLanguage.entries.first { it.ordinal == this.id }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateAppException.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.update\n\ndata class UpdateAppException(val throwable: Throwable) : Exception()\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateManager.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.update\n\nimport android.content.Context\nimport androidx.activity.result.ActivityResultLauncher\nimport androidx.activity.result.IntentSenderRequest\nimport com.google.android.play.core.appupdate.AppUpdateInfo\nimport com.google.android.play.core.appupdate.AppUpdateManager\nimport com.google.android.play.core.appupdate.AppUpdateManagerFactory\nimport com.google.android.play.core.appupdate.AppUpdateOptions\nimport com.google.android.play.core.install.model.AppUpdateType\nimport com.google.android.play.core.install.model.UpdateAvailability\nimport dagger.hilt.android.qualifiers.ApplicationContext\nimport javax.inject.Inject\n\nclass UpdateManager @Inject constructor(\n    @ApplicationContext private val context: Context,\n    private val updateStateFactory: UpdateStateFactory,\n) {\n\n    private val appUpdateManager: AppUpdateManager by lazy {\n        AppUpdateManagerFactory.create(context)\n    }\n\n    fun checkForUpdates(\n        activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>,\n        onUpdateDownloaded: () -> Unit,\n        onUpdateFailure: (Throwable) -> Unit,\n    ) {\n        val appUpdateInfoTask = appUpdateManager.appUpdateInfo\n\n        appUpdateManager.registerListener(\n            updateStateFactory.getUpdateStateListener(\n                onDownloaded = {\n                    onUpdateDownloaded()\n                }\n            )\n        )\n\n        appUpdateInfoTask.addOnSuccessListener { appUpdateInfo ->\n            if (appUpdateInfo.updateAvailability() == UpdateAvailability.UPDATE_AVAILABLE &&\n                appUpdateInfo.isUpdateTypeAllowed(AppUpdateType.FLEXIBLE)\n            ) {\n                update(\n                    appUpdateManager = appUpdateManager,\n                    appUpdateInfo = appUpdateInfo,\n                    activityResultLauncher = activityResultLauncher,\n                )\n            }\n        }.addOnFailureListener { exception ->\n            onUpdateFailure(UpdateAppException(exception))\n        }\n    }\n\n    fun unregisterListeners() {\n        appUpdateManager.unregisterListener(updateStateFactory.getUpdateStateListener())\n    }\n\n    fun completeUpdate() {\n        appUpdateManager.completeUpdate()\n    }\n\n    private fun update(\n        appUpdateManager: AppUpdateManager,\n        appUpdateInfo: AppUpdateInfo,\n        activityResultLauncher: ActivityResultLauncher<IntentSenderRequest>\n    ) {\n        appUpdateManager.startUpdateFlowForResult(\n            appUpdateInfo,\n            activityResultLauncher,\n            AppUpdateOptions.newBuilder(AppUpdateType.FLEXIBLE).build()\n        )\n    }\n}\n"
  },
  {
    "path": "app/src/main/java/com/github/odaridavid/weatherapp/ui/update/UpdateStateFactory.kt",
    "content": "package com.github.odaridavid.weatherapp.ui.update\n\nimport com.google.android.play.core.install.InstallStateUpdatedListener\nimport com.google.android.play.core.install.model.InstallStatus\nimport javax.inject.Inject\n\nclass UpdateStateFactory @Inject constructor() {\n\n    fun getUpdateStateListener(\n        onDownloading: ((bytesDownloaded: Long, totalBytesToDownload: Long) -> Unit)? = null,\n        onDownloaded: (() -> Unit)? = null,\n    ) = InstallStateUpdatedListener { state ->\n        when (state.installStatus()) {\n            InstallStatus.DOWNLOADING -> {\n                val bytesDownloaded = state.bytesDownloaded()\n                val totalBytesToDownload = state.totalBytesToDownload()\n                if (onDownloading != null) {\n                    onDownloading(bytesDownloaded, totalBytesToDownload)\n                }\n                // Show update progress bar.\n            }\n            InstallStatus.DOWNLOADED -> {\n                // Notify the user that the update is ready to be installed.\n                if (onDownloaded != null) {\n                    onDownloaded()\n                }\n\n            }\n            InstallStatus.INSTALLING,\n            InstallStatus.INSTALLED,\n            InstallStatus.FAILED,\n            InstallStatus.CANCELED,\n            InstallStatus.PENDING,\n            InstallStatus.UNKNOWN -> {\n                // No-op\n            }\n            else -> {\n                // No-op\n            }\n        }\n    }\n\n}\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_arrow_back.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"#000000\" android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\" android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_exclude_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_info_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\" android:height=\"24dp\" android:tint=\"#000000\" android:viewportHeight=\"24\" android:viewportWidth=\"24\" android:width=\"24dp\">\n      \n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M11,7h2v2h-2V7zM11,11h2v6h-2V11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z\"/>\n    \n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_language.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_launcher_background.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path\n        android:fillColor=\"#3DDC84\"\n        android:pathData=\"M0,0h108v108h-108z\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M9,0L9,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,0L19,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,0L29,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,0L39,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,0L49,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,0L59,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,0L69,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,0L79,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M89,0L89,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M99,0L99,108\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,9L108,9\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,19L108,19\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,29L108,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,39L108,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,49L108,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,59L108,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,69L108,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,79L108,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,89L108,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M0,99L108,99\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,29L89,29\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,39L89,39\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,49L89,49\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,59L89,59\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,69L89,69\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M19,79L89,79\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M29,19L29,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M39,19L39,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M49,19L49,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M59,19L59,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M69,19L69,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n    <path\n        android:fillColor=\"#00000000\"\n        android:pathData=\"M79,19L79,89\"\n        android:strokeWidth=\"0.8\"\n        android:strokeColor=\"#33FFFFFF\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_settings.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#000000\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_time_24.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable/ic_units.xml",
    "content": "<vector android:height=\"24dp\" android:tint=\"#000000\"\n    android:viewportHeight=\"24\" android:viewportWidth=\"24\"\n    android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M14.17,13.71l1.4,-2.42c0.09,-0.15 0.05,-0.34 -0.08,-0.45l-1.48,-1.16c0.03,-0.22 0.05,-0.45 0.05,-0.68s-0.02,-0.46 -0.05,-0.69l1.48,-1.16c0.13,-0.11 0.17,-0.3 0.08,-0.45l-1.4,-2.42c-0.09,-0.15 -0.27,-0.21 -0.43,-0.15L12,4.83c-0.36,-0.28 -0.75,-0.51 -1.18,-0.69l-0.26,-1.85C10.53,2.13 10.38,2 10.21,2h-2.8C7.24,2 7.09,2.13 7.06,2.3L6.8,4.15C6.38,4.33 5.98,4.56 5.62,4.84l-1.74,-0.7c-0.16,-0.06 -0.34,0 -0.43,0.15l-1.4,2.42C1.96,6.86 2,7.05 2.13,7.16l1.48,1.16C3.58,8.54 3.56,8.77 3.56,9s0.02,0.46 0.05,0.69l-1.48,1.16C2,10.96 1.96,11.15 2.05,11.3l1.4,2.42c0.09,0.15 0.27,0.21 0.43,0.15l1.74,-0.7c0.36,0.28 0.75,0.51 1.18,0.69l0.26,1.85C7.09,15.87 7.24,16 7.41,16h2.8c0.17,0 0.32,-0.13 0.35,-0.3l0.26,-1.85c0.42,-0.18 0.82,-0.41 1.18,-0.69l1.74,0.7C13.9,13.92 14.08,13.86 14.17,13.71zM8.81,11c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C10.81,10.1 9.91,11 8.81,11z\"/>\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M21.92,18.67l-0.96,-0.74c0.02,-0.14 0.04,-0.29 0.04,-0.44c0,-0.15 -0.01,-0.3 -0.04,-0.44l0.95,-0.74c0.08,-0.07 0.11,-0.19 0.05,-0.29l-0.9,-1.55c-0.05,-0.1 -0.17,-0.13 -0.28,-0.1l-1.11,0.45c-0.23,-0.18 -0.48,-0.33 -0.76,-0.44l-0.17,-1.18C18.73,13.08 18.63,13 18.53,13h-1.79c-0.11,0 -0.21,0.08 -0.22,0.19l-0.17,1.18c-0.27,0.12 -0.53,0.26 -0.76,0.44l-1.11,-0.45c-0.1,-0.04 -0.22,0 -0.28,0.1l-0.9,1.55c-0.05,0.1 -0.04,0.22 0.05,0.29l0.95,0.74c-0.02,0.14 -0.03,0.29 -0.03,0.44c0,0.15 0.01,0.3 0.03,0.44l-0.95,0.74c-0.08,0.07 -0.11,0.19 -0.05,0.29l0.9,1.55c0.05,0.1 0.17,0.13 0.28,0.1l1.11,-0.45c0.23,0.18 0.48,0.33 0.76,0.44l0.17,1.18c0.02,0.11 0.11,0.19 0.22,0.19h1.79c0.11,0 0.21,-0.08 0.22,-0.19l0.17,-1.18c0.27,-0.12 0.53,-0.26 0.75,-0.44l1.12,0.45c0.1,0.04 0.22,0 0.28,-0.1l0.9,-1.55C22.03,18.86 22,18.74 21.92,18.67zM17.63,18.83c-0.74,0 -1.35,-0.6 -1.35,-1.35s0.6,-1.35 1.35,-1.35s1.35,0.6 1.35,1.35S18.37,18.83 17.63,18.83z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_arrow_back.xml",
    "content": "<vector android:autoMirrored=\"true\" android:height=\"24dp\"\n    android:tint=\"#FFFFFF\" android:viewportHeight=\"24\"\n    android:viewportWidth=\"24\" android:width=\"24dp\" xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <path android:fillColor=\"@android:color/white\" android:pathData=\"M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z\"/>\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_exclude_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M7,11v2h10v-2L7,11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_info_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11,7h2v2h-2V7zM11,11h2v6h-2V11zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8s8,3.59 8,8S16.41,20 12,20z\" />\n\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_language.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM18.92,8h-2.95c-0.32,-1.25 -0.78,-2.45 -1.38,-3.56 1.84,0.63 3.37,1.91 4.33,3.56zM12,4.04c0.83,1.2 1.48,2.53 1.91,3.96h-3.82c0.43,-1.43 1.08,-2.76 1.91,-3.96zM4.26,14C4.1,13.36 4,12.69 4,12s0.1,-1.36 0.26,-2h3.38c-0.08,0.66 -0.14,1.32 -0.14,2 0,0.68 0.06,1.34 0.14,2L4.26,14zM5.08,16h2.95c0.32,1.25 0.78,2.45 1.38,3.56 -1.84,-0.63 -3.37,-1.9 -4.33,-3.56zM8.03,8L5.08,8c0.96,-1.66 2.49,-2.93 4.33,-3.56C8.81,5.55 8.35,6.75 8.03,8zM12,19.96c-0.83,-1.2 -1.48,-2.53 -1.91,-3.96h3.82c-0.43,1.43 -1.08,2.76 -1.91,3.96zM14.34,14L9.66,14c-0.09,-0.66 -0.16,-1.32 -0.16,-2 0,-0.68 0.07,-1.35 0.16,-2h4.68c0.09,0.65 0.16,1.32 0.16,2 0,0.68 -0.07,1.34 -0.16,2zM14.59,19.56c0.6,-1.11 1.06,-2.31 1.38,-3.56h2.95c-0.96,1.65 -2.49,2.93 -4.33,3.56zM16.36,14c0.08,-0.66 0.14,-1.32 0.14,-2 0,-0.68 -0.06,-1.34 -0.14,-2h3.38c0.16,0.64 0.26,1.31 0.26,2s-0.1,1.36 -0.26,2h-3.38z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_settings.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/black\"\n        android:pathData=\"M19.14,12.94c0.04,-0.3 0.06,-0.61 0.06,-0.94c0,-0.32 -0.02,-0.64 -0.07,-0.94l2.03,-1.58c0.18,-0.14 0.23,-0.41 0.12,-0.61l-1.92,-3.32c-0.12,-0.22 -0.37,-0.29 -0.59,-0.22l-2.39,0.96c-0.5,-0.38 -1.03,-0.7 -1.62,-0.94L14.4,2.81c-0.04,-0.24 -0.24,-0.41 -0.48,-0.41h-3.84c-0.24,0 -0.43,0.17 -0.47,0.41L9.25,5.35C8.66,5.59 8.12,5.92 7.63,6.29L5.24,5.33c-0.22,-0.08 -0.47,0 -0.59,0.22L2.74,8.87C2.62,9.08 2.66,9.34 2.86,9.48l2.03,1.58C4.84,11.36 4.8,11.69 4.8,12s0.02,0.64 0.07,0.94l-2.03,1.58c-0.18,0.14 -0.23,0.41 -0.12,0.61l1.92,3.32c0.12,0.22 0.37,0.29 0.59,0.22l2.39,-0.96c0.5,0.38 1.03,0.7 1.62,0.94l0.36,2.54c0.05,0.24 0.24,0.41 0.48,0.41h3.84c0.24,0 0.44,-0.17 0.47,-0.41l0.36,-2.54c0.59,-0.24 1.13,-0.56 1.62,-0.94l2.39,0.96c0.22,0.08 0.47,0 0.59,-0.22l1.92,-3.32c0.12,-0.22 0.07,-0.47 -0.12,-0.61L19.14,12.94zM12,15.6c-1.98,0 -3.6,-1.62 -3.6,-3.6s1.62,-3.6 3.6,-3.6s3.6,1.62 3.6,3.6S13.98,15.6 12,15.6z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_time_24.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M12.5,7H11v6l5.25,3.15 0.75,-1.23 -4.5,-2.67z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-night/ic_units.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:tint=\"#FFFFFF\"\n    android:viewportWidth=\"24\"\n    android:viewportHeight=\"24\">\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M14.17,13.71l1.4,-2.42c0.09,-0.15 0.05,-0.34 -0.08,-0.45l-1.48,-1.16c0.03,-0.22 0.05,-0.45 0.05,-0.68s-0.02,-0.46 -0.05,-0.69l1.48,-1.16c0.13,-0.11 0.17,-0.3 0.08,-0.45l-1.4,-2.42c-0.09,-0.15 -0.27,-0.21 -0.43,-0.15L12,4.83c-0.36,-0.28 -0.75,-0.51 -1.18,-0.69l-0.26,-1.85C10.53,2.13 10.38,2 10.21,2h-2.8C7.24,2 7.09,2.13 7.06,2.3L6.8,4.15C6.38,4.33 5.98,4.56 5.62,4.84l-1.74,-0.7c-0.16,-0.06 -0.34,0 -0.43,0.15l-1.4,2.42C1.96,6.86 2,7.05 2.13,7.16l1.48,1.16C3.58,8.54 3.56,8.77 3.56,9s0.02,0.46 0.05,0.69l-1.48,1.16C2,10.96 1.96,11.15 2.05,11.3l1.4,2.42c0.09,0.15 0.27,0.21 0.43,0.15l1.74,-0.7c0.36,0.28 0.75,0.51 1.18,0.69l0.26,1.85C7.09,15.87 7.24,16 7.41,16h2.8c0.17,0 0.32,-0.13 0.35,-0.3l0.26,-1.85c0.42,-0.18 0.82,-0.41 1.18,-0.69l1.74,0.7C13.9,13.92 14.08,13.86 14.17,13.71zM8.81,11c-1.1,0 -2,-0.9 -2,-2c0,-1.1 0.9,-2 2,-2s2,0.9 2,2C10.81,10.1 9.91,11 8.81,11z\" />\n    <path\n        android:fillColor=\"@android:color/white\"\n        android:pathData=\"M21.92,18.67l-0.96,-0.74c0.02,-0.14 0.04,-0.29 0.04,-0.44c0,-0.15 -0.01,-0.3 -0.04,-0.44l0.95,-0.74c0.08,-0.07 0.11,-0.19 0.05,-0.29l-0.9,-1.55c-0.05,-0.1 -0.17,-0.13 -0.28,-0.1l-1.11,0.45c-0.23,-0.18 -0.48,-0.33 -0.76,-0.44l-0.17,-1.18C18.73,13.08 18.63,13 18.53,13h-1.79c-0.11,0 -0.21,0.08 -0.22,0.19l-0.17,1.18c-0.27,0.12 -0.53,0.26 -0.76,0.44l-1.11,-0.45c-0.1,-0.04 -0.22,0 -0.28,0.1l-0.9,1.55c-0.05,0.1 -0.04,0.22 0.05,0.29l0.95,0.74c-0.02,0.14 -0.03,0.29 -0.03,0.44c0,0.15 0.01,0.3 0.03,0.44l-0.95,0.74c-0.08,0.07 -0.11,0.19 -0.05,0.29l0.9,1.55c0.05,0.1 0.17,0.13 0.28,0.1l1.11,-0.45c0.23,0.18 0.48,0.33 0.76,0.44l0.17,1.18c0.02,0.11 0.11,0.19 0.22,0.19h1.79c0.11,0 0.21,-0.08 0.22,-0.19l0.17,-1.18c0.27,-0.12 0.53,-0.26 0.75,-0.44l1.12,0.45c0.1,0.04 0.22,0 0.28,-0.1l0.9,-1.55C22.03,18.86 22,18.74 21.92,18.67zM17.63,18.83c-0.74,0 -1.35,-0.6 -1.35,-1.35s0.6,-1.35 1.35,-1.35s1.35,0.6 1.35,1.35S18.37,18.83 17.63,18.83z\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/drawable-v24/ic_launcher_foreground.xml",
    "content": "<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:aapt=\"http://schemas.android.com/aapt\"\n    android:width=\"108dp\"\n    android:height=\"108dp\"\n    android:viewportWidth=\"108\"\n    android:viewportHeight=\"108\">\n    <path android:pathData=\"M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z\">\n        <aapt:attr name=\"android:fillColor\">\n            <gradient\n                android:endX=\"85.84757\"\n                android:endY=\"92.4963\"\n                android:startX=\"42.9492\"\n                android:startY=\"49.59793\"\n                android:type=\"linear\">\n                <item\n                    android:color=\"#44000000\"\n                    android:offset=\"0.0\" />\n                <item\n                    android:color=\"#00000000\"\n                    android:offset=\"1.0\" />\n            </gradient>\n        </aapt:attr>\n    </path>\n    <path\n        android:fillColor=\"#FFFFFF\"\n        android:fillType=\"nonZero\"\n        android:pathData=\"M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z\"\n        android:strokeWidth=\"1\"\n        android:strokeColor=\"#00000000\" />\n</vector>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<adaptive-icon xmlns:android=\"http://schemas.android.com/apk/res/android\">\n    <background android:drawable=\"@drawable/ic_launcher_background\" />\n    <foreground android:drawable=\"@drawable/ic_launcher_foreground\" />\n</adaptive-icon>\n"
  },
  {
    "path": "app/src/main/res/values/colors.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n    <color name=\"immersive_sys_ui\">#33000000</color>\n    <color name=\"nav_bar\">@color/immersive_sys_ui</color>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/strings.xml",
    "content": "<resources>\n    <string name=\"app_name\" translatable=\"false\">WeatherApp</string>\n\n    <!--Home Screen-->\n    <string name=\"home_content_description_setting_icon\">Setting Icon</string>\n    <string name=\"home_title_currently\">Currently</string>\n    <string name=\"home_feels_like_description\">Feels like %1$s</string>\n    <string name=\"home_today_forecast_title\">Today\\'s Forecast</string>\n    <string name=\"home_weekly_forecast_title\">Weekly Forecast</string>\n    <string name=\"home_min_temp\">Min %1$s</string>\n    <string name=\"home_max_temp\">Max %1$s</string>\n    <string name=\"home_error_occured\">An issue occurred</string>\n    <string name=\"home_error_try_again\">Try Again</string>\n    <string name=\"home_enable_weather_in_settings\">Enable %1$s weather in settings to see data.</string>\n    <string name=\"home_weather_type_hourly\">Hourly</string>\n    <string name=\"home_weather_type_daily\">Daily</string>\n    <string name=\"home_weather_type_currently\">Current</string>\n\n    <!-- Settings Screen-->\n    <string name=\"settings_screen_title\">Settings</string>\n    <string name=\"back_button_content_description_icon\">Back Button</string>\n    <string name=\"settings_language_label\">Language</string>\n    <string name=\"settings_unit_label\">Units</string>\n    <string name=\"settings_time_format\">Time Format</string>\n    <string name=\"settings_about\">About</string>\n    <string name=\"settings_exclude_label\">Exclude</string>\n    <string name=\"settings_content_description_lang_icon\">Language Icon</string>\n    <string name=\"settings_content_description_unit_icon\">Units Icon</string>\n    <string name=\"settings_content_description_time_icon\">Time Icon</string>\n    <string name=\"settings_content_description_about_icon\">About Icon</string>\n    <string name=\"settings_content_description_exclude_icon\">Exclude icon</string>\n    <string name=\"settings_confirm\">Confirm</string>\n    <string name=\"settings_save\">Save</string>\n\n    <!--    About Screen-->\n    <string name=\"about_screen_title\">About</string>\n\n    <!--    Permissions-->\n    <string name=\"location_rationale_title\">Permission Request</string>\n    <string name=\"location_rationale_description\">Hey there, we need location permission to show you relevant / accurate weather information.</string>\n    <string name=\"location_rationale_button_grant\">Allow</string>\n    <string name=\"location_rationale_button_deny\">Deny</string>\n    <string name=\"location_no_permission_screen_description\">You need to grant location access to use the app</string>\n    <string name=\"location_settings_not_enabled\">Check your location settings and enable them</string>\n    <string name=\"error_unauthorized\">You may need to log in or sign up to see this :)</string>\n    <string name=\"error_client\">Oops,something fishy is up on your end :)</string>\n    <string name=\"error_server\">Oops! Something is wrong on our end :(</string>\n    <string name=\"error_generic\">Something is happening that\\'s disturbing the force :(</string>\n    <string name=\"error_connection\">Check your internet connection and try again</string>\n\n    <!--    Update-->\n    <string name=\"update_available\">Update is available for install</string>\n    <string name=\"install_update\">Install</string>\n\n</resources>\n"
  },
  {
    "path": "app/src/main/res/values/themes.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<resources>\n\n    <style name=\"Theme.WeatherApp\" parent=\"@android:style/Theme.Material.Light.NoActionBar\">\n        <item name=\"android:colorPrimary\">#ff00ff</item>\n        <item name=\"android:colorAccent\">#ff00ff</item>\n        <item name=\"android:statusBarColor\">@color/immersive_sys_ui</item>\n        <item name=\"android:navigationBarColor\">@color/nav_bar</item>\n    </style>\n</resources>\n"
  },
  {
    "path": "app/src/main/res/xml/backup_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample backup rules file; uncomment and customize as necessary.\n   See https://developer.android.com/guide/topics/data/autobackup\n   for details.\n   Note: This file is ignored for devices older that API 31\n   See https://developer.android.com/about/versions/12/backup-restore\n-->\n<full-backup-content>\n    <!--\n   <include domain=\"sharedpref\" path=\".\"/>\n   <exclude domain=\"sharedpref\" path=\"device.xml\"/>\n-->\n</full-backup-content>\n"
  },
  {
    "path": "app/src/main/res/xml/data_extraction_rules.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?><!--\n   Sample data extraction rules file; uncomment and customize as necessary.\n   See https://developer.android.com/about/versions/12/backup-restore#xml-changes\n   for details.\n-->\n<data-extraction-rules>\n    <cloud-backup>\n        <!-- TODO: Use <include> and <exclude> to control what is backed up.\n        <include .../>\n        <exclude .../>\n        -->\n    </cloud-backup>\n    <!--\n    <device-transfer>\n        <include .../>\n        <exclude .../>\n    </device-transfer>\n    -->\n</data-extraction-rules>\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/HomeViewModelTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport app.cash.turbine.test\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.api.WeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.remote.DefaultRemoteWeatherDataSource\nimport com.github.odaridavid.weatherapp.data.weather.remote.OpenWeatherService\nimport com.github.odaridavid.weatherapp.data.weather.remote.WeatherResponse\nimport com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository\nimport com.github.odaridavid.weatherapp.fakes.fakeSuccessMappedWeatherResponse\nimport com.github.odaridavid.weatherapp.fakes.fakeSuccessWeatherResponse\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport com.github.odaridavid.weatherapp.rules.MainCoroutineRule\nimport com.github.odaridavid.weatherapp.ui.home.HomeScreenIntent\nimport com.github.odaridavid.weatherapp.ui.home.HomeScreenViewState\nimport com.github.odaridavid.weatherapp.ui.home.HomeViewModel\nimport com.google.common.truth.Truth\nimport io.mockk.coEvery\nimport io.mockk.impl.annotations.MockK\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.flow.flowOf\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.ResponseBody.Companion.toResponseBody\nimport org.junit.Before\nimport org.junit.Rule\nimport org.junit.Test\nimport retrofit2.Response\nimport java.util.TimeZone\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass HomeViewModelTest {\n\n    @MockK\n    val mockOpenWeatherService = mockk<OpenWeatherService>(relaxed = true)\n\n    @MockK\n    val mockLogger = mockk<Logger>(relaxed = true)\n\n    private val settingsRepository: SettingsRepository by lazy {\n        FakeSettingsRepository()\n    }\n\n    @get:Rule\n    val coroutineRule = MainCoroutineRule()\n\n    @Before\n    fun setup() {\n        TimeZone.setDefault(TimeZone.getTimeZone(\"UTC\"))\n    }\n\n    @Test\n    fun `when fetching weather data is successful, then display correct data`() = runBlocking {\n        coEvery {\n            mockOpenWeatherService.getWeatherData(\n                any(), any(), any(), any(), any(), any()\n            )\n        } returns Response.success<WeatherResponse>(\n            fakeSuccessWeatherResponse\n        )\n\n        settingsRepository.setFormat(TimeFormat.TWELVE_HOUR)\n\n        val weatherRepository = createWeatherRepository()\n\n        val viewModel = createViewModel(\n            weatherRepository = weatherRepository\n        )\n\n        viewModel.processIntent(HomeScreenIntent.LoadWeatherData)\n\n        val expectedState = HomeScreenViewState(\n            units = Units.METRIC,\n            defaultLocation = DefaultLocation(\n                longitude = 0.0, latitude = 0.0\n            ),\n            locationName = \"-\",\n            language = SupportedLanguage.ENGLISH,\n            weather = fakeSuccessMappedWeatherResponse,\n            isLoading = false,\n            errorMessageId = null\n        )\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                Truth.assertThat(state).isEqualTo(expectedState)\n            }\n        }\n    }\n\n    @Test\n    fun `when fetching weather data is unsuccessful, then display correct error state`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(), any(), any(), any(), any(), any()\n                )\n            } returns Response.error<WeatherResponse>(\n                404, \"{}\".toResponseBody()\n            )\n\n            val weatherRepository = createWeatherRepository()\n\n            val viewModel = createViewModel(weatherRepository = weatherRepository)\n\n            viewModel.processIntent(HomeScreenIntent.LoadWeatherData)\n\n            val expectedState = HomeScreenViewState(\n                units = Units.METRIC,\n                defaultLocation = DefaultLocation(\n                    longitude = 0.0, latitude = 0.0\n                ),\n                locationName = \"-\",\n                language = SupportedLanguage.ENGLISH,\n                weather = null,\n                isLoading = false,\n                errorMessageId = R.string.error_client\n            )\n\n            viewModel.state.test {\n                awaitItem().also { state ->\n                    Truth.assertThat(state).isEqualTo(expectedState)\n                }\n            }\n        }\n\n    @Test\n    fun `when we init the screen, then update the state`() = runBlocking {\n        coEvery {\n            mockOpenWeatherService.getWeatherData(\n                any(), any(), any(), any(), any(), any()\n            )\n        } returns Response.success<WeatherResponse>(\n            fakeSuccessWeatherResponse\n        )\n\n        val settingsRepository = mockk<SettingsRepository>() {\n            coEvery { getDefaultLocation() } returns flowOf(DefaultLocation(0.0, 0.0))\n            coEvery { getLanguage() } returns flowOf(SupportedLanguage.ENGLISH)\n            coEvery { getUnits() } returns flowOf(Units.METRIC)\n            coEvery { getFormat() } returns flowOf(TimeFormat.TWELVE_HOUR)\n            coEvery { getExcludedData() } returns flowOf(\"minutely,alerts\")\n        }\n\n        val viewModel = createViewModel(\n            settingsRepository = settingsRepository,\n            weatherRepository = createWeatherRepository(\n                settingsRepository = settingsRepository\n            )\n        )\n\n        val expectedState = HomeScreenViewState(\n            units = Units.METRIC,\n            defaultLocation = DefaultLocation(\n                longitude = 0.0, latitude = 0.0\n            ),\n            locationName = \"-\",\n            language = SupportedLanguage.ENGLISH,\n            weather = fakeSuccessMappedWeatherResponse,\n            isLoading = false,\n            errorMessageId = null\n        )\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                Truth.assertThat(state).isEqualTo(expectedState)\n            }\n        }\n    }\n\n    @Test\n    fun `when we receive a city name, the state is updated with it`() = runBlocking {\n        coEvery {\n            mockOpenWeatherService.getWeatherData(\n                any(), any(), any(), any(), any(), any()\n            )\n        } returns Response.success<WeatherResponse>(\n            fakeSuccessWeatherResponse\n        )\n        val settingsRepository = mockk<SettingsRepository>() {\n            coEvery { getDefaultLocation() } returns flowOf(DefaultLocation(0.0, 0.0))\n            coEvery { getLanguage() } returns flowOf(SupportedLanguage.ENGLISH)\n            coEvery { getUnits() } returns flowOf(Units.METRIC)\n            coEvery { getFormat() } returns flowOf(TimeFormat.TWELVE_HOUR)\n            coEvery { getExcludedData() } returns flowOf(\"minutely,alerts\")\n        }\n\n        val viewModel = createViewModel(\n            settingsRepository = settingsRepository,\n            weatherRepository = createWeatherRepository(\n                settingsRepository = settingsRepository\n            )\n        )\n\n        viewModel.processIntent(HomeScreenIntent.DisplayCityName(cityName = \"Paradise\"))\n\n        val expectedState = HomeScreenViewState(\n            units = Units.METRIC,\n            defaultLocation = DefaultLocation(\n                longitude = 0.0, latitude = 0.0\n            ),\n            locationName = \"Paradise\",\n            language = SupportedLanguage.ENGLISH,\n            weather = fakeSuccessMappedWeatherResponse,\n            isLoading = false,\n            errorMessageId = null\n        )\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                Truth.assertThat(state).isEqualTo(expectedState)\n            }\n        }\n    }\n\n    private fun createViewModel(\n        weatherRepository: WeatherRepository,\n        settingsRepository: SettingsRepository = this.settingsRepository\n    ): HomeViewModel =\n        HomeViewModel(\n            weatherRepository = weatherRepository,\n            settingsRepository = settingsRepository\n        )\n\n    private fun createWeatherRepository(\n        settingsRepository: SettingsRepository = this.settingsRepository\n    ) = DefaultWeatherRepository(\n        remoteWeatherDataSource = DefaultRemoteWeatherDataSource(\n            mockOpenWeatherService\n        ),\n        logger = mockLogger,\n        settingsRepository = settingsRepository,\n    )\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/MainViewModelTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport app.cash.turbine.test\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository\nimport com.github.odaridavid.weatherapp.rules.MainCoroutineRule\nimport io.mockk.every\nimport io.mockk.impl.annotations.MockK\nimport io.mockk.mockk\nimport io.mockk.verify\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.runTest\nimport org.junit.Rule\nimport org.junit.Test\n\nclass MainViewModelTest {\n\n    @OptIn(ExperimentalCoroutinesApi::class)\n    @get:Rule\n    val coroutineRule = MainCoroutineRule()\n\n    @MockK\n    val logger = mockk<Logger>().apply {\n        every { logException(any()) } returns Unit\n    }\n\n    private val settingsRepository: SettingsRepository by lazy {\n        FakeSettingsRepository()\n    }\n\n    @Test\n    fun `when we grant permission, then the state is updated as expected`() = runTest {\n        val viewModel = createMainViewModel()\n\n        viewModel.processIntent(MainViewIntent.GrantPermission(true))\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.isPermissionGranted)\n            }\n        }\n    }\n\n    @Test\n    fun `when we check location settings, then the state is updated as expected`() = runTest {\n        val viewModel = createMainViewModel()\n\n        viewModel.processIntent(MainViewIntent.CheckLocationSettings(true))\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.isLocationSettingEnabled)\n            }\n        }\n    }\n\n    // TODO Use parameterized tests\n    @Test\n    fun `when we deny permission, then the state is updated as expected`() = runTest {\n        val viewModel = createMainViewModel()\n\n        viewModel.processIntent(MainViewIntent.GrantPermission(false))\n\n        viewModel.state.test {\n            awaitItem().also { state ->\n                assert(!state.isPermissionGranted)\n            }\n        }\n    }\n\n    @Test\n    fun `when we log an exception,then the right methods are called`() = runTest {\n        val viewModel = createMainViewModel(\n            logger = logger,\n        )\n        viewModel.state.test {\n            viewModel.processIntent(MainViewIntent.LogException(Exception(\"Test\")))\n            awaitItem()\n        }\n\n        verify { logger.logException(any()) }\n    }\n\n    @Test\n    fun `when we receive a location, then the state is updated as expected`() = runTest {\n        val viewModel = createMainViewModel(\n            settingsRepository = settingsRepository,\n        )\n        viewModel.state.test {\n            viewModel.processIntent(MainViewIntent.ReceiveLocation(0.0, 0.0))\n            awaitItem().also { state ->\n                assert(state.defaultLocation?.latitude == 0.0)\n                assert(state.defaultLocation?.longitude == 0.0)\n            }\n        }\n    }\n\n    private fun createMainViewModel(\n        settingsRepository: SettingsRepository = this.settingsRepository,\n        logger: Logger = mockk(),\n    ): MainViewModel {\n        return MainViewModel(settingsRepository = settingsRepository, logger = logger)\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/SettingsRepositoryTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository\nimport com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport kotlinx.coroutines.flow.map\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Test\n\nclass SettingsRepositoryTest {\n\n    @Test\n    fun `when we update language, then we get the updated language`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.setLanguage(SupportedLanguage.FRENCH)\n            settingsRepo.getLanguage().map { language ->\n                assert(language == SupportedLanguage.FRENCH)\n            }\n        }\n    }\n\n    @Test\n    fun `when we fetch the language, then we get the default language`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.getLanguage().map { language ->\n                assert(language == SupportedLanguage.ENGLISH)\n            }\n        }\n    }\n\n    @Test\n    fun `when we update units, then we get the updated units`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.setUnits(Units.IMPERIAL)\n            settingsRepo.getUnits().map { units ->\n                assert(units == Units.IMPERIAL)\n            }\n        }\n    }\n\n    @Test\n    fun `when we fetch the units, then we get the default units`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.getUnits().map { units ->\n                assert(units == Units.METRIC)\n            }\n        }\n    }\n\n    @Test\n    fun `when we update time format, then we get the updated time format`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.setFormat(TimeFormat.TWELVE_HOUR)\n            settingsRepo.getFormat().map { format ->\n                assert(format == TimeFormat.TWELVE_HOUR)\n            }\n        }\n    }\n\n    @Test\n    fun `when we fetch the time format, then we get the default time format`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.getFormat().map { format ->\n                assert(format == TimeFormat.TWENTY_FOUR_HOUR)\n            }\n        }\n    }\n\n    @Test\n    fun `when we fetch the location, then we get the default location`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.getDefaultLocation().map { location ->\n                assert(location.latitude == DefaultSettingsRepository.DEFAULT_LATITUDE)\n                assert(location.longitude == DefaultSettingsRepository.DEFAULT_LONGITUDE)\n            }\n        }\n    }\n\n    @Test\n    fun `when we update the location, then we get the correct location`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.setDefaultLocation(DefaultLocation(0.0, 0.0))\n            settingsRepo.getDefaultLocation().map { location ->\n                assert(location.latitude == 0.0)\n                assert(location.longitude == 0.0)\n            }\n        }\n    }\n\n    @Test\n    fun `when we update excluded data, then we get the updated excluded data`() {\n        val settingsRepo = createSettingsRepository()\n        runBlocking {\n            settingsRepo.setExcludedData(\n                listOf(\n                    ExcludedData.ALERTS,\n                    ExcludedData.MINUTELY,\n                    ExcludedData.DAILY\n                )\n            )\n            settingsRepo.getExcludedData().map { excludedData ->\n                assert(excludedData == \"${ExcludedData.ALERTS.value},${ExcludedData.MINUTELY.value},${ExcludedData.DAILY.value}\")\n            }\n        }\n    }\n\n    private fun createSettingsRepository(): SettingsRepository = FakeSettingsRepository()\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/SettingsViewModelTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport app.cash.turbine.test\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport com.github.odaridavid.weatherapp.rules.MainCoroutineRule\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsScreenIntent\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsScreenViewState\nimport com.github.odaridavid.weatherapp.ui.settings.SettingsViewModel\nimport io.mockk.impl.annotations.MockK\nimport io.mockk.mockk\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.runBlocking\nimport org.junit.Rule\nimport org.junit.Test\n\n@OptIn(ExperimentalCoroutinesApi::class)\nclass SettingsViewModelTest {\n\n    private val settingsRepository: SettingsRepository = FakeSettingsRepository()\n\n    @MockK\n    private val logger: Logger = mockk()\n\n    @get:Rule\n    val coroutineRule = MainCoroutineRule()\n\n    @Test\n    fun `when we load screen data, then the state is updated as expected`() = runBlocking {\n        val settingsViewModel = createSettingsScreenViewModel()\n\n        settingsViewModel.processIntent(SettingsScreenIntent.LoadSettingScreenData)\n\n        val expectedState = SettingsScreenViewState(\n            selectedUnit = Units.METRIC,\n            selectedLanguage = SupportedLanguage.ENGLISH,\n            availableLanguages = SupportedLanguage.entries,\n            availableUnits = Units.entries,\n            selectedTimeFormat = TimeFormat.TWENTY_FOUR_HOUR,\n            availableFormats = TimeFormat.entries,\n            versionInfo = \"1.0.0\",\n            selectedExcludedData = listOf(ExcludedData.MINUTELY, ExcludedData.ALERTS),\n            excludedData = ExcludedData.entries,\n            selectedExcludedDataDisplayValue = \"minutely,alerts\"\n        )\n\n        settingsViewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.error == null)\n                assert(state == expectedState)\n            }\n        }\n    }\n\n    @Test\n    fun `when we change units, then the units are updated`() = runBlocking {\n        val settingsViewModel = createSettingsScreenViewModel()\n\n        settingsViewModel.processIntent(SettingsScreenIntent.ChangeUnits(selectedUnits = Units.STANDARD))\n\n        settingsViewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.error == null)\n                assert(state.selectedUnit == Units.STANDARD)\n            }\n        }\n    }\n\n    @Test\n    fun `when we change language, then the language is updated `() = runBlocking {\n        val settingsViewModel = createSettingsScreenViewModel()\n\n        settingsViewModel.processIntent(SettingsScreenIntent.ChangeLanguage(selectedLanguage = SupportedLanguage.AFRIKAANS))\n\n        settingsViewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.error == null)\n                assert(state.selectedLanguage == SupportedLanguage.AFRIKAANS)\n            }\n        }\n    }\n\n    @Test\n    fun `when we change time format, then the format is updated `() = runBlocking {\n        val settingsViewModel = createSettingsScreenViewModel()\n\n        settingsViewModel.processIntent(SettingsScreenIntent.ChangeTimeFormat(selectedTimeFormat = TimeFormat.TWENTY_FOUR_HOUR))\n\n        settingsViewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.error == null)\n                assert(state.selectedTimeFormat == TimeFormat.TWENTY_FOUR_HOUR)\n            }\n        }\n    }\n\n    @Test\n    fun `when we change excluded data, then the excluded data is updated `() = runBlocking {\n        val settingsViewModel = createSettingsScreenViewModel()\n\n        settingsViewModel.processIntent(\n            SettingsScreenIntent.ChangeExcludedData(\n                selectedExcludedData = listOf(\n                    ExcludedData.CURRENT,\n                    ExcludedData.DAILY,\n                    ExcludedData.NONE\n                )\n            )\n        )\n\n        settingsViewModel.state.test {\n            awaitItem().also { state ->\n                assert(state.error == null)\n                assert(\n                    state.selectedExcludedData == listOf(\n                        ExcludedData.CURRENT,\n                        ExcludedData.DAILY,\n                        ExcludedData.NONE\n                    )\n                )\n                assert(state.selectedExcludedDataDisplayValue == \"current,daily,none\")\n            }\n        }\n    }\n\n    private fun createSettingsScreenViewModel(): SettingsViewModel = SettingsViewModel(\n        settingsRepository = settingsRepository,\n        logger = logger,\n    )\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/UIMapperTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport com.github.odaridavid.weatherapp.designsystem.organism.BottomSheetItem\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport com.github.odaridavid.weatherapp.ui.settings.toBottomSheetModel\nimport com.github.odaridavid.weatherapp.ui.settings.toExcludedData\nimport com.github.odaridavid.weatherapp.ui.settings.toSupportedLanguage\nimport com.github.odaridavid.weatherapp.ui.settings.toTimeFormat\nimport com.github.odaridavid.weatherapp.ui.settings.toUnits\nimport org.junit.Test\n\nclass UIMapperTest {\n\n    @Test\n    fun `when we map units to bottom sheet items, then we get the expected items`() {\n        val expectedItems = listOf(\n            BottomSheetItem(\n                id = 0,\n                name = \"standard\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 1,\n                name = \"metric\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 2,\n                name = \"imperial\",\n                isSelected = false\n            ),\n        )\n\n        val actual = Units.entries.map { it.toBottomSheetModel() }\n\n        assert(actual == expectedItems) {\n            \"Expected $expectedItems but was $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map time format to bottom sheet items, then we get the expected items`() {\n        val expectedItems = listOf(\n            BottomSheetItem(\n                id = 0,\n                name = \"24 hours\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 1,\n                name = \"12 hours\",\n                isSelected = false\n            ),\n        )\n\n        val actual = TimeFormat.entries.map { it.toBottomSheetModel() }\n\n        assert(actual == expectedItems) {\n            \"Expected $expectedItems but was $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map excluded data to bottom sheet items, then we get the expected items`() {\n        val expectedItems = listOf(\n            BottomSheetItem(\n                id = 0,\n                name = \"CURRENT\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 1,\n                name = \"HOURLY\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 2,\n                name = \"DAILY\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 3,\n                name = \"MINUTELY\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 4,\n                name = \"ALERTS\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = -1,\n                name = \"NONE\",\n                isSelected = false\n            ),\n        )\n\n        val actual = ExcludedData.entries.map { it.toBottomSheetModel() }\n\n        assert(actual == expectedItems) {\n            \"Expected $expectedItems but was $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map supported language to bottom sheet items, then we get the expected items`() {\n        val expectedItems = listOf(\n            BottomSheetItem(\n                id = 0,\n                name = \"Afrikaans\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 1,\n                name = \"Albanian\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 2,\n                name = \"Arabic\",\n                isSelected = false\n            ),\n            BottomSheetItem(\n                id = 3,\n                name = \"Azerbaijani\",\n                isSelected = false\n            ),\n        )\n\n        val actual = SupportedLanguage.entries.take(4).map { it.toBottomSheetModel() }\n\n        assert(actual == expectedItems) {\n            \"Expected: $expectedItems \\n Actual: $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map bottom sheet item to supported language, then we get the expected language`() {\n        val expectedLanguage = SupportedLanguage.AFRIKAANS\n        val bottomSheetItem = BottomSheetItem(\n            id = 0,\n            name = \"Afrikaans\",\n            isSelected = false\n        )\n\n        val actual = bottomSheetItem.toSupportedLanguage()\n\n        assert(actual == expectedLanguage) {\n            \"Expected: $expectedLanguage \\n Actual: $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map bottom sheet item to units, then we get the expected units`() {\n        val expectedUnits = Units.METRIC\n        val bottomSheetItem = BottomSheetItem(\n            id = 1,\n            name = \"metric\",\n            isSelected = false\n        )\n\n        val actual = bottomSheetItem.toUnits()\n\n        assert(actual == expectedUnits) {\n            \"Expected: $expectedUnits \\n Actual: $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map bottom sheet item to time format, then we get the expected time format`() {\n        val expectedTimeFormat = TimeFormat.TWENTY_FOUR_HOUR\n        val bottomSheetItem = BottomSheetItem(\n            id = 0,\n            name = \"24 hours\",\n            isSelected = false\n        )\n\n        val actual = bottomSheetItem.toTimeFormat()\n\n        assert(actual == expectedTimeFormat) {\n            \"Expected: $expectedTimeFormat \\n Actual: $actual\"\n        }\n    }\n\n    @Test\n    fun `when we map bottom sheet item to excluded data, then we get the expected excluded data`() {\n        val expectedExcludedData = ExcludedData.CURRENT\n        val bottomSheetItem = BottomSheetItem(\n            id = 0,\n            name = \"CURRENT\",\n            isSelected = false\n        )\n\n        val actual = bottomSheetItem.toExcludedData()\n\n        assert(actual == expectedExcludedData) {\n            \"Expected: $expectedExcludedData \\n Actual: $actual\"\n        }\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/WeatherRepositoryTest.kt",
    "content": "package com.github.odaridavid.weatherapp\n\nimport com.github.odaridavid.weatherapp.api.Logger\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.api.WeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.DefaultWeatherRepository\nimport com.github.odaridavid.weatherapp.data.weather.remote.DefaultRemoteWeatherDataSource\nimport com.github.odaridavid.weatherapp.data.weather.remote.OpenWeatherService\nimport com.github.odaridavid.weatherapp.data.weather.remote.RemoteWeatherDataSource\nimport com.github.odaridavid.weatherapp.data.weather.remote.WeatherResponse\nimport com.github.odaridavid.weatherapp.fakes.FakeSettingsRepository\nimport com.github.odaridavid.weatherapp.fakes.fakeSuccessMappedWeatherResponse\nimport com.github.odaridavid.weatherapp.fakes.fakeSuccessWeatherResponse\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ErrorType\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.google.common.truth.Truth\nimport io.mockk.coEvery\nimport io.mockk.impl.annotations.MockK\nimport io.mockk.mockk\nimport kotlinx.coroutines.runBlocking\nimport okhttp3.ResponseBody.Companion.toResponseBody\nimport org.junit.Before\nimport org.junit.Test\nimport retrofit2.Response\nimport java.io.IOException\nimport java.util.TimeZone\n\nclass WeatherRepositoryTest {\n\n    @MockK\n    val mockOpenWeatherService = mockk<OpenWeatherService>(relaxed = true)\n\n    private val settingsRepository: SettingsRepository by lazy {\n        FakeSettingsRepository()\n    }\n\n    @MockK\n    val mockLogger = mockk<Logger>(relaxed = true)\n\n    // TODO Look into parameterized testing to cover different mapper scenarios\n\n    @Before\n    fun setup() {\n        TimeZone.setDefault(TimeZone.getTimeZone(\"UTC\"))\n    }\n\n    @Test\n    fun `when we fetch weather data successfully, then a successfully mapped result is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } returns Response.success<WeatherResponse>(\n                fakeSuccessWeatherResponse\n            )\n\n            settingsRepository.setFormat(TimeFormat.TWELVE_HOUR)\n\n            val weatherRepository = createWeatherRepository()\n\n            val expectedResult = fakeSuccessMappedWeatherResponse\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n            Truth.assertThat(actualResults).isInstanceOf(Result.Success::class.java)\n            Truth.assertThat((actualResults as Result.Success).data).isEqualTo(expectedResult)\n        }\n\n    @Test\n    fun `when we fetch weather data and a server error occurs, then a server error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } returns Response.error<WeatherResponse>(\n                500,\n                \"{}\".toResponseBody()\n            )\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType).isEqualTo(ErrorType.SERVER)\n        }\n\n    @Test\n    fun `when we fetch weather data and a client error occurs, then a client error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } returns Response.error<WeatherResponse>(\n                404,\n                \"{}\".toResponseBody()\n            )\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType).isEqualTo(ErrorType.CLIENT)\n        }\n\n    @Test\n    fun `when we fetch weather data and an unauthorized error occurs, then a client error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } returns Response.error<WeatherResponse>(\n                401,\n                \"{}\".toResponseBody()\n            )\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType)\n                .isEqualTo(ErrorType.CLIENT)\n        }\n\n    @Test\n    fun `when we fetch weather data and a generic error occurs, then a generic error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } returns Response.error<WeatherResponse>(\n                800,\n                \"{}\".toResponseBody()\n            )\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType)\n                .isEqualTo(ErrorType.GENERIC)\n        }\n\n    @Test\n    fun `when we fetch weather data and an IOException is thrown, then a connection error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } throws IOException()\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType)\n                .isEqualTo(ErrorType.IO_CONNECTION)\n        }\n\n    @Test\n    fun `when we fetch weather data and an unknown Exception is thrown, then a generic error is emitted`() =\n        runBlocking {\n            coEvery {\n                mockOpenWeatherService.getWeatherData(\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any(),\n                    any()\n                )\n            } throws Exception()\n\n            val weatherRepository = createWeatherRepository()\n\n            val actualResults = weatherRepository.fetchWeatherData(\n                defaultLocation = DefaultLocation(\n                    longitude = 10.0,\n                    latitude = 12.90\n                ),\n                language = \"English\",\n                units = \"metric\"\n            )\n\n            Truth.assertThat(actualResults).isInstanceOf(Result.Error::class.java)\n            Truth.assertThat((actualResults as Result.Error).errorType)\n                .isEqualTo(ErrorType.GENERIC)\n        }\n\n    private fun createWeatherRepository(\n        logger: Logger = mockLogger,\n        remoteWeatherDataSource: RemoteWeatherDataSource = DefaultRemoteWeatherDataSource(\n            openWeatherService = mockOpenWeatherService\n        ),\n        settingsRepository: SettingsRepository = this.settingsRepository,\n    ): WeatherRepository = DefaultWeatherRepository(\n        remoteWeatherDataSource = remoteWeatherDataSource,\n        logger = logger,\n        settingsRepository = settingsRepository,\n    )\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/fakes/FakeSettingsRepository.kt",
    "content": "package com.github.odaridavid.weatherapp.fakes\n\nimport com.github.odaridavid.weatherapp.api.SettingsRepository\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository.Companion.KEY_EXCLUDED_DATA\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository.Companion.KEY_LANGUAGE\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository.Companion.KEY_LAT_LNG\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository.Companion.KEY_TIME_FORMAT\nimport com.github.odaridavid.weatherapp.data.settings.DefaultSettingsRepository.Companion.KEY_UNITS\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport kotlinx.coroutines.flow.Flow\nimport kotlinx.coroutines.flow.flow\n\nclass FakeSettingsRepository : SettingsRepository {\n\n    private val settingsMap = mutableMapOf<String, Any>()\n    override suspend fun setLanguage(language: SupportedLanguage) {\n        settingsMap[KEY_LANGUAGE] = language\n    }\n\n    override suspend fun getLanguage(): Flow<SupportedLanguage> = flow {\n        emit(settingsMap[KEY_LANGUAGE] as? SupportedLanguage ?: SupportedLanguage.ENGLISH)\n    }\n\n    override suspend fun setUnits(units: Units) {\n        settingsMap[KEY_UNITS] = units\n    }\n\n    override suspend fun getUnits(): Flow<Units> = flow {\n        emit(settingsMap[KEY_UNITS] as? Units ?: Units.METRIC)\n    }\n\n    override fun getAppVersion(): String = \"1.0.0\"\n\n    override suspend fun setDefaultLocation(defaultLocation: DefaultLocation) {\n        settingsMap[KEY_LAT_LNG] = \"${defaultLocation.latitude}/${defaultLocation.longitude}\"\n    }\n\n    override suspend fun getDefaultLocation(): Flow<DefaultLocation> = flow {\n        val latLng = settingsMap[KEY_LAT_LNG] as? String ?: \"0.0/0.0\"\n        val latLngList = latLng.split(\"/\")\n        DefaultLocation(latitude = latLngList[0].toDouble(), longitude = latLngList[1].toDouble())\n    }\n\n    override suspend fun getFormat(): Flow<TimeFormat> = flow {\n        emit(settingsMap[KEY_TIME_FORMAT] as? TimeFormat ?: TimeFormat.TWENTY_FOUR_HOUR)\n    }\n\n    override suspend fun setFormat(format: TimeFormat) {\n        settingsMap[KEY_TIME_FORMAT] = format\n    }\n\n    override suspend fun getExcludedData(): Flow<String> = flow {\n        emit(\n            settingsMap[KEY_EXCLUDED_DATA] as? String\n                ?: \"${ExcludedData.MINUTELY.value},${ExcludedData.ALERTS.value}\"\n        )\n    }\n\n    override suspend fun setExcludedData(excludedData: List<ExcludedData>) {\n        val formattedData = excludedData.joinToString(separator = \",\") { it.value }\n        settingsMap[KEY_EXCLUDED_DATA] = formattedData\n    }\n}\n"
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/fakes/Fakes.kt",
    "content": "package com.github.odaridavid.weatherapp.fakes\n\nimport com.github.odaridavid.weatherapp.data.weather.remote.CurrentWeatherResponse\nimport com.github.odaridavid.weatherapp.data.weather.remote.DailyWeatherResponse\nimport com.github.odaridavid.weatherapp.data.weather.remote.HourlyWeatherResponse\nimport com.github.odaridavid.weatherapp.data.weather.remote.TemperatureResponse\nimport com.github.odaridavid.weatherapp.data.weather.remote.WeatherResponse\nimport com.github.odaridavid.weatherapp.model.CurrentWeather\nimport com.github.odaridavid.weatherapp.model.DailyWeather\nimport com.github.odaridavid.weatherapp.model.HourlyWeather\nimport com.github.odaridavid.weatherapp.model.Temperature\nimport com.github.odaridavid.weatherapp.model.Weather\n\n// TODO Populate the responses with more data to test the mappers\nval fakeSuccessWeatherResponse = WeatherResponse(\n    current = CurrentWeatherResponse(\n        temperature = 3.0f,\n        feelsLike = 2.0f,\n        weather = listOf()\n    ),\n    hourly = listOf(\n        HourlyWeatherResponse(\n            forecastedTime = 1618310400,\n            temperature = 3.0f,\n            weather = listOf()\n        )\n    ),\n    daily = listOf(\n        DailyWeatherResponse(\n            forecastedTime = 1618310400,\n            temperature = TemperatureResponse(min = 0.0f, max = 10.0f),\n            weather = listOf()\n        )\n    )\n)\n\n\nval fakeSuccessMappedWeatherResponse = Weather(\n    current = CurrentWeather(\n        temperature = \"3°C\",\n        feelsLike = \"2°C\",\n        weather = listOf()\n    ),\n    hourly = listOf(\n        HourlyWeather(\n            forecastedTime = \"10:40 AM\",\n            temperature = \"3°C\",\n            weather = listOf()\n        )\n    ),\n    daily = listOf(\n        DailyWeather(\n            forecastedTime = \"Tuesday 13/4\",\n            temperature = Temperature(min = \"0°C\", max = \"10°C\"),\n            weather = listOf(),\n        )\n    )\n)\n// TODO Parameterized tests to cover different formattings, time/date formats, temperature specifically."
  },
  {
    "path": "app/src/test/java/com/github/odaridavid/weatherapp/rules/MainCoroutineRule.kt",
    "content": "package com.github.odaridavid.weatherapp.rules\n\nimport kotlinx.coroutines.Dispatchers\nimport kotlinx.coroutines.ExperimentalCoroutinesApi\nimport kotlinx.coroutines.test.TestDispatcher\nimport kotlinx.coroutines.test.UnconfinedTestDispatcher\nimport kotlinx.coroutines.test.resetMain\nimport kotlinx.coroutines.test.setMain\nimport org.junit.rules.TestWatcher\nimport org.junit.runner.Description\n\n@ExperimentalCoroutinesApi\nclass MainCoroutineRule(\n    private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(),\n) : TestWatcher() {\n\n    override fun starting(description: Description) {\n        Dispatchers.setMain(testDispatcher)\n    }\n\n    override fun finished(description: Description) {\n        Dispatchers.resetMain()\n    }\n}"
  },
  {
    "path": "build.gradle.kts",
    "content": "import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask\n\nplugins {\n    alias(libs.plugins.com.android.application) apply false\n    alias(libs.plugins.com.android.library) apply false\n    alias(libs.plugins.org.jetbrains.kotlin.android) apply false\n    alias(libs.plugins.mapsplatform.secrets.gradle.plugin) apply false\n    alias(libs.plugins.dagger.hilt.android) apply false\n    alias(libs.plugins.org.jetbrains.kotlin.plugin.serialization) apply false\n    alias(libs.plugins.kotlinMultiplatform) apply false\n    alias(libs.plugins.about.lib.plugin) apply false\n    alias(libs.plugins.firebase.perf.plugin) apply false\n    alias(libs.plugins.compose.compiler) apply false\n//    TODO Move some of these to toml file\n    id(\"com.github.ben-manes.versions\") version \"0.41.0\"\n    id(\"nl.littlerobots.version-catalog-update\") version \"0.8.4\"\n    id(\"io.gitlab.arturbosch.detekt\") version \"1.23.3\"\n    id(\"org.jlleitschuh.gradle.ktlint\") version \"12.1.1\"\n}\n\nbuildscript {\n    dependencies {\n        classpath(libs.com.google.services)\n        classpath(libs.com.firebase.crashlytics.plugin)\n        classpath(libs.gradle.versions.plugin)\n        classpath(libs.littlerobots.plugin)\n        classpath(libs.detekt.gradle.plugin)\n        classpath(libs.gradle)\n    }\n}\n\nversionCatalogUpdate {\n    pin {\n        versions.addAll(\"kotlin-android\")\n    }\n}\n\nfun isNonStable(version: String): Boolean {\n    val nonStableKeyword = listOf(\"BETA\", \"ALPHA\", \"DEV\").any { version.uppercase().contains(it) }\n    val regex = \"^[0-9,.v-]+(-r)?$\".toRegex()\n    val isStable = nonStableKeyword.not() || regex.matches(version)\n    return isStable.not()\n}\n\ntasks.withType<DependencyUpdatesTask> {\n    rejectVersionIf {\n        isNonStable(candidate.version)\n    }\n}\n"
  },
  {
    "path": "gradle/libs.versions.toml",
    "content": "[versions]\nabout-lib = \"12.0.0-a04\"\nactivity-compose = \"1.10.1\"\nandroid-gradle-plugin = \"8.9.0\"\nandroidx-test-rules = \"1.6.1\"\nandroidx-test-runner = \"1.6.2\"\nchucker = \"4.1.0\"\ncoil-compose = \"2.7.0\"\ncom-google-services = \"4.4.2\"\ncompose-bom = \"2025.03.00\"\ncompose-constraint = \"1.1.1\"\ncompose-material3 = \"1.3.1\"\ncompose-navigation = \"2.8.9\"\ncore-ktx = \"1.6.1\"\ncore-ktx-version = \"1.15.0\"\ncoroutines = \"1.10.1\"\ncoroutines-test = \"1.10.1\"\ncrashlytics-plugin = \"3.0.3\"\ndatastore = \"1.1.3\"\ndetekt-gradle-plugin = \"1.23.8\"\nfirebase-bom = \"33.10.0\"\nfirebase-perf = \"1.4.2\"\ngradle = \"8.9.0\"\ngradle-versions-plugin = \"0.52.0\"\nhilt = \"2.55\"\nhilt-nav-compose = \"1.2.0\"\ninappupdate = \"2.1.0\"\njunit = \"4.13.2\"\nkotlin = \"2.1.20-RC2\"\nkotlin-serialization = \"2.1.20-RC2\"\nkotlinx-coroutines-android = \"1.10.1\"\nkotlinx-serialization = \"1.8.0\"\nkotlinx-serialization-converter = \"1.0.0\"\nleakcanary = \"2.14\"\nlifecycle-runtime-ktx = \"2.8.7\"\nlifecycle-viewmodel-compose = \"2.8.7\"\nmapsplatform-secrets = \"2.0.1\"\nmockk = \"1.13.17\"\nokhttp = \"4.12.0\"\nplay-services-location = \"21.3.0\"\nplugin = \"0.8.5\"\nretrofit = \"2.11.0\"\ntruth = \"1.4.4\"\nturbine = \"1.2.0\"\n\n[libraries]\nabout-lib-compose-ui = { module = \"com.mikepenz:aboutlibraries-compose-m3\", version.ref = \"about-lib\" }\nabout-lib-core = { module = \"com.mikepenz:aboutlibraries-core\", version.ref = \"about-lib\" }\nactivity-compose = { module = \"androidx.activity:activity-compose\", version.ref = \"activity-compose\" }\nandroid-test-rules = { module = \"androidx.test:rules\", version.ref = \"androidx-test-rules\" }\nandroid-test-runner = { module = \"androidx.test:runner\", version.ref = \"androidx-test-runner\" }\nchucker-debug = { module = \"com.github.chuckerteam.chucker:library\", version.ref = \"chucker\" }\nchucker-release = { module = \"com.github.chuckerteam.chucker:library-no-op\", version.ref = \"chucker\" }\ncoil = { module = \"io.coil-kt:coil-compose\", version.ref = \"coil-compose\" }\ncom-firebase-crashlytics-plugin = { module = \"com.google.firebase:firebase-crashlytics-gradle\", version.ref = \"crashlytics-plugin\" }\ncom-google-services = { module = \"com.google.gms:google-services\", version.ref = \"com-google-services\" }\ncompose-bom = { module = \"androidx.compose:compose-bom\", version.ref = \"compose-bom\" }\ncompose-constraint = { module = \"androidx.constraintlayout:constraintlayout-compose\", version.ref = \"compose-constraint\" }\ncompose-material3 = { module = \"androidx.compose.material3:material3-android\", version.ref = \"compose-material3\" }\ncompose-navigation = { module = \"androidx.navigation:navigation-compose\", version.ref = \"compose-navigation\" }\ncompose-preview = { module = \"androidx.compose.ui:ui-tooling-preview\" }\ncompose-ui = { module = \"androidx.compose.ui:ui\" }\ncompose-ui-test-manifest = { module = \"androidx.compose.ui:ui-test-manifest\" }\ncompose-ui-tooling = { module = \"androidx.compose.ui:ui-tooling\" }\ncompose-viewmodel-lifecycle = { module = \"androidx.lifecycle:lifecycle-viewmodel-compose\", version.ref = \"lifecycle-viewmodel-compose\" }\ncore-ktx = { module = \"androidx.core:core-ktx\", version.ref = \"core-ktx-version\" }\ncoroutines = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-core\", version.ref = \"coroutines\" }\ncoroutines-test = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-test\", version.ref = \"coroutines-test\" }\ndatastore = { module = \"androidx.datastore:datastore-preferences\", version.ref = \"datastore\" }\ndetekt-gradle-plugin = { module = \"io.gitlab.arturbosch.detekt:detekt-gradle-plugin\", version.ref = \"detekt-gradle-plugin\" }\nfirebase-analytics = { module = \"com.google.firebase:firebase-analytics-ktx\" }\nfirebase-bom = { module = \"com.google.firebase:firebase-bom\", version.ref = \"firebase-bom\" }\nfirebase-crashlytics = { module = \"com.google.firebase:firebase-crashlytics-ktx\" }\nfirebase-perfomance-monitoring = { module = \"com.google.firebase:firebase-perf\" }\ngradle = { module = \"com.android.tools.build:gradle\", version.ref = \"gradle\" }\ngradle-versions-plugin = { module = \"com.github.ben-manes:gradle-versions-plugin\", version.ref = \"gradle-versions-plugin\" }\nhilt-android = { module = \"com.google.dagger:hilt-android\", version.ref = \"hilt\" }\nhilt-compiler = { module = \"com.google.dagger:hilt-compiler\", version.ref = \"hilt\" }\nhilt-nav-compose = { module = \"androidx.hilt:hilt-navigation-compose\", version.ref = \"hilt-nav-compose\" }\ninapp-update = { module = \"com.google.android.play:app-update\", version.ref = \"inappupdate\" }\ninapp-update-ktx = { module = \"com.google.android.play:app-update-ktx\", version.ref = \"inappupdate\" }\njunit = { module = \"junit:junit\", version.ref = \"junit\" }\nkotlinx-coroutines-android = { module = \"org.jetbrains.kotlinx:kotlinx-coroutines-android\", version.ref = \"kotlinx-coroutines-android\" }\nkotlinx-serialization = { module = \"org.jetbrains.kotlinx:kotlinx-serialization-json\", version.ref = \"kotlinx-serialization\" }\nkotlinx-serialization-converter = { module = \"com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter\", version.ref = \"kotlinx-serialization-converter\" }\nleakcanary = { module = \"com.squareup.leakcanary:leakcanary-android\", version.ref = \"leakcanary\" }\nlifecycle-runtime-ktx = { module = \"androidx.lifecycle:lifecycle-runtime-ktx\", version.ref = \"lifecycle-runtime-ktx\" }\nlittlerobots-plugin = { module = \"nl.littlerobots.vcu:plugin\", version.ref = \"plugin\" }\nmock-agent = { module = \"io.mockk:mockk-agent\", version.ref = \"mockk\" }\nmock-android = { module = \"io.mockk:mockk-android\", version.ref = \"mockk\" }\nokhttp = { module = \"com.squareup.okhttp3:okhttp\" }\nokhttp-bom = { module = \"com.squareup.okhttp3:okhttp-bom\", version.ref = \"okhttp\" }\nokhttp-logging-interceptor = { module = \"com.squareup.okhttp3:logging-interceptor\" }\nplayservices-location = { module = \"com.google.android.gms:play-services-location\", version.ref = \"play-services-location\" }\nretrofit = { module = \"com.squareup.retrofit2:retrofit\", version.ref = \"retrofit\" }\ntest-core-ktx = { module = \"androidx.test:core-ktx\", version.ref = \"core-ktx\" }\ntruth = { module = \"com.google.truth:truth\", version.ref = \"truth\" }\nturbine = { module = \"app.cash.turbine:turbine\", version.ref = \"turbine\" }\n\n[plugins]\nabout-lib-plugin = { id = \"com.mikepenz.aboutlibraries.plugin\", version.ref = \"about-lib\" }\ncom-android-application = { id = \"com.android.application\", version.ref = \"android-gradle-plugin\" }\ncom-android-library = { id = \"com.android.library\", version.ref = \"android-gradle-plugin\" }\ncompose-compiler = { id = \"org.jetbrains.kotlin.plugin.compose\", version.ref = \"kotlin\" }\ndagger-hilt-android = { id = \"com.google.dagger.hilt.android\", version.ref = \"hilt\" }\nfirebase-perf-plugin = { id = \"com.google.firebase.firebase-perf\", version.ref = \"firebase-perf\" }\nkotlinMultiplatform = { id = \"org.jetbrains.kotlin.multiplatform\", version.ref = \"kotlin\" }\nmapsplatform-secrets-gradle-plugin = { id = \"com.google.android.libraries.mapsplatform.secrets-gradle-plugin\", version.ref = \"mapsplatform-secrets\" }\norg-jetbrains-kotlin-android = { id = \"org.jetbrains.kotlin.android\", version.ref = \"kotlin\" }\norg-jetbrains-kotlin-plugin-serialization = { id = \"org.jetbrains.kotlin.plugin.serialization\", version.ref = \"kotlin-serialization\" }\n\n[bundles]\nandroid-test = [\n    \"android-test-rules\",\n    \"android-test-runner\",\n    \"test-core-ktx\",\n]\nandroidx = [\n    \"activity-compose\",\n    \"core-ktx\",\n    \"datastore\",\n    \"hilt-nav-compose\",\n    \"lifecycle-runtime-ktx\",\n]\ncompose = [\n    \"compose-constraint\",\n    \"compose-material3\",\n    \"compose-navigation\",\n    \"compose-preview\",\n    \"compose-ui\",\n    \"compose-viewmodel-lifecycle\",\n]\nfirebase = [\n    \"firebase-analytics\",\n    \"firebase-crashlytics\",\n    \"firebase-perfomance-monitoring\",\n]\ngoogle-play = [\n    \"inapp-update\",\n    \"inapp-update-ktx\",\n]\n"
  },
  {
    "path": "gradle/wrapper/gradle-wrapper.properties",
    "content": "#Tue Jan 14 09:55:22 CET 2025\ndistributionBase=GRADLE_USER_HOME\ndistributionPath=wrapper/dists\ndistributionUrl=https\\://services.gradle.org/distributions/gradle-8.11.1-bin.zip\nzipStoreBase=GRADLE_USER_HOME\nzipStorePath=wrapper/dists\n"
  },
  {
    "path": "gradle.properties",
    "content": "# Project-wide Gradle settings.\n# IDE (e.g. Android Studio) users:\n# Gradle settings configured through the IDE *will override*\n# any settings specified in this file.\n# For more details on how to configure your build environment visit\n# http://www.gradle.org/docs/current/userguide/build_environment.html\n# Specifies the JVM arguments used for the daemon process.\n# The setting is particularly useful for tweaking memory settings.\norg.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8\n# When configured, Gradle will run in incubating parallel mode.\n# This option should only be used with decoupled projects. More details, visit\n# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects\n# org.gradle.parallel=true\n# AndroidX package structure to make it clearer which packages are bundled with the\n# Android operating system, and which are packaged with your app's APK\n# https://developer.android.com/topic/libraries/support-library/androidx-rn\nandroid.useAndroidX=true\n# Kotlin code style for this project: \"official\" or \"obsolete\":\nkotlin.code.style=official\n# Enables namespacing of each library's R class so that its R class includes only the\n# resources declared in the library itself and none from the library's dependencies,\n# thereby reducing the size of the R class for that library\nandroid.nonTransitiveRClass=true\nandroid.defaults.buildfeatures.buildconfig=true\nandroid.nonFinalResIds=false\n"
  },
  {
    "path": "gradlew",
    "content": "#!/usr/bin/env bash\n\n#\n# Copyright 2015 the original author or authors.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#      https://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n#\n\n##############################################################################\n##\n##  Gradle start up script for UN*X\n##\n##############################################################################\n\n# Attempt to set APP_HOME\n# Resolve links: $0 may be a link\nPRG=\"$0\"\n# Need this for relative symlinks.\nwhile [ -h \"$PRG\" ] ; do\n    ls=`ls -ld \"$PRG\"`\n    link=`expr \"$ls\" : '.*-> \\(.*\\)$'`\n    if expr \"$link\" : '/.*' > /dev/null; then\n        PRG=\"$link\"\n    else\n        PRG=`dirname \"$PRG\"`\"/$link\"\n    fi\ndone\nSAVED=\"`pwd`\"\ncd \"`dirname \\\"$PRG\\\"`/\" >/dev/null\nAPP_HOME=\"`pwd -P`\"\ncd \"$SAVED\" >/dev/null\n\nAPP_NAME=\"Gradle\"\nAPP_BASE_NAME=`basename \"$0\"`\n\n# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nDEFAULT_JVM_OPTS='\"-Xmx64m\" \"-Xms64m\"'\n\n# Use the maximum available, or set MAX_FD != -1 to use that value.\nMAX_FD=\"maximum\"\n\nwarn () {\n    echo \"$*\"\n}\n\ndie () {\n    echo\n    echo \"$*\"\n    echo\n    exit 1\n}\n\n# OS specific support (must be 'true' or 'false').\ncygwin=false\nmsys=false\ndarwin=false\nnonstop=false\ncase \"`uname`\" in\n  CYGWIN* )\n    cygwin=true\n    ;;\n  Darwin* )\n    darwin=true\n    ;;\n  MINGW* )\n    msys=true\n    ;;\n  NONSTOP* )\n    nonstop=true\n    ;;\nesac\n\nCLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar\n\n\n# Determine the Java command to use to start the JVM.\nif [ -n \"$JAVA_HOME\" ] ; then\n    if [ -x \"$JAVA_HOME/jre/sh/java\" ] ; then\n        # IBM's JDK on AIX uses strange locations for the executables\n        JAVACMD=\"$JAVA_HOME/jre/sh/java\"\n    else\n        JAVACMD=\"$JAVA_HOME/bin/java\"\n    fi\n    if [ ! -x \"$JAVACMD\" ] ; then\n        die \"ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\n    fi\nelse\n    JAVACMD=\"java\"\n    which java >/dev/null 2>&1 || die \"ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\n\nPlease set the JAVA_HOME variable in your environment to match the\nlocation of your Java installation.\"\nfi\n\n# Increase the maximum file descriptors if we can.\nif [ \"$cygwin\" = \"false\" -a \"$darwin\" = \"false\" -a \"$nonstop\" = \"false\" ] ; then\n    MAX_FD_LIMIT=`ulimit -H -n`\n    if [ $? -eq 0 ] ; then\n        if [ \"$MAX_FD\" = \"maximum\" -o \"$MAX_FD\" = \"max\" ] ; then\n            MAX_FD=\"$MAX_FD_LIMIT\"\n        fi\n        ulimit -n $MAX_FD\n        if [ $? -ne 0 ] ; then\n            warn \"Could not set maximum file descriptor limit: $MAX_FD\"\n        fi\n    else\n        warn \"Could not query maximum file descriptor limit: $MAX_FD_LIMIT\"\n    fi\nfi\n\n# For Darwin, add options to specify how the application appears in the dock\nif $darwin; then\n    GRADLE_OPTS=\"$GRADLE_OPTS \\\"-Xdock:name=$APP_NAME\\\" \\\"-Xdock:icon=$APP_HOME/media/gradle.icns\\\"\"\nfi\n\n# For Cygwin or MSYS, switch paths to Windows format before running java\nif [ \"$cygwin\" = \"true\" -o \"$msys\" = \"true\" ] ; then\n    APP_HOME=`cygpath --path --mixed \"$APP_HOME\"`\n    CLASSPATH=`cygpath --path --mixed \"$CLASSPATH\"`\n\n    JAVACMD=`cygpath --unix \"$JAVACMD\"`\n\n    # We build the pattern for arguments to be converted via cygpath\n    ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`\n    SEP=\"\"\n    for dir in $ROOTDIRSRAW ; do\n        ROOTDIRS=\"$ROOTDIRS$SEP$dir\"\n        SEP=\"|\"\n    done\n    OURCYGPATTERN=\"(^($ROOTDIRS))\"\n    # Add a user-defined pattern to the cygpath arguments\n    if [ \"$GRADLE_CYGPATTERN\" != \"\" ] ; then\n        OURCYGPATTERN=\"$OURCYGPATTERN|($GRADLE_CYGPATTERN)\"\n    fi\n    # Now convert the arguments - kludge to limit ourselves to /bin/sh\n    i=0\n    for arg in \"$@\" ; do\n        CHECK=`echo \"$arg\"|egrep -c \"$OURCYGPATTERN\" -`\n        CHECK2=`echo \"$arg\"|egrep -c \"^-\"`                                 ### Determine if an option\n\n        if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then                    ### Added a condition\n            eval `echo args$i`=`cygpath --path --ignore --mixed \"$arg\"`\n        else\n            eval `echo args$i`=\"\\\"$arg\\\"\"\n        fi\n        i=`expr $i + 1`\n    done\n    case $i in\n        0) set -- ;;\n        1) set -- \"$args0\" ;;\n        2) set -- \"$args0\" \"$args1\" ;;\n        3) set -- \"$args0\" \"$args1\" \"$args2\" ;;\n        4) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" ;;\n        5) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" ;;\n        6) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" ;;\n        7) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" ;;\n        8) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" ;;\n        9) set -- \"$args0\" \"$args1\" \"$args2\" \"$args3\" \"$args4\" \"$args5\" \"$args6\" \"$args7\" \"$args8\" ;;\n    esac\nfi\n\n# Escape application args\nsave () {\n    for i do printf %s\\\\n \"$i\" | sed \"s/'/'\\\\\\\\''/g;1s/^/'/;\\$s/\\$/' \\\\\\\\/\" ; done\n    echo \" \"\n}\nAPP_ARGS=`save \"$@\"`\n\n# Collect all arguments for the java command, following the shell quoting and substitution rules\neval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS \"\\\"-Dorg.gradle.appname=$APP_BASE_NAME\\\"\" -classpath \"\\\"$CLASSPATH\\\"\" org.gradle.wrapper.GradleWrapperMain \"$APP_ARGS\"\n\nexec \"$JAVACMD\" \"$@\"\n"
  },
  {
    "path": "gradlew.bat",
    "content": "@rem\n@rem Copyright 2015 the original author or authors.\n@rem\n@rem Licensed under the Apache License, Version 2.0 (the \"License\");\n@rem you may not use this file except in compliance with the License.\n@rem You may obtain a copy of the License at\n@rem\n@rem      https://www.apache.org/licenses/LICENSE-2.0\n@rem\n@rem Unless required by applicable law or agreed to in writing, software\n@rem distributed under the License is distributed on an \"AS IS\" BASIS,\n@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n@rem See the License for the specific language governing permissions and\n@rem limitations under the License.\n@rem\n\n@if \"%DEBUG%\" == \"\" @echo off\n@rem ##########################################################################\n@rem\n@rem  Gradle startup script for Windows\n@rem\n@rem ##########################################################################\n\n@rem Set local scope for the variables with windows NT shell\nif \"%OS%\"==\"Windows_NT\" setlocal\n\nset DIRNAME=%~dp0\nif \"%DIRNAME%\" == \"\" set DIRNAME=.\nset APP_BASE_NAME=%~n0\nset APP_HOME=%DIRNAME%\n\n@rem Resolve any \".\" and \"..\" in APP_HOME to make it shorter.\nfor %%i in (\"%APP_HOME%\") do set APP_HOME=%%~fi\n\n@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.\nset DEFAULT_JVM_OPTS=\"-Xmx64m\" \"-Xms64m\"\n\n@rem Find java.exe\nif defined JAVA_HOME goto findJavaFromJavaHome\n\nset JAVA_EXE=java.exe\n%JAVA_EXE% -version >NUL 2>&1\nif \"%ERRORLEVEL%\" == \"0\" goto execute\n\necho.\necho ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:findJavaFromJavaHome\nset JAVA_HOME=%JAVA_HOME:\"=%\nset JAVA_EXE=%JAVA_HOME%/bin/java.exe\n\nif exist \"%JAVA_EXE%\" goto execute\n\necho.\necho ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%\necho.\necho Please set the JAVA_HOME variable in your environment to match the\necho location of your Java installation.\n\ngoto fail\n\n:execute\n@rem Setup the command line\n\nset CLASSPATH=%APP_HOME%\\gradle\\wrapper\\gradle-wrapper.jar\n\n\n@rem Execute Gradle\n\"%JAVA_EXE%\" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% \"-Dorg.gradle.appname=%APP_BASE_NAME%\" -classpath \"%CLASSPATH%\" org.gradle.wrapper.GradleWrapperMain %*\n\n:end\n@rem End local scope for the variables with windows NT shell\nif \"%ERRORLEVEL%\"==\"0\" goto mainEnd\n\n:fail\nrem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of\nrem the _cmd.exe /c_ return code!\nif  not \"\" == \"%GRADLE_EXIT_CONSOLE%\" exit 1\nexit /b 1\n\n:mainEnd\nif \"%OS%\"==\"Windows_NT\" endlocal\n\n:omega\n"
  },
  {
    "path": "iOSApp/iOSApp/Assets.xcassets/AccentColor.colorset/Contents.json",
    "content": "{\n  \"colors\" : [\n    {\n      \"idiom\" : \"universal\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iOSApp/iOSApp/Assets.xcassets/AppIcon.appiconset/Contents.json",
    "content": "{\n  \"images\" : [\n    {\n      \"idiom\" : \"universal\",\n      \"platform\" : \"ios\",\n      \"size\" : \"1024x1024\"\n    }\n  ],\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iOSApp/iOSApp/Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iOSApp/iOSApp/ContentView.swift",
    "content": "//\n//  ContentView.swift\n//  iOSApp\n//\n//  Created by David Odari Kiribwa on 07.03.24.\n//\n\nimport SwiftUI\nimport shared\n\nstruct ContentView: View {\n    var body: some View {\n        VStack {\n            Image(systemName: \"globe\")\n                .imageScale(.large)\n                .foregroundStyle(.tint) }\n        .padding()\n    }\n}\n\n#Preview {\n    ContentView()\n}\n"
  },
  {
    "path": "iOSApp/iOSApp/Preview Content/Preview Assets.xcassets/Contents.json",
    "content": "{\n  \"info\" : {\n    \"author\" : \"xcode\",\n    \"version\" : 1\n  }\n}\n"
  },
  {
    "path": "iOSApp/iOSApp/iOSAppApp.swift",
    "content": "//\n//  iOSAppApp.swift\n//  iOSApp\n//\n//  Created by David Odari Kiribwa on 07.03.24.\n//\n\nimport SwiftUI\n\n@main\nstruct iOSAppApp: App {\n    var body: some Scene {\n        WindowGroup {\n            ContentView()\n        }\n    }\n}\n"
  },
  {
    "path": "iOSApp/iOSApp.xcodeproj/project.pbxproj",
    "content": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 56;\n\tobjects = {\n\n/* Begin PBXBuildFile section */\n\t\t666856C12B993D62003C0CC3 /* iOSAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666856C02B993D62003C0CC3 /* iOSAppApp.swift */; };\n\t\t666856C32B993D62003C0CC3 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666856C22B993D62003C0CC3 /* ContentView.swift */; };\n\t\t666856C52B993D66003C0CC3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666856C42B993D66003C0CC3 /* Assets.xcassets */; };\n\t\t666856C82B993D66003C0CC3 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 666856C72B993D66003C0CC3 /* Preview Assets.xcassets */; };\n\t\t666856D22B993D66003C0CC3 /* iOSAppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666856D12B993D66003C0CC3 /* iOSAppTests.swift */; };\n\t\t666856DC2B993D66003C0CC3 /* iOSAppUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666856DB2B993D66003C0CC3 /* iOSAppUITests.swift */; };\n\t\t666856DE2B993D66003C0CC3 /* iOSAppUITestsLaunchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 666856DD2B993D66003C0CC3 /* iOSAppUITestsLaunchTests.swift */; };\n/* End PBXBuildFile section */\n\n/* Begin PBXContainerItemProxy section */\n\t\t666856CE2B993D66003C0CC3 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 666856B52B993D62003C0CC3 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 666856BC2B993D62003C0CC3;\n\t\t\tremoteInfo = iOSApp;\n\t\t};\n\t\t666856D82B993D66003C0CC3 /* PBXContainerItemProxy */ = {\n\t\t\tisa = PBXContainerItemProxy;\n\t\t\tcontainerPortal = 666856B52B993D62003C0CC3 /* Project object */;\n\t\t\tproxyType = 1;\n\t\t\tremoteGlobalIDString = 666856BC2B993D62003C0CC3;\n\t\t\tremoteInfo = iOSApp;\n\t\t};\n/* End PBXContainerItemProxy section */\n\n/* Begin PBXFileReference section */\n\t\t666856BD2B993D62003C0CC3 /* iOSApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iOSApp.app; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t666856C02B993D62003C0CC3 /* iOSAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppApp.swift; sourceTree = \"<group>\"; };\n\t\t666856C22B993D62003C0CC3 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = \"<group>\"; };\n\t\t666856C42B993D66003C0CC3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = \"<group>\"; };\n\t\t666856C72B993D66003C0CC3 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = \"Preview Assets.xcassets\"; sourceTree = \"<group>\"; };\n\t\t666856CD2B993D66003C0CC3 /* iOSAppTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSAppTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t666856D12B993D66003C0CC3 /* iOSAppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppTests.swift; sourceTree = \"<group>\"; };\n\t\t666856D72B993D66003C0CC3 /* iOSAppUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = iOSAppUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };\n\t\t666856DB2B993D66003C0CC3 /* iOSAppUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppUITests.swift; sourceTree = \"<group>\"; };\n\t\t666856DD2B993D66003C0CC3 /* iOSAppUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iOSAppUITestsLaunchTests.swift; sourceTree = \"<group>\"; };\n/* End PBXFileReference section */\n\n/* Begin PBXFrameworksBuildPhase section */\n\t\t666856BA2B993D62003C0CC3 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856CA2B993D66003C0CC3 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856D42B993D66003C0CC3 /* Frameworks */ = {\n\t\t\tisa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXFrameworksBuildPhase section */\n\n/* Begin PBXGroup section */\n\t\t666856B42B993D62003C0CC3 = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856BF2B993D62003C0CC3 /* iOSApp */,\n\t\t\t\t666856D02B993D66003C0CC3 /* iOSAppTests */,\n\t\t\t\t666856DA2B993D66003C0CC3 /* iOSAppUITests */,\n\t\t\t\t666856BE2B993D62003C0CC3 /* Products */,\n\t\t\t);\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t666856BE2B993D62003C0CC3 /* Products */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856BD2B993D62003C0CC3 /* iOSApp.app */,\n\t\t\t\t666856CD2B993D66003C0CC3 /* iOSAppTests.xctest */,\n\t\t\t\t666856D72B993D66003C0CC3 /* iOSAppUITests.xctest */,\n\t\t\t);\n\t\t\tname = Products;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t666856BF2B993D62003C0CC3 /* iOSApp */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856C02B993D62003C0CC3 /* iOSAppApp.swift */,\n\t\t\t\t666856C22B993D62003C0CC3 /* ContentView.swift */,\n\t\t\t\t666856C42B993D66003C0CC3 /* Assets.xcassets */,\n\t\t\t\t666856C62B993D66003C0CC3 /* Preview Content */,\n\t\t\t);\n\t\t\tpath = iOSApp;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t666856C62B993D66003C0CC3 /* Preview Content */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856C72B993D66003C0CC3 /* Preview Assets.xcassets */,\n\t\t\t);\n\t\t\tpath = \"Preview Content\";\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t666856D02B993D66003C0CC3 /* iOSAppTests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856D12B993D66003C0CC3 /* iOSAppTests.swift */,\n\t\t\t);\n\t\t\tpath = iOSAppTests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n\t\t666856DA2B993D66003C0CC3 /* iOSAppUITests */ = {\n\t\t\tisa = PBXGroup;\n\t\t\tchildren = (\n\t\t\t\t666856DB2B993D66003C0CC3 /* iOSAppUITests.swift */,\n\t\t\t\t666856DD2B993D66003C0CC3 /* iOSAppUITestsLaunchTests.swift */,\n\t\t\t);\n\t\t\tpath = iOSAppUITests;\n\t\t\tsourceTree = \"<group>\";\n\t\t};\n/* End PBXGroup section */\n\n/* Begin PBXNativeTarget section */\n\t\t666856BC2B993D62003C0CC3 /* iOSApp */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 666856E12B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSApp\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t666856EB2B993F81003C0CC3 /* ShellScript */,\n\t\t\t\t666856B92B993D62003C0CC3 /* Sources */,\n\t\t\t\t666856BA2B993D62003C0CC3 /* Frameworks */,\n\t\t\t\t666856BB2B993D62003C0CC3 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t);\n\t\t\tname = iOSApp;\n\t\t\tproductName = iOSApp;\n\t\t\tproductReference = 666856BD2B993D62003C0CC3 /* iOSApp.app */;\n\t\t\tproductType = \"com.apple.product-type.application\";\n\t\t};\n\t\t666856CC2B993D66003C0CC3 /* iOSAppTests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 666856E42B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSAppTests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t666856C92B993D66003C0CC3 /* Sources */,\n\t\t\t\t666856CA2B993D66003C0CC3 /* Frameworks */,\n\t\t\t\t666856CB2B993D66003C0CC3 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t666856CF2B993D66003C0CC3 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = iOSAppTests;\n\t\t\tproductName = iOSAppTests;\n\t\t\tproductReference = 666856CD2B993D66003C0CC3 /* iOSAppTests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.unit-test\";\n\t\t};\n\t\t666856D62B993D66003C0CC3 /* iOSAppUITests */ = {\n\t\t\tisa = PBXNativeTarget;\n\t\t\tbuildConfigurationList = 666856E72B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSAppUITests\" */;\n\t\t\tbuildPhases = (\n\t\t\t\t666856D32B993D66003C0CC3 /* Sources */,\n\t\t\t\t666856D42B993D66003C0CC3 /* Frameworks */,\n\t\t\t\t666856D52B993D66003C0CC3 /* Resources */,\n\t\t\t);\n\t\t\tbuildRules = (\n\t\t\t);\n\t\t\tdependencies = (\n\t\t\t\t666856D92B993D66003C0CC3 /* PBXTargetDependency */,\n\t\t\t);\n\t\t\tname = iOSAppUITests;\n\t\t\tproductName = iOSAppUITests;\n\t\t\tproductReference = 666856D72B993D66003C0CC3 /* iOSAppUITests.xctest */;\n\t\t\tproductType = \"com.apple.product-type.bundle.ui-testing\";\n\t\t};\n/* End PBXNativeTarget section */\n\n/* Begin PBXProject section */\n\t\t666856B52B993D62003C0CC3 /* Project object */ = {\n\t\t\tisa = PBXProject;\n\t\t\tattributes = {\n\t\t\t\tBuildIndependentTargetsInParallel = 1;\n\t\t\t\tLastSwiftUpdateCheck = 1520;\n\t\t\t\tLastUpgradeCheck = 1520;\n\t\t\t\tTargetAttributes = {\n\t\t\t\t\t666856BC2B993D62003C0CC3 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 15.2;\n\t\t\t\t\t};\n\t\t\t\t\t666856CC2B993D66003C0CC3 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 15.2;\n\t\t\t\t\t\tTestTargetID = 666856BC2B993D62003C0CC3;\n\t\t\t\t\t};\n\t\t\t\t\t666856D62B993D66003C0CC3 = {\n\t\t\t\t\t\tCreatedOnToolsVersion = 15.2;\n\t\t\t\t\t\tTestTargetID = 666856BC2B993D62003C0CC3;\n\t\t\t\t\t};\n\t\t\t\t};\n\t\t\t};\n\t\t\tbuildConfigurationList = 666856B82B993D62003C0CC3 /* Build configuration list for PBXProject \"iOSApp\" */;\n\t\t\tcompatibilityVersion = \"Xcode 14.0\";\n\t\t\tdevelopmentRegion = en;\n\t\t\thasScannedForEncodings = 0;\n\t\t\tknownRegions = (\n\t\t\t\ten,\n\t\t\t\tBase,\n\t\t\t);\n\t\t\tmainGroup = 666856B42B993D62003C0CC3;\n\t\t\tproductRefGroup = 666856BE2B993D62003C0CC3 /* Products */;\n\t\t\tprojectDirPath = \"\";\n\t\t\tprojectRoot = \"\";\n\t\t\ttargets = (\n\t\t\t\t666856BC2B993D62003C0CC3 /* iOSApp */,\n\t\t\t\t666856CC2B993D66003C0CC3 /* iOSAppTests */,\n\t\t\t\t666856D62B993D66003C0CC3 /* iOSAppUITests */,\n\t\t\t);\n\t\t};\n/* End PBXProject section */\n\n/* Begin PBXResourcesBuildPhase section */\n\t\t666856BB2B993D62003C0CC3 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t666856C82B993D66003C0CC3 /* Preview Assets.xcassets in Resources */,\n\t\t\t\t666856C52B993D66003C0CC3 /* Assets.xcassets in Resources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856CB2B993D66003C0CC3 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856D52B993D66003C0CC3 /* Resources */ = {\n\t\t\tisa = PBXResourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXResourcesBuildPhase section */\n\n/* Begin PBXShellScriptBuildPhase section */\n\t\t666856EB2B993F81003C0CC3 /* ShellScript */ = {\n\t\t\tisa = PBXShellScriptBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t);\n\t\t\tinputFileListPaths = (\n\t\t\t);\n\t\t\tinputPaths = (\n\t\t\t);\n\t\t\toutputFileListPaths = (\n\t\t\t);\n\t\t\toutputPaths = (\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t\tshellPath = /bin/sh;\n\t\t\tshellScript = \"cd \\\"$SRCROOT/..\\\"\\n./gradlew :shared:embedAndSignAppleFrameworkForXcode\\n\";\n\t\t};\n/* End PBXShellScriptBuildPhase section */\n\n/* Begin PBXSourcesBuildPhase section */\n\t\t666856B92B993D62003C0CC3 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t666856C32B993D62003C0CC3 /* ContentView.swift in Sources */,\n\t\t\t\t666856C12B993D62003C0CC3 /* iOSAppApp.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856C92B993D66003C0CC3 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t666856D22B993D66003C0CC3 /* iOSAppTests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n\t\t666856D32B993D66003C0CC3 /* Sources */ = {\n\t\t\tisa = PBXSourcesBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = (\n\t\t\t\t666856DE2B993D66003C0CC3 /* iOSAppUITestsLaunchTests.swift in Sources */,\n\t\t\t\t666856DC2B993D66003C0CC3 /* iOSAppUITests.swift in Sources */,\n\t\t\t);\n\t\t\trunOnlyForDeploymentPostprocessing = 0;\n\t\t};\n/* End PBXSourcesBuildPhase section */\n\n/* Begin PBXTargetDependency section */\n\t\t666856CF2B993D66003C0CC3 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 666856BC2B993D62003C0CC3 /* iOSApp */;\n\t\t\ttargetProxy = 666856CE2B993D66003C0CC3 /* PBXContainerItemProxy */;\n\t\t};\n\t\t666856D92B993D66003C0CC3 /* PBXTargetDependency */ = {\n\t\t\tisa = PBXTargetDependency;\n\t\t\ttarget = 666856BC2B993D62003C0CC3 /* iOSApp */;\n\t\t\ttargetProxy = 666856D82B993D66003C0CC3 /* PBXContainerItemProxy */;\n\t\t};\n/* End PBXTargetDependency section */\n\n/* Begin XCBuildConfiguration section */\n\t\t666856DF2B993D66003C0CC3 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = dwarf;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_TESTABILITY = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGCC_DYNAMIC_NO_PIC = NO;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_OPTIMIZATION_LEVEL = 0;\n\t\t\t\tGCC_PREPROCESSOR_DEFINITIONS = (\n\t\t\t\t\t\"DEBUG=1\",\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t);\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.2;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tONLY_ACTIVE_ARCH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_ACTIVE_COMPILATION_CONDITIONS = \"DEBUG $(inherited)\";\n\t\t\t\tSWIFT_OPTIMIZATION_LEVEL = \"-Onone\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t666856E02B993D66003C0CC3 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_SEARCH_USER_PATHS = NO;\n\t\t\t\tASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;\n\t\t\t\tCLANG_ANALYZER_NONNULL = YES;\n\t\t\t\tCLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;\n\t\t\t\tCLANG_CXX_LANGUAGE_STANDARD = \"gnu++20\";\n\t\t\t\tCLANG_ENABLE_MODULES = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_ARC = YES;\n\t\t\t\tCLANG_ENABLE_OBJC_WEAK = YES;\n\t\t\t\tCLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;\n\t\t\t\tCLANG_WARN_BOOL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_COMMA = YES;\n\t\t\t\tCLANG_WARN_CONSTANT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;\n\t\t\t\tCLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;\n\t\t\t\tCLANG_WARN_DOCUMENTATION_COMMENTS = YES;\n\t\t\t\tCLANG_WARN_EMPTY_BODY = YES;\n\t\t\t\tCLANG_WARN_ENUM_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_INFINITE_RECURSION = YES;\n\t\t\t\tCLANG_WARN_INT_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;\n\t\t\t\tCLANG_WARN_OBJC_LITERAL_CONVERSION = YES;\n\t\t\t\tCLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;\n\t\t\t\tCLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;\n\t\t\t\tCLANG_WARN_RANGE_LOOP_ANALYSIS = YES;\n\t\t\t\tCLANG_WARN_STRICT_PROTOTYPES = YES;\n\t\t\t\tCLANG_WARN_SUSPICIOUS_MOVE = YES;\n\t\t\t\tCLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;\n\t\t\t\tCLANG_WARN_UNREACHABLE_CODE = YES;\n\t\t\t\tCLANG_WARN__DUPLICATE_METHOD_MATCH = YES;\n\t\t\t\tCOPY_PHASE_STRIP = NO;\n\t\t\t\tDEBUG_INFORMATION_FORMAT = \"dwarf-with-dsym\";\n\t\t\t\tENABLE_NS_ASSERTIONS = NO;\n\t\t\t\tENABLE_STRICT_OBJC_MSGSEND = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = YES;\n\t\t\t\tGCC_C_LANGUAGE_STANDARD = gnu17;\n\t\t\t\tGCC_NO_COMMON_BLOCKS = YES;\n\t\t\t\tGCC_WARN_64_TO_32_BIT_CONVERSION = YES;\n\t\t\t\tGCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;\n\t\t\t\tGCC_WARN_UNDECLARED_SELECTOR = YES;\n\t\t\t\tGCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;\n\t\t\t\tGCC_WARN_UNUSED_FUNCTION = YES;\n\t\t\t\tGCC_WARN_UNUSED_VARIABLE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.2;\n\t\t\t\tLOCALIZATION_PREFERS_STRING_CATALOGS = YES;\n\t\t\t\tMTL_ENABLE_DEBUG_INFO = NO;\n\t\t\t\tMTL_FAST_MATH = YES;\n\t\t\t\tSDKROOT = iphoneos;\n\t\t\t\tSWIFT_COMPILATION_MODE = wholemodule;\n\t\t\t\tVALIDATE_PRODUCT = YES;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t666856E22B993D66003C0CC3 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iOSApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = 7NFZ339747;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = \"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\";\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSApp;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t666856E32B993D66003C0CC3 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;\n\t\t\t\tASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tDEVELOPMENT_ASSET_PATHS = \"\\\"iOSApp/Preview Content\\\"\";\n\t\t\t\tDEVELOPMENT_TEAM = 7NFZ339747;\n\t\t\t\tENABLE_PREVIEWS = YES;\n\t\t\t\tENABLE_USER_SCRIPT_SANDBOXING = NO;\n\t\t\t\tFRAMEWORK_SEARCH_PATHS = \"$(SRCROOT)/../shared/build/xcode-frameworks/$(CONFIGURATION)/$(SDK_NAME)\";\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;\n\t\t\t\tINFOPLIST_KEY_UILaunchScreen_Generation = YES;\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = \"UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tINFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = \"UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight\";\n\t\t\t\tLD_RUNPATH_SEARCH_PATHS = (\n\t\t\t\t\t\"$(inherited)\",\n\t\t\t\t\t\"@executable_path/Frameworks\",\n\t\t\t\t);\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSApp;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = YES;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t666856E52B993D66003C0CC3 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.2;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSAppTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/iOSApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSApp\";\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t666856E62B993D66003C0CC3 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tBUNDLE_LOADER = \"$(TEST_HOST)\";\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tIPHONEOS_DEPLOYMENT_TARGET = 17.2;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSAppTests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_HOST = \"$(BUILT_PRODUCTS_DIR)/iOSApp.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/iOSApp\";\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n\t\t666856E82B993D66003C0CC3 /* Debug */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSAppUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = iOSApp;\n\t\t\t};\n\t\t\tname = Debug;\n\t\t};\n\t\t666856E92B993D66003C0CC3 /* Release */ = {\n\t\t\tisa = XCBuildConfiguration;\n\t\t\tbuildSettings = {\n\t\t\t\tALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;\n\t\t\t\tCODE_SIGN_STYLE = Automatic;\n\t\t\t\tCURRENT_PROJECT_VERSION = 1;\n\t\t\t\tGENERATE_INFOPLIST_FILE = YES;\n\t\t\t\tMARKETING_VERSION = 1.0;\n\t\t\t\tPRODUCT_BUNDLE_IDENTIFIER = com.odaridavid.github.weatherapp.iOSAppUITests;\n\t\t\t\tPRODUCT_NAME = \"$(TARGET_NAME)\";\n\t\t\t\tSWIFT_EMIT_LOC_STRINGS = NO;\n\t\t\t\tSWIFT_VERSION = 5.0;\n\t\t\t\tTARGETED_DEVICE_FAMILY = \"1,2\";\n\t\t\t\tTEST_TARGET_NAME = iOSApp;\n\t\t\t};\n\t\t\tname = Release;\n\t\t};\n/* End XCBuildConfiguration section */\n\n/* Begin XCConfigurationList section */\n\t\t666856B82B993D62003C0CC3 /* Build configuration list for PBXProject \"iOSApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t666856DF2B993D66003C0CC3 /* Debug */,\n\t\t\t\t666856E02B993D66003C0CC3 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t666856E12B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSApp\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t666856E22B993D66003C0CC3 /* Debug */,\n\t\t\t\t666856E32B993D66003C0CC3 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t666856E42B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSAppTests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t666856E52B993D66003C0CC3 /* Debug */,\n\t\t\t\t666856E62B993D66003C0CC3 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n\t\t666856E72B993D66003C0CC3 /* Build configuration list for PBXNativeTarget \"iOSAppUITests\" */ = {\n\t\t\tisa = XCConfigurationList;\n\t\t\tbuildConfigurations = (\n\t\t\t\t666856E82B993D66003C0CC3 /* Debug */,\n\t\t\t\t666856E92B993D66003C0CC3 /* Release */,\n\t\t\t);\n\t\t\tdefaultConfigurationIsVisible = 0;\n\t\t\tdefaultConfigurationName = Release;\n\t\t};\n/* End XCConfigurationList section */\n\t};\n\trootObject = 666856B52B993D62003C0CC3 /* Project object */;\n}\n"
  },
  {
    "path": "iOSApp/iOSApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Workspace\n   version = \"1.0\">\n   <FileRef\n      location = \"self:\">\n   </FileRef>\n</Workspace>\n"
  },
  {
    "path": "iOSApp/iOSApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>IDEDidComputeMac32BitWarning</key>\n\t<true/>\n</dict>\n</plist>\n"
  },
  {
    "path": "iOSApp/iOSApp.xcodeproj/xcuserdata/odari.xcuserdatad/xcschemes/xcschememanagement.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>SchemeUserState</key>\n\t<dict>\n\t\t<key>iOSApp.xcscheme_^#shared#^_</key>\n\t\t<dict>\n\t\t\t<key>orderHint</key>\n\t\t\t<integer>0</integer>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "iOSApp/iOSAppTests/iOSAppTests.swift",
    "content": "//\n//  iOSAppTests.swift\n//  iOSAppTests\n//\n//  Created by David Odari Kiribwa on 07.03.24.\n//\n\nimport XCTest\n@testable import iOSApp\n\nfinal class iOSAppTests: XCTestCase {\n\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // This is an example of a functional test case.\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n        // Any test you write for XCTest can be annotated as throws and async.\n        // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.\n        // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards.\n    }\n\n    func testPerformanceExample() throws {\n        // This is an example of a performance test case.\n        self.measure {\n            // Put the code you want to measure the time of here.\n        }\n    }\n\n}\n"
  },
  {
    "path": "iOSApp/iOSAppUITests/iOSAppUITests.swift",
    "content": "//\n//  iOSAppUITests.swift\n//  iOSAppUITests\n//\n//  Created by David Odari Kiribwa on 07.03.24.\n//\n\nimport XCTest\n\nfinal class iOSAppUITests: XCTestCase {\n\n    override func setUpWithError() throws {\n        // Put setup code here. This method is called before the invocation of each test method in the class.\n\n        // In UI tests it is usually best to stop immediately when a failure occurs.\n        continueAfterFailure = false\n\n        // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.\n    }\n\n    override func tearDownWithError() throws {\n        // Put teardown code here. This method is called after the invocation of each test method in the class.\n    }\n\n    func testExample() throws {\n        // UI tests must launch the application that they test.\n        let app = XCUIApplication()\n        app.launch()\n\n        // Use XCTAssert and related functions to verify your tests produce the correct results.\n    }\n\n    func testLaunchPerformance() throws {\n        if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {\n            // This measures how long it takes to launch your application.\n            measure(metrics: [XCTApplicationLaunchMetric()]) {\n                XCUIApplication().launch()\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "iOSApp/iOSAppUITests/iOSAppUITestsLaunchTests.swift",
    "content": "//\n//  iOSAppUITestsLaunchTests.swift\n//  iOSAppUITests\n//\n//  Created by David Odari Kiribwa on 07.03.24.\n//\n\nimport XCTest\n\nfinal class iOSAppUITestsLaunchTests: XCTestCase {\n\n    override class var runsForEachTargetApplicationUIConfiguration: Bool {\n        true\n    }\n\n    override func setUpWithError() throws {\n        continueAfterFailure = false\n    }\n\n    func testLaunch() throws {\n        let app = XCUIApplication()\n        app.launch()\n\n        // Insert steps here to perform after app launch but before taking a screenshot,\n        // such as logging into a test account or navigating somewhere in the app\n\n        let attachment = XCTAttachment(screenshot: app.screenshot())\n        attachment.name = \"Launch Screen\"\n        attachment.lifetime = .keepAlways\n        add(attachment)\n    }\n}\n"
  },
  {
    "path": "pull_request_template.md",
    "content": "## Related Issue\n\n## Description\n\n## How Can It Be Tested\n\n## Screenshots (If Applicable)\n\n## Additional Comments\n\n## Checklist\n\n- [ ] New tests were added/Existing Modified\n"
  },
  {
    "path": "settings.gradle.kts",
    "content": "pluginManagement {\n    repositories {\n        gradlePluginPortal()\n        google()\n        mavenCentral()\n    }\n}\ndependencyResolutionManagement {\n    repositories {\n        google()\n        mavenCentral()\n    }\n}\nrootProject.name = \"WeatherApp\"\ninclude(\":app\")\ninclude(\":shared\")\n"
  },
  {
    "path": "shared/build.gradle.kts",
    "content": "plugins {\n    alias(libs.plugins.kotlinMultiplatform)\n    alias(libs.plugins.com.android.library)\n}\n\nkotlin {\n    androidTarget {\n        compilations.all {\n            kotlinOptions {\n                jvmTarget = \"1.8\"\n            }\n        }\n    }\n\n    listOf(\n        iosX64(),\n        iosArm64(),\n        iosSimulatorArm64()\n    ).forEach {\n        it.binaries.framework {\n            baseName = \"shared\"\n            isStatic = true\n        }\n    }\n\n    sourceSets {\n        commonMain.dependencies {\n            implementation(libs.coroutines)\n        }\n        commonTest.dependencies {\n            // TODO Add common test dependencies\n        }\n    }\n}\n\nandroid {\n    namespace = \"com.github.odaridavid.weatherapp.shared\"\n    compileSdk = 34\n    defaultConfig {\n        minSdk = 23\n    }\n    compileOptions {\n        sourceCompatibility = JavaVersion.VERSION_1_8\n        targetCompatibility = JavaVersion.VERSION_1_8\n    }\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/api/Logger.kt",
    "content": "package com.github.odaridavid.weatherapp.api\n\ninterface Logger {\n    fun logException(throwable: Throwable)\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/api/SettingsRepository.kt",
    "content": "package com.github.odaridavid.weatherapp.api\n\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.ExcludedData\nimport com.github.odaridavid.weatherapp.model.SupportedLanguage\nimport com.github.odaridavid.weatherapp.model.TimeFormat\nimport com.github.odaridavid.weatherapp.model.Units\nimport kotlinx.coroutines.flow.Flow\n\ninterface SettingsRepository {\n    suspend fun setLanguage(language: SupportedLanguage)\n\n    suspend fun getLanguage(): Flow<SupportedLanguage>\n\n    suspend fun setUnits(units: Units)\n\n    suspend fun getUnits(): Flow<Units>\n\n    fun getAppVersion(): String\n\n    suspend fun setDefaultLocation(defaultLocation: DefaultLocation)\n\n    suspend fun getDefaultLocation(): Flow<DefaultLocation>\n\n    suspend fun getFormat(): Flow<TimeFormat>\n\n    suspend fun setFormat(format: TimeFormat)\n\n    suspend fun getExcludedData(): Flow<String>\n\n    suspend fun setExcludedData(excludedData: List<ExcludedData>)\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/api/WeatherRepository.kt",
    "content": "package com.github.odaridavid.weatherapp.api\n\nimport com.github.odaridavid.weatherapp.model.DefaultLocation\nimport com.github.odaridavid.weatherapp.model.Result\nimport com.github.odaridavid.weatherapp.model.Weather\n\ninterface WeatherRepository {\n\n    suspend fun fetchWeatherData(\n        defaultLocation: DefaultLocation,\n        language: String,\n        units: String\n    ) : Result<Weather>\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/DefaultLocation.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\ndata class DefaultLocation(val longitude: Double, val latitude: Double)\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/ExcludedData.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\nenum class ExcludedData(val value: String, val id: Int) {\n    CURRENT(\"current\", 0),\n    HOURLY(\"hourly\", 1),\n    DAILY(\"daily\", 2),\n    MINUTELY(\"minutely\", 3),\n    ALERTS(\"alerts\", 4),\n    NONE(\"none\", -1),\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/Result.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\nsealed class Result<T> {\n    data class Success<T>(val data: T) : Result<T>()\n\n    data class Error<T>(val errorType: ErrorType) : Result<T>()\n}\n\nenum class ErrorType {\n    CLIENT,\n    SERVER,\n    GENERIC,\n    IO_CONNECTION,\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/SupportedLanguage.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\nenum class SupportedLanguage(val languageName: String, val languageValue: String) {\n    AFRIKAANS(\"Afrikaans\", \"af\"),\n    ALBANIAN(\"Albanian\", \"al\"),\n    ARABIC(\"Arabic\", \"ar\"),\n    AZERBAIJANI(\"Azerbaijani\", \"az\"),\n    BULGARIAN(\"Bulgarian\", \"bg\"),\n    CATALAN(\"Catalan\", \"ca\"),\n    CZECH(\"Czech\", \"cz\"),\n    DANISH(\"Danish\", \"da\"),\n    GERMAN(\"German\", \"de\"),\n    GREEK(\"Greek\", \"el\"),\n    ENGLISH(\"English\", \"en\"),\n    BASQUE(\"Basque\", \"eu\"),\n    PERSIAN(\"Persian (Farsi)\", \"fa\"),\n    FINNISH(\"Finnish\", \"fi\"),\n    FRENCH(\"French\", \"fr\"),\n    GALICIAN(\"Galician\", \"gl\"),\n    HEBREW(\"Hebrew\", \"he\"),\n    HINDI(\"Hindi\", \"hi\"),\n    CROATIAN(\"Croatian\", \"hr\"),\n    HUNGARIAN(\"Hungarian\", \"hu\"),\n    INDONESIAN(\"Indonesian\", \"id\"),\n    ITALIAN(\"Italian\", \"it\"),\n    JAPANESE(\"Japanese\", \"ja\"),\n    KOREAN(\"Korean\", \"kr\"),\n    LATVIAN(\"Latvian\", \"la\"),\n    LITHUANIAN(\"Lithuanian\", \"lt\"),\n    MACEDONIAN(\"Macedonian\", \"mk\"),\n    NORWEGIAN(\"Norwegian\", \"no\"),\n    DUTCH(\"Dutch\", \"nl\"),\n    POLISH(\"Polish\", \"pl\"),\n    PORTUGUESE(\"Portuguese\", \"pt\"),\n    PORTUGUESE_BRAZIL(\"Português  Brasil\", \"pt_br\"),\n    ROMANIAN(\"Romanian\", \"ro\"),\n    RUSSIAN(\"Russian\", \"ru\"),\n    SWEDISH(\"Swedish\", \"sv, se\"),\n    SLOVAK(\"Slovak\", \"sk\"),\n    SLOVANIAN(\"Slovenian\", \"sl\"),\n    SPANISH(\"Spanish\", \"sp, es\"),\n    SERBIAN(\"Serbian\", \"sr\"),\n    THAI(\"Thai\", \"th\"),\n    TURKISH(\"Turkish\", \"tr\"),\n    UKRAINIAN(\"Ukrainian\", \"ua, uk\"),\n    VIETNAMESE(\"Vietnamese\", \"vi\"),\n    CHINESE_SIMPLIFIED(\"Chinese Simplified\", \"zh_cn\"),\n    CHINESE_TRADITIONAL(\"Chinese Traditional\", \"zh_tw\"),\n    ZULU(\"Zulu\", \"zu\")\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/Throwables.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\ndata class ClientException(override val message: String) : Throwable(message = message)\n\ndata class ServerException(override val message: String) : Throwable(message = message)\n\ndata class GenericException(override val message: String) : Throwable(message = message)\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/TimeFormat.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\nenum class TimeFormat(val value: String) {\n    TWENTY_FOUR_HOUR(\"24 hours\"),\n    TWELVE_HOUR(\"12 hours\")\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/Units.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\nenum class Units(val value: String, val tempLabel: String) {\n    STANDARD(\"standard\",\"°F\"),\n    METRIC(\"metric\",\"°C\"),\n    IMPERIAL(\"imperial\",\"°F\"),\n}\n"
  },
  {
    "path": "shared/src/commonMain/kotlin/com/github/odaridavid/weatherapp/model/Weather.kt",
    "content": "package com.github.odaridavid.weatherapp.model\n\ndata class Weather(\n    val current: CurrentWeather?,\n    val hourly: List<HourlyWeather>?,\n    val daily: List<DailyWeather>?\n)\n\ndata class CurrentWeather(\n    val temperature: String,\n    val feelsLike: String,\n    val weather: List<WeatherInfo>\n)\n\ndata class HourlyWeather(\n    val forecastedTime: String,\n    val temperature: String,\n    val weather: List<WeatherInfo>\n)\n\ndata class DailyWeather(\n    val forecastedTime: String,\n    val temperature: Temperature,\n    val weather: List<WeatherInfo>\n)\n\ndata class WeatherInfo(\n    val id: Int,\n    val main: String,\n    val description: String,\n    val icon: String\n)\n\ndata class Temperature(\n    val min: String,\n    val max: String,\n)\n"
  }
]