├── .DS_Store ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── readme │ ├── draft-release.png │ ├── intellij-platform-plugin-template.png │ ├── qodana.png │ ├── run-debug-configurations.png │ ├── run-logs.png │ ├── settings-secrets.png │ ├── ui-testing.png │ └── use-this-template.png └── workflows │ ├── build.yml │ ├── release.yml │ └── run-ui-tests.yml ├── .gitignore ├── .idea ├── gradle.xml └── icon.png ├── .run ├── Run IDE for UI Tests.run.xml ├── Run IDE with Plugin.run.xml ├── Run Plugin Tests.run.xml ├── Run Plugin Verification.run.xml └── Run Qodana.run.xml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── assets └── demo.gif ├── build.gradle.kts ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src ├── main ├── kotlin │ └── com │ │ └── levinzonr │ │ └── arch │ │ └── jetpackcompose │ │ └── plugin │ │ ├── core │ │ ├── BaseDialog.kt │ │ ├── BaseViewModel.kt │ │ ├── Links.kt │ │ ├── ObservableValue.kt │ │ ├── ProgressDialog.kt │ │ ├── PropertyKeys.kt │ │ ├── ResourceLoader.kt │ │ ├── TemplateGenerator.kt │ │ └── persistence │ │ │ ├── PreferencesDataSource.kt │ │ │ └── PreferencesDataSourceImpl.kt │ │ ├── dependencies │ │ ├── Dependencies.kt │ │ ├── PluginDependencies.kt │ │ └── ProjectDependencies.kt │ │ └── features │ │ ├── ai │ │ └── domain │ │ │ ├── AIGenerator.kt │ │ │ ├── FeatureBreakdownGenerator.kt │ │ │ └── models │ │ │ ├── AIResponse.kt │ │ │ ├── Action.kt │ │ │ ├── ActionTypeSerializer.kt │ │ │ ├── FeatureBreakdown.kt │ │ │ └── StateProperties.kt │ │ ├── feedback │ │ └── FeedbackActions.kt │ │ ├── livetemplates │ │ └── TemplatesContext.kt │ │ ├── navigation │ │ ├── NavigationSettings.kt │ │ └── NavigationType.kt │ │ ├── newcomponent │ │ ├── ComposeComponentAction.kt │ │ ├── ComposeComponentDialog.kt │ │ └── ComposeComponentViewModel.kt │ │ ├── newfeature │ │ ├── domain │ │ │ ├── models │ │ │ │ ├── ActionsType.kt │ │ │ │ ├── FeatureConfiguration.kt │ │ │ │ ├── FeatureProperties.kt │ │ │ │ └── InjectionConfiguration.kt │ │ │ └── repository │ │ │ │ └── FeatureConfigurationRepository.kt │ │ ├── injection │ │ │ ├── AdvancedViewModelFactory.kt │ │ │ ├── ComposeArchDialogViewModelFactory.kt │ │ │ └── ExperimentalFeaturesRepositoryFactory.kt │ │ └── ui │ │ │ ├── ComposeArchAction.kt │ │ │ ├── ComposeArchDialog.kt │ │ │ ├── ComposeArchDialogViewModel.kt │ │ │ └── advanced │ │ │ ├── AdvancedDialog.kt │ │ │ └── AdvancedViewModel.kt │ │ ├── ollama │ │ ├── OllamaGenerator.kt │ │ └── OllamaSettings.kt │ │ └── settings │ │ ├── PluginSettings.kt │ │ ├── SettingsViewModel.kt │ │ ├── data │ │ └── SettingsRepositoryImpl.kt │ │ ├── domain │ │ ├── Settings.kt │ │ └── SettingsRepository.kt │ │ └── injection │ │ └── SettingsViewModelFactory.kt └── resources │ ├── META-INF │ ├── plugin.xml │ └── pluginIcon.svg │ ├── ai_response_example.json │ ├── fileTemplates │ └── internal │ │ ├── ComposeComponent.kt.ft │ │ ├── ComposeContract.kt.ft │ │ ├── ComposeCoordinator.kt.ft │ │ ├── ComposeRoute.kt.ft │ │ ├── ComposeScreen.kt.ft │ │ └── ComposeViewModel.kt.ft │ ├── icons │ ├── action_logo.svg │ └── icon_small.svg │ ├── liveTemplates │ ├── ComposeFoundation.xml │ └── ComposeUIArch.xml │ └── messages │ └── MyBundle.properties └── test └── testData └── rename ├── foo.xml └── foo_after.xml /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.DS_Store -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'bug' 6 | assignees: 'levinzonr' 7 | --- 8 | 9 | **Describe the bug:** 10 | 11 | 12 | **Steps to reproduce:** 13 | 14 | 15 | **Expected behavior:** 16 | 17 | 18 | **Additional context:** 19 | 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Feels like something is missing or can be improved? Ask away! 4 | title: '' 5 | labels: 'enhancement' 6 | assignees: 'levinzonr' 7 | --- 8 | 9 | **Choose what area you would us to improve** 10 | - [ ] Live templates (Composable functions shortcuts) 11 | - [ ] Architecture Templates (Templates for new features and components) 12 | - [ ] Other 13 | 14 | **Please describe the feature and/or improvement:** 15 | 16 | 17 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Dependabot configuration: 2 | # https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | # Maintain dependencies for Gradle dependencies 7 | - package-ecosystem: "gradle" 8 | directory: "/" 9 | target-branch: "next" 10 | schedule: 11 | interval: "daily" 12 | # Maintain dependencies for GitHub Actions 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | target-branch: "next" 16 | schedule: 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/readme/draft-release.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/draft-release.png -------------------------------------------------------------------------------- /.github/readme/intellij-platform-plugin-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/intellij-platform-plugin-template.png -------------------------------------------------------------------------------- /.github/readme/qodana.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/qodana.png -------------------------------------------------------------------------------- /.github/readme/run-debug-configurations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/run-debug-configurations.png -------------------------------------------------------------------------------- /.github/readme/run-logs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/run-logs.png -------------------------------------------------------------------------------- /.github/readme/settings-secrets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/settings-secrets.png -------------------------------------------------------------------------------- /.github/readme/ui-testing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/ui-testing.png -------------------------------------------------------------------------------- /.github/readme/use-this-template.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.github/readme/use-this-template.png -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow is created for testing and preparing the plugin release in the following steps: 2 | # - Validate Gradle Wrapper. 3 | # - Run 'test' and 'verifyPlugin' tasks. 4 | # - Run Qodana inspections. 5 | # - Run the 'buildPlugin' task and prepare artifact for further tests. 6 | # - Run the 'runPluginVerifier' task. 7 | # - Create a draft release. 8 | # 9 | # The workflow is triggered on push and pull_request events. 10 | # 11 | # GitHub Actions reference: https://help.github.com/en/actions 12 | # 13 | 14 | name: Build 15 | on: 16 | # Trigger the workflow on pushes to only the 'main' branch (this avoids duplicate checks being run e.g., for dependabot pull requests) 17 | push: 18 | branches: [ main ] 19 | # Trigger the workflow on any pull request 20 | pull_request: 21 | 22 | jobs: 23 | 24 | # Prepare environment and build the plugin 25 | build: 26 | name: Build 27 | runs-on: ubuntu-latest 28 | outputs: 29 | version: ${{ steps.properties.outputs.version }} 30 | changelog: ${{ steps.properties.outputs.changelog }} 31 | pluginVerifierHomeDir: ${{ steps.properties.outputs.pluginVerifierHomeDir }} 32 | steps: 33 | 34 | # Check out current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v3 37 | 38 | # Validate wrapper 39 | - name: Gradle Wrapper Validation 40 | uses: gradle/wrapper-validation-action@v1.1.0 41 | 42 | # Set up Java environment for the next steps 43 | - name: Setup Java 44 | uses: actions/setup-java@v3 45 | with: 46 | distribution: zulu 47 | java-version: 17 48 | 49 | # Setup Gradle 50 | - name: Setup Gradle 51 | uses: gradle/gradle-build-action@v2 52 | with: 53 | gradle-home-cache-cleanup: true 54 | 55 | # Set environment variables 56 | - name: Export Properties 57 | id: properties 58 | shell: bash 59 | run: | 60 | PROPERTIES="$(./gradlew properties --console=plain -q)" 61 | VERSION="$(echo "$PROPERTIES" | grep "^version:" | cut -f2- -d ' ')" 62 | CHANGELOG="$(./gradlew getChangelog --unreleased --no-header --console=plain -q)" 63 | 64 | echo "version=$VERSION" >> $GITHUB_OUTPUT 65 | echo "pluginVerifierHomeDir=~/.pluginVerifier" >> $GITHUB_OUTPUT 66 | 67 | echo "changelog<> $GITHUB_OUTPUT 68 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 69 | echo "EOF" >> $GITHUB_OUTPUT 70 | 71 | ./gradlew listProductsReleases # prepare list of IDEs for Plugin Verifier 72 | 73 | # Build plugin 74 | - name: Build plugin 75 | run: ./gradlew buildPlugin 76 | 77 | # Prepare plugin archive content for creating artifact 78 | - name: Prepare Plugin Artifact 79 | id: artifact 80 | shell: bash 81 | run: | 82 | cd ${{ github.workspace }}/build/distributions 83 | FILENAME=`ls *.zip` 84 | unzip "$FILENAME" -d content 85 | 86 | echo "filename=${FILENAME:0:-4}" >> $GITHUB_OUTPUT 87 | 88 | # Store already-built plugin as an artifact for downloading 89 | - name: Upload artifact 90 | uses: actions/upload-artifact@v4 91 | with: 92 | name: ${{ steps.artifact.outputs.filename }} 93 | path: ./build/distributions/content/*/* 94 | 95 | # Run tests and upload a code coverage report 96 | test: 97 | name: Test 98 | needs: [ build ] 99 | runs-on: ubuntu-latest 100 | steps: 101 | 102 | # Check out current repository 103 | - name: Fetch Sources 104 | uses: actions/checkout@v3 105 | 106 | # Set up Java environment for the next steps 107 | - name: Setup Java 108 | uses: actions/setup-java@v3 109 | with: 110 | distribution: zulu 111 | java-version: 17 112 | 113 | # Setup Gradle 114 | - name: Setup Gradle 115 | uses: gradle/gradle-build-action@v2 116 | with: 117 | gradle-home-cache-cleanup: true 118 | 119 | # Run tests 120 | - name: Run Tests 121 | run: ./gradlew check 122 | 123 | # Collect Tests Result of failed tests 124 | - name: Collect Tests Result 125 | if: ${{ failure() }} 126 | uses: actions/upload-artifact@v4 127 | with: 128 | name: tests-result 129 | path: ${{ github.workspace }}/build/reports/tests 130 | 131 | # Upload the Kover report to CodeCov 132 | - name: Upload Code Coverage Report 133 | uses: codecov/codecov-action@v3 134 | with: 135 | files: ${{ github.workspace }}/build/reports/kover/report.xml 136 | 137 | # Run Qodana inspections and provide report 138 | inspectCode: 139 | name: Inspect code 140 | needs: [ build ] 141 | runs-on: ubuntu-latest 142 | permissions: 143 | contents: write 144 | checks: write 145 | pull-requests: write 146 | steps: 147 | 148 | # Free GitHub Actions Environment Disk Space 149 | - name: Maximize Build Space 150 | uses: jlumbroso/free-disk-space@main 151 | with: 152 | tool-cache: false 153 | large-packages: false 154 | 155 | # Check out current repository 156 | - name: Fetch Sources 157 | uses: actions/checkout@v3 158 | 159 | # Set up Java environment for the next steps 160 | - name: Setup Java 161 | uses: actions/setup-java@v3 162 | with: 163 | distribution: zulu 164 | java-version: 17 165 | 166 | 167 | # Run plugin structure verification along with IntelliJ Plugin Verifier 168 | verify: 169 | name: Verify plugin 170 | needs: [ build ] 171 | runs-on: ubuntu-latest 172 | steps: 173 | 174 | # Free GitHub Actions Environment Disk Space 175 | - name: Maximize Build Space 176 | uses: jlumbroso/free-disk-space@main 177 | with: 178 | tool-cache: false 179 | large-packages: false 180 | 181 | # Check out current repository 182 | - name: Fetch Sources 183 | uses: actions/checkout@v3 184 | 185 | # Set up Java environment for the next steps 186 | - name: Setup Java 187 | uses: actions/setup-java@v3 188 | with: 189 | distribution: zulu 190 | java-version: 17 191 | 192 | # Setup Gradle 193 | - name: Setup Gradle 194 | uses: gradle/gradle-build-action@v2 195 | with: 196 | gradle-home-cache-cleanup: true 197 | 198 | # Cache Plugin Verifier IDEs 199 | - name: Setup Plugin Verifier IDEs Cache 200 | uses: actions/cache@v3 201 | with: 202 | path: ${{ needs.build.outputs.pluginVerifierHomeDir }}/ides 203 | key: plugin-verifier-${{ hashFiles('build/listProductsReleases.txt') }} 204 | 205 | # Run Verify Plugin task and IntelliJ Plugin Verifier tool 206 | - name: Run Plugin Verification tasks 207 | run: ./gradlew runPluginVerifier -Dplugin.verifier.home.dir=${{ needs.build.outputs.pluginVerifierHomeDir }} 208 | 209 | # Collect Plugin Verifier Result 210 | - name: Collect Plugin Verifier Result 211 | if: ${{ always() }} 212 | uses: actions/upload-artifact@v4 213 | with: 214 | name: pluginVerifier-result 215 | path: ${{ github.workspace }}/build/reports/pluginVerifier 216 | 217 | # Prepare a draft release for GitHub Releases page for the manual verification 218 | # If accepted and published, release workflow would be triggered 219 | releaseDraft: 220 | name: Release draft 221 | if: github.event_name != 'pull_request' 222 | needs: [ build, test, inspectCode, verify ] 223 | runs-on: ubuntu-latest 224 | permissions: 225 | contents: write 226 | steps: 227 | 228 | # Check out current repository 229 | - name: Fetch Sources 230 | uses: actions/checkout@v3 231 | 232 | # Set up Java environment for the next steps 233 | - name: Setup Java 234 | uses: actions/setup-java@v3 235 | with: 236 | distribution: zulu 237 | java-version: 17 238 | 239 | # Remove old release drafts by using the curl request for the available releases with a draft flag 240 | - name: Remove Old Release Drafts 241 | env: 242 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 243 | run: | 244 | gh api repos/{owner}/{repo}/releases \ 245 | --jq '.[] | select(.draft == true) | .id' \ 246 | | xargs -I '{}' gh api -X DELETE repos/{owner}/{repo}/releases/{} 247 | 248 | # Create a new release draft which is not publicly visible and requires manual acceptance 249 | - name: Create Release Draft 250 | env: 251 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 252 | run: | 253 | gh release create v${{ needs.build.outputs.version }} \ 254 | --draft \ 255 | --title "v${{ needs.build.outputs.version }}" \ 256 | --notes "$(cat << 'EOM' 257 | ${{ needs.build.outputs.changelog }} 258 | EOM 259 | )" -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow created for handling the release process based on the draft release prepared with the Build workflow. 2 | # Running the publishPlugin task requires all following secrets to be provided: PUBLISH_TOKEN, PRIVATE_KEY, PRIVATE_KEY_PASSWORD, CERTIFICATE_CHAIN. 3 | # See https://plugins.jetbrains.com/docs/intellij/plugin-signing.html for more information. 4 | 5 | name: Release 6 | on: 7 | release: 8 | types: [prereleased, released] 9 | 10 | jobs: 11 | 12 | # Prepare and publish the plugin to JetBrains Marketplace repository 13 | release: 14 | name: Publish Plugin 15 | runs-on: ubuntu-latest 16 | permissions: 17 | contents: write 18 | pull-requests: write 19 | steps: 20 | 21 | # Check out current repository 22 | - name: Fetch Sources 23 | uses: actions/checkout@v3 24 | with: 25 | ref: ${{ github.event.release.tag_name }} 26 | 27 | # Set up Java environment for the next steps 28 | - name: Setup Java 29 | uses: actions/setup-java@v3 30 | with: 31 | distribution: zulu 32 | java-version: 17 33 | 34 | # Setup Gradle 35 | - name: Setup Gradle 36 | uses: gradle/gradle-build-action@v2 37 | with: 38 | gradle-home-cache-cleanup: true 39 | 40 | # Set environment variables 41 | - name: Export Properties 42 | id: properties 43 | shell: bash 44 | run: | 45 | CHANGELOG="$(cat << 'EOM' | sed -e 's/^[[:space:]]*$//g' -e '/./,$!d' 46 | ${{ github.event.release.body }} 47 | EOM 48 | )" 49 | 50 | echo "changelog<> $GITHUB_OUTPUT 51 | echo "$CHANGELOG" >> $GITHUB_OUTPUT 52 | echo "EOF" >> $GITHUB_OUTPUT 53 | 54 | # Update Unreleased section with the current release note 55 | - name: Patch Changelog 56 | if: ${{ steps.properties.outputs.changelog != '' }} 57 | env: 58 | CHANGELOG: ${{ steps.properties.outputs.changelog }} 59 | run: | 60 | ./gradlew patchChangelog --release-note="$CHANGELOG" 61 | 62 | # Publish the plugin to JetBrains Marketplace 63 | - name: Publish Plugin 64 | env: 65 | PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} 66 | CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} 67 | PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} 68 | PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} 69 | run: ./gradlew publishPlugin 70 | 71 | # Upload artifact as a release asset 72 | - name: Upload Release Asset 73 | env: 74 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 75 | run: gh release upload ${{ github.event.release.tag_name }} ./build/distributions/* 76 | 77 | # Create a pull request 78 | - name: Create Pull Request 79 | if: ${{ steps.properties.outputs.changelog != '' }} 80 | env: 81 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 82 | run: | 83 | VERSION="${{ github.event.release.tag_name }}" 84 | BRANCH="changelog-update-$VERSION" 85 | LABEL="release changelog" 86 | 87 | git config user.email "action@github.com" 88 | git config user.name "GitHub Action" 89 | 90 | git checkout -b $BRANCH 91 | git commit -am "Changelog update - $VERSION" 92 | git push --set-upstream origin $BRANCH 93 | 94 | gh label create "$LABEL" \ 95 | --description "Pull requests with release changelog update" \ 96 | || true 97 | 98 | gh pr create \ 99 | --title "Changelog update - \`$VERSION\`" \ 100 | --body "Current pull request contains patched \`CHANGELOG.md\` file for the \`$VERSION\` version." \ 101 | --label "$LABEL" \ 102 | --head $BRANCH -------------------------------------------------------------------------------- /.github/workflows/run-ui-tests.yml: -------------------------------------------------------------------------------- 1 | # GitHub Actions Workflow for launching UI tests on Linux, Windows, and Mac in the following steps: 2 | # - Prepare and launch IDE with your plugin and robot-server plugin, which is needed to interact with the UI. 3 | # - Wait for IDE to start. 4 | # - Run UI tests with a separate Gradle task. 5 | # 6 | # Please check https://github.com/JetBrains/intellij-ui-test-robot for information about UI tests with IntelliJ Platform. 7 | # 8 | # Workflow is triggered manually. 9 | 10 | name: Run UI Tests 11 | on: 12 | workflow_dispatch 13 | 14 | jobs: 15 | 16 | testUI: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | include: 22 | - os: ubuntu-latest 23 | runIde: | 24 | export DISPLAY=:99.0 25 | Xvfb -ac :99 -screen 0 1920x1080x16 & 26 | gradle runIdeForUiTests & 27 | - os: windows-latest 28 | runIde: start gradlew.bat runIdeForUiTests 29 | - os: macos-latest 30 | runIde: ./gradlew runIdeForUiTests & 31 | 32 | steps: 33 | 34 | # Check out current repository 35 | - name: Fetch Sources 36 | uses: actions/checkout@v3 37 | 38 | # Set up Java environment for the next steps 39 | - name: Setup Java 40 | uses: actions/setup-java@v3 41 | with: 42 | distribution: zulu 43 | java-version: 17 44 | 45 | # Setup Gradle 46 | - name: Setup Gradle 47 | uses: gradle/gradle-build-action@v2 48 | with: 49 | gradle-home-cache-cleanup: true 50 | 51 | # Run IDEA prepared for UI testing 52 | - name: Run IDE 53 | run: ${{ matrix.runIde }} 54 | 55 | # Wait for IDEA to be started 56 | - name: Health Check 57 | uses: jtalk/url-health-check-action@v3 58 | with: 59 | url: http://127.0.0.1:8082 60 | max-attempts: 15 61 | retry-delay: 30s 62 | 63 | # Run tests 64 | - name: Tests 65 | run: ./gradlew test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | .idea 3 | .qodana 4 | build 5 | local.properties 6 | chain.crt 7 | private.pem -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/.idea/icon.png -------------------------------------------------------------------------------- /.run/Run IDE for UI Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Plugin Tests.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /.run/Run Plugin Verification.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Run Qodana.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 16 | 19 | 21 | true 22 | true 23 | false 24 | 25 | 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Jetpack Compose UI Architecture IDE plugin Changelog 4 | 5 | ## Unreleased 6 | 7 | ## 1.5.3 - 2025-05-04 8 | 9 | ### Added 10 | - Support for the latest IDE versions 11 | 12 | ## 1.5.2 - 2025-04-9 13 | 14 | ### Added 15 | - Support for the latest IDE versions 16 | 17 | 18 | ## 1.5.1 - 2025-02-10 19 | 20 | ### Fixed 21 | - Fixed the issue with PreviewParameterProvider template (`ppp`) 22 | 23 | ## 1.5.0 - 2024-12-21 24 | 25 | ### Added 26 | - Support Jetpack Compose Type-Safe Navigation Generation option 27 | 28 | ### Changed 29 | - Preview Parameter Provider is now generated as part of the Screen template instead of the Contract. This makes it easier to build new previews for the screen without switching between files 30 | 31 | ### Fixed 32 | - Fixed the issue of comments for Destination contract having hardcoded destination name 33 | 34 | ## 1.4.2 - 2024-09-19 35 | 36 | ### Added 37 | - Support for the latest IDE versions 38 | 39 | ## 1.4.1 - 2024-09-09 40 | - Added [Navigation Compose Typed](https://github.com/kiwicom/navigation-compose-typed) and [Compose Destinations](https://github.com/kiwicom/navigation-compose-typed) as the navigation options in the New Feature Dialog. This will generate the navigation code in the Route/Contract file based on the selected option. You can configure this in the Advanced Options Dialog. The code will be generated based on the selected navigation library if the option is enabled 41 | - Added Sealed Class and Data Class as the Actions Type options in the New Feature Dialog. This will allow you to choose between `Sealed Class` and `Data Class` for the Actions in the Contract file. You can configure this in the Advanced Options Dialog 42 | 43 | ### Fixed 44 | - Remove unnecessary Composable import from contract template 45 | - Fix UI for the Advanced Options Dialog 46 | 47 | ## 1.4.0 - 2024-09-07 48 | 49 | ### Added 50 | - Added [Navigation Compose Typed](https://github.com/kiwicom/navigation-compose-typed) and [Compose Destinations](https://github.com/kiwicom/navigation-compose-typed) as the navigation options in the New Feature Dialog. This will generate the navigation code in the Route/Contract file based on the selected option. You can configure this in the Advanced Options Dialog. The code will be generated based on the selected navigation library if the option is enabled 51 | - Added Sealed Class and Data Class as the Actions Type options in the New Feature Dialog. This will allow you to choose between `Sealed Class` and `Data Class` for the Actions in the Contract file. You can configure this in the Advanced Options Dialog 52 | 53 | ### Fixed 54 | - Remove unnecessary Composable import from contract template 55 | 56 | ## 1.3.2 - 2024-08-17 57 | 58 | ### Added 59 | - Added support for the latest IDE versions 60 | 61 | ## 1.3.1 - 2024-05-21 62 | 63 | ### Fixed 64 | - Fixed New Feature Generation when not using the AI client 65 | 66 | ## 1.3.0 - 2024-05-11 67 | This release features an integration with Generative AI to build more tailored content when creating Jetpack Compose Features. 68 | 69 | Ollama AI will be the first AI client to be integrated with the plugin. Ollama AI is a powerful AI that can generate code snippets, classes, and even whole features based on the provided prompt. 70 | With this integration, you can now provide a prompt for the feature you want to create and the AI will generate the feature template for you. 71 | 72 | ### Added 73 | - Plugin now has a dedicated settings page where you can configure the AI client, as a stepping stone right now we only support Ollama AI. 74 | - New Feature Dialog now has a Prompt field. By using this prompt your basic feature template will be enhanced with some methods and calls prefilled based on your use case 75 | - Checkout our README for a more detailed description of the integration and how to use it 76 | 77 | ## 1.2.1 - 2024-05-09 78 | 79 | ### Changed 80 | - The `PreviewParameterProvider` is now generated as part of the Contract instead of the separate file. This will make it easier to manage the preview parameters and the contract in one place and feature will be less overloaded with files 81 | 82 | ### Fixed 83 | - Fixed the issue with the `PreviewParameterProvider` and its ppp template having unnecessary annotation and imports 84 | - Fixed the issue with `Screen` not having proper state used when PreviewParameterProvider generation is enabled 85 | 86 | ## 1.2.0 - 2024-04-29 87 | 88 | ### Added 89 | - New Feature dialog now has an option to generate PreviewParametersProvider template for your State. You can toggle this option in the **More options** dialog (by @HanBI24) 90 | - PreviewParametersProvider template is now available in the Live Templates. Use `ppp` shortcut to generate a new PreviewParametersProvider template (by @HanBI24) 91 | 92 | ## 1.1.3 - 2024-04-15 93 | 94 | ### Fixed 95 | - Fix the issue with the Live Templates not being available in the IDE 96 | 97 | ## 1.1.2 - 2024-04-12 98 | 99 | ### Added 100 | - Support for the latest IDE versions 101 | 102 | ## 1.1.1 - 2024-01-15 103 | 104 | ### Added 105 | - Support for latest IDE versions 106 | 107 | ## 1.1.0 - 2023-10-01 108 | 109 | ### Added 110 | - New Feature dialog now also has an Advanced **More options** dialog which provides a place for more configuration options when creating new features 111 | - `Koin` support: Alongside `Hilt`, it is now possible to generate template files with `Koin` framework in mind. The coordinator will now respect the ViewModel Injection option from the **More options** dialog to put proper imports and factory methods based on the selected option. 112 | - `collectAsStateWithLifecycle` option is now available via **More Options** dialog 113 | 114 | ### Removed 115 | - `LocalActions` utility has been removed from the Contract template 116 | 117 | ## 1.0.1 - 2023-08-10 118 | 119 | ### Added 120 | - With the release of 1.0.0 I am very excited to announce official documentation to accompany this plugin. I'm planning to expand the plugin functionality and will be more than pleased to hear some feedback about the Architecture that it implements, so feel free to leave a feedback or start a discussion/issue in the GitHub repository! 121 | - [Compose UI Arch Docs](https://levinzonr.github.io/compose-ui-arch-docs/) - Ever-evolving set of rules and principles that is a foundation of this Plugin architecture templates 122 | - [Jetpack Compose UI Architecture Article](https://engineering.monstar-lab.com/en/post/2023/07/14/Jetpack-Compose-UI-Architecture/) Introduction to all the components with the example and motivation behind it 123 | 124 | ## 1.0.0 125 | 126 | ### Added 127 | - With the release of 1.0.0 I am very excited to announce official documentation to accompany this plugin. I'm planning to expand the plugin functionality and will be more than pleased to hear some feedback about the Architecture that it implements, so feel free to leave feedback or start a discussion/issue in the GitHub repository! 128 | - [Compose UI Arch Docs](https://levinzonr.github.io/compose-ui-arch-docs/) - Ever-evolving set of rules and principles that is a foundation of this Plugin architecture templates 129 | - [Jetpack Compose UI Architecture Article](https://engineering.monstar-lab.com/en/post/2023/07/14/Jetpack-Compose-UI-Architecture/) - Introduction to all the components with the example and motivation behind it 130 | 131 | ## 0.6.0 132 | 133 | ### Added 134 | - Added multiple live templates configurations to accelerate development with `@Compose` even more. There are currently two groups of Live Templates: Foundation and UI Arch. Foundation templates contains heavily used layouts and effects: 135 | - `col` -> Create Composable Column with pre-defined modifier and Arrangement 136 | - `row`-> Create Composable Row with pre-defined modifier and Arrangement 137 | - `box` -> Create Composable Box with Modifier 138 | - `laun` -> Create Launched Effect with Unit Key 139 | - `disp` -> Create Disposable Effect with Unit Key and `onDispose` Prepared 140 | 141 | ### Fixed 142 | - Fix `collectAsLifecycle` flag not being set properly 143 | 144 | ## 0.5.1 145 | 146 | ### Added 147 | - Add Support for the newer IDEs versions 148 | 149 | ## 0.5.0 150 | 151 | ### Changed 152 | - Removed `Experimental` opt-in from the template when `collectAsStateWithLifecycle()` is used 153 | - Updated the UI for the `New Feature` dialog. Since there are no more experimental options this plugin provides, all options are grouped into one section 154 | - `collectAsStateWithLifecycle()` will be enabled by default. User preference is still persisted 155 | 156 | ## 0.4.0 157 | 158 | ### Added 159 | - Add support for the newer versions of the IDE 160 | 161 | ## 0.3.0 162 | 163 | ### Added 164 | - `NewFeature` Dialog now includes the experimental features which. They are provided either by the plugin or some other external library 165 | - Collect flows in lifecycle aware manner! `collectAsStateWithLifecycle()` is now available under the experimental features. This method requires opt-in and plugin will fill up the opt-in related code for you. Your preference will be persisted across the IDE restarts 166 | 167 | ## 0.2.0 168 | 169 | ### Changed 170 | - Adjust `Screen` Template to include name in the Preview 171 | - `LocalActions` are not used in the `Route` and `Screen` templates anymore 172 | 173 | ### Fixed 174 | - Plugin now won't try to create files and packages that are already exists 175 | - Adjust `ViewModel` template to not create public flow everytime 176 | 177 | ## 0.1.1 178 | 179 | ### Fixed 180 | - Use `cooridinator` in rememberActions function instead of `state` 181 | 182 | ## 0.1.0 183 | 184 | ### Added 185 | - Open `Contract` in editor after new feature is generated 186 | - Open `UI Component` in editor after new UI component is generated 187 | 188 | ## 0.0.1-beta02 189 | 190 | ### Fixed 191 | - Fix Invalid references and imports in generated files 192 | - Fix `Can not add Action twice` error in IDE startup 193 | 194 | ## 0.0.1-beta01 195 | 196 | ### Changed 197 | - `Coordinator` is now renamed to `Route` and what is used to be called `CoordinatorState` is now called `Coordinator`. 198 | These new names are more self-explanatory and shorter :) 199 | 200 | ## 0.0.1-alpha08 201 | 202 | ### Changed 203 | - Default Actions in the Composition Local is now set to Error to avoid having multiple sources of truth 204 | - Use asStateFlow in the ViewModel. This will ensure the consumer of the flow wont be able to mutate it outside the ViewModel 205 | 206 | ## 0.0.1-alpha07 207 | 208 | ### Added 209 | - Generate new [Feat]CoordinatorState component 210 | - Add support for the latest IDEs builds 211 | - Made feature package generation optional - controlled by the checkbox 212 | 213 | ### Changed 214 | - Remove UIEvents posted from the ViewModel and LaunchedEffect in coordinator that used to handle it 215 | 216 | ## 0.0.1-alpha06 217 | 218 | ### Fixed 219 | - Replace `MutableSharedFlow` with `MutableStateFlow` in `ViewModel` 220 | 221 | ## 0.0.1-alpha05 222 | 223 | ### Fixed 224 | - Fix Contract Template generation 225 | 226 | ## 0.0.1-alpha04 227 | 228 | ### Added 229 | - New action to create UI Component 230 | - New feature Action now generates the package structure as well 231 | - UI updates with notes about created files 232 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | 3 | This project and the corresponding community is governed by the [JetBrains Open Source and Community Code of Conduct](https://confluence.jetbrains.com/display/ALL/JetBrains+Open+Source+and+Community+Code+of+Conduct). Please make sure you read it. 4 | 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright 2000-2021 JetBrains s.r.o. 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jetpack Compose UI Architecture Plugin 2 | 3 | ![Build](https://github.com/levinzonr/jetpack-compose-ui-arch-plugin/workflows/Build/badge.svg) 4 | ![AndroidWeekly](https://androidweekly.net/issues/issue-583/badge) 5 | As Seen In - jetc.dev Newsletter Issue #178 6 | 7 | 8 | **Jetpack Compose UI Architecture Plugin** provides a set of templates that to make the development of new Jetpack Compose features faster. 9 | 10 | Feels like something is missing or can be improved? Please submit a 11 | - [🔧 Bug report](https://github.com/levinzonr/jetpack-compose-ui-arch-plugin/issues/new?assignees=levinzonr&labels=bug&projects=&template=bug_report.md&title=) 12 | - [🙋 Feature request](https://github.com/levinzonr/jetpack-compose-ui-arch-plugin/issues/new?assignees=levinzonr&labels=enhancement&projects=&template=feature_request.md&title=) 13 | 14 | To learn more about the architecture and templates are based on it, you can check following sources 15 | - [Jetpack Compose UI Architecture Article](https://engineering.monstar-lab.com/en/post/2023/07/14/Jetpack-Compose-UI-Architecture/) - Introduction to all the components with the example and motivation behind it 16 | - [Compose UI Architecture Docs](https://levinzonr.github.io/compose-ui-arch-docs/) - GitHub pages that's hosts more in-depth explanations, rules and examples. 17 | 18 | ## New Feature Dialog 19 | This dialog allows you to create several files related to Jetpack Compose UI Architecture. After you provide a name for your feature, plugin will generate following: 20 | - `[featname]` - package (lowercased) (optional) 21 | - `[FeatName]Contract` - contains UIState, Actions emitted from the UI layer, PreviewParameterProvider (optional) and Destination for a Route (optional) 22 | - `[FeatName]Screen` - Basic, stateless, UI of your Screen, if PreviewParameterProvider is enabled, it will be used to render the preview 23 | - `[FeatName]Coordinator` - Main State holder of the Screen, controls the screen UI logic and Interactions 24 | - `[FeatName]Route` - Main entry point, tied to `Coordinator` and delegates all the Actions to it, Emits Screen. Optionally can contain navigation code 25 | - `[FeatName]ViewModel` - Basic implementation of your ViewModel 26 | 27 | ### Advanced Options Dialog 28 | The dialog also provides several options to customize the generated code, can be accessed by using "More Options" button when creating new feature 29 | - **Preview Parameter Provider** - If enabled, the plugin will generate a `PreviewParameterProvider` for you. This will allow you to provide different states to your preview 30 | - **Navigation Type** - Allows you to choose between [Navigation Compose Typed](https://github.com/kiwicom/navigation-compose-typed) and [Compose Destinations](https://github.com/raamcosta/compose-destinations) to generate the navigation code in the Route/Contract file 31 | - **ViewModel Injection** - Allows you to configure DI for your ViewModel. Hilt and Koin are supported 32 | - **Actions Type** - Allows you to choose between `Sealed Class` and `Data Class` for the Actions in the Contract file 33 | 34 | 35 | ### AI Integration 36 | Starting with version 1.3.0 the plugin now has a dedicated settings page where you can configure the AI client. As a stepping stone right now, we only support Ollama AI. The New Feature Dialog now has a Prompt field. By using this prompt, your basic feature template will be enhanced with some methods and calls prefilled based on your use case. 37 | Here is a quick example of how it works: 38 | 39 | > Prerequisite: You need to have an Ollama model hosted somewhere. You can use the Ollama CLI to host the model on your local machine or use the Ollama cloud service. 40 | 41 | [![Watch the video](https://img.youtube.com/vi/YsNlym0g0HU/default.jpg)](https://youtu.be/YsNlym0g0HU) 42 | 43 | 44 | #### Configure AI Client 45 | 1. Go to `Settings/Preferences | Tools | Jetpack Compose UI Arch Plugin` / Alternatively, you can use the search bar in the settings and type `Jetpack Compose UI Arch Plugin` or open this dialog from new Feature Dialog 46 | 2. Specify the URL where your Ollama model is hosted 47 | 3. Specify the model name, the default model name is `codegemma`, this model is recommended 48 | 4. (Optional) Specify the timeout. 49 | 5. Click Apply and then `Test Connection` to verify the connection 50 | 51 | #### Using AI in New Feature Dialog 52 | To new feature dialog, you will see a new field called `Prompt`. Here you can specify a prompt for the AI model. The AI model will generate the feature template based on this prompt. 53 | By leaving the prompt empty, you will get the default feature template. 54 | 55 | The prompt can be anything really, but the recommended way would to provide the short description and element you expect from your UI. 56 | For example, given you are creating a login screen you can type: `username, password, loading state` 57 | 58 | ## Live Templates 59 | Along with the templates provided with the Dialogs, this plugin also provides several live templates. There are currently two groups of Live Templates available: Compose Foundation and UI Arch 60 | 61 | ![](/assets/demo.gif) 62 | 63 | ### Compose Foundation Live Templates 64 | This set of templates contains most commonly used layouts and effects that can be used in any project 65 | - `col` -> Create Composable `Column` with pre-defined `Modifier` and Arrangement 66 | - `row`-> Create Composable `Row` with pre-defined `Modifier` and Arrangement 67 | - `box` -> Create Composable `Box` with `Modifier` 68 | - `laun` -> Create Launched Effect with Unit Key 69 | - `disp` -> Create Disposable Effect with Unit Key and `onDispose` Prepared 70 | 71 | ### Compose UI Arch Live Templates 72 | Currently, has only one template, that operates similar to the New Component Dialog 73 | - `compon` -> Creates new Compose UI Component with the `Modifier` as the parameter and a Preview\ 74 | - `ppp` -> Creates a new PreviewParameterProvider setup 75 | 76 | 77 | ## New Component Dialog 78 | New component dialog simply allows you to create new Jetpack Compose UI Component a bit quicker. It will generate simple component with the `Modifier` as the parameter and Preview setup for you. 79 | 80 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/assets/demo.gif -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.changelog.Changelog 2 | import org.jetbrains.changelog.markdownToHTML 3 | 4 | fun properties(key: String) = providers.gradleProperty(key) 5 | fun environment(key: String) = providers.environmentVariable(key) 6 | 7 | plugins { 8 | id("java") // Java support 9 | alias(libs.plugins.kotlin) // Kotlin support 10 | alias(libs.plugins.gradleIntelliJPlugin) // Gradle IntelliJ Plugin 11 | alias(libs.plugins.changelog) // Gradle Changelog Plugin 12 | alias(libs.plugins.qodana) // Gradle Qodana Plugin 13 | alias(libs.plugins.kover) // Gradle Kover Plugin 14 | alias(libs.plugins.kotlin.serialization) // Kotlin Serialization Plugin 15 | } 16 | 17 | group = properties("pluginGroup").get() 18 | version = properties("pluginVersion").get() 19 | 20 | // Configure project's dependencies 21 | repositories { 22 | mavenCentral() 23 | } 24 | 25 | // Dependencies are managed with Gradle version catalog - read more: https://docs.gradle.org/current/userguide/platforms.html#sub:version-catalog 26 | dependencies { 27 | implementation(libs.kotlin.coroutines.swing) 28 | implementation(libs.kotlin.coroutines.core) 29 | implementation(libs.kotlin.serialization) 30 | implementation(libs.ai.ollama) 31 | } 32 | 33 | // Set the JVM language level used to build the project. Use Java 11 for 2020.3+, and Java 17 for 2022.2+. 34 | kotlin { 35 | jvmToolchain(17) 36 | } 37 | 38 | // Configure Gradle IntelliJ Plugin - read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 39 | intellij { 40 | pluginName = properties("pluginName") 41 | version = properties("platformVersion") 42 | type = properties("platformType") 43 | 44 | // Plugin Dependencies. Uses `platformPlugins` property from the gradle.properties file. 45 | plugins = properties("platformPlugins").map { it.split(',').map(String::trim).filter(String::isNotEmpty) } 46 | } 47 | 48 | // Configure Gradle Changelog Plugin - read more: https://github.com/JetBrains/gradle-changelog-plugin 49 | changelog { 50 | groups.empty() 51 | repositoryUrl = properties("pluginRepositoryUrl") 52 | } 53 | 54 | // Configure Gradle Qodana Plugin - read more: https://github.com/JetBrains/gradle-qodana-plugin 55 | qodana { 56 | cachePath = provider { file(".qodana").canonicalPath } 57 | reportPath = provider { file("build/reports/inspections").canonicalPath } 58 | saveReport = true 59 | showReport = environment("QODANA_SHOW_REPORT").map { it.toBoolean() }.getOrElse(false) 60 | } 61 | 62 | // Configure Gradle Kover Plugin - read more: https://github.com/Kotlin/kotlinx-kover#configuration 63 | koverReport { 64 | defaults { 65 | xml { 66 | onCheck = true 67 | } 68 | } 69 | } 70 | 71 | tasks { 72 | wrapper { 73 | gradleVersion = properties("gradleVersion").get() 74 | } 75 | 76 | patchPluginXml { 77 | version = properties("pluginVersion") 78 | sinceBuild = properties("pluginSinceBuild") 79 | untilBuild = properties("pluginUntilBuild") 80 | 81 | // Extract the section from README.md and provide for the plugin's manifest 82 | pluginDescription = providers.fileContents(layout.projectDirectory.file("README.md")).asText.map { 83 | val start = "" 84 | val end = "" 85 | 86 | with (it.lines()) { 87 | if (!containsAll(listOf(start, end))) { 88 | throw GradleException("Plugin description section not found in README.md:\n$start ... $end") 89 | } 90 | subList(indexOf(start) + 1, indexOf(end)).joinToString("\n").let(::markdownToHTML) 91 | } 92 | } 93 | 94 | val changelog = project.changelog // local variable for configuration cache compatibility 95 | // Get the latest available change notes from the changelog file 96 | changeNotes = properties("pluginVersion").map { pluginVersion -> 97 | with(changelog) { 98 | renderItem( 99 | (getOrNull(pluginVersion) ?: getUnreleased()) 100 | .withHeader(false) 101 | .withEmptySections(false), 102 | Changelog.OutputType.HTML, 103 | ) 104 | } 105 | } 106 | } 107 | 108 | // Configure UI tests plugin 109 | // Read more: https://github.com/JetBrains/intellij-ui-test-robot 110 | runIdeForUiTests { 111 | systemProperty("robot-server.port", "8082") 112 | systemProperty("ide.mac.message.dialogs.as.sheets", "false") 113 | systemProperty("jb.privacy.policy.text", "") 114 | systemProperty("jb.consents.confirmation.enabled", "false") 115 | } 116 | 117 | signPlugin { 118 | certificateChain = environment("CERTIFICATE_CHAIN") 119 | privateKey = environment("PRIVATE_KEY") 120 | password = environment("PRIVATE_KEY_PASSWORD") 121 | } 122 | 123 | publishPlugin { 124 | dependsOn("patchChangelog") 125 | token = environment("PUBLISH_TOKEN") 126 | // The pluginVersion is based on the SemVer (https://semver.org) and supports pre-release labels, like 2.1.7-alpha.3 127 | // Specify pre-release label to publish the plugin in a custom Release Channel automatically. Read more: 128 | // https://plugins.jetbrains.com/docs/intellij/deployment.html#specifying-a-release-channel 129 | channels = properties("pluginVersion").map { listOf(it.split('-').getOrElse(1) { "default" }.split('.').first()) } 130 | } 131 | } -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # IntelliJ Platform Artifacts Repositories 2 | # -> https://plugins.jetbrains.com/docs/intellij/intellij-artifacts.html 3 | 4 | pluginGroup = com.levinzonr.arch.jetpackcompose.plugin 5 | pluginName = jetpack-compose-arch-plugin 6 | pluginVersion = 1.5.3 7 | # See https://plugins.jetbrains.com/docs/intellij/build-number-ranges.html 8 | # for insight into build numbers and IntelliJ Platform versions. 9 | pluginSinceBuild = 223 10 | pluginUntilBuild = 252.* 11 | 12 | # IntelliJ Platform Properties -> https://github.com/JetBrains/gradle-intellij-plugin#intellij-platform-properties 13 | platformType = IC 14 | platformVersion = 2022.3.3 15 | 16 | # Plugin Dependencies -> https://plugins.jetbrains.com/docs/intellij/plugin-dependencies.html 17 | # Example: platformPlugins = com.intellij.java, com.jetbrains.php:203.4449.22 18 | platformPlugins = 19 | 20 | # Java language level used to compile sources and to generate the files for - Java 11 is required since 2020.3 21 | javaVersion = 17 22 | 23 | # Gradle Releases -> https://github.com/gradle/gradle/releases 24 | gradleVersion = 8.2.1 25 | 26 | # Opt-out flag for bundling Kotlin standard library. 27 | # See https://plugins.jetbrains.com/docs/intellij/kotlin.html#kotlin-standard-library for details. 28 | # suppress inspection "UnusedProperty" 29 | kotlin.stdlib.default.dependency = false 30 | 31 | # Enable Gradle Configuration Cache -> https://docs.gradle.org/current/userguide/configuration_cache.html 32 | org.gradle.configuration-cache = true 33 | 34 | # Enable Gradle Build Cache -> https://docs.gradle.org/current/userguide/build_cache.html 35 | org.gradle.caching = true 36 | 37 | # Enable Gradle Kotlin DSL Lazy Property Assignment -> https://docs.gradle.org/current/userguide/kotlin_dsl.html#kotdsl:assignment 38 | systemProp.org.gradle.unsafe.kotlin.assignment = true -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | # libraries 3 | annotations = "24.0.1" 4 | kotlin_coroutines = "1.8.0" 5 | kotlin_serialization = "1.6.3" 6 | 7 | ai_ollama = "1.0.63" 8 | 9 | # plugins 10 | kotlin = "1.9.21" 11 | changelog = "2.1.2" 12 | gradleIntelliJPlugin = "1.15.0" 13 | qodana = "0.1.13" 14 | kover = "0.7.2" 15 | 16 | [libraries] 17 | ai_ollama = { module = "io.github.amithkoujalgi:ollama4j", version.ref = "ai_ollama" } 18 | annotations = { group = "org.jetbrains", name = "annotations", version.ref = "annotations" } 19 | kotlin_coroutines_swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlin_coroutines" } 20 | kotlin_coroutines_core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlin_coroutines" } 21 | kotlin_serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlin_serialization" } 22 | 23 | [plugins] 24 | changelog = { id = "org.jetbrains.changelog", version.ref = "changelog" } 25 | gradleIntelliJPlugin = { id = "org.jetbrains.intellij", version.ref = "gradleIntelliJPlugin" } 26 | kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 27 | kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } 28 | qodana = { id = "org.jetbrains.qodana", version.ref = "qodana" } 29 | kotlin_serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/levinzonr/jetpack-compose-ui-arch-plugin/4a99d4c6e82402f39f13f5c8af846c51abc91390/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.2.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "jetpack-compose-ui-arch-plugin" 2 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import com.intellij.openapi.ui.DialogPanel 4 | import com.intellij.openapi.ui.DialogWrapper 5 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.Dependencies 6 | import javax.swing.JComponent 7 | 8 | abstract class BaseDialog: DialogWrapper(true) { 9 | 10 | lateinit var panel: DialogPanel 11 | protected val dialogScope = Dependencies.mainScope 12 | override fun createCenterPanel(): JComponent? { 13 | panel = createPanel() 14 | return panel 15 | } 16 | 17 | abstract fun createPanel() : DialogPanel 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/BaseViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.Dependencies 4 | 5 | abstract class BaseViewModel() { 6 | protected val scope = Dependencies.mainScope 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/Links.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | object Links { 4 | const val DOCS = "https://levinzonr.github.io/compose-ui-arch-docs" 5 | const val BUG_REPORT = "https://github.com/levinzonr/jetpack-compose-ui-arch-plugin/issues/new?assignees=levinzonr&labels=bug&projects=&template=bug_report.md&title=" 6 | const val FEATURE_REQUEST = "https://github.com/levinzonr/jetpack-compose-ui-arch-plugin/issues/new?assignees=levinzonr&labels=enhancement&projects=&template=feature_request.md&title=" 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/ObservableValue.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import com.intellij.openapi.Disposable 4 | import com.intellij.openapi.observable.properties.ObservableMutableProperty 5 | 6 | class ObservableValue(defaultValue: T) : ObservableMutableProperty { 7 | private var value = defaultValue 8 | private val listeners = mutableListOf<(T) -> Unit>() 9 | 10 | override fun set(value: T) { 11 | if (this.value != value) { 12 | this.value = value 13 | listeners.forEach { it.invoke(value) } 14 | } 15 | } 16 | 17 | override fun afterChange(listener: (T) -> Unit) { 18 | listeners.add(listener) 19 | } 20 | 21 | override fun afterChange(listener: (T) -> Unit, parentDisposable: Disposable) { 22 | listeners.add(listener) 23 | } 24 | 25 | override fun get(): T { 26 | return value 27 | } 28 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/ProgressDialog.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import com.intellij.openapi.progress.util.ProgressWindow 4 | import com.intellij.openapi.project.ProjectManager 5 | 6 | class ProgressDialog( 7 | private val title: String, 8 | private val text: String 9 | ) { 10 | private val project by lazy { ProjectManager.getInstance().defaultProject } 11 | private var currentDialog: ProgressWindow? = null 12 | 13 | fun show() { 14 | currentDialog = createDialog().apply { 15 | start() 16 | } 17 | } 18 | 19 | fun hide() { 20 | currentDialog?.stop() 21 | currentDialog = null 22 | } 23 | 24 | 25 | private fun createDialog() = ProgressWindow(false, project).apply { 26 | this.title = this@ProgressDialog.title 27 | this.text = this@ProgressDialog.text 28 | this.text2 = this@ProgressDialog.text 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/PropertyKeys.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | object PropertyKeys { 4 | const val COORDINATOR_ACTIONS: String = "COORDINATOR_ACTIONS" 5 | const val ACTIONS_HANDLERS: String = "ACTION_HANDLERS" 6 | const val PackageName = "PACKAGE_NAME" 7 | const val Name = "NAME" 8 | const val UseFlowWithLifecycle = "USE_FLOW_WITH_LIFECYCLE" 9 | const val UsePreviewParameterProvider = "USE_PREVIEW_PARAMETER_PROVIDER" 10 | const val VIEW_MODEL_INJECTION = "VIEW_MODEL_INJECTION" 11 | const val STATE_PROPS = "STATE_PROPS" 12 | const val ACTIONS = "ACTIONS" 13 | const val AI_USED = "AI_USED" 14 | const val NAVIGATION_ENABLED = "NAVIGATION_ENABLED" 15 | const val NAVIGATION_CLASS_SUFFIX = "NAVIGATION_CLASS_SUFFIX" 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/ResourceLoader.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import io.github.classgraph.ClassGraph 4 | import java.io.File 5 | import java.io.IOException 6 | import java.io.InputStream 7 | import java.net.URL 8 | 9 | 10 | /** 11 | * This work in (JUnit, Jar, etc.) 12 | * It's better to create a new instance each time 13 | * 14 | * @author Aleksei Vlasov (github.com/adideas) 15 | */ 16 | class IntellijIdeaResourceLoader { 17 | var dir: String? = null 18 | 19 | init { 20 | if (Thread.currentThread().contextClassLoader == null) { 21 | // without jar 22 | dir = System.getProperty("user.dir") 23 | if (dir == null) { 24 | val currentDirFile = File(".") 25 | val helper = currentDirFile.absolutePath 26 | try { 27 | dir = helper.substring(0, helper.length - currentDirFile.canonicalPath.length) 28 | } catch (ignore: Exception) { 29 | } 30 | } 31 | dir = File(dir, "resource").absolutePath 32 | } 33 | } 34 | 35 | fun getResource(anyClassFromYouProject: Class<*>, filePath: String?): URL? { 36 | if (dir != null) { 37 | try { 38 | return URL("file://" + File(dir, filePath).absolutePath) 39 | } catch (ignore: Exception) { 40 | } 41 | } 42 | var url: URL? = null 43 | url = anyClassFromYouProject.classLoader.getResource(filePath) 44 | if (url != null) { 45 | return url 46 | } 47 | 48 | Thread.currentThread().contextClassLoader = anyClassFromYouProject.classLoader 49 | url = Thread.currentThread().contextClassLoader.getResource(filePath) 50 | if (url != null) { 51 | return url 52 | } 53 | 54 | val resources = ClassGraph().acceptPaths(filePath).scan().allResources 55 | if (!resources.isEmpty()) { 56 | resources[0].uri 57 | } 58 | return null 59 | } 60 | 61 | fun getResourceAsStream(anyClassFromYouProject: Class<*>, filePath: String?): InputStream? { 62 | val url = getResource(anyClassFromYouProject, filePath) 63 | if (url != null) { 64 | try { 65 | return url.openStream() 66 | } catch (ignore: IOException) { 67 | } 68 | } 69 | 70 | return null 71 | } 72 | } 73 | 74 | 75 | inline fun T.loadResource(filePath: String): InputStream? { 76 | return IntellijIdeaResourceLoader().getResourceAsStream(T::class.java, filePath) 77 | } 78 | 79 | inline fun T.loadStringResource(filePath: String): String? { 80 | return loadResource(filePath) 81 | ?.readAllBytes() 82 | ?.toString(Charsets.UTF_8) 83 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/TemplateGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core 2 | 3 | import com.intellij.ide.fileTemplates.FileTemplateManager 4 | import com.intellij.ide.fileTemplates.FileTemplateUtil 5 | import com.intellij.openapi.project.Project 6 | import com.intellij.psi.PsiDirectory 7 | import com.intellij.psi.PsiFile 8 | import com.intellij.psi.impl.file.PsiDirectoryFactory 9 | import java.util.* 10 | 11 | class TemplateGenerator(private val project: Project) { 12 | 13 | fun generateKt( 14 | templateName: String, 15 | fileName: String, 16 | directory: PsiDirectory, 17 | properties: MutableMap 18 | ) : PsiFile { 19 | try { 20 | 21 | val existing = directory.findFile("${fileName}.kt") 22 | if (existing != null) return existing 23 | 24 | 25 | val manager = FileTemplateManager.getInstance(project) 26 | val template = manager.getInternalTemplate(templateName) 27 | properties[PropertyKeys.PackageName] = requireNotNull(directory.getPackageName()) 28 | return FileTemplateUtil.createFromTemplate( 29 | template, 30 | "${fileName}.kt", 31 | properties.toProperties(), 32 | directory 33 | ) as PsiFile 34 | } catch (e: Exception) { 35 | throw IllegalStateException("Failed to generate file $fileName", e) 36 | } 37 | } 38 | 39 | private fun PsiDirectory.getPackageName(): String? { 40 | return PsiDirectoryFactory.getInstance(project).getQualifiedName(this, false) 41 | } 42 | 43 | 44 | private fun Map.toProperties(): Properties { 45 | return Properties().apply { 46 | this@toProperties.forEach { setProperty(it.key, it.value.toString()) } 47 | } 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/persistence/PreferencesDataSource.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core.persistence 2 | 3 | 4 | interface PreferencesDataSource { 5 | fun put(key: String, value: Boolean) 6 | fun get(key: String, default: Boolean) : Boolean 7 | 8 | fun put(key: String, value: String) 9 | fun get(key: String, default: String) : String 10 | } 11 | 12 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/core/persistence/PreferencesDataSourceImpl.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.core.persistence 2 | 3 | import com.intellij.ide.util.PropertiesComponent 4 | 5 | class PreferencesDataSourceImpl( 6 | private val component: PropertiesComponent 7 | ) : PreferencesDataSource { 8 | override fun put(key: String, value: Boolean) { 9 | component.setValue(key.projectKey, value) 10 | } 11 | 12 | override fun get(key: String, default: Boolean): Boolean { 13 | return component.getBoolean(key.projectKey) 14 | } 15 | 16 | override fun put(key: String, value: String) { 17 | component.setValue(key, value) 18 | } 19 | 20 | override fun get(key: String, default: String): String { 21 | return component.getValue(key) ?: default 22 | } 23 | 24 | private val String.projectKey: String get() { 25 | return "com.levinzonr.arch.jetpackcompose.plugin_$this" 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/dependencies/Dependencies.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.dependencies 2 | 3 | import kotlinx.coroutines.CoroutineScope 4 | import kotlinx.coroutines.Dispatchers 5 | import kotlinx.coroutines.swing.Swing 6 | import kotlinx.serialization.json.Json 7 | 8 | object Dependencies { 9 | val ioScope = CoroutineScope(Dispatchers.IO) 10 | val mainScope = CoroutineScope(Dispatchers.Swing) 11 | val ioDispatcher = Dispatchers.IO 12 | val json = Json { 13 | ignoreUnknownKeys = false 14 | isLenient = false 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/dependencies/PluginDependencies.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.dependencies 2 | 3 | import com.intellij.ide.util.PropertiesComponent 4 | import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSourceImpl 5 | 6 | object PluginDependencies { 7 | private val properties = PropertiesComponent.getInstance() 8 | val preferences = PreferencesDataSourceImpl(properties) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/dependencies/ProjectDependencies.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.dependencies 2 | 3 | import com.intellij.ide.util.PropertiesComponent 4 | import com.intellij.openapi.application.Application 5 | import com.intellij.openapi.application.ApplicationManager 6 | import com.intellij.openapi.fileEditor.FileEditorManager 7 | import com.intellij.openapi.project.Project 8 | import com.levinzonr.arch.jetpackcompose.plugin.core.TemplateGenerator 9 | 10 | class ProjectDependencies(val project: Project?) { 11 | val generator = TemplateGenerator(project!!) 12 | val editor: FileEditorManager = FileEditorManager.getInstance(project!!) 13 | val properties: PropertiesComponent = PropertiesComponent.getInstance(project!!) 14 | val application: Application = ApplicationManager.getApplication() 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/AIGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.AIResponse 4 | 5 | interface AIGenerator { 6 | suspend fun generate(ruleset: String, userPrompt: String): AIResponse 7 | suspend fun ping(): Boolean 8 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/FeatureBreakdownGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.loadStringResource 4 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.Dependencies 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.AIResponse 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.FeatureBreakdown 7 | import kotlinx.coroutines.delay 8 | import kotlinx.serialization.json.Json 9 | import kotlin.time.Duration.Companion.seconds 10 | 11 | class FeatureBreakdownGenerator( 12 | private val generator: AIGenerator, 13 | private val json: Json = Dependencies.json 14 | ) { 15 | 16 | object MockGenerator : AIGenerator { 17 | override suspend fun generate(ruleset: String, userPrompt: String): AIResponse { 18 | delay(5.seconds) 19 | return AIResponse(loadStringResource("ai_response_example.json")!!) 20 | } 21 | 22 | override suspend fun ping(): Boolean { 23 | return true 24 | } 25 | 26 | } 27 | 28 | suspend fun generate(name: String, userPrompt: String): Result { 29 | return runCatching { 30 | val richUserPrompt = prompt(name, userPrompt) 31 | val example = loadStringResource("ai_response_example.json")!! 32 | val response = generator.generate(prompt_core(example), richUserPrompt) 33 | println(response.response) 34 | json.decodeFromString(FeatureBreakdown.serializer(), response.response) 35 | } 36 | } 37 | 38 | companion object { 39 | 40 | fun prompt_core(json: String) = """ 41 | Your job is to generated JSON file that would look like in this example: $json. " 42 | Only respond with the JSON content. Nothing else. RAW JSON String, Not markdown: Omit ```json ``` thing " 43 | 44 | """ 45 | 46 | fun prompt(name: String, description: String) = """ 47 | Generate a JSON string that will act as feature breakdown for a feature called $name" 48 | Based on the feature name and other input and requirements provided bellow, come up with properties that would describe the UI state of the feature 49 | The properties should have a type that correspond with Kotlin types. For example: String, Int, List etc." 50 | The properties should have a default type, unless specified otherwise it should be the most simple values" 51 | For example false for Boolean, emptyList() for list, empty string for String and etc " 52 | The default values should always!! be as String JSON type, i.e "false" for Boolean, "" for String, \"emptyList()\" for List etc" 53 | The JSON should also contain a list of actions that the feature will have." 54 | The naming convention for the actions should be following:" 55 | - In case its a button click or anything that prompts other action down the line: on[some]ButtonClick, replace [some] with the button name" 56 | - In case its text field or any component where user types something: on[some]Change, replace [some] with name of the field or component " 57 | Some actions might contain some parameters, i.e String if its a text field change, or Item if its an item click. And each param must have a name." 58 | If possible this name should correspond with the name you would choose for the state parameters. " 59 | I.e in case there is a username inside state and actions that changes that username, the name should username" 60 | Provide empty array of parameters in case its a simple click interaction on a button" 61 | Actions should also have a type which is a string value and can be one of the following:" 62 | - domain - domain type means that this action should be handled by the external component or system such as database or an API service (examples: login)" 63 | - state_change - this type of action only affects the state of the feature, examples: password value change in text field. This actions should always come with an appropriate parameter that resembles a new value of the state" 64 | - other - this type of action is for any other type of action that does not fit into the above categories such as navigation" 65 | Next each action should have an imperativeName form, it will be used in ViewModels, such as: changePassword or login" 66 | 67 | "The name of the feature is $name and requirements from user that must be taken into account is following:" 68 | $description 69 | 70 | """ 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/AIResponse.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models 2 | 3 | data class AIResponse( 4 | val response: String, 5 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/Action.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class Action( 7 | val name: String, 8 | val params: List, 9 | val imperativeName: String, 10 | val type: Type 11 | ) { 12 | 13 | @Serializable 14 | data class Param(val name: String, val type: String) 15 | 16 | @Serializable(with = ActionTypeSerializer::class) 17 | enum class Type { 18 | Domain, StateChange, Other 19 | } 20 | 21 | val namedParams = params.joinToString(",") { "${it.name}: ${it.type}" } 22 | val typedParams = params.joinToString(",") { it.type } 23 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/ActionTypeSerializer.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models 2 | 3 | import kotlinx.serialization.KSerializer 4 | import kotlinx.serialization.descriptors.PrimitiveKind 5 | import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor 6 | import kotlinx.serialization.descriptors.SerialDescriptor 7 | import kotlinx.serialization.encoding.Decoder 8 | import kotlinx.serialization.encoding.Encoder 9 | 10 | object ActionTypeSerializer : KSerializer { 11 | override val descriptor: SerialDescriptor 12 | get() = PrimitiveSerialDescriptor("Type", PrimitiveKind.STRING) 13 | 14 | override fun serialize(encoder: Encoder, value: Action.Type) { 15 | encoder.encodeString( 16 | when(value) { 17 | Action.Type.Domain -> "domain" 18 | Action.Type.StateChange -> "state_change" 19 | Action.Type.Other -> "other" 20 | } 21 | ) 22 | } 23 | 24 | override fun deserialize(decoder: Decoder): Action.Type { 25 | return when(decoder.decodeString()) { 26 | "domain" -> Action.Type.Domain 27 | "state_change" -> Action.Type.StateChange 28 | else -> Action.Type.Other 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/FeatureBreakdown.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | 6 | @Serializable 7 | data class FeatureBreakdown( 8 | val properties: List, 9 | val actions: List 10 | ) { 11 | 12 | val propertyStatements = properties.joinToString("\n") { 13 | val defaultValue = if (it.defaultValue == "") "\"\"" else it.defaultValue 14 | "val ${it.name}: ${it.type} = ${defaultValue}," 15 | } 16 | 17 | val actionStatements = actions.joinToString("\n") { 18 | "val ${it.name}: (${it.typedParams}) -> Unit = {}," 19 | } 20 | 21 | val actionHandlers = actions.joinToString("\n") { 22 | "${it.name} = coordinator::handle${it.name.drop(2)}," 23 | } 24 | 25 | val coordinatorActions = actions.joinToString("\n") { 26 | 27 | if (it.type != Action.Type.Other) { 28 | val params = it.params.joinToString(", ") { it.name } 29 | """ 30 | fun handle${it.name.drop(2)}(${it.namedParams}) { 31 | viewModel.${it.imperativeName}($params) 32 | } 33 | """ 34 | } else { 35 | """ 36 | fun handle${it.name.drop(2)}() { 37 | // handle ${it.imperativeName} action 38 | } 39 | """ 40 | } 41 | } 42 | 43 | 44 | fun actionsHandlers(baseName: String): String { 45 | return """ 46 | ${ 47 | actions.joinToString("\n") { 48 | if (it.params.isEmpty()) { 49 | "data object ${it.name.drop(2)} : ${baseName}Action" 50 | } else { 51 | "data class ${it.name.drop(2)}(${ 52 | it.params.joinToString(",\n") { 53 | "val ${it.name}: ${it.type}" 54 | } 55 | }) : ${baseName}Action" 56 | } 57 | } 58 | } 59 | """.trimIndent() 60 | } 61 | 62 | fun coordinatorHandlers(baseName: String): String { 63 | return """ 64 | fun handle(action: ${baseName}Action) { 65 | when(action) { 66 | ${ 67 | actions.joinToString("\n") { 68 | val imperativeHandler = if (it.type != Action.Type.Other) { 69 | "viewModel.${it.imperativeName}(${it.params.joinToString(","){ 70 | "action.${it.name}" 71 | }})" 72 | } else { 73 | "// handle ${it.imperativeName} action" 74 | } 75 | if (it.params.isEmpty()) { 76 | 77 | "${baseName}Action.${it.name.drop(2)} -> {\n $imperativeHandler \n}" 78 | } else { 79 | "is ${baseName}Action.${it.name.drop(2)} -> {\n $imperativeHandler \n}" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | 86 | 87 | """.trimIndent() 88 | } 89 | 90 | 91 | val viewModelActions = actions.filter { it.type != Action.Type.Other }.joinToString("\n") { 92 | val params = it.namedParams 93 | val param = it.params.firstOrNull()?.name 94 | if (it.type == Action.Type.StateChange && param != null) { 95 | """ 96 | fun ${it.imperativeName}($params) { 97 | _stateFlow.update { state -> 98 | state.copy($param = $param) 99 | } 100 | } 101 | """.trimIndent() 102 | } else 103 | """ 104 | fun ${it.imperativeName}($params) { 105 | } 106 | """.trimIndent() 107 | } 108 | 109 | 110 | } 111 | 112 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ai/domain/models/StateProperties.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | @Serializable 6 | data class StateProperties( 7 | val name: String, 8 | val type: String, 9 | val defaultValue: String 10 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/feedback/FeedbackActions.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.feedback 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.ui.dsl.builder.Row 5 | import com.levinzonr.arch.jetpackcompose.plugin.core.Links 6 | 7 | 8 | fun Row.feedbackActions() { 9 | link("\uD83D\uDD27 Bug report") { 10 | BrowserUtil.open(Links.BUG_REPORT) 11 | } 12 | link("\uD83D\uDE4B Feature request") { 13 | BrowserUtil.open(Links.FEATURE_REQUEST) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/livetemplates/TemplatesContext.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.livetemplates 2 | 3 | import com.intellij.codeInsight.template.TemplateActionContext 4 | import com.intellij.codeInsight.template.TemplateContextType 5 | 6 | 7 | class TemplatesContext : TemplateContextType(Name) { 8 | override fun isInContext(context: TemplateActionContext): Boolean { 9 | return context.file.name.endsWith(".kt") 10 | } 11 | 12 | companion object { 13 | private const val Name = "Compose Live Templates Context" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/navigation/NavigationSettings.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.navigation 2 | 3 | data class NavigationSettings( 4 | val classSuffix: String = "Destination", 5 | val type: NavigationType = NavigationType.Kiwi, 6 | ) { 7 | enum class NavigationType { 8 | Kiwi, ComposeDestinations 9 | } 10 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/navigation/NavigationType.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.navigation 2 | 3 | enum class NavigationType { 4 | Kiwi, ComposeDestinations, Jetpack 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newcomponent/ComposeComponentAction.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newcomponent 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.features.newcomponent.ComposeComponentDialog 4 | import com.intellij.openapi.actionSystem.AnAction 5 | import com.intellij.openapi.actionSystem.AnActionEvent 6 | import com.intellij.openapi.actionSystem.CommonDataKeys 7 | import com.intellij.psi.PsiDirectory 8 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 9 | import com.levinzonr.arch.jetpackcompose.plugin.features.newcomponent.ComposeComponentViewModel 10 | 11 | class ComposeComponentAction: AnAction() { 12 | 13 | override fun actionPerformed(e: AnActionEvent) { 14 | val directory = e.getData(CommonDataKeys.PSI_ELEMENT) as PsiDirectory 15 | val viewModel = ComposeComponentViewModel(directory, ProjectDependencies(e.project)) 16 | ComposeComponentDialog(viewModel).show() 17 | } 18 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newcomponent/ComposeComponentDialog.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newcomponent 2 | 3 | import com.intellij.openapi.ui.DialogPanel 4 | import com.intellij.ui.dsl.builder.Align 5 | import com.intellij.ui.dsl.builder.bindText 6 | import com.intellij.ui.dsl.builder.panel 7 | import com.levinzonr.arch.jetpackcompose.plugin.core.BaseDialog 8 | import com.levinzonr.arch.jetpackcompose.plugin.features.feedback.feedbackActions 9 | import kotlinx.coroutines.flow.launchIn 10 | import kotlinx.coroutines.flow.onEach 11 | 12 | class ComposeComponentDialog( 13 | private val viewModel: ComposeComponentViewModel 14 | ) : BaseDialog() { 15 | 16 | init { 17 | init() 18 | viewModel.successFlow 19 | .onEach { close(0) } 20 | .launchIn(dialogScope) 21 | } 22 | 23 | override fun createPanel(): DialogPanel { 24 | return panel { 25 | row { label("New Jetpack Compose UI Component") } 26 | row { textField().focused().bindText(viewModel::name).align(Align.FILL) } 27 | row { comment("Creates a new Composable Component and its Preview based on the name given") } 28 | row { feedbackActions() } 29 | } 30 | } 31 | 32 | override fun doOKAction() { 33 | panel.apply() 34 | viewModel.onOkButtonClick() 35 | } 36 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newcomponent/ComposeComponentViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newcomponent 2 | 3 | import com.intellij.psi.PsiDirectory 4 | import com.levinzonr.arch.jetpackcompose.plugin.core.PropertyKeys 5 | import com.levinzonr.arch.jetpackcompose.plugin.core.BaseViewModel 6 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 7 | import kotlinx.coroutines.flow.MutableSharedFlow 8 | import kotlinx.coroutines.launch 9 | import java.util.Locale 10 | import java.util.Locale.getDefault 11 | 12 | class ComposeComponentViewModel( 13 | private val directory: PsiDirectory, 14 | private val projectDependencies: ProjectDependencies 15 | ) : BaseViewModel(){ 16 | var name: String = "" 17 | get() = field.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() } 18 | val successFlow = MutableSharedFlow() 19 | 20 | fun onOkButtonClick() { 21 | val properties: MutableMap = mutableMapOf(PropertyKeys.Name to name) 22 | val file = projectDependencies.generator.generateKt("ComposeComponent", name, directory, properties) 23 | projectDependencies.editor.openFile(file.virtualFile, true) 24 | scope.launch { successFlow.emit(Unit) } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/ActionsType.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models 2 | 3 | enum class ActionsType { 4 | Data, Sealed 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationType 4 | 5 | 6 | data class FeatureConfiguration( 7 | val useCollectFlowWithLifecycle: Boolean, 8 | val usePreviewParameterProvider: Boolean, 9 | val injection: InjectionConfiguration, 10 | val navigationType: NavigationType, 11 | val actionsType: ActionsType 12 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/FeatureProperties.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.PropertyKeys 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.FeatureBreakdown 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationSettings 6 | 7 | data class FeatureProperties( 8 | val name: String, 9 | val config: FeatureConfiguration, 10 | val breakdown: FeatureBreakdown?, 11 | val createNavigationCode: Boolean, 12 | ) { 13 | 14 | fun toProperties(): MutableMap { 15 | return mutableMapOf( 16 | PropertyKeys.Name to name, 17 | PropertyKeys.UseFlowWithLifecycle to config.useCollectFlowWithLifecycle, 18 | PropertyKeys.VIEW_MODEL_INJECTION to config.injection.name, 19 | PropertyKeys.STATE_PROPS to breakdown?.propertyStatements.orEmpty(), 20 | PropertyKeys.ACTIONS to breakdown?.actionStatements.orEmpty(), 21 | PropertyKeys.ACTIONS_HANDLERS to breakdown?.actionHandlers.orEmpty(), 22 | PropertyKeys.AI_USED to (breakdown != null), 23 | PropertyKeys.UsePreviewParameterProvider to config.usePreviewParameterProvider, 24 | PropertyKeys.VIEW_MODEL_INJECTION to config.injection.name, 25 | PropertyKeys.COORDINATOR_ACTIONS to breakdown?.coordinatorActions.orEmpty(), 26 | PropertyKeys.NAVIGATION_ENABLED to createNavigationCode, 27 | PropertyKeys.NAVIGATION_CLASS_SUFFIX to "Destination", 28 | "VM_ACTIONS" to breakdown?.viewModelActions.orEmpty(), 29 | "NAV_TYPE" to config.navigationType.name, 30 | "ACTIONS_TYPE" to config.actionsType.name, 31 | "COORDINATOR_HANDLERS" to breakdown?.coordinatorHandlers(name).orEmpty(), 32 | "SEALED_ACTIONS" to breakdown?.actionsHandlers(name).orEmpty() 33 | ) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/models/InjectionConfiguration.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models 2 | 3 | enum class InjectionConfiguration { 4 | Hilt, Koin 5 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/domain/repository/FeatureConfigurationRepository.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSource 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationType 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.ActionsType 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.FeatureConfiguration 7 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.InjectionConfiguration 8 | 9 | class FeatureConfigurationRepository( 10 | private val dataSource: PreferencesDataSource, 11 | ) { 12 | 13 | fun put(features: FeatureConfiguration) { 14 | dataSource.apply { 15 | put(KEY_INJECTION, features.injection.name) 16 | put(KEY_FLOW_LIFECYCLE, features.useCollectFlowWithLifecycle) 17 | put(KEY_PREVIEW_PARAMETER_PROVIDER, features.usePreviewParameterProvider) 18 | put(KEY_NAVIGATION_TYPE, features.navigationType.name) 19 | put(KEY_ACTIONS_TYPE, features.actionsType.name) 20 | } 21 | } 22 | 23 | fun get() : FeatureConfiguration { 24 | val injection = dataSource.get(KEY_INJECTION, InjectionConfiguration.Hilt.name) 25 | val navigationType = dataSource.get(KEY_NAVIGATION_TYPE, NavigationType.Kiwi.name) 26 | val actionsType = dataSource.get(KEY_ACTIONS_TYPE, ActionsType.Data.name) 27 | return FeatureConfiguration( 28 | useCollectFlowWithLifecycle = dataSource.get(KEY_FLOW_LIFECYCLE, true), 29 | usePreviewParameterProvider = dataSource.get(KEY_PREVIEW_PARAMETER_PROVIDER, true), 30 | injection = InjectionConfiguration.valueOf(injection), 31 | navigationType = NavigationType.valueOf(navigationType), 32 | actionsType = ActionsType.valueOf(actionsType) 33 | ) 34 | } 35 | 36 | fun update(block: (FeatureConfiguration) -> FeatureConfiguration) { 37 | put(get().let(block)) 38 | } 39 | 40 | companion object { 41 | private const val KEY_FLOW_LIFECYCLE = "use_flow_with_lifecycle" 42 | private const val KEY_PREVIEW_PARAMETER_PROVIDER = "use_preview_parameter_provider" 43 | private const val KEY_INJECTION = "view_model_injection" 44 | private const val KEY_NAVIGATION_TYPE = "navigation_type" 45 | private const val KEY_ACTIONS_TYPE = "actions_type" 46 | } 47 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/injection/AdvancedViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.injection 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSourceImpl 4 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced.AdvancedViewModel 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository.FeatureConfigurationRepository 7 | 8 | object AdvancedViewModelFactory { 9 | 10 | fun create(dependencies: ProjectDependencies) : AdvancedViewModel { 11 | return AdvancedViewModel( 12 | FeatureConfigurationRepository(PreferencesDataSourceImpl(dependencies.properties)) 13 | ) 14 | } 15 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/injection/ComposeArchDialogViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.injection 2 | 3 | import com.intellij.psi.PsiDirectory 4 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.PluginDependencies 5 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.FeatureBreakdownGenerator 7 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.ComposeArchDialogViewModel 8 | import com.levinzonr.arch.jetpackcompose.plugin.features.ollama.OllamaGenerator 9 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.data.SettingsRepositoryImpl 10 | 11 | object ComposeArchDialogViewModelFactory { 12 | 13 | 14 | 15 | fun create(psiDirectory: PsiDirectory, dependencies: ProjectDependencies) : ComposeArchDialogViewModel { 16 | val settings = SettingsRepositoryImpl(PluginDependencies.preferences) 17 | return ComposeArchDialogViewModel( 18 | directory = psiDirectory, 19 | generator = dependencies.generator, 20 | repository = ExperimentalFeaturesRepositoryFactory.create(dependencies), 21 | editorManager = dependencies.editor, 22 | application = dependencies.application, 23 | featureBreakdownGenerator = FeatureBreakdownGenerator(OllamaGenerator(settings.get().ollama)), 24 | settingsRepository = settings 25 | ) 26 | } 27 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/injection/ExperimentalFeaturesRepositoryFactory.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.injection 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSourceImpl 4 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository.FeatureConfigurationRepository 6 | 7 | object ExperimentalFeaturesRepositoryFactory { 8 | 9 | fun create(dependencies: ProjectDependencies) : FeatureConfigurationRepository { 10 | return FeatureConfigurationRepository( 11 | dataSource = PreferencesDataSourceImpl( 12 | component = dependencies.properties 13 | ) 14 | ) 15 | } 16 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/ComposeArchAction.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui 2 | 3 | import com.intellij.openapi.actionSystem.AnAction 4 | import com.intellij.openapi.actionSystem.AnActionEvent 5 | import com.intellij.openapi.actionSystem.CommonDataKeys 6 | import com.intellij.psi.PsiDirectory 7 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.ProjectDependencies 8 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.injection.AdvancedViewModelFactory 9 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.injection.ComposeArchDialogViewModelFactory 10 | 11 | class ComposeArchAction: AnAction() { 12 | 13 | override fun actionPerformed(e: AnActionEvent) { 14 | val deps = ProjectDependencies(e.project) 15 | val directory = e.getData(CommonDataKeys.PSI_ELEMENT) as PsiDirectory 16 | val viewModel = ComposeArchDialogViewModelFactory.create(directory, deps) 17 | val advancedViewModel = AdvancedViewModelFactory.create(deps) 18 | ComposeArchDialog(viewModel, advancedViewModel).show() 19 | } 20 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/ComposeArchDialog.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.options.ShowSettingsUtil 5 | import com.intellij.openapi.progress.ProgressIndicator 6 | import com.intellij.openapi.progress.ProgressManager 7 | import com.intellij.openapi.progress.Task 8 | import com.intellij.openapi.progress.util.ProgressWindow 9 | import com.intellij.openapi.project.Project 10 | import com.intellij.openapi.project.ProjectManager 11 | import com.intellij.openapi.ui.DialogPanel 12 | import com.intellij.openapi.ui.Messages 13 | import com.intellij.ui.dsl.builder.* 14 | import com.levinzonr.arch.jetpackcompose.plugin.core.BaseDialog 15 | import com.levinzonr.arch.jetpackcompose.plugin.core.Links 16 | import com.levinzonr.arch.jetpackcompose.plugin.core.ProgressDialog 17 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced.AdvancedDialog 18 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced.AdvancedViewModel 19 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.PluginSettings 20 | import kotlinx.coroutines.flow.launchIn 21 | import kotlinx.coroutines.flow.onEach 22 | import kotlinx.coroutines.flow.onStart 23 | 24 | class ComposeArchDialog( 25 | private val viewModel: ComposeArchDialogViewModel, 26 | private val advancedViewModel: AdvancedViewModel 27 | ) : BaseDialog() { 28 | 29 | 30 | val progressDialog = ProgressDialog( 31 | "Waiting for AI to respond", 32 | "Longer query might take longer time, Please stand by" 33 | ) 34 | 35 | 36 | init { 37 | init() 38 | viewModel 39 | .successFlow 40 | .onEach { close(0) } 41 | .launchIn(dialogScope) 42 | title = "New Jetpack Compose Feature" 43 | 44 | viewModel.errorFlow 45 | .onEach { 46 | Messages.showMessageDialog(it, "Error", Messages.getErrorIcon()) 47 | repaint() 48 | } 49 | .launchIn(dialogScope) 50 | 51 | 52 | viewModel.loadingFlow 53 | .onEach { 54 | println("$it") 55 | if (it) { 56 | progressDialog.show() 57 | } else { 58 | progressDialog.hide() 59 | } 60 | }.launchIn(dialogScope) 61 | 62 | } 63 | 64 | 65 | override fun createPanel(): DialogPanel { 66 | return panel { 67 | group { 68 | 69 | row { 70 | text( 71 | text = "Creates a set of files for the new Feature.
" + 72 | " All files will be placed in the package with the same name as the feature", 73 | maxLineLength = DEFAULT_COMMENT_WIDTH 74 | ) 75 | } 76 | row { 77 | textField() 78 | .focused() 79 | .bindText(viewModel::name) 80 | .align(Align.FILL) 81 | .comment("Name of the feature
i.e Login") 82 | } 83 | 84 | } 85 | 86 | group("AI Helper - Experimental") { 87 | row { 88 | text( 89 | text = "You can use AI buddy of your choice to generate more tailored content.
" + 90 | "Needs to be configured and enabled first", 91 | maxLineLength = DEFAULT_COMMENT_WIDTH 92 | ) 93 | link("⚙\uFE0F Configure") { 94 | PluginSettings.open() 95 | } 96 | } 97 | row { 98 | textField() 99 | .comment("AI Prompt with requirements
i.e: username, password, forgot passwrod button") 100 | .bindText(viewModel::description) 101 | .align(Align.FILL) 102 | } 103 | } 104 | 105 | 106 | 107 | group("Options") { 108 | row { 109 | checkBox("Also create package for the feature") 110 | .bindSelected(viewModel::createFeaturePackage) 111 | 112 | 113 | } 114 | 115 | row { 116 | checkBox("Create Navigation code") 117 | .bindSelected(viewModel::createNavigationCode) 118 | } 119 | 120 | row { 121 | link("More options") { 122 | AdvancedDialog(advancedViewModel).show() 123 | } 124 | } 125 | } 126 | 127 | row { 128 | link("ℹ\uFE0F Learn more") { 129 | BrowserUtil.browse(Links.DOCS) 130 | } 131 | } 132 | 133 | } 134 | } 135 | 136 | override fun doOKAction() { 137 | panel.apply() 138 | viewModel.onOkButtonClick() 139 | } 140 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/ComposeArchDialogViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui 2 | 3 | import com.intellij.openapi.application.* 4 | import com.intellij.openapi.fileEditor.FileEditorManager 5 | import com.intellij.psi.PsiDirectory 6 | import com.levinzonr.arch.jetpackcompose.plugin.core.* 7 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.FeatureBreakdownGenerator 8 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.FeatureBreakdown 9 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository.FeatureConfigurationRepository 10 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.FeatureProperties 11 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain.SettingsRepository 12 | import kotlinx.coroutines.* 13 | import kotlinx.coroutines.flow.MutableSharedFlow 14 | import java.util.Locale 15 | import java.util.Locale.getDefault 16 | 17 | class ComposeArchDialogViewModel( 18 | private val directory: PsiDirectory, 19 | private val generator: TemplateGenerator, 20 | private val repository: FeatureConfigurationRepository, 21 | private val editorManager: FileEditorManager, 22 | private val application: Application, 23 | private val featureBreakdownGenerator: FeatureBreakdownGenerator, 24 | private val settingsRepository: SettingsRepository 25 | ) : BaseViewModel() { 26 | 27 | var name: String = "" 28 | get() = field.replaceFirstChar { if (it.isLowerCase()) it.titlecase(getDefault()) else it.toString() } 29 | 30 | val successFlow = MutableSharedFlow() 31 | val errorFlow = MutableSharedFlow() 32 | val loadingFlow = MutableSharedFlow() 33 | 34 | var description: String = "" 35 | 36 | var createFeaturePackage: Boolean = true 37 | var createNavigationCode: Boolean = false 38 | 39 | fun onOkButtonClick() { 40 | scope.launch { 41 | if (description.isNotBlank()) { 42 | loadingFlow.emit(true) 43 | featureBreakdownGenerator.generate(name, description) 44 | .onFailure { errorFlow.emit(it.message ?: "Unkown API Error") } 45 | .onSuccess { breakdown -> 46 | generateFiles(breakdown) 47 | } 48 | loadingFlow.emit(false) 49 | } else { 50 | generateFiles(null) 51 | } 52 | } 53 | } 54 | 55 | private fun generateFiles(breakdown: FeatureBreakdown?) { 56 | val config = repository.get() 57 | val properties = FeatureProperties( 58 | name, config, breakdown, createNavigationCode).toProperties() 59 | invokeLater(ModalityState.defaultModalityState()) { 60 | runWriteAction { 61 | val featPackage = 62 | if (createFeaturePackage) directory.createSubdirectory(name.lowercase()) else directory 63 | val file = generator.generateKt("ComposeContract", "${name}Contract", featPackage, properties) 64 | generator.generateKt("ComposeScreen", "${name}Screen", featPackage, properties) 65 | generator.generateKt("ComposeViewModel", "${name}ViewModel", featPackage, properties) 66 | generator.generateKt("ComposeCoordinator", "${name}Coordinator", featPackage, properties) 67 | generator.generateKt("ComposeRoute", "${name}Route", featPackage, properties) 68 | 69 | if (featPackage.findSubdirectory("components") == null) { 70 | featPackage.createSubdirectory("components") 71 | } 72 | 73 | editorManager.openFile(file.virtualFile, true) 74 | } 75 | scope.launch { successFlow.emit(Unit) } 76 | } 77 | 78 | } 79 | 80 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedDialog.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.ui.DialogPanel 5 | import com.intellij.ui.dsl.builder.DEFAULT_COMMENT_WIDTH 6 | import com.intellij.ui.dsl.builder.bindSelected 7 | import com.intellij.ui.dsl.builder.panel 8 | import com.levinzonr.arch.jetpackcompose.plugin.core.BaseDialog 9 | import com.levinzonr.arch.jetpackcompose.plugin.features.feedback.feedbackActions 10 | 11 | class AdvancedDialog( 12 | private val viewModel: AdvancedViewModel 13 | ) : BaseDialog() { 14 | 15 | init { 16 | init() 17 | } 18 | 19 | override fun createPanel(): DialogPanel { 20 | return panel { 21 | 22 | row { 23 | rowComment("Configure advanced settings for the feature" + 24 | "settings will be persisted for the current project.") 25 | } 26 | 27 | group("State Collection") { 28 | row { 29 | checkBox("Use collectAsStateWithLifecycle") 30 | .bindSelected(viewModel::useCollectFlowWithLifecycle) 31 | } 32 | row { comment("Collect the flow in the Route component in a lifecycle aware way") } 33 | } 34 | group("Preview Parameter") { 35 | row { 36 | checkBox("Use PreviewParameterProvider") 37 | .bindSelected(viewModel::usePreviewParameterProvider) 38 | } 39 | row { comment("Also generate a PreviewParameterProvider to preview various States in a Screen component.") } 40 | } 41 | group("ViewModel Injection") { 42 | buttonsGroup { 43 | row { 44 | radioButton("Hilt") 45 | .bindSelected(viewModel::hiltSetter) 46 | } 47 | row { 48 | radioButton("Koin") 49 | .bindSelected(viewModel::koinSetter) 50 | } 51 | } 52 | } 53 | 54 | group("Navigation Type") { 55 | buttonsGroup { 56 | 57 | row { 58 | radioButton("Jetpack Navigation") 59 | .bindSelected(viewModel::jetpackNavSetter) 60 | } 61 | 62 | row { 63 | link("https://developer.android.com/guide/navigation/design/type-safety") { 64 | BrowserUtil.open("https://developer.android.com/guide/navigation/design/type-safety") 65 | } 66 | } 67 | 68 | row { 69 | radioButton("Kiwi Compose Navigation Typed") 70 | .bindSelected(viewModel::kiwiSetter) 71 | } 72 | 73 | row { 74 | link("https://github.com/kiwicom/navigation-compose-typed") { 75 | BrowserUtil.open("https://github.com/kiwicom/navigation-compose-typed") 76 | } 77 | } 78 | 79 | row { 80 | radioButton("Compose Destinations") 81 | .bindSelected(viewModel::composeDestinationsSetter) 82 | } 83 | 84 | row { 85 | link("https://github.com/raamcosta/compose-destinations") { 86 | BrowserUtil.open("https://github.com/raamcosta/compose-destinations") 87 | } 88 | } 89 | 90 | } 91 | } 92 | 93 | group("Actions Provider") { 94 | buttonsGroup { 95 | 96 | row { 97 | radioButton("Data Class") 98 | .bindSelected(viewModel::dataClassActionsSetter) 99 | } 100 | 101 | row { 102 | comment("All actions will be generated as data classes (default)") 103 | } 104 | 105 | row { 106 | radioButton("Sealed Interface") 107 | .bindSelected(viewModel::sealedActionsSetter) 108 | } 109 | 110 | row { 111 | comment("Actions will be generated as a sealed interface i.e LoginAction.UsernameChange") 112 | } 113 | 114 | 115 | } 116 | } 117 | 118 | group { 119 | row { 120 | text( 121 | text = "Missing some configuration to make your life easier? Feel free to submit a feature request!", 122 | maxLineLength = DEFAULT_COMMENT_WIDTH 123 | ) 124 | } 125 | row { feedbackActions() } 126 | } 127 | } 128 | } 129 | 130 | override fun doOKAction() { 131 | panel.apply() 132 | close(0) 133 | } 134 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/newfeature/ui/advanced/AdvancedViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.ui.advanced 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationType 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.ActionsType 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.repository.FeatureConfigurationRepository 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.newfeature.domain.models.InjectionConfiguration 7 | 8 | class AdvancedViewModel( 9 | private val featureConfigurationRepository: FeatureConfigurationRepository 10 | ) { 11 | 12 | var useCollectFlowWithLifecycle: Boolean 13 | get() = featureConfigurationRepository.get().useCollectFlowWithLifecycle 14 | set(value) { 15 | val current = featureConfigurationRepository.get() 16 | featureConfigurationRepository.put(current.copy(useCollectFlowWithLifecycle = value)) 17 | } 18 | 19 | var usePreviewParameterProvider: Boolean 20 | get() = featureConfigurationRepository.get().usePreviewParameterProvider 21 | set(value) { 22 | val current = featureConfigurationRepository.get() 23 | featureConfigurationRepository.put(current.copy(usePreviewParameterProvider = value)) 24 | } 25 | 26 | 27 | private var injectionConfig 28 | get() = featureConfigurationRepository.get().injection 29 | set(value) { 30 | featureConfigurationRepository.update { it.copy(injection = value) } 31 | } 32 | 33 | private var navigationType: NavigationType 34 | get() = featureConfigurationRepository.get().navigationType 35 | set(value) { 36 | featureConfigurationRepository.put(featureConfigurationRepository.get().copy(navigationType = value)) 37 | } 38 | 39 | private var actionsType: ActionsType 40 | get() = featureConfigurationRepository.get().actionsType 41 | set(value) { 42 | featureConfigurationRepository.put(featureConfigurationRepository.get().copy(actionsType = value)) 43 | } 44 | 45 | 46 | var kiwiSetter: Boolean 47 | get() = navigationType == NavigationType.Kiwi 48 | set(value) { 49 | if (value) navigationType = NavigationType.Kiwi 50 | } 51 | 52 | var composeDestinationsSetter: Boolean 53 | get() = navigationType == NavigationType.ComposeDestinations 54 | set(value) { 55 | if (value) navigationType = NavigationType.ComposeDestinations 56 | } 57 | 58 | var jetpackNavSetter: Boolean 59 | get() = navigationType == NavigationType.Jetpack 60 | set(value) { 61 | if (value) navigationType = NavigationType.Jetpack 62 | } 63 | 64 | var koinSetter: Boolean 65 | get() = injectionConfig == InjectionConfiguration.Koin 66 | set(value) { 67 | if (value) { 68 | injectionConfig = InjectionConfiguration.Koin 69 | } 70 | } 71 | 72 | var hiltSetter: Boolean 73 | get() = injectionConfig == InjectionConfiguration.Hilt 74 | set(value) { 75 | if (value) injectionConfig = InjectionConfiguration.Hilt 76 | } 77 | 78 | 79 | var sealedActionsSetter: Boolean 80 | get() = actionsType == ActionsType.Sealed 81 | set(value) { 82 | if (value) actionsType = ActionsType.Sealed 83 | } 84 | 85 | var dataClassActionsSetter: Boolean 86 | get() = actionsType == ActionsType.Data 87 | set(value) { 88 | if (value) actionsType = ActionsType.Data 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ollama/OllamaGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ollama 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.Dependencies 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.AIGenerator 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.ai.domain.models.AIResponse 6 | import io.github.amithkoujalgi.ollama4j.core.OllamaAPI 7 | import io.github.amithkoujalgi.ollama4j.core.utils.OptionsBuilder 8 | import io.github.amithkoujalgi.ollama4j.core.utils.PromptBuilder 9 | import kotlinx.coroutines.withContext 10 | import kotlin.coroutines.CoroutineContext 11 | 12 | class OllamaGenerator( 13 | private val settings: OllamaSettings, 14 | private val dispatcher: CoroutineContext = Dependencies.ioDispatcher, 15 | ) : AIGenerator { 16 | 17 | private val api = OllamaAPI(settings.host) 18 | 19 | init { 20 | api.setVerbose(true) 21 | api.setRequestTimeoutSeconds(settings.timeoutSeconds) 22 | } 23 | 24 | override suspend fun generate(ruleset: String, userPrompt: String): AIResponse { 25 | return withContext(dispatcher) { 26 | val prompt = PromptBuilder() 27 | .add(ruleset) 28 | .addSeparator() 29 | .addLine(userPrompt) 30 | println("Ollamruns : ${settings.model}") 31 | val response = api.generate(settings.model, prompt.build(), OptionsBuilder().build()) 32 | // ollama refuses to return json without code blocks 33 | val trimmedResponse = response.response 34 | .replace("```json", "") 35 | .replace("```", "") 36 | AIResponse(trimmedResponse) 37 | } 38 | 39 | } 40 | 41 | override suspend fun ping(): Boolean { 42 | api.generate(settings.model, "Hello!", OptionsBuilder().build()) 43 | return true 44 | } 45 | } 46 | 47 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/ollama/OllamaSettings.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.ollama 2 | 3 | data class OllamaSettings( 4 | val host: String = "http://localhost:11434/", 5 | val model: String = "codegemma", 6 | val timeoutSeconds: Long = 35 7 | ) -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/PluginSettings.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings 2 | 3 | import com.intellij.ide.BrowserUtil 4 | import com.intellij.openapi.options.Configurable 5 | import com.intellij.openapi.options.ShowSettingsUtil 6 | import com.intellij.openapi.project.Project 7 | import com.intellij.openapi.project.ProjectManager 8 | import com.intellij.openapi.ui.DialogPanel 9 | import com.intellij.ui.dsl.builder.* 10 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.injection.SettingsViewModelFactory 11 | import javax.swing.JComponent 12 | 13 | class PluginSettings : Configurable { 14 | 15 | private val viewModel: SettingsViewModel = SettingsViewModelFactory.create() 16 | private lateinit var dialogPanel: DialogPanel 17 | 18 | override fun createComponent(): JComponent? { 19 | dialogPanel = panel { 20 | group("AI Settings") { 21 | row { 22 | text("Configure the AI settings for the plugin and manage the AI models, API keys, and other settings") 23 | } 24 | group("\uD83E\uDD99 Ollama") { 25 | row { 26 | text( 27 | text = "Ollama is an advanced AI tool that allows users to easily set up and run large " + 28 | "language models locally
" + 29 | "
In order to use Ollama, you need to have the Ollama CLI installed on your machine. codegemma model is recommended", 30 | maxLineLength = DEFAULT_COMMENT_WIDTH 31 | ) 32 | 33 | link("Install Ollama") { 34 | BrowserUtil.open("https://ollama.ai/") 35 | } 36 | } 37 | 38 | row("Host") { 39 | textField() 40 | .bindText(viewModel::host) 41 | 42 | } 43 | row("Model") { 44 | textField() 45 | .bindText(viewModel::model) 46 | } 47 | 48 | row("Timeout in seconds") { 49 | intTextField() 50 | .bindIntText(viewModel::timeoutSeconds) 51 | } 52 | 53 | row { 54 | button( 55 | text = "Test connection (needs to applied first)", 56 | ) { 57 | viewModel.testOllamaConnection() 58 | } 59 | text(text = "") 60 | .bindText(viewModel.ollamaConnectionStatus) 61 | } 62 | 63 | 64 | 65 | } 66 | } 67 | } 68 | 69 | return dialogPanel 70 | } 71 | 72 | override fun isModified(): Boolean { 73 | return dialogPanel.isModified() 74 | } 75 | 76 | override fun apply() { 77 | dialogPanel.apply() 78 | viewModel.apply() 79 | } 80 | 81 | override fun reset() { 82 | viewModel.reset() 83 | } 84 | 85 | override fun disposeUIResources() {} 86 | 87 | override fun getDisplayName(): String { 88 | return "Jetpack Compose UI Architecture Plugin Settings" 89 | } 90 | 91 | companion object { 92 | fun open( 93 | project: Project = ProjectManager.getInstance().defaultProject 94 | ) { 95 | ShowSettingsUtil.getInstance().showSettingsDialog( 96 | project, 97 | PluginSettings::class.java 98 | ) 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/SettingsViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.BaseViewModel 4 | import com.levinzonr.arch.jetpackcompose.plugin.core.ObservableValue 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationSettings 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.ollama.OllamaGenerator 7 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain.SettingsRepository 8 | import kotlinx.coroutines.launch 9 | 10 | class SettingsViewModel( 11 | private val settingsRepository: SettingsRepository 12 | ) : BaseViewModel() { 13 | 14 | private val settings = settingsRepository.get() 15 | private var newSettings = settings 16 | 17 | var host: String 18 | get() = newSettings.ollama.host 19 | set(value) { 20 | newSettings = newSettings.copy(ollama = newSettings.ollama.copy(host = value)) 21 | } 22 | 23 | var ollamaConnectionStatus = ObservableValue("") 24 | 25 | 26 | var model: String 27 | get() = newSettings.ollama.model 28 | set(value) { 29 | newSettings = newSettings.copy(ollama = newSettings.ollama.copy(model = value)) 30 | } 31 | 32 | var timeoutSeconds: Int 33 | get() = newSettings.ollama.timeoutSeconds.toInt() 34 | set(value) { 35 | newSettings = newSettings.copy(ollama = newSettings.ollama.copy(timeoutSeconds = value.toLong())) 36 | } 37 | 38 | fun reset() { 39 | newSettings = settings 40 | } 41 | 42 | fun apply() { 43 | settingsRepository.set(newSettings) 44 | } 45 | 46 | fun testOllamaConnection() { 47 | ollamaConnectionStatus.set("Testing...") 48 | scope.launch { 49 | try { 50 | val ollama = OllamaGenerator(newSettings.ollama) 51 | val success = ollama.ping() 52 | val result = if (success) "Success" else "Failed" 53 | ollamaConnectionStatus.set(result) 54 | } catch (e: Exception) { 55 | ollamaConnectionStatus.set(e.message ?: e.toString()) 56 | } 57 | } 58 | } 59 | 60 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/data/SettingsRepositoryImpl.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings.data 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.core.persistence.PreferencesDataSource 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationSettings 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.ollama.OllamaSettings 6 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain.Settings 7 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain.SettingsRepository 8 | 9 | class SettingsRepositoryImpl( 10 | private val dataSource: PreferencesDataSource 11 | ): SettingsRepository { 12 | 13 | 14 | override fun get(): Settings { 15 | val default = Settings.DEFAULT 16 | val host = dataSource.get(KEY_OLLAMA_HOST, default.ollama.host) 17 | val model = dataSource.get(KEY_OLLAMA_MODEL, default.ollama.model) 18 | val timeout = dataSource.get(KEY_OLLAMA_TIMEOUT, default.ollama.timeoutSeconds.toString()).toLong() 19 | 20 | val navSuffix = dataSource.get(KEY_NAV_SUFFIX, default.navigationSettings.classSuffix) 21 | 22 | return Settings( 23 | ollama = OllamaSettings(host, model, timeout), 24 | navigationSettings = NavigationSettings(navSuffix) 25 | ) 26 | } 27 | 28 | override fun set(settings: Settings) { 29 | dataSource.put(KEY_OLLAMA_HOST, settings.ollama.host) 30 | dataSource.put(KEY_OLLAMA_MODEL, settings.ollama.model) 31 | dataSource.put(KEY_OLLAMA_TIMEOUT, settings.ollama.timeoutSeconds.toString()) 32 | 33 | dataSource.put(KEY_NAV_SUFFIX, settings.navigationSettings.classSuffix) 34 | } 35 | 36 | companion object { 37 | private const val KEY_OLLAMA_HOST = "settings:ollama:host" 38 | private const val KEY_OLLAMA_MODEL = "settings:ollama:model" 39 | private const val KEY_OLLAMA_TIMEOUT = "settings:ollama:timeout" 40 | 41 | private const val KEY_NAV_SUFFIX = "settings:navigation:suffix" 42 | } 43 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/domain/Settings.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.features.navigation.NavigationSettings 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.ollama.OllamaSettings 5 | 6 | data class Settings( 7 | val ollama: OllamaSettings, 8 | val navigationSettings: NavigationSettings 9 | ) { 10 | companion object { 11 | val DEFAULT = Settings(OllamaSettings(), NavigationSettings()) 12 | } 13 | } -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/domain/SettingsRepository.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings.domain 2 | 3 | interface SettingsRepository { 4 | fun get(): Settings 5 | fun set(settings: Settings) 6 | } 7 | -------------------------------------------------------------------------------- /src/main/kotlin/com/levinzonr/arch/jetpackcompose/plugin/features/settings/injection/SettingsViewModelFactory.kt: -------------------------------------------------------------------------------- 1 | package com.levinzonr.arch.jetpackcompose.plugin.features.settings.injection 2 | 3 | import com.levinzonr.arch.jetpackcompose.plugin.dependencies.PluginDependencies 4 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.SettingsViewModel 5 | import com.levinzonr.arch.jetpackcompose.plugin.features.settings.data.SettingsRepositoryImpl 6 | 7 | object SettingsViewModelFactory { 8 | 9 | fun create() : SettingsViewModel { 10 | return SettingsViewModel( 11 | settingsRepository = SettingsRepositoryImpl(PluginDependencies.preferences) 12 | ) 13 | } 14 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | com.levinzonr.arch.jetpackcompose.plugin 5 | 6 | 8 | Jetpack Compose UI Architecture Templates 9 | 10 | 11 | Roman Levinzon 12 | 13 | 16 | 17 | Set of templates for Jetpack Compose UI Architecture 18 | 19 | 20 | 21 | 23 | com.intellij.modules.platform 24 | 25 | 27 | 28 | 29 | 30 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 47 | 53 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 73 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/ai_response_example.json: -------------------------------------------------------------------------------- 1 | { 2 | "properties": [ 3 | { 4 | "name": "username", 5 | "type": "String", 6 | "defaultValue": "" 7 | }, 8 | { 9 | "name": "password", 10 | "type": "String", 11 | "defaultValue": "" 12 | }, 13 | { 14 | "name": "items", 15 | "type": "List", 16 | "defaultValue": "emptyList()" 17 | } 18 | ], 19 | "actions": [ 20 | { 21 | "name": "onLoginClick", 22 | "params": [], 23 | "type": "domain", 24 | "imperativeName": "login" 25 | }, 26 | { 27 | "name": "onForgotPasswordClick", 28 | "params": [], 29 | "type": "other", 30 | "imperativeName": "forgotPassword" 31 | }, 32 | { 33 | "name": "onUsernameChange", 34 | "params": [ 35 | { 36 | "name": "username", 37 | "type": "String" 38 | } 39 | ], 40 | "type": "state_change", 41 | "imperativeName": "changeUsername" 42 | } 43 | ] 44 | } -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeComponent.kt.ft: -------------------------------------------------------------------------------- 1 | package ${PACKAGE_NAME} 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.Composable 6 | import androidx.compose.ui.Modifier 7 | import androidx.compose.ui.tooling.preview.Preview 8 | 9 | @Composable 10 | fun ${NAME}( 11 | modifier: Modifier = Modifier 12 | ) { 13 | Box(modifier) { 14 | Text(text = "${NAME}") 15 | } 16 | } 17 | 18 | @Preview(name = "${NAME}") 19 | @Composable 20 | private fun Preview${NAME}() { 21 | ${NAME}() 22 | } -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeContract.kt.ft: -------------------------------------------------------------------------------- 1 | package ${PACKAGE_NAME} 2 | 3 | #if (${USE_PREVIEW_PARAMETER_PROVIDER} == "true") 4 | import androidx.compose.ui.tooling.preview.PreviewParameterProvider 5 | #end 6 | #if ($NAVIGATION_ENABLED == "true" && $NAV_TYPE == "Kiwi") 7 | import com.kiwi.navigationcompose.typed.Destination 8 | #end 9 | #if ($NAVIGATION_ENABLED == "true" && ($NAV_TYPE == "Kiwi" || $NAV_TYPE == "Jetpack")) 10 | import kotlinx.serialization.Serializable 11 | #end 12 | 13 | #if ($NAVIGATION_ENABLED == "true" && $NAV_TYPE == "Kiwi") 14 | /** 15 | * Object used for a type safe destination to a ${NAME} route 16 | */ 17 | @Serializable 18 | object ${NAME}${NAVIGATION_CLASS_SUFFIX} : Destination 19 | #end 20 | 21 | #if (${NAVIGATION_ENABLED} == "true" && ${NAV_TYPE} == "Jetpack") 22 | /** 23 | * Object used for a type safe destination to a ${NAME} route 24 | */ 25 | @Serializable 26 | object ${NAME}${NAVIGATION_CLASS_SUFFIX} 27 | #end 28 | 29 | /** 30 | * UI State that represents ${NAME}Screen 31 | **/ 32 | #if (${AI_USED} == "false") 33 | class ${NAME}State 34 | #else 35 | data class ${NAME}State( 36 | ${STATE_PROPS} 37 | ) 38 | #end 39 | 40 | /** 41 | * ${NAME} Actions emitted from the UI Layer 42 | * passed to the coordinator to handle 43 | **/ 44 | #if (${ACTIONS_TYPE} == "Data") 45 | data class ${NAME}Actions( 46 | #if (${AI_USED} == true) 47 | ${ACTIONS} 48 | #else 49 | val onClick: () -> Unit = {} 50 | #end 51 | ) 52 | #end 53 | 54 | #if (${ACTIONS_TYPE} == "Sealed") 55 | sealed interface ${NAME}Action { 56 | #if (${AI_USED} == true) 57 | ${SEALED_ACTIONS} 58 | #else 59 | data object OnClick : ${NAME}Action 60 | #end 61 | } 62 | #end 63 | 64 | -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeCoordinator.kt.ft: -------------------------------------------------------------------------------- 1 | package $PACKAGE_NAME 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.remember 5 | #if (${VIEW_MODEL_INJECTION} == "Hilt") import androidx.hilt.navigation.compose.hiltViewModel 6 | #else import org.koin.androidx.compose.koinViewModel 7 | #end 8 | 9 | /** 10 | * Screen's coordinator which is responsible for handling actions from the UI layer 11 | * and one-shot actions based on the new UI state 12 | */ 13 | class ${NAME}Coordinator( 14 | val viewModel: ${NAME}ViewModel 15 | ) { 16 | val screenStateFlow = viewModel.stateFlow 17 | #if (${ACTIONS_TYPE} == "Sealed" && ${AI_USED} == "false") 18 | fun handle(action: ${NAME}Action) { 19 | when (action) { 20 | ${NAME}Action.OnClick -> { /* Handle action */ } 21 | } 22 | } 23 | #end 24 | 25 | #if (${ACTIONS_TYPE} == "Sealed" && ${AI_USED} == "true") 26 | ${COORDINATOR_HANDLERS} 27 | #end 28 | 29 | #if (${AI_USED} == "true" && ${ACTIONS_TYPE} == "Data") 30 | ${COORDINATOR_ACTIONS} 31 | #end 32 | 33 | 34 | } 35 | 36 | @Composable 37 | fun remember${NAME}Coordinator( 38 | viewModel: ${NAME}ViewModel = #if (${VIEW_MODEL_INJECTION} == "Hilt") hiltViewModel() #else koinViewModel() #end 39 | ) : ${NAME}Coordinator { 40 | return remember(viewModel) { 41 | ${NAME}Coordinator( 42 | viewModel = viewModel 43 | ) 44 | } 45 | } -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeRoute.kt.ft: -------------------------------------------------------------------------------- 1 | package ${PACKAGE_NAME} 2 | 3 | import androidx.compose.runtime.Composable 4 | #if (${USE_FLOW_WITH_LIFECYCLE} == "false") 5 | import androidx.compose.runtime.collectAsState 6 | #end 7 | import androidx.compose.runtime.getValue 8 | import androidx.compose.runtime.remember 9 | 10 | #if (${NAVIGATION_ENABLED} == "true" && ${NAV_TYPE} == "ComposeDestinations") 11 | import com.ramcosta.composedestinations.annotation.Destination 12 | import com.ramcosta.composedestinations.annotation.RootGraph 13 | #end 14 | 15 | #if (${USE_FLOW_WITH_LIFECYCLE} == "true") 16 | import androidx.lifecycle.compose.collectAsStateWithLifecycle 17 | #end 18 | 19 | 20 | #if (${NAVIGATION_ENABLED} == "true" && ${NAV_TYPE} == "ComposeDestinations") 21 | @Destination 22 | #end 23 | @Composable 24 | fun ${NAME}Route( 25 | coordinator: ${NAME}Coordinator = remember${NAME}Coordinator() 26 | ) { 27 | // State observing and declarations 28 | #if (${USE_FLOW_WITH_LIFECYCLE} == "true") val uiState by coordinator.screenStateFlow.collectAsStateWithLifecycle(${NAME}State()) 29 | #else val uiState by coordinator.screenStateFlow.collectAsState(${NAME}State()) 30 | #end 31 | 32 | // UI Actions 33 | #if (${ACTIONS_TYPE} == "Data") 34 | val actions = remember${NAME}Actions(coordinator) 35 | #end 36 | #if (${ACTIONS_TYPE} == "Sealed") 37 | val actionsHandler: (${NAME}Action) -> Unit = { action -> 38 | coordinator.handle(action) 39 | } 40 | #end 41 | 42 | #if (${ACTIONS_TYPE} == "Data") 43 | // UI Rendering 44 | ${NAME}Screen(uiState, actions) 45 | #end 46 | #if (${ACTIONS_TYPE} == "Sealed") 47 | // UI Rendering 48 | ${NAME}Screen( 49 | state = uiState, 50 | onAction = actionsHandler 51 | ) 52 | #end 53 | } 54 | 55 | 56 | #if (${ACTIONS_TYPE} == "Data") 57 | @Composable 58 | fun remember${NAME}Actions(coordinator: ${NAME}Coordinator): ${NAME}Actions { 59 | return remember(coordinator) { 60 | #if (!${AI_USED}) 61 | ${NAME}Actions( 62 | onClick = coordinator::doStuff 63 | ) 64 | #else 65 | ${NAME}Actions( 66 | ${ACTION_HANDLERS} 67 | ) 68 | #end 69 | } 70 | } 71 | #end -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeScreen.kt.ft: -------------------------------------------------------------------------------- 1 | package $PACKAGE_NAME 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.ui.tooling.preview.Preview 5 | #if (${USE_PREVIEW_PARAMETER_PROVIDER} == "true")import androidx.compose.ui.tooling.preview.PreviewParameter 6 | #end 7 | 8 | @Composable 9 | fun ${NAME}Screen( 10 | state: ${NAME}State, 11 | #if (${ACTIONS_TYPE} == "Data") 12 | actions: ${NAME}Actions 13 | #else 14 | onAction: (${NAME}Action) -> Unit 15 | #end 16 | ) { 17 | // TODO UI Rendering 18 | } 19 | 20 | #if (${USE_PREVIEW_PARAMETER_PROVIDER} == "true") 21 | @Composable 22 | @Preview(name = "${NAME}") 23 | private fun ${NAME}ScreenPreview( 24 | @PreviewParameter(${NAME}StatePreviewParameterProvider::class) 25 | state: ${NAME}State 26 | ) { 27 | ${NAME}Screen( 28 | state = state, 29 | #if (${ACTIONS_TYPE} == "Data") 30 | actions = ${NAME}Actions() 31 | #else 32 | onAction = {} 33 | #end 34 | ) 35 | } 36 | #else 37 | @Composable 38 | @Preview(name = "${NAME}") 39 | private fun ${NAME}ScreenPreview() { 40 | ${NAME}Screen( 41 | state = ${NAME}State(), 42 | #if (${ACTIONS_TYPE} == "Data") 43 | actions = ${NAME}Actions() 44 | #else 45 | onAction = {} 46 | #end 47 | ) 48 | } 49 | #end 50 | 51 | #if (${USE_PREVIEW_PARAMETER_PROVIDER} == "true") 52 | /** 53 | * PreviewParameter Provider for ${NAME}Screen Preview 54 | * Add values to the sequence to see the preview in different states 55 | **/ 56 | class ${NAME}StatePreviewParameterProvider : PreviewParameterProvider<${NAME}State> { 57 | override val values: Sequence<${NAME}State> 58 | get() = sequenceOf( 59 | ${NAME}State(), 60 | ) 61 | } 62 | #end 63 | -------------------------------------------------------------------------------- /src/main/resources/fileTemplates/internal/ComposeViewModel.kt.ft: -------------------------------------------------------------------------------- 1 | package $PACKAGE_NAME 2 | 3 | import androidx.lifecycle.SavedStateHandle 4 | import androidx.lifecycle.ViewModel 5 | #if (${VIEW_MODEL_INJECTION} == "Hilt") 6 | import dagger.hilt.android.lifecycle.HiltViewModel 7 | import javax.inject.Inject 8 | #end 9 | import kotlinx.coroutines.flow.StateFlow 10 | import kotlinx.coroutines.flow.MutableStateFlow 11 | import kotlinx.coroutines.flow.asStateFlow 12 | #if (${AI_USED} == "true") 13 | import kotlinx.coroutines.flow.update 14 | #end 15 | 16 | #if (${VIEW_MODEL_INJECTION} == "Hilt") 17 | @HiltViewModel 18 | #end 19 | class ${NAME}ViewModel #if (${VIEW_MODEL_INJECTION} == "Hilt") @Inject constructor #end( 20 | savedStateHandle: SavedStateHandle 21 | ) : ViewModel() { 22 | 23 | private val _stateFlow: MutableStateFlow<${NAME}State> = MutableStateFlow(${NAME}State()) 24 | 25 | val stateFlow: StateFlow<${NAME}State> = _stateFlow.asStateFlow() 26 | 27 | 28 | #if (${AI_USED} == "true") 29 | ${VM_ACTIONS} 30 | #end 31 | 32 | } -------------------------------------------------------------------------------- /src/main/resources/icons/action_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/resources/icons/icon_small.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/main/resources/liveTemplates/ComposeFoundation.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 27 | 28 | 40 | 41 | 42 | 54 | 55 | 68 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /src/main/resources/liveTemplates/ComposeUIArch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/resources/messages/MyBundle.properties: -------------------------------------------------------------------------------- 1 | name=My Plugin 2 | applicationService=Application service 3 | projectService=Project service: {0} 4 | -------------------------------------------------------------------------------- /src/test/testData/rename/foo.xml: -------------------------------------------------------------------------------- 1 | 2 | 1>Foo 3 | 4 | -------------------------------------------------------------------------------- /src/test/testData/rename/foo_after.xml: -------------------------------------------------------------------------------- 1 | 2 | Foo 3 | 4 | --------------------------------------------------------------------------------