├── .github └── workflows │ ├── publish.yml │ └── review.yml ├── .gitignore ├── .idea └── detekt.xml ├── Dangerfile ├── Gemfile ├── LICENSE ├── README.md ├── build.gradle.kts ├── config ├── detekt │ ├── baseline.xml │ └── detekt.yml └── lint │ ├── lint-baseline.xml │ └── lint.xml ├── content └── screenshots │ ├── delete_rows.png │ ├── insert_row.png │ ├── table_content.png │ ├── tables_list.png │ └── update_value.png ├── core ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── AndroidEnvironmentProvider.kt │ ├── commonMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── core │ │ ├── DatabaseWrapper.kt │ │ ├── EnvironmentProvider.kt │ │ ├── data │ │ ├── Column.kt │ │ ├── ColumnType.kt │ │ └── Row.kt │ │ └── mapper │ │ ├── ColumnsSqlMapper.kt │ │ ├── CursorWrapper.kt │ │ ├── RowsSqlMapper.kt │ │ ├── SingleStringSqlMapper.kt │ │ ├── SqlMapper.kt │ │ └── StringSqlMapper.kt │ └── iosMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ └── IosEnvironmentProvider.kt ├── gradle.properties ├── gradle ├── libs.versions.toml ├── maven-publishing.gradle.kts └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── room-adapter ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── room │ │ └── RoomEnvironmentProvider.kt │ ├── commonMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── room │ │ ├── RoomCursorWrapper.kt │ │ └── RoomWrapper.kt │ ├── iosMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── room │ │ └── RoomEnvironmentProvider.kt │ └── jvmMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ └── adapter │ └── room │ └── RoomEnvironmentProvider.kt ├── runtime ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── DelightSqlViewerActivity.kt │ │ ├── core │ │ └── util │ │ │ ├── LaunchManager.kt │ │ │ └── ShortcutManager.kt │ │ └── feature │ │ └── table │ │ └── presentation │ │ └── ScreenCloser.kt │ ├── commonMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── App.kt │ │ ├── DelightSqlViewer.kt │ │ ├── core │ │ ├── component │ │ │ ├── Component.kt │ │ │ ├── DefaultRootComponent.kt │ │ │ ├── Resumable.kt │ │ │ ├── RootComponent.kt │ │ │ └── RootContent.kt │ │ ├── data │ │ │ └── Theme.kt │ │ ├── extension │ │ │ └── String.kt │ │ ├── presentation │ │ │ ├── Alert.kt │ │ │ ├── CheckboxWithText.kt │ │ │ └── ErrorBox.kt │ │ └── util │ │ │ ├── LaunchManager.kt │ │ │ └── ShortcutManager.kt │ │ └── feature │ │ ├── insert │ │ └── presentation │ │ │ ├── DefaultInsertComponent.kt │ │ │ ├── InsertComponent.kt │ │ │ ├── InsertContent.kt │ │ │ ├── InsertState.kt │ │ │ └── InsertValueType.kt │ │ ├── structure │ │ └── presentation │ │ │ ├── DefaultStructureComponent.kt │ │ │ ├── StructureComponent.kt │ │ │ ├── StructureContent.kt │ │ │ └── StructureState.kt │ │ ├── table │ │ └── presentation │ │ │ ├── DefaultTablesListComponent.kt │ │ │ ├── ScreenCloser.kt │ │ │ ├── TablesListComponent.kt │ │ │ ├── TablesListContent.kt │ │ │ └── TablesListState.kt │ │ ├── update │ │ └── presentation │ │ │ ├── DefaultUpdateComponent.kt │ │ │ ├── UpdateComponent.kt │ │ │ ├── UpdateContent.kt │ │ │ └── UpdateState.kt │ │ └── viewer │ │ └── presentation │ │ ├── DefaultViewerComponent.kt │ │ ├── ViewerComponent.kt │ │ ├── ViewerContent.kt │ │ └── ViewerState.kt │ ├── iosMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── DelightSqlViewerSceneDelegate.kt │ │ ├── ShortcutActionHandler.kt │ │ ├── core │ │ └── util │ │ │ ├── IosSceneController.kt │ │ │ ├── LaunchManager.kt │ │ │ └── ShortcutManager.kt │ │ └── feature │ │ └── table │ │ └── presentation │ │ └── ScreenCloser.kt │ └── jvmMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ ├── core │ └── util │ │ ├── LaunchManager.kt │ │ └── ShortcutManager.kt │ └── feature │ └── table │ └── presentation │ └── ScreenCloser.kt ├── sample ├── android │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── ru │ │ │ └── bartwell │ │ │ └── delightsqlviewer │ │ │ └── sample │ │ │ └── android │ │ │ └── MainActivity.kt │ │ └── res │ │ └── values │ │ └── styles.xml ├── desktop │ ├── build.gradle.kts │ └── src │ │ └── jvmMain │ │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── sample │ │ └── desktop │ │ └── Main.kt ├── ios │ ├── iosSample.xcodeproj │ │ ├── project.pbxproj │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── iosSample.xcscheme │ ├── iosSample.xcworkspace │ │ └── contents.xcworkspacedata │ └── iosSample │ │ ├── AppDelegate.swift │ │ ├── AppTheme.swift │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Info.plist │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── iOSApp.swift └── shared │ ├── build.gradle.kts │ └── src │ ├── androidMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── sample │ │ └── shared │ │ └── database │ │ ├── room │ │ └── DatabaseBuilder.kt │ │ └── sqldelight │ │ └── DriverFactory.kt │ ├── commonMain │ ├── kotlin │ │ └── ru │ │ │ └── bartwell │ │ │ └── delightsqlviewer │ │ │ └── sample │ │ │ └── shared │ │ │ ├── App.kt │ │ │ ├── AppCustomColorScheme.kt │ │ │ ├── DatabaseInitializer.kt │ │ │ └── database │ │ │ ├── room │ │ │ ├── Database.kt │ │ │ ├── DatabaseBuilder.kt │ │ │ ├── Table1Dao.kt │ │ │ ├── Table1Entity.kt │ │ │ ├── Table2Dao.kt │ │ │ └── Table2Entity.kt │ │ │ └── sqldelight │ │ │ └── DriverFactory.kt │ └── sqldelight │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── sample │ │ └── shared │ │ ├── table1.sq │ │ └── table2.sq │ ├── iosMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── sample │ │ └── shared │ │ └── database │ │ ├── room │ │ └── DatabaseBuilder.kt │ │ └── sqldelight │ │ └── DriverFactory.kt │ └── jvmMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ └── sample │ └── shared │ └── database │ ├── room │ └── DatabaseBuilder.kt │ └── sqldelight │ └── DriverFactory.kt ├── settings.gradle.kts ├── settings.properties ├── sqldelight-adapter ├── build.gradle.kts └── src │ ├── androidMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── sqldelight │ │ └── SqlDelightEnvironmentProvider.kt │ ├── commonMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── sqldelight │ │ ├── SqlDelightCursorWrapper.kt │ │ └── SqlDelightWrapper.kt │ ├── iosMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ └── adapter │ │ └── sqldelight │ │ └── SqlDelightEnvironmentProvider.kt │ └── jvmMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ └── adapter │ └── sqldelight │ └── SqlDelightEnvironmentProvider.kt ├── stub ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── AndroidEnvironmentProvider.kt │ │ └── adapter │ │ ├── room │ │ └── RoomEnvironmentProvider.kt │ │ └── sqldelight │ │ └── SqlDelightEnvironmentProvider.kt │ ├── commonMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── DelightSqlViewer.kt │ │ └── core │ │ └── EnvironmentProvider.kt │ ├── iosMain │ └── kotlin │ │ └── ru │ │ └── bartwell │ │ └── delightsqlviewer │ │ ├── ShortcutActionHandler.kt │ │ └── adapter │ │ ├── room │ │ └── RoomEnvironmentProvider.kt │ │ └── sqldelight │ │ └── SqlDelightEnvironmentProvider.kt │ └── jvmMain │ └── kotlin │ └── ru │ └── bartwell │ └── delightsqlviewer │ └── adapter │ ├── room │ └── RoomEnvironmentProvider.kt │ └── sqldelight │ └── SqlDelightEnvironmentProvider.kt └── version.properties /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Library to Maven Central 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Check out repository 14 | uses: actions/checkout@v3 15 | 16 | - name: Set up JDK 17 17 | uses: actions/setup-java@v3 18 | with: 19 | distribution: temurin 20 | java-version: 17 21 | 22 | - name: Extract version from tag 23 | id: get_version 24 | run: | 25 | TAG_NAME="${GITHUB_REF_NAME}" 26 | VERSION="${TAG_NAME#v}" 27 | echo "version=$VERSION" >> $GITHUB_OUTPUT 28 | 29 | - name: Write version to version.properties 30 | run: | 31 | echo "libraryVersionName=${{ steps.get_version.outputs.version }}" > version.properties 32 | cat version.properties 33 | 34 | - name: Publish to Maven Central 35 | run: ./gradlew publish 36 | env: 37 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 38 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 39 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 40 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 41 | SIGNING_SECRET_KEY: ${{ secrets.SIGNING_SECRET_KEY }} 42 | 43 | - name: Create GitHub Release 44 | id: create_release 45 | uses: actions/create-release@v1 46 | with: 47 | tag_name: ${{ github.ref_name }} 48 | release_name: ${{ github.ref_name }} 49 | body: "" 50 | draft: false 51 | prerelease: false 52 | env: 53 | GITHUB_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} 54 | -------------------------------------------------------------------------------- /.github/workflows/review.yml: -------------------------------------------------------------------------------- 1 | name: Review PR with Danger 2 | 3 | on: 4 | pull_request: 5 | branches: ["develop"] 6 | types: [opened, synchronize, closed] 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | 13 | - name: Clone repo 14 | uses: actions/checkout@v3 15 | with: 16 | fetch-depth: 100 17 | 18 | - name: set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | java-version: '17' 22 | distribution: 'temurin' 23 | cache: gradle 24 | 25 | - name: Set up Ruby 3.0 26 | uses: actions/setup-ruby@v1 27 | 28 | - uses: actions/cache@v4 29 | with: 30 | path: vendor/bundle 31 | key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-gems- 34 | 35 | - name: Bundle install 36 | run: | 37 | gem install bundler 38 | bundle config path vendor/bundle 39 | bundle install --jobs 4 --retry 3 40 | 41 | - name: Grant execute permission for gradlew 42 | run: chmod +x gradlew 43 | 44 | - name: Assemble debug build 45 | run: ./gradlew clean assembleDebug 46 | 47 | - name: Run Lint 48 | run: ./gradlew lintDebug 49 | 50 | - name: Run Detekt 51 | run: ./gradlew detektCheckAll 52 | 53 | - name: Run Danger 54 | run: | 55 | gem install danger 56 | bundle exec danger --verbose --dangerfile=Dangerfile --danger_id=danger-pr --fail-on-errors=true 57 | env: 58 | DANGER_GITHUB_API_TOKEN: ${{ secrets.DANGER_GITHUB_API_TOKEN }} 59 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | .idea/* 4 | !.idea/detekt.xml 5 | .DS_Store 6 | build 7 | captures 8 | .externalNativeBuild 9 | .cxx 10 | local.properties 11 | xcuserdata 12 | .kotlin 13 | /sample/desktop/sample.db 14 | -------------------------------------------------------------------------------- /.idea/detekt.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 12 | -------------------------------------------------------------------------------- /Dangerfile: -------------------------------------------------------------------------------- 1 | has_wip_label = github.pr_labels.any? { |label| label.include? "Engineers at work" } 2 | has_wip_title = github.pr_title.include? "[WIP]" 3 | if has_wip_label || has_wip_title 4 | warn("PR is marked as Work in Progress") 5 | end 6 | 7 | warn("Big PR") if git.lines_of_code > 5000 8 | 9 | module_dirs = {} 10 | File.open("settings.gradle.kts", "r") do |file| 11 | file.each_line do |line| 12 | line = line.strip 13 | if line =~ /^project\(":(.*?)"\)\.projectDir\s*=\s*file\("([^"]+)"\)/ 14 | module_dirs[$1] = $2 15 | end 16 | end 17 | end 18 | 19 | File.open("settings.gradle.kts", "r") do |file_handle| 20 | file_handle.each_line do |line| 21 | line = line.strip 22 | if line.start_with?("include(") 23 | match = line.match(/include\((.*)\)/) 24 | if match 25 | module_string = match[1].gsub(/["']/, '') 26 | gradleModule = module_string.gsub(":", "") 27 | if module_dirs.has_key?(gradleModule) && module_dirs[gradleModule].include?("sample") 28 | next 29 | end 30 | detektFile = "#{gradleModule}/build/reports/detekt/detekt.xml" 31 | if File.file?(detektFile) 32 | kotlin_detekt.report_file = detektFile 33 | kotlin_detekt.skip_gradle_task = true 34 | kotlin_detekt.severity = "warning" 35 | kotlin_detekt.filtering = true 36 | kotlin_detekt.detekt(inline_mode: true) 37 | else 38 | warn("No Detekt report found in #{detektFile} for module #{gradleModule}") 39 | end 40 | else 41 | warn("Could not parse module name from line: #{line}") 42 | end 43 | end 44 | end 45 | end 46 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } 4 | 5 | gem 'danger' 6 | gem 'danger-android_lint' 7 | gem 'danger-kotlin_detekt' 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delight SQL Viewer 2 | 3 | **Delight SQL Viewer** is a Kotlin Multiplatform library for Android, iOS, and Desktop applications. It supports both [SQLDelight](https://github.com/cashapp/sqldelight) and [Room Multiplatform](https://developer.android.com/kotlin/multiplatform/room) databases. With version **2.0.0**, developers and testers can view, edit, add, and delete records directly within the app—making debugging and QA efficient by enabling real-time inspection and modification of your app’s database state. 4 | 5 | ## Screenshots 6 | 7 | 8 | tables_list 9 | 10 | 11 | table_content 12 | 13 | 14 | insert_row 15 | 16 | 17 | update_value 18 | 19 | 20 | delete_rows 21 | 22 | 23 | ## Features 24 | 25 | - **Multiplatform Support:** Runs on Android, iOS, and Desktop. 26 | - **Dual Database Support:** Seamlessly work with both SQLDelight and Room databases. 27 | - **Database Inspection:** View, edit, add, and delete records directly from your app. 28 | - **App Shortcuts (Android and iOS):** Automatically adds a shortcut entry for quick access (configurable). 29 | - **Easy Integration:** Add the necessary dependencies and initialize in your platform-specific code. 30 | - **Configurable for Debug/Release:** For debug builds, include full functionality; for release builds, switch to a lightweight stub to reduce app size. 31 | 32 | ## Table of Contents 33 | 34 | 1. [Installation](#installation) 35 | 2. [Initialization](#initialization) 36 | - [Android](#android) 37 | - [iOS](#ios) 38 | - [Desktop](#desktop) 39 | 3. [Launching the Viewer](#launching-the-viewer) 40 | - [Using a Custom Theme](#using-a-custom-theme) 41 | 4. [Shortcuts](#shortcuts) 42 | - [Android Shortcut](#android-shortcut) 43 | - [iOS Shortcut](#ios-shortcut) 44 | - [Desktop](#desktop-shortcut) 45 | 5. [Excluding the Library in Release Builds](#excluding-the-library-in-release-builds) 46 | - [Using the Stub Library](#using-the-stub-library) 47 | - [Omitting Initialization and Launch](#omitting-initialization-and-launch) 48 | 6. [Contributing](#contributing) 49 | 7. [License](#license) 50 | 51 | --- 52 | 53 | ## Installation 54 | 55 | Delight SQL Viewer is published to Maven Central. Add the dependency to your `shared` (common) module: 56 | 57 | ```kotlin 58 | // In shared/build.gradle.kts 59 | 60 | kotlin { 61 | sourceSets { 62 | val commonMain by getting { 63 | dependencies { 64 | api("ru.bartwell.delightsqlviewer:runtime:2.0.0") 65 | api("ru.bartwell.delightsqlviewer:core:2.0.0") 66 | // Choose the adapter based on your database: 67 | api("ru.bartwell.delightsqlviewer:sqldelight-adapter:2.0.0") 68 | // or 69 | // api("ru.bartwell.delightsqlviewer:room-adapter:2.0.0") 70 | } 71 | } 72 | } 73 | } 74 | ``` 75 | 76 | For iOS, export the dependencies in your `framework {}` block to make them available in your iOS framework: 77 | 78 | ```kotlin 79 | // In shared/build.gradle.kts 80 | 81 | framework { 82 | export("ru.bartwell.delightsqlviewer:runtime:2.0.0") 83 | export("ru.bartwell.delightsqlviewer:core:2.0.0") 84 | // Choose the adapter based on your database: 85 | export("ru.bartwell.delightsqlviewer:sqldelight-adapter:2.0.0") 86 | // or 87 | // export("ru.bartwell.delightsqlviewer:room-adapter:2.0.0") 88 | } 89 | ``` 90 | 91 | --- 92 | 93 | ## Initialization 94 | 95 | After adding the appropriate dependencies, initialize Delight SQL Viewer in your platform-specific code by providing the corresponding environment provider along with the database driver. 96 | 97 | ### Android 98 | 99 | For **SQLDelight**: 100 | 101 | ```kotlin 102 | DelightSqlViewer.init(object : SqlDelightEnvironmentProvider() { 103 | override fun getDriver() = sqlDelightDriver 104 | override fun getContext() = this@MainActivity 105 | }) 106 | ``` 107 | 108 | For **Room**: 109 | 110 | ```kotlin 111 | DelightSqlViewer.init(object : RoomEnvironmentProvider() { 112 | override fun getDriver() = roomDatabase 113 | override fun getContext() = this@MainActivity 114 | }) 115 | ``` 116 | 117 | *Note:* Both providers require an Android `Context` along with the respective database instance. 118 | 119 | ### iOS 120 | 121 | For **SQLDelight**: 122 | 123 | ```swift 124 | let provider = SqlDelightEnvironmentProvider(driver: sqlDriver) 125 | DelightSqlViewer.shared.doInit(provider: provider, isShortcutEnabled: true) 126 | ``` 127 | 128 | For **Room**: 129 | 130 | ```swift 131 | let provider = RoomEnvironmentProvider(driver: roomDatabase) 132 | DelightSqlViewer.shared.doInit(provider: provider, isShortcutEnabled: true) 133 | ``` 134 | 135 | *Note:* The `isShortcutEnabled` parameter determines whether a shortcut is added to the app icon. 136 | 137 | ### Desktop 138 | 139 | For **SQLDelight**: 140 | 141 | ```kotlin 142 | DelightSqlViewer.init(object : SqlDelightEnvironmentProvider() { 143 | override fun getDriver() = sqlDelightDriver 144 | }) 145 | ``` 146 | 147 | For **Room**: 148 | 149 | ```kotlin 150 | DelightSqlViewer.init(object : RoomEnvironmentProvider() { 151 | override fun getDriver() = roomDatabase 152 | }) 153 | ``` 154 | 155 | - There is no built-in shortcut on Desktop. Simply call `DelightSqlViewer.launch()` from your own UI controls. 156 | 157 | --- 158 | 159 | ## Launching the Viewer 160 | 161 | Once initialized, you can launch the viewer with a simple call: 162 | 163 | ### Android / Desktop 164 | 165 | ```kotlin 166 | Button(onClick = { DelightSqlViewer.launch() }) { 167 | Text(text = "Launch viewer") 168 | } 169 | ``` 170 | 171 | ### iOS 172 | 173 | ```swift 174 | Button("Launch viewer") { 175 | DelightSqlViewer.shared.launch() 176 | } 177 | ``` 178 | 179 | #### Using a Custom Theme 180 | 181 | Starting with version **2.1.0**, you can specify a custom theme when launching the viewer. By default, `DelightSqlViewer.launch()` uses `Theme.Auto`, but you may also call: 182 | 183 | ```kotlin 184 | DelightSqlViewer.launch(theme = Theme.Dark) 185 | // or 186 | DelightSqlViewer.launch(theme = Theme.Light) 187 | // or 188 | DelightSqlViewer.launch(theme = Theme.Custom(myColorScheme)) 189 | ``` 190 | 191 | Theme.Custom takes a Material 3 ColorScheme, allowing you to tailor the viewer’s UI to match your app's design. 192 | 193 | For **iOS**, pass the chosen theme to the `launch(theme:)` method: 194 | 195 | ```swift 196 | Button("Launch viewer in Dark theme") { 197 | DelightSqlViewer.shared.launch(theme: Theme.Dark()) 198 | } 199 | ``` 200 | 201 | --- 202 | 203 | ## Shortcuts 204 | 205 | ### Android Shortcut 206 | 207 | By default, Delight SQL Viewer adds a shortcut to your app’s launcher icon (accessible via long-press). To disable it, pass `isShortcutEnabled = false` during initialization: 208 | 209 | ```kotlin 210 | DelightSqlViewer.init( 211 | object : AndroidEnvironmentProvider { 212 | override fun getDriver() = sqlDelightDriver // or roomDatabase for Room 213 | override fun getContext() = this@MainActivity 214 | }, 215 | isShortcutEnabled = false 216 | ) 217 | ``` 218 | 219 | ### iOS Shortcut 220 | 221 | On iOS, the library adds a shortcut on the app icon by default. To handle shortcut actions, configure your `AppDelegate` or `UISceneDelegate` as follows: 222 | 223 | ```swift 224 | class AppDelegate: NSObject, UIApplicationDelegate { 225 | func application( 226 | _ application: UIApplication, 227 | configurationForConnecting connectingSceneSession: UISceneSession, 228 | options: UIScene.ConnectionOptions 229 | ) -> UISceneConfiguration { 230 | return ShortcutActionHandler.shared.getConfiguration(session: connectingSceneSession) 231 | } 232 | } 233 | ``` 234 | 235 | ### Desktop Shortcut 236 | 237 | Shortcuts are not supported on Desktop. Use your UI controls to manually trigger the viewer. 238 | 239 | --- 240 | 241 | ## Excluding the Library in Release Builds 242 | 243 | Since the viewer is primarily for debugging and development, you may want to exclude it from release builds. 244 | 245 | ### Using the Stub Library 246 | 247 | For release builds, depend on the stub module instead of the full implementation: 248 | 249 | ```kotlin 250 | val isRelease = /* your logic to determine release vs. debug */ 251 | 252 | framework { 253 | if (isRelease) { 254 | // Use the stub library for release builds. 255 | export("ru.bartwell.delightsqlviewer:stub:2.0.0") 256 | } else { 257 | // Use the full implementation for debug builds. 258 | export("ru.bartwell.delightsqlviewer:runtime:2.0.0") 259 | export("ru.bartwell.delightsqlviewer:core:2.0.0") 260 | // Choose the appropriate adapter: 261 | export("ru.bartwell.delightsqlviewer:sqldelight-adapter:2.0.0") 262 | // or 263 | // export("ru.bartwell.delightsqlviewer:room-adapter:2.0.0") 264 | } 265 | } 266 | ``` 267 | 268 | And in your dependencies: 269 | 270 | ```kotlin 271 | dependencies { 272 | if (isRelease) { 273 | api("ru.bartwell.delightsqlviewer:stub:2.0.0") 274 | } else { 275 | api("ru.bartwell.delightsqlviewer:runtime:2.0.0") 276 | api("ru.bartwell.delightsqlviewer:core:2.0.0") 277 | // Choose the appropriate adapter: 278 | api("ru.bartwell.delightsqlviewer:sqldelight-adapter:2.0.0") 279 | // or 280 | // api("ru.bartwell.delightsqlviewer:room-adapter:2.0.0") 281 | } 282 | } 283 | ``` 284 | 285 | ### Omitting Initialization and Launch 286 | 287 | Alternatively, you can simply avoid calling `DelightSqlViewer.init(...)` and `DelightSqlViewer.launch()` in release builds. However, using the stub dependency is generally preferred to prevent unnecessary code inclusion. 288 | 289 | --- 290 | 291 | ## Contributing 292 | 293 | Contributions are welcome! Please feel free to open an issue or submit a pull request with any improvements or suggestions. 294 | 295 | --- 296 | 297 | ## License 298 | 299 | ``` 300 | Copyright 2025 Artem Bazhanov 301 | 302 | Licensed under the Apache License, Version 2.0 (the "License"); 303 | you may not use this file except in compliance with the License. 304 | You may obtain a copy of the License at 305 | 306 | http://www.apache.org/licenses/LICENSE-2.0 307 | ``` 308 | 309 | Delight SQL Viewer is distributed under the [Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0). 310 | 311 | --- 312 | 313 | **Happy debugging!** If you have any questions or need further assistance, feel free to open an issue. 314 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import io.gitlab.arturbosch.detekt.Detekt 2 | import java.util.Properties 3 | 4 | plugins { 5 | alias(libs.plugins.androidApplication).apply(false) 6 | alias(libs.plugins.androidLibrary).apply(false) 7 | alias(libs.plugins.kotlinAndroid).apply(false) 8 | alias(libs.plugins.kotlinMultiplatform).apply(false) 9 | alias(libs.plugins.compose.compiler).apply(false) 10 | alias(libs.plugins.jetbrainsCompose).apply(false) 11 | alias(libs.plugins.detekt) 12 | } 13 | 14 | val detektFormatting = libs.detekt.formatting.get() 15 | val detektRulesCompose = libs.detekt.rules.compose.get() 16 | 17 | allprojects { 18 | ext { 19 | fun loadProperties(filePath: String): Properties { 20 | val file = file("$rootDir/$filePath") 21 | require(file.canRead()) { "Cannot read file: ${file.absolutePath}" } 22 | return Properties().apply { 23 | file.inputStream().use { load(it) } 24 | } 25 | } 26 | 27 | val settingsProperties = loadProperties("settings.properties") 28 | val versionProperties = loadProperties("version.properties") 29 | val isRelease = settingsProperties["isRelease"]?.toString()?.toBooleanStrictOrNull() 30 | ?: error("Missing or invalid 'isRelease' in settings.properties") 31 | val libraryVersionName = versionProperties["libraryVersionName"]?.toString() 32 | ?: error("Invalid version name in version.properties") 33 | 34 | extra.apply { 35 | set("isRelease", isRelease) 36 | set("libraryVersionName", libraryVersionName) 37 | } 38 | } 39 | 40 | apply(plugin = "io.gitlab.arturbosch.detekt") 41 | 42 | val projectSource = file(projectDir) 43 | val configFile = files("$rootDir/config/detekt/detekt.yml") 44 | val baselineFile = file("$rootDir/config/detekt/baseline.xml") 45 | val kotlinFiles = "**/*.kt" 46 | val ignoredFiles = listOf("**/resources/**", "**/build/**") 47 | 48 | fun configureDetektTask(taskName: String, autoFix: Boolean) { 49 | tasks.register(taskName, Detekt::class) { 50 | description = "Detekt analysis for all modules" 51 | parallel = false 52 | ignoreFailures = true 53 | autoCorrect = autoFix 54 | buildUponDefaultConfig = true 55 | setSource(projectSource) 56 | baseline.set(baselineFile) 57 | config.setFrom(configFile) 58 | include(kotlinFiles) 59 | exclude(ignoredFiles) 60 | reports { 61 | html.required.set(false) 62 | xml.required.set(true) 63 | txt.required.set(false) 64 | } 65 | } 66 | } 67 | 68 | configureDetektTask("detektCheckAll", project.hasProperty("detektAutoFix")) 69 | configureDetektTask("detektFixAll", true) 70 | 71 | dependencies { 72 | detektPlugins(detektFormatting) 73 | detektPlugins(detektRulesCompose) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /config/detekt/baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /config/lint/lint-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /config/lint/lint.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /content/screenshots/delete_rows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/content/screenshots/delete_rows.png -------------------------------------------------------------------------------- /content/screenshots/insert_row.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/content/screenshots/insert_row.png -------------------------------------------------------------------------------- /content/screenshots/table_content.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/content/screenshots/table_content.png -------------------------------------------------------------------------------- /content/screenshots/tables_list.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/content/screenshots/tables_list.png -------------------------------------------------------------------------------- /content/screenshots/update_value.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/content/screenshots/update_value.png -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.kotlinSerialization) 7 | id("maven-publish") 8 | id("signing") 9 | } 10 | 11 | group = "ru.bartwell.delightsqlviewer" 12 | version = extra["libraryVersionName"] as String 13 | 14 | kotlin { 15 | androidTarget { 16 | compilations.all { 17 | compileTaskProvider.configure { 18 | compilerOptions { 19 | jvmTarget.set(JvmTarget.JVM_1_8) 20 | } 21 | } 22 | } 23 | publishLibraryVariants("release") 24 | } 25 | 26 | listOf( 27 | iosX64(), 28 | iosArm64(), 29 | iosSimulatorArm64() 30 | ).forEach { 31 | it.binaries.framework { 32 | baseName = "core" 33 | isStatic = true 34 | } 35 | } 36 | 37 | jvm() 38 | 39 | sourceSets { 40 | commonMain.dependencies { 41 | implementation( libs.kotlinx.serialization.json) 42 | implementation(libs.kotlinx.coroutines.core) 43 | } 44 | } 45 | 46 | explicitApi() 47 | } 48 | 49 | android { 50 | namespace = "ru.bartwell.delightsqlviewer.core" 51 | compileSdk = 34 52 | 53 | defaultConfig { 54 | minSdk = 24 55 | } 56 | 57 | compileOptions { 58 | sourceCompatibility = JavaVersion.VERSION_1_8 59 | targetCompatibility = JavaVersion.VERSION_1_8 60 | } 61 | } 62 | 63 | apply(from = "$rootDir/gradle/maven-publishing.gradle.kts") 64 | -------------------------------------------------------------------------------- /core/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/AndroidEnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import android.content.Context 4 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 5 | 6 | public interface AndroidEnvironmentProvider : EnvironmentProvider { 7 | public fun getContext(): Context 8 | } 9 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/DatabaseWrapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core 2 | 3 | import kotlinx.coroutines.flow.Flow 4 | import ru.bartwell.delightsqlviewer.core.data.Column 5 | import ru.bartwell.delightsqlviewer.core.mapper.SqlMapper 6 | 7 | public abstract class DatabaseWrapper { 8 | public abstract fun query(sql: String, mapper: SqlMapper): Flow> 9 | public abstract fun querySingle(sql: String, mapper: SqlMapper): Flow 10 | public abstract fun updateSingle(table: String, id: Long, column: Column, value: String?): Flow 11 | public abstract fun insert(table: String, values: Map): Flow 12 | public abstract fun delete(table: String, ids: List): Flow 13 | 14 | protected fun buildUpdateQuery(table: String, column: Column): String = 15 | "UPDATE $table SET ${column.name} = ? WHERE rowid = ?" 16 | 17 | protected fun buildInsertQuery(table: String, values: Map): String { 18 | return if (values.isEmpty()) { 19 | "INSERT INTO $table DEFAULT VALUES;" 20 | } else { 21 | val columnsPart = values.keys.joinToString(",") { it.name } 22 | val valuesPart = List(values.size) { "?" }.joinToString(",") 23 | "INSERT INTO $table ($columnsPart) VALUES ($valuesPart)" 24 | } 25 | } 26 | 27 | protected fun buildDeleteQuery(table: String, ids: List): String { 28 | val whereClause = "rowid=" + ids.joinToString(" OR rowid=") 29 | return "DELETE FROM $table WHERE $whereClause;" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/EnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core 2 | 3 | public interface EnvironmentProvider { 4 | public fun getDriver(): T 5 | public fun getWrapper(): DatabaseWrapper 6 | } 7 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/data/Column.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.data 2 | 3 | import kotlinx.serialization.Serializable 4 | 5 | private const val ROW_ID_COLUMN_NAME = "rowid" 6 | 7 | @Serializable 8 | public data class Column( 9 | val name: String, 10 | val type: ColumnType, 11 | val isNotNullable: Boolean, 12 | val defaultValue: String?, 13 | ) { 14 | val isRowId: Boolean 15 | get() = name == ROW_ID_COLUMN_NAME 16 | 17 | public companion object { 18 | public val ROW_ID_COLUMN: Column = Column( 19 | name = ROW_ID_COLUMN_NAME, 20 | type = ColumnType.INTEGER, 21 | isNotNullable = true, 22 | defaultValue = "", 23 | ) 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/data/ColumnType.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.data 2 | 3 | public enum class ColumnType { 4 | INTEGER, TEXT, REAL, BLOB, 5 | } 6 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/data/Row.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.data 2 | 3 | public data class Row( 4 | val id: Long, 5 | val data: List, 6 | ) 7 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/ColumnsSqlMapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | import ru.bartwell.delightsqlviewer.core.data.ColumnType 5 | 6 | public class ColumnsSqlMapper : SqlMapper { 7 | @Suppress("MagicNumber") 8 | override fun map(cursor: CursorWrapper<*>): Column? { 9 | val name = cursor.getString(1) 10 | val type = cursor.getString(2) 11 | return if (name != null && type != null) { 12 | Column( 13 | name = name, 14 | type = ColumnType.valueOf(type), 15 | isNotNullable = cursor.getBoolean(3) ?: false, 16 | defaultValue = cursor.getString(4), 17 | ) 18 | } else { 19 | null 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/CursorWrapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | import ru.bartwell.delightsqlviewer.core.data.ColumnType 5 | 6 | public interface CursorWrapper { 7 | public val value: T 8 | public fun getString(index: Int): String? 9 | public fun getBoolean(index: Int): Boolean? 10 | public fun getLong(index: Int): Long? 11 | public fun getDouble(index: Int): Double? 12 | public fun getBytes(index: Int): ByteArray? 13 | 14 | @OptIn(ExperimentalStdlibApi::class) 15 | public fun getStringOrNull(column: Column, index: Int): String? = when (column.type) { 16 | ColumnType.INTEGER -> getLong(index)?.toString() 17 | ColumnType.TEXT -> getString(index) 18 | ColumnType.REAL -> getDouble(index)?.toString() 19 | ColumnType.BLOB -> getBytes(index)?.toHexString() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/RowsSqlMapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | import ru.bartwell.delightsqlviewer.core.data.Row 5 | 6 | public class RowsSqlMapper(private val columns: List) : SqlMapper { 7 | public override fun map(cursor: CursorWrapper<*>): Row? { 8 | var id: Long? = null 9 | val data = mutableListOf() 10 | for (column in columns.withIndex()) { 11 | if (column.value.isRowId) { 12 | id = cursor.getLong(column.index) 13 | } else { 14 | data.add(cursor.getStringOrNull(column.value, column.index)) 15 | } 16 | } 17 | return id?.let { Row(it, data) } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/SingleStringSqlMapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | 5 | public class SingleStringSqlMapper(private val column: Column) : SqlMapper { 6 | override fun map(cursor: CursorWrapper<*>): String? { 7 | return cursor.getStringOrNull(column, 0) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/SqlMapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | public interface SqlMapper { 4 | public fun map(cursor: CursorWrapper<*>): T? 5 | } 6 | -------------------------------------------------------------------------------- /core/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/mapper/StringSqlMapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.mapper 2 | 3 | public class StringSqlMapper : SqlMapper { 4 | override fun map(cursor: CursorWrapper<*>): String? { 5 | return cursor.getString(0) 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /core/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/IosEnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 4 | 5 | public abstract class IosEnvironmentProvider(protected val driver: T) : EnvironmentProvider { 6 | final override fun getDriver(): T = driver 7 | } 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | #Gradle 2 | org.gradle.jvmargs=-Xmx2048M -Dfile.encoding=UTF-8 -Dkotlin.daemon.jvm.options\="-Xmx2048M" 3 | org.gradle.caching=true 4 | org.gradle.configuration-cache=true 5 | kotlin.native.enableKlibsCrossCompilation=true 6 | 7 | #Kotlin 8 | kotlin.code.style=official 9 | 10 | #Android 11 | android.useAndroidX=true 12 | android.nonTransitiveRClass=true 13 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.5.2" 3 | kotlinx-coroutines = "1.10.1" 4 | kotlinx-serialization-json = "1.7.3" 5 | sqldelight = "2.0.2" 6 | room = "2.7.0-rc01" 7 | room-driver = "2.5.0-rc01" 8 | kotlin = "2.1.21" 9 | ksp = "2.1.21-2.0.1" 10 | compose = "1.8.1" 11 | compose-material3 = "1.3.1" 12 | androidx-activity-compose = "1.10.1" 13 | decompose = "3.1.0" 14 | decompose-essenty = "2.3.0" 15 | detekt = "1.23.7" 16 | detekt-rules-compose = "1.4.0" 17 | 18 | [libraries] 19 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 20 | androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity-compose" } 21 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 22 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 23 | kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization-json" } 24 | compose-material3 = { module = "androidx.compose.material3:material3", version.ref = "compose-material3" } 25 | decompose = { module = "com.arkivanov.decompose:decompose", version.ref = "decompose" } 26 | decompose-extensions-compose = { module = "com.arkivanov.decompose:extensions-compose", version.ref = "decompose" } 27 | decompose-essenty-lifecycle-coroutines = { module = "com.arkivanov.essenty:lifecycle-coroutines", version.ref = "decompose-essenty" } 28 | sqldelight-android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqldelight" } 29 | sqldelight-native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqldelight" } 30 | sqldelight-driver-sqlite = { module = "app.cash.sqldelight:sqlite-driver", version.ref = "sqldelight" } 31 | room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" } 32 | room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" } 33 | room-driver = { module = "androidx.sqlite:sqlite-bundled", version.ref = "room-driver" } 34 | detekt-formatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" } 35 | detekt-rules-compose = { module = "ru.kode:detekt-rules-compose", version.ref = "detekt-rules-compose" } 36 | 37 | [plugins] 38 | androidApplication = { id = "com.android.application", version.ref = "agp" } 39 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 40 | kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } 41 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 42 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 43 | compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } 44 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose" } 45 | kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } 46 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } 47 | sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } 48 | room = { id = "androidx.room", version.ref = "room" } 49 | -------------------------------------------------------------------------------- /gradle/maven-publishing.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.gradle.api.publish.PublishingExtension 2 | import org.gradle.api.publish.maven.MavenPublication 3 | import org.gradle.api.publish.maven.tasks.PublishToMavenLocal 4 | import org.gradle.api.publish.maven.tasks.PublishToMavenRepository 5 | import org.gradle.api.tasks.bundling.Jar 6 | import org.gradle.plugins.signing.Sign 7 | import org.gradle.plugins.signing.SigningExtension 8 | 9 | plugins.withId("maven-publish") { 10 | val javadocJar = tasks.findByName("javadocJar") as? Jar 11 | ?: tasks.create("javadocJar", Jar::class.java) { 12 | archiveClassifier.set("javadoc") 13 | from(file("empty-javadoc")) 14 | } 15 | 16 | extensions.configure("publishing") { 17 | publications.withType(MavenPublication::class.java).configureEach { 18 | artifact(javadocJar) 19 | pom { 20 | name.set("Delight SQL Viewer") 21 | description.set( 22 | "Delight SQL Viewer is a multiplatform library " + 23 | "that integrates database viewing and editing into your application" 24 | ) 25 | url.set("https://github.com/bartwell/delight-sql-viewer") 26 | licenses { 27 | license { 28 | name.set("Apache License, Version 2.0") 29 | url.set("https://www.apache.org/licenses/LICENSE-2.0.txt") 30 | } 31 | } 32 | scm { 33 | connection.set("scm:git:git://github.com/bartwell/delight-sql-viewer.git") 34 | developerConnection.set("scm:git:ssh://github.com/bartwell/delight-sql-viewer.git") 35 | url.set("https://github.com/bartwell/delight-sql-viewer") 36 | } 37 | developers { 38 | developer { 39 | id.set("BArtWell") 40 | name.set("Artem Bazhanov") 41 | email.set("web@bartwell.ru") 42 | } 43 | } 44 | } 45 | } 46 | 47 | repositories { 48 | maven { 49 | name = "OSSRH" 50 | url = uri("https://oss.sonatype.org/service/local/staging/deploy/maven2/") 51 | credentials { 52 | username = findProperty("ossrhUsername") as String? 53 | ?: System.getenv("OSSRH_USERNAME") 54 | password = findProperty("ossrhPassword") as String? 55 | ?: System.getenv("OSSRH_PASSWORD") 56 | } 57 | } 58 | } 59 | } 60 | } 61 | 62 | plugins.withId("signing") { 63 | extensions.configure("signing") { 64 | useInMemoryPgpKeys( 65 | findProperty("signingKeyId") as String? ?: System.getenv("SIGNING_KEY_ID"), 66 | findProperty("signingSecretKey") as String? ?: System.getenv("SIGNING_SECRET_KEY"), 67 | findProperty("signingPassword") as String? ?: System.getenv("SIGNING_PASSWORD") 68 | ) 69 | val publishingExtension = project.extensions.getByType(PublishingExtension::class.java) 70 | sign(*publishingExtension.publications.toTypedArray()) 71 | } 72 | } 73 | 74 | tasks.withType().configureEach { 75 | dependsOn(tasks.withType()) 76 | } 77 | tasks.withType().configureEach { 78 | dependsOn(tasks.withType()) 79 | } 80 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bartwell/delight-sql-viewer/2a91736105f59e545b6d3b3cff2d7e4cd543e2da/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Thu Feb 13 16:29:26 MSK 2025 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or 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 UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /room-adapter/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | id("maven-publish") 7 | id("signing") 8 | } 9 | 10 | group = "ru.bartwell.delightsqlviewer" 11 | version = extra["libraryVersionName"] as String 12 | 13 | kotlin { 14 | androidTarget { 15 | compilations.all { 16 | compileTaskProvider.configure { 17 | compilerOptions { 18 | jvmTarget.set(JvmTarget.JVM_1_8) 19 | } 20 | } 21 | } 22 | publishLibraryVariants("release") 23 | } 24 | 25 | listOf( 26 | iosX64(), 27 | iosArm64(), 28 | iosSimulatorArm64() 29 | ).forEach { 30 | it.binaries.framework { 31 | baseName = "room-adapter" 32 | isStatic = true 33 | } 34 | } 35 | 36 | jvm() 37 | 38 | sourceSets { 39 | commonMain.dependencies { 40 | implementation(projects.core) 41 | implementation(libs.room.runtime) 42 | } 43 | } 44 | 45 | explicitApi() 46 | } 47 | 48 | android { 49 | namespace = "ru.bartwell.delightsqlviewer.adapter.room" 50 | compileSdk = 34 51 | 52 | defaultConfig { 53 | minSdk = 24 54 | } 55 | 56 | compileOptions { 57 | sourceCompatibility = JavaVersion.VERSION_1_8 58 | targetCompatibility = JavaVersion.VERSION_1_8 59 | } 60 | } 61 | 62 | apply(from = "$rootDir/gradle/maven-publishing.gradle.kts") 63 | -------------------------------------------------------------------------------- /room-adapter/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/adapter/room/RoomEnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.adapter.room 2 | 3 | import androidx.room.RoomDatabase 4 | import ru.bartwell.delightsqlviewer.AndroidEnvironmentProvider 5 | import ru.bartwell.delightsqlviewer.core.DatabaseWrapper 6 | 7 | public abstract class RoomEnvironmentProvider : AndroidEnvironmentProvider { 8 | final override fun getWrapper(): DatabaseWrapper = RoomWrapper(getDriver()) 9 | } 10 | -------------------------------------------------------------------------------- /room-adapter/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/adapter/room/RoomCursorWrapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.adapter.room 2 | 3 | import androidx.sqlite.SQLiteStatement 4 | import ru.bartwell.delightsqlviewer.core.mapper.CursorWrapper 5 | 6 | internal class RoomCursorWrapper(override val value: SQLiteStatement) : CursorWrapper { 7 | override fun getString(index: Int): String? = value.getTextOrNull(index) 8 | override fun getBoolean(index: Int): Boolean? = value.getBooleanOrNull(index) 9 | override fun getLong(index: Int): Long? = value.getLongOrNull(index) 10 | override fun getDouble(index: Int): Double? = value.getDoubleOrNull(index) 11 | override fun getBytes(index: Int): ByteArray? = value.getBlobOrNull(index) 12 | } 13 | 14 | private fun SQLiteStatement.getTextOrNull(index: Int): String? = getNullable(index) { getText(index) } 15 | private fun SQLiteStatement.getBooleanOrNull(index: Int): Boolean? = getNullable(index) { getBoolean(index) } 16 | private fun SQLiteStatement.getLongOrNull(index: Int): Long? = getNullable(index) { getLong(index) } 17 | private fun SQLiteStatement.getDoubleOrNull(index: Int): Double? = getNullable(index) { getDouble(index) } 18 | private fun SQLiteStatement.getBlobOrNull(index: Int): ByteArray? = getNullable(index) { getBlob(index) } 19 | 20 | private fun SQLiteStatement.getNullable(index: Int, block: (Int) -> T): T? { 21 | if (isNull(index)) return null 22 | @Suppress("SwallowedException", "TooGenericExceptionCaught") 23 | return try { 24 | block(index) 25 | } catch (e: NullPointerException) { 26 | null 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /room-adapter/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/adapter/room/RoomWrapper.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.adapter.room 2 | 3 | import androidx.room.RoomDatabase 4 | import androidx.room.useReaderConnection 5 | import androidx.room.useWriterConnection 6 | import androidx.sqlite.SQLiteStatement 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.flow 9 | import ru.bartwell.delightsqlviewer.core.DatabaseWrapper 10 | import ru.bartwell.delightsqlviewer.core.data.Column 11 | import ru.bartwell.delightsqlviewer.core.data.ColumnType 12 | import ru.bartwell.delightsqlviewer.core.mapper.SqlMapper 13 | 14 | public class RoomWrapper(internal val database: RoomDatabase) : DatabaseWrapper() { 15 | 16 | override fun query(sql: String, mapper: SqlMapper): Flow> = flow { 17 | val result = mutableListOf() 18 | database.useReaderConnection { connection -> 19 | connection.usePrepared(sql) { statement -> 20 | while (statement.step()) { 21 | mapper.map(RoomCursorWrapper(statement))?.let { 22 | result.add(it) 23 | } 24 | } 25 | } 26 | } 27 | emit(result) 28 | } 29 | 30 | override fun querySingle(sql: String, mapper: SqlMapper): Flow = flow { 31 | var result: T? = null 32 | database.useReaderConnection { connection -> 33 | connection.usePrepared(sql) { statement -> 34 | statement.step() 35 | result = mapper.map(RoomCursorWrapper(statement)) 36 | } 37 | } 38 | emit(result) 39 | } 40 | 41 | override fun updateSingle(table: String, id: Long, column: Column, value: String?): Flow = flow { 42 | val sql = buildUpdateQuery(table, column) 43 | database.useWriterConnection { connection -> 44 | connection.usePrepared(sql) { statement -> 45 | statement.bindValue(index = 1, column = column, value = value) 46 | statement.bindLong(index = 2, value = id) 47 | statement.step() 48 | } 49 | } 50 | emit(Unit) 51 | } 52 | 53 | override fun insert(table: String, values: Map): Flow = flow { 54 | val sql = buildInsertQuery(table, values) 55 | database.useWriterConnection { connection -> 56 | connection.usePrepared(sql) { statement -> 57 | if (values.isNotEmpty()) { 58 | for ((index, entry) in values.entries.withIndex()) { 59 | statement.bindValue(index = index + 1, column = entry.key, value = entry.value) 60 | } 61 | } 62 | statement.step() 63 | } 64 | } 65 | emit(Unit) 66 | } 67 | 68 | override fun delete(table: String, ids: List): Flow = flow { 69 | if (ids.isNotEmpty()) { 70 | val sql = buildDeleteQuery(table, ids) 71 | database.useWriterConnection { connection -> 72 | connection.usePrepared(sql) { statement -> 73 | statement.step() 74 | } 75 | } 76 | } 77 | emit(Unit) 78 | } 79 | } 80 | 81 | private fun SQLiteStatement.bindLong(index: Int, value: Long?) = value?.let { 82 | bindLong(index, it) 83 | } ?: bindNull(index) 84 | 85 | private fun SQLiteStatement.bindText(index: Int, value: String?) = value?.let { 86 | bindText(index, it) 87 | } ?: bindNull(index) 88 | 89 | private fun SQLiteStatement.bindDouble(index: Int, value: Double?) = value?.let { 90 | bindDouble(index, it) 91 | } ?: bindNull(index) 92 | 93 | private fun SQLiteStatement.bindBlob(index: Int, value: ByteArray?) = value?.let { 94 | bindBlob(index, it) 95 | } ?: bindNull(index) 96 | 97 | @OptIn(ExperimentalStdlibApi::class) 98 | private fun SQLiteStatement.bindValue(index: Int, column: Column, value: String?) { 99 | when (column.type) { 100 | ColumnType.INTEGER -> this.bindLong(index, value?.toLong()) 101 | ColumnType.TEXT -> this.bindText(index, value) 102 | ColumnType.REAL -> this.bindDouble(index, value?.toDouble()) 103 | ColumnType.BLOB -> this.bindBlob(index, value?.hexToByteArray()) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /room-adapter/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/adapter/room/RoomEnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.adapter.room 2 | 3 | import androidx.room.RoomDatabase 4 | import ru.bartwell.delightsqlviewer.IosEnvironmentProvider 5 | import ru.bartwell.delightsqlviewer.core.DatabaseWrapper 6 | 7 | public class RoomEnvironmentProvider(driver: RoomDatabase) : IosEnvironmentProvider(driver) { 8 | override fun getWrapper(): DatabaseWrapper = RoomWrapper(driver) 9 | } 10 | -------------------------------------------------------------------------------- /room-adapter/src/jvmMain/kotlin/ru/bartwell/delightsqlviewer/adapter/room/RoomEnvironmentProvider.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.adapter.room 2 | 3 | import androidx.room.RoomDatabase 4 | import ru.bartwell.delightsqlviewer.core.DatabaseWrapper 5 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 6 | 7 | public abstract class RoomEnvironmentProvider : EnvironmentProvider { 8 | final override fun getWrapper(): DatabaseWrapper = RoomWrapper(getDriver()) 9 | } 10 | -------------------------------------------------------------------------------- /runtime/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.androidLibrary) 6 | alias(libs.plugins.jetbrainsCompose) 7 | alias(libs.plugins.compose.compiler) 8 | alias(libs.plugins.kotlinSerialization) 9 | alias(libs.plugins.sqldelight) 10 | id("maven-publish") 11 | id("signing") 12 | } 13 | 14 | group = "ru.bartwell.delightsqlviewer" 15 | version = extra["libraryVersionName"] as String 16 | 17 | kotlin { 18 | androidTarget { 19 | compilations.all { 20 | compileTaskProvider.configure { 21 | compilerOptions { 22 | jvmTarget.set(JvmTarget.JVM_1_8) 23 | } 24 | } 25 | } 26 | publishLibraryVariants("release") 27 | } 28 | 29 | listOf( 30 | iosX64(), 31 | iosArm64(), 32 | iosSimulatorArm64() 33 | ).forEach { 34 | it.binaries.framework { 35 | baseName = "runtime" 36 | isStatic = true 37 | } 38 | } 39 | 40 | jvm() 41 | 42 | sourceSets { 43 | commonMain.dependencies { 44 | implementation(projects.core) 45 | implementation(compose.runtime) 46 | implementation(compose.foundation) 47 | implementation(compose.material3) 48 | implementation(compose.materialIconsExtended) 49 | implementation(libs.decompose) 50 | implementation(libs.decompose.extensions.compose) 51 | implementation(libs.decompose.essenty.lifecycle.coroutines) 52 | } 53 | commonTest.dependencies { 54 | implementation(libs.kotlin.test) 55 | } 56 | androidMain.dependencies { 57 | implementation(libs.androidx.activity.compose) 58 | implementation(libs.sqldelight.android.driver) 59 | } 60 | appleMain.dependencies { 61 | implementation(libs.sqldelight.native.driver) 62 | } 63 | jvmMain.dependencies { 64 | implementation(compose.desktop.currentOs) 65 | implementation(libs.sqldelight.driver.sqlite) 66 | } 67 | } 68 | 69 | explicitApi() 70 | } 71 | 72 | android { 73 | namespace = "ru.bartwell.delightsqlviewer" 74 | compileSdk = 34 75 | 76 | defaultConfig { 77 | minSdk = 24 78 | } 79 | 80 | compileOptions { 81 | sourceCompatibility = JavaVersion.VERSION_1_8 82 | targetCompatibility = JavaVersion.VERSION_1_8 83 | } 84 | 85 | buildFeatures { 86 | compose = true 87 | } 88 | } 89 | 90 | sqldelight {} 91 | 92 | apply(from = "$rootDir/gradle/maven-publishing.gradle.kts") 93 | -------------------------------------------------------------------------------- /runtime/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/DelightSqlViewerActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import androidx.activity.enableEdgeToEdge 7 | import com.arkivanov.decompose.defaultComponentContext 8 | import ru.bartwell.delightsqlviewer.core.component.DefaultRootComponent 9 | 10 | public class DelightSqlViewerActivity : ComponentActivity() { 11 | override fun onCreate(savedInstanceState: Bundle?) { 12 | super.onCreate(savedInstanceState) 13 | enableEdgeToEdge() 14 | val rootComponent = DefaultRootComponent(defaultComponentContext()) 15 | setContent { 16 | App(rootComponent) 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/core/util/LaunchManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import android.content.Intent 4 | import ru.bartwell.delightsqlviewer.AndroidEnvironmentProvider 5 | import ru.bartwell.delightsqlviewer.DelightSqlViewerActivity 6 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 7 | 8 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 9 | internal actual object LaunchManager { 10 | actual fun launch(environmentProvider: EnvironmentProvider<*>) { 11 | val provider = environmentProvider as AndroidEnvironmentProvider 12 | val context = provider.getContext() 13 | val intent = Intent(context, DelightSqlViewerActivity::class.java) 14 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 15 | context.startActivity(intent) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/core/util/ShortcutManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import android.content.Intent 4 | import android.content.pm.ShortcutInfo 5 | import android.content.pm.ShortcutManager 6 | import android.graphics.drawable.Icon 7 | import android.os.Build 8 | import ru.bartwell.delightsqlviewer.AndroidEnvironmentProvider 9 | import ru.bartwell.delightsqlviewer.DelightSqlViewerActivity 10 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 11 | 12 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 13 | internal actual object ShortcutManager { 14 | internal actual fun setup(environmentProvider: EnvironmentProvider<*>) { 15 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { 16 | val provider = environmentProvider as AndroidEnvironmentProvider 17 | val context = provider.getContext() 18 | context.getSystemService(ShortcutManager::class.java)?.let { shortcutManager -> 19 | val intent = Intent(context, DelightSqlViewerActivity::class.java) 20 | intent.setAction(Intent.ACTION_VIEW) 21 | val shortcut = ShortcutInfo.Builder(context, id) 22 | .setShortLabel(title) 23 | .setLongLabel(subtitle) 24 | .setIcon(Icon.createWithResource(context, android.R.drawable.ic_menu_info_details)) 25 | .setIntent(intent) 26 | .build() 27 | shortcutManager.dynamicShortcuts = listOf(shortcut) 28 | } 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /runtime/src/androidMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/ScreenCloser.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import androidx.activity.compose.LocalActivity 4 | import androidx.compose.runtime.Composable 5 | 6 | @Composable 7 | internal actual fun screenCloser(): () -> Unit { 8 | val activity = LocalActivity.current 9 | return { activity?.finish() } 10 | } 11 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/App.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.isSystemInDarkTheme 5 | import androidx.compose.foundation.layout.fillMaxSize 6 | import androidx.compose.foundation.layout.imePadding 7 | import androidx.compose.foundation.layout.systemBarsPadding 8 | import androidx.compose.material3.ColorScheme 9 | import androidx.compose.material3.MaterialTheme 10 | import androidx.compose.material3.Scaffold 11 | import androidx.compose.material3.darkColorScheme 12 | import androidx.compose.material3.lightColorScheme 13 | import androidx.compose.runtime.Composable 14 | import androidx.compose.ui.Modifier 15 | import ru.bartwell.delightsqlviewer.core.component.RootComponent 16 | import ru.bartwell.delightsqlviewer.core.component.RootContent 17 | import ru.bartwell.delightsqlviewer.core.data.Theme 18 | 19 | @Composable 20 | internal fun App(rootComponent: RootComponent) { 21 | MaterialTheme( 22 | colorScheme = DelightSqlViewer.theme.toColorScheme(), 23 | ) { 24 | Scaffold( 25 | modifier = Modifier.fillMaxSize() 26 | .background(MaterialTheme.colorScheme.background) 27 | .systemBarsPadding() 28 | .imePadding() 29 | ) { 30 | RootContent( 31 | modifier = Modifier.fillMaxSize(), 32 | component = rootComponent, 33 | ) 34 | } 35 | } 36 | } 37 | 38 | @Composable 39 | private fun Theme.toColorScheme(): ColorScheme = when (this) { 40 | Theme.Auto -> if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme() 41 | Theme.Dark -> darkColorScheme() 42 | Theme.Light -> lightColorScheme() 43 | is Theme.Custom -> scheme 44 | } 45 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/DelightSqlViewer.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import ru.bartwell.delightsqlviewer.core.DatabaseWrapper 4 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 5 | import ru.bartwell.delightsqlviewer.core.data.Theme 6 | import ru.bartwell.delightsqlviewer.core.util.LaunchManager 7 | import ru.bartwell.delightsqlviewer.core.util.ShortcutManager 8 | import ru.bartwell.delightsqlviewer.core.util.id 9 | 10 | public object DelightSqlViewer { 11 | 12 | private var environmentProvider: EnvironmentProvider<*>? = null 13 | internal var theme: Theme = Theme.Auto 14 | 15 | public fun init(provider: EnvironmentProvider<*>) { 16 | init(provider, true) 17 | } 18 | 19 | public fun init(provider: EnvironmentProvider<*>, isShortcutEnabled: Boolean) { 20 | environmentProvider = provider 21 | if (isShortcutEnabled) { 22 | ShortcutManager.setup(provider) 23 | } 24 | } 25 | 26 | internal fun getDriver(): DatabaseWrapper { 27 | val driver = environmentProvider?.getWrapper() 28 | requireNotNull(driver) { "Driver is null. Did you call DelightSqlViewer.init()?" } 29 | return driver 30 | } 31 | 32 | public fun launch() { 33 | launch(theme = Theme.Auto) 34 | } 35 | 36 | public fun launch(theme: Theme) { 37 | this.theme = theme 38 | environmentProvider?.let { LaunchManager.launch(it) } 39 | } 40 | 41 | public fun getShortcutId(): String = ShortcutManager.id 42 | } 43 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/component/Component.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.component 2 | 3 | internal interface Component 4 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/component/DefaultRootComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.component 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.router.stack.ChildStack 5 | import com.arkivanov.decompose.router.stack.StackNavigation 6 | import com.arkivanov.decompose.router.stack.childStack 7 | import com.arkivanov.decompose.router.stack.pop 8 | import com.arkivanov.decompose.router.stack.pushNew 9 | import com.arkivanov.decompose.value.Value 10 | import kotlinx.serialization.Serializable 11 | import ru.bartwell.delightsqlviewer.core.data.Column 12 | import ru.bartwell.delightsqlviewer.feature.insert.presentation.DefaultInsertComponent 13 | import ru.bartwell.delightsqlviewer.feature.structure.presentation.DefaultStructureComponent 14 | import ru.bartwell.delightsqlviewer.feature.table.presentation.DefaultTablesListComponent 15 | import ru.bartwell.delightsqlviewer.feature.update.presentation.DefaultUpdateComponent 16 | import ru.bartwell.delightsqlviewer.feature.viewer.presentation.DefaultViewerComponent 17 | 18 | internal class DefaultRootComponent( 19 | componentContext: ComponentContext, 20 | ) : RootComponent, ComponentContext by componentContext { 21 | 22 | private val nav = StackNavigation() 23 | 24 | override val stack: Value>> = childStack( 25 | source = nav, 26 | serializer = Config.serializer(), 27 | initialConfiguration = Config.TablesList, 28 | handleBackButton = true, 29 | childFactory = ::child, 30 | ) 31 | 32 | init { 33 | stack.subscribe { childStack -> 34 | val component = childStack.active.instance.component 35 | if (component is Resumable) { 36 | component.onResume() 37 | } 38 | } 39 | } 40 | 41 | private fun child( 42 | config: Config, 43 | componentContext: ComponentContext 44 | ): RootComponent.Child<*> = when (config) { 45 | Config.TablesList -> RootComponent.Child.TablesList( 46 | DefaultTablesListComponent( 47 | componentContext = componentContext, 48 | listItemClicked = { table -> 49 | nav.pushNew(Config.Viewer(table)) 50 | } 51 | ) 52 | ) 53 | 54 | is Config.Viewer -> { 55 | RootComponent.Child.Viewer( 56 | DefaultViewerComponent( 57 | componentContext = componentContext, 58 | table = config.table, 59 | onFinished = { 60 | nav.pop() 61 | }, 62 | structureClick = { table -> nav.pushNew(Config.Structure(table)) }, 63 | insertClick = { table, columns -> nav.pushNew(Config.Insert(table, columns)) }, 64 | cellClick = { table, column, rowId -> 65 | nav.pushNew(Config.Update(table, column, rowId)) 66 | } 67 | ) 68 | ) 69 | } 70 | 71 | is Config.Update -> RootComponent.Child.Update( 72 | DefaultUpdateComponent( 73 | componentContext = componentContext, 74 | table = config.table, 75 | column = config.column, 76 | rowId = config.rowId, 77 | onFinished = { 78 | nav.pop() 79 | }, 80 | ) 81 | ) 82 | 83 | is Config.Insert -> RootComponent.Child.Insert( 84 | DefaultInsertComponent( 85 | componentContext = componentContext, 86 | table = config.table, 87 | columns = config.columns, 88 | onFinished = { 89 | nav.pop() 90 | }, 91 | ) 92 | ) 93 | 94 | is Config.Structure -> RootComponent.Child.Structure( 95 | DefaultStructureComponent( 96 | componentContext = componentContext, 97 | table = config.table, 98 | onFinished = { 99 | nav.pop() 100 | }, 101 | ) 102 | ) 103 | } 104 | 105 | @Serializable 106 | private sealed interface Config { 107 | @Serializable 108 | data object TablesList : Config 109 | 110 | @Serializable 111 | data class Viewer(val table: String) : Config 112 | 113 | @Serializable 114 | data class Update( 115 | val table: String, 116 | val column: Column, 117 | val rowId: Long, 118 | ) : Config 119 | 120 | @Serializable 121 | data class Insert(val table: String, val columns: List) : Config 122 | 123 | @Serializable 124 | data class Structure(val table: String) : Config 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/component/Resumable.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.component 2 | 3 | internal interface Resumable { 4 | fun onResume() 5 | } 6 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/component/RootComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.component 2 | 3 | import com.arkivanov.decompose.router.stack.ChildStack 4 | import com.arkivanov.decompose.value.Value 5 | import ru.bartwell.delightsqlviewer.feature.insert.presentation.InsertComponent 6 | import ru.bartwell.delightsqlviewer.feature.structure.presentation.StructureComponent 7 | import ru.bartwell.delightsqlviewer.feature.table.presentation.TablesListComponent 8 | import ru.bartwell.delightsqlviewer.feature.update.presentation.UpdateComponent 9 | import ru.bartwell.delightsqlviewer.feature.viewer.presentation.ViewerComponent 10 | 11 | internal interface RootComponent { 12 | val stack: Value>> 13 | 14 | sealed class Child(val component: T) { 15 | class TablesList(component: TablesListComponent) : Child(component) 16 | class Viewer(component: ViewerComponent) : Child(component) 17 | class Update(component: UpdateComponent) : Child(component) 18 | class Insert(component: InsertComponent) : Child(component) 19 | class Structure(component: StructureComponent) : Child(component) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/component/RootContent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.component 2 | 3 | import androidx.compose.foundation.layout.fillMaxSize 4 | import androidx.compose.runtime.Composable 5 | import androidx.compose.ui.Modifier 6 | import com.arkivanov.decompose.extensions.compose.stack.Children 7 | import com.arkivanov.decompose.extensions.compose.stack.animation.slide 8 | import com.arkivanov.decompose.extensions.compose.stack.animation.stackAnimation 9 | import ru.bartwell.delightsqlviewer.feature.insert.presentation.InsertContent 10 | import ru.bartwell.delightsqlviewer.feature.structure.presentation.StructureContent 11 | import ru.bartwell.delightsqlviewer.feature.table.presentation.TablesListContent 12 | import ru.bartwell.delightsqlviewer.feature.update.presentation.UpdateContent 13 | import ru.bartwell.delightsqlviewer.feature.viewer.presentation.ViewerContent 14 | 15 | @Composable 16 | internal fun RootContent( 17 | component: RootComponent, 18 | modifier: Modifier = Modifier, 19 | ) { 20 | Children( 21 | stack = component.stack, 22 | modifier = modifier, 23 | animation = stackAnimation(slide()), 24 | ) { 25 | when (val child = it.instance) { 26 | is RootComponent.Child.Viewer -> ViewerContent( 27 | component = child.component, 28 | modifier = Modifier.fillMaxSize(), 29 | ) 30 | 31 | is RootComponent.Child.TablesList -> TablesListContent( 32 | component = child.component, 33 | modifier = Modifier.fillMaxSize(), 34 | ) 35 | 36 | is RootComponent.Child.Update -> UpdateContent( 37 | component = child.component, 38 | modifier = Modifier.fillMaxSize(), 39 | ) 40 | 41 | is RootComponent.Child.Insert -> InsertContent( 42 | component = child.component, 43 | modifier = Modifier.fillMaxSize(), 44 | ) 45 | 46 | is RootComponent.Child.Structure -> StructureContent( 47 | component = child.component, 48 | modifier = Modifier.fillMaxSize(), 49 | ) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/data/Theme.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.data 2 | 3 | import androidx.compose.material3.ColorScheme 4 | 5 | public sealed class Theme { 6 | public data object Light : Theme() 7 | public data object Dark : Theme() 8 | public data object Auto : Theme() 9 | public class Custom(public val scheme: ColorScheme) : Theme() 10 | } 11 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/extension/String.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.extension 2 | 3 | internal fun String?.orNull() = this ?: "null" 4 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/presentation/Alert.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.presentation 2 | 3 | import androidx.compose.material3.AlertDialog 4 | import androidx.compose.material3.Text 5 | import androidx.compose.material3.TextButton 6 | import androidx.compose.runtime.Composable 7 | 8 | @Composable 9 | internal fun ErrorAlert( 10 | message: String, 11 | onDismiss: () -> Unit, 12 | ) { 13 | AlertDialog( 14 | onDismissRequest = onDismiss, 15 | title = { Text(text = "Error") }, 16 | text = { Text(text = message) }, 17 | confirmButton = { 18 | TextButton(onClick = onDismiss) { 19 | Text(text = "Ok") 20 | } 21 | }, 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/presentation/CheckboxWithText.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.presentation 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.ColumnScope 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.padding 7 | import androidx.compose.material3.Checkbox 8 | import androidx.compose.material3.MaterialTheme 9 | import androidx.compose.material3.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.unit.dp 15 | 16 | @Composable 17 | internal fun ColumnScope.CheckboxWithText( 18 | text: String, 19 | isChecked: Boolean, 20 | modifier: Modifier = Modifier, 21 | onClick: () -> Unit, 22 | ) { 23 | Row( 24 | modifier = modifier 25 | .align(Alignment.End) 26 | .padding(horizontal = 8.dp) 27 | .clip(MaterialTheme.shapes.large) 28 | .clickable(onClick = onClick) 29 | .padding(horizontal = 8.dp, vertical = 8.dp), 30 | verticalAlignment = Alignment.CenterVertically, 31 | ) { 32 | Text( 33 | modifier = Modifier.padding(horizontal = 8.dp), 34 | text = text, 35 | ) 36 | Checkbox( 37 | checked = isChecked, 38 | onCheckedChange = null, 39 | ) 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/presentation/ErrorBox.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.presentation 2 | 3 | import androidx.compose.foundation.layout.Box 4 | import androidx.compose.foundation.layout.BoxScope 5 | import androidx.compose.foundation.layout.padding 6 | import androidx.compose.material3.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.unit.dp 11 | 12 | @Composable 13 | internal fun ErrorBox( 14 | error: String?, 15 | modifier: Modifier = Modifier, 16 | content: @Composable BoxScope.() -> Unit 17 | ) { 18 | Box(modifier = modifier) { 19 | if (error == null) { 20 | content() 21 | } else { 22 | Text( 23 | modifier = Modifier.align(Alignment.Center) 24 | .padding(16.dp), 25 | text = error, 26 | ) 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/util/LaunchManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | internal expect object LaunchManager { 7 | fun launch(environmentProvider: EnvironmentProvider<*>) 8 | } 9 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/core/util/ShortcutManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | internal expect object ShortcutManager { 7 | internal fun setup(environmentProvider: EnvironmentProvider<*>) 8 | } 9 | 10 | internal val ShortcutManager.id: String 11 | get() = "delight_sql_viewer_shortcut" 12 | 13 | internal val ShortcutManager.title: String 14 | get() = "SQL Viewer" 15 | 16 | internal val ShortcutManager.subtitle: String 17 | get() = "Open SQL Viewer" 18 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/insert/presentation/DefaultInsertComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.insert.presentation 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 11 | import ru.bartwell.delightsqlviewer.core.data.Column 12 | 13 | internal class DefaultInsertComponent( 14 | componentContext: ComponentContext, 15 | table: String, 16 | columns: List, 17 | private val onFinished: () -> Unit, 18 | ) : InsertComponent, ComponentContext by componentContext { 19 | 20 | private val _model = MutableValue(InsertState(table = table, columns = columns)) 21 | override val model: Value = _model 22 | 23 | override fun onBackPressed() = onFinished() 24 | 25 | override fun onValueChange(column: Column, text: String) { 26 | val newMap = model.value.values.toMutableMap() 27 | newMap[column] = text 28 | _model.value = _model.value.copy(values = newMap) 29 | } 30 | 31 | override fun onValueTypeChange(column: Column, type: InsertValueType) { 32 | val newMap = model.value.valueTypes.toMutableMap() 33 | newMap[column] = type 34 | _model.value = _model.value.copy(valueTypes = newMap) 35 | } 36 | 37 | override fun onSaveClick() { 38 | val values = mutableMapOf() 39 | for (column in model.value.columns) { 40 | values[column] = when (model.value.valueTypes[column]) { 41 | InsertValueType.DEFAULT, null -> continue 42 | InsertValueType.NULL -> null 43 | InsertValueType.VALUE -> model.value.values[column].orEmpty() 44 | } 45 | } 46 | DelightSqlViewer.getDriver() 47 | .insert(model.value.table, values) 48 | .onEach { 49 | onBackPressed() 50 | } 51 | .catch { _model.value = _model.value.copy(insertError = it.toString()) } 52 | .launchIn(coroutineScope()) 53 | } 54 | 55 | override fun onAlertDismiss() { 56 | _model.value = _model.value.copy(insertError = null) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/insert/presentation/InsertComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.insert.presentation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import ru.bartwell.delightsqlviewer.core.component.Component 5 | import ru.bartwell.delightsqlviewer.core.data.Column 6 | 7 | internal interface InsertComponent : Component { 8 | val model: Value 9 | 10 | fun onBackPressed() 11 | fun onValueChange(column: Column, text: String) 12 | fun onValueTypeChange(column: Column, type: InsertValueType) 13 | fun onSaveClick() 14 | fun onAlertDismiss() 15 | } 16 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/insert/presentation/InsertContent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.insert.presentation 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.Row 7 | import androidx.compose.foundation.layout.Spacer 8 | import androidx.compose.foundation.layout.fillMaxSize 9 | import androidx.compose.foundation.layout.fillMaxWidth 10 | import androidx.compose.foundation.layout.height 11 | import androidx.compose.foundation.layout.padding 12 | import androidx.compose.foundation.rememberScrollState 13 | import androidx.compose.foundation.text.KeyboardActions 14 | import androidx.compose.foundation.text.KeyboardOptions 15 | import androidx.compose.foundation.verticalScroll 16 | import androidx.compose.material.icons.Icons 17 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 18 | import androidx.compose.material3.Button 19 | import androidx.compose.material3.ExperimentalMaterial3Api 20 | import androidx.compose.material3.Icon 21 | import androidx.compose.material3.IconButton 22 | import androidx.compose.material3.MaterialTheme 23 | import androidx.compose.material3.OutlinedCard 24 | import androidx.compose.material3.OutlinedTextField 25 | import androidx.compose.material3.RadioButton 26 | import androidx.compose.material3.Text 27 | import androidx.compose.material3.TopAppBar 28 | import androidx.compose.runtime.Composable 29 | import androidx.compose.runtime.getValue 30 | import androidx.compose.ui.Alignment 31 | import androidx.compose.ui.Modifier 32 | import androidx.compose.ui.text.input.ImeAction 33 | import androidx.compose.ui.text.input.KeyboardType 34 | import androidx.compose.ui.unit.dp 35 | import com.arkivanov.decompose.extensions.compose.subscribeAsState 36 | import ru.bartwell.delightsqlviewer.core.data.Column 37 | import ru.bartwell.delightsqlviewer.core.data.ColumnType 38 | import ru.bartwell.delightsqlviewer.core.presentation.ErrorAlert 39 | 40 | @OptIn(ExperimentalMaterial3Api::class) 41 | @Composable 42 | internal fun InsertContent( 43 | component: InsertComponent, 44 | modifier: Modifier = Modifier, 45 | ) { 46 | val state by component.model.subscribeAsState() 47 | 48 | Column(modifier = modifier.fillMaxSize()) { 49 | TopAppBar( 50 | title = { Text("Insert row") }, 51 | navigationIcon = { 52 | IconButton(onClick = component::onBackPressed) { 53 | Icon( 54 | imageVector = Icons.AutoMirrored.Outlined.ArrowBack, 55 | contentDescription = "Back" 56 | ) 57 | } 58 | }, 59 | ) 60 | 61 | val scrollState = rememberScrollState() 62 | Column( 63 | modifier = Modifier.fillMaxWidth() 64 | .verticalScroll(scrollState) 65 | ) { 66 | val lastColumn = state.columns.last() 67 | state.columns.forEach { column -> 68 | Spacer(modifier = Modifier.height(16.dp)) 69 | Card( 70 | state = state, 71 | column = column, 72 | isLastColumn = column == lastColumn, 73 | onValueChange = component::onValueChange, 74 | onValueTypeChange = component::onValueTypeChange, 75 | onSaveClick = component::onSaveClick 76 | ) 77 | } 78 | 79 | Spacer(modifier = Modifier.height(8.dp)) 80 | 81 | Button( 82 | modifier = Modifier 83 | .fillMaxWidth() 84 | .padding(16.dp), 85 | onClick = component::onSaveClick, 86 | ) { 87 | Text("Insert") 88 | } 89 | 90 | Spacer(modifier = Modifier.height(32.dp)) 91 | } 92 | } 93 | 94 | state.insertError?.let { error -> 95 | ErrorAlert(error, component::onAlertDismiss) 96 | } 97 | } 98 | 99 | @Composable 100 | private fun Card( 101 | state: InsertState, 102 | column: Column, 103 | isLastColumn: Boolean, 104 | onValueChange: (Column, String) -> Unit, 105 | onValueTypeChange: (Column, InsertValueType) -> Unit, 106 | onSaveClick: () -> Unit, 107 | ) { 108 | val keyboardType: KeyboardType 109 | val isSingleLine: Boolean 110 | when (column.type) { 111 | ColumnType.INTEGER -> { 112 | keyboardType = KeyboardType.Number 113 | isSingleLine = true 114 | } 115 | 116 | ColumnType.REAL -> { 117 | keyboardType = KeyboardType.Decimal 118 | isSingleLine = true 119 | } 120 | 121 | ColumnType.TEXT, ColumnType.BLOB -> { 122 | keyboardType = KeyboardType.Text 123 | isSingleLine = false 124 | } 125 | } 126 | 127 | val imeAction: ImeAction 128 | val keyboardActions: KeyboardActions 129 | if (isLastColumn) { 130 | imeAction = ImeAction.Done 131 | keyboardActions = KeyboardActions(onDone = { onSaveClick() }) 132 | } else { 133 | imeAction = if (column.type == ColumnType.TEXT) ImeAction.Default else ImeAction.Next 134 | keyboardActions = KeyboardActions.Default 135 | } 136 | OutlinedCard( 137 | modifier = Modifier.fillMaxWidth() 138 | .padding(horizontal = 16.dp), 139 | ) { 140 | Text( 141 | modifier = Modifier.padding(horizontal = 16.dp) 142 | .padding(top = 16.dp, bottom = 8.dp), 143 | text = column.name + " (" + column.type.name.lowercase() + ")", 144 | style = MaterialTheme.typography.titleMedium, 145 | ) 146 | OutlinedTextField( 147 | modifier = Modifier.fillMaxWidth() 148 | .padding(horizontal = 16.dp), 149 | value = getFieldValue(state, column), 150 | onValueChange = { onValueChange(column, it) }, 151 | enabled = state.valueTypes[column] == InsertValueType.VALUE, 152 | singleLine = isSingleLine, 153 | keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction), 154 | keyboardActions = keyboardActions, 155 | ) 156 | 157 | TypesGroup( 158 | column = column, 159 | selectedItem = state.valueTypes.getOrDefault(column), 160 | onChange = onValueTypeChange, 161 | ) 162 | } 163 | } 164 | 165 | private fun getFieldValue(state: InsertState, column: Column): String { 166 | val nullValue = "[NULL]" 167 | return when (state.valueTypes.getOrDefault(column)) { 168 | InsertValueType.DEFAULT -> column.defaultValue ?: nullValue 169 | InsertValueType.NULL -> nullValue 170 | InsertValueType.VALUE -> state.values[column].orEmpty() 171 | } 172 | } 173 | 174 | @Composable 175 | private fun TypesGroup( 176 | column: Column, 177 | selectedItem: InsertValueType, 178 | onChange: (Column, InsertValueType) -> Unit, 179 | ) { 180 | val items = InsertValueType.entries.filter { it != InsertValueType.NULL || !column.isNotNullable } 181 | Row( 182 | modifier = Modifier.fillMaxWidth() 183 | .padding(vertical = 8.dp) 184 | ) { 185 | items.forEach { item -> 186 | Row( 187 | modifier = Modifier 188 | .weight(1f) 189 | .clickable { onChange(column, item) } 190 | .padding(4.dp), 191 | verticalAlignment = Alignment.CenterVertically, 192 | horizontalArrangement = Arrangement.Center, 193 | ) { 194 | RadioButton(selected = selectedItem == item, onClick = null) 195 | Text( 196 | text = item.title, 197 | modifier = Modifier.padding(start = 4.dp), 198 | ) 199 | } 200 | } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/insert/presentation/InsertState.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.insert.presentation 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | 5 | internal data class InsertState( 6 | val table: String, 7 | val columns: List, 8 | val values: Map = emptyMap(), 9 | val valueTypes: Map = emptyMap(), 10 | val insertError: String? = null, 11 | ) 12 | 13 | internal fun Map.getOrDefault(column: Column) = this[column] ?: InsertValueType.entries[0] 14 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/insert/presentation/InsertValueType.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.insert.presentation 2 | 3 | internal enum class InsertValueType(val title: String) { 4 | DEFAULT("Default"), 5 | NULL("null"), 6 | VALUE("Value"), 7 | } 8 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/structure/presentation/DefaultStructureComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.structure.presentation 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 11 | import ru.bartwell.delightsqlviewer.core.mapper.ColumnsSqlMapper 12 | 13 | internal class DefaultStructureComponent( 14 | componentContext: ComponentContext, 15 | table: String, 16 | private val onFinished: () -> Unit, 17 | ) : StructureComponent, ComponentContext by componentContext { 18 | 19 | private val _model = MutableValue(StructureState(table = table)) 20 | override val model: Value = _model 21 | 22 | init { 23 | DelightSqlViewer.getDriver() 24 | .query("PRAGMA table_info($table);", ColumnsSqlMapper()) 25 | .onEach { _model.value = _model.value.copy(columns = it) } 26 | .catch { _model.value = _model.value.copy(error = it.toString()) } 27 | .launchIn(coroutineScope()) 28 | } 29 | 30 | override fun onBackPressed() = onFinished() 31 | } 32 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/structure/presentation/StructureComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.structure.presentation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import ru.bartwell.delightsqlviewer.core.component.Component 5 | 6 | internal interface StructureComponent : Component { 7 | val model: Value 8 | 9 | fun onBackPressed() 10 | } 11 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/structure/presentation/StructureContent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.structure.presentation 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Arrangement 5 | import androidx.compose.foundation.layout.Column 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.material.icons.Icons 12 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 13 | import androidx.compose.material3.ExperimentalMaterial3Api 14 | import androidx.compose.material3.Icon 15 | import androidx.compose.material3.IconButton 16 | import androidx.compose.material3.MaterialTheme 17 | import androidx.compose.material3.OutlinedCard 18 | import androidx.compose.material3.Text 19 | import androidx.compose.material3.TopAppBar 20 | import androidx.compose.runtime.Composable 21 | import androidx.compose.runtime.getValue 22 | import androidx.compose.runtime.mutableStateOf 23 | import androidx.compose.runtime.remember 24 | import androidx.compose.runtime.setValue 25 | import androidx.compose.ui.Modifier 26 | import androidx.compose.ui.text.style.TextOverflow 27 | import androidx.compose.ui.unit.dp 28 | import com.arkivanov.decompose.extensions.compose.subscribeAsState 29 | import ru.bartwell.delightsqlviewer.core.presentation.ErrorBox 30 | 31 | @OptIn(ExperimentalMaterial3Api::class) 32 | @Composable 33 | internal fun StructureContent( 34 | component: StructureComponent, 35 | modifier: Modifier = Modifier, 36 | ) { 37 | val state by component.model.subscribeAsState() 38 | Column(modifier = modifier) { 39 | TopAppBar( 40 | title = { Text(state.table) }, 41 | navigationIcon = { 42 | IconButton(onClick = component::onBackPressed) { 43 | Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = "Back") 44 | } 45 | } 46 | ) 47 | ErrorBox( 48 | modifier = Modifier.fillMaxSize(), 49 | error = state.error 50 | ) { 51 | LazyColumn( 52 | modifier = Modifier.padding(16.dp), 53 | verticalArrangement = Arrangement.spacedBy(16.dp), 54 | ) { 55 | items(state.columns) { column -> 56 | OutlinedCard(modifier = Modifier.fillMaxWidth()) { 57 | Column(modifier = Modifier.padding(16.dp)) { 58 | Text( 59 | text = "Name: ${column.name}", 60 | style = MaterialTheme.typography.titleMedium 61 | ) 62 | Text( 63 | text = "Type: ${column.type}", 64 | style = MaterialTheme.typography.bodyMedium 65 | ) 66 | Text( 67 | text = "Not Null: ${if (column.isNotNullable) "Yes" else "No"}", 68 | style = MaterialTheme.typography.bodySmall 69 | ) 70 | 71 | if (column.defaultValue == null) { 72 | Text( 73 | text = "Default value: null", 74 | style = MaterialTheme.typography.bodySmall, 75 | ) 76 | } else { 77 | var expanded by remember { mutableStateOf(false) } 78 | Text( 79 | text = "Default value: " + column.defaultValue, 80 | style = MaterialTheme.typography.bodySmall, 81 | maxLines = if (expanded) Int.MAX_VALUE else 2, 82 | overflow = TextOverflow.Ellipsis, 83 | modifier = Modifier.clickable { expanded = !expanded } 84 | ) 85 | } 86 | } 87 | } 88 | } 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/structure/presentation/StructureState.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.structure.presentation 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | 5 | internal data class StructureState( 6 | val table: String, 7 | val columns: List = emptyList(), 8 | val error: String? = null, 9 | ) 10 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/DefaultTablesListComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 11 | import ru.bartwell.delightsqlviewer.core.mapper.StringSqlMapper 12 | 13 | internal class DefaultTablesListComponent( 14 | componentContext: ComponentContext, 15 | private val listItemClicked: (String) -> Unit, 16 | ) : TablesListComponent, ComponentContext by componentContext { 17 | 18 | private val _model = MutableValue(TablesListState()) 19 | override val model: Value = _model 20 | 21 | init { 22 | val sql = "SELECT name FROM sqlite_master WHERE type='table' " + 23 | "AND name NOT IN ('sqlite_sequence', 'sqlite_stat1', " + 24 | "'sqlite_stat4', 'android_metadata', 'room_master_table');" 25 | DelightSqlViewer.getDriver() 26 | .query(sql, StringSqlMapper()) 27 | .onEach { _model.value = _model.value.copy(tables = it) } 28 | .catch { _model.value = _model.value.copy(error = it.toString()) } 29 | .launchIn(coroutineScope()) 30 | } 31 | 32 | override fun onListItemClicked(table: String) = listItemClicked(table) 33 | } 34 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/ScreenCloser.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | @Composable 6 | internal expect fun screenCloser(): () -> Unit 7 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/TablesListComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import ru.bartwell.delightsqlviewer.core.component.Component 5 | 6 | internal interface TablesListComponent : Component { 7 | val model: Value 8 | 9 | fun onListItemClicked(table: String) 10 | } 11 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/TablesListContent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import androidx.compose.foundation.clickable 4 | import androidx.compose.foundation.layout.Column 5 | import androidx.compose.foundation.layout.Row 6 | import androidx.compose.foundation.layout.fillMaxSize 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.foundation.lazy.LazyColumn 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.foundation.lazy.rememberLazyListState 12 | import androidx.compose.material.icons.Icons 13 | import androidx.compose.material.icons.outlined.Close 14 | import androidx.compose.material3.ExperimentalMaterial3Api 15 | import androidx.compose.material3.Icon 16 | import androidx.compose.material3.IconButton 17 | import androidx.compose.material3.Text 18 | import androidx.compose.material3.TopAppBar 19 | import androidx.compose.runtime.Composable 20 | import androidx.compose.runtime.getValue 21 | import androidx.compose.ui.Modifier 22 | import androidx.compose.ui.unit.dp 23 | import com.arkivanov.decompose.extensions.compose.subscribeAsState 24 | import ru.bartwell.delightsqlviewer.core.presentation.ErrorBox 25 | 26 | @OptIn(ExperimentalMaterial3Api::class) 27 | @Composable 28 | internal fun TablesListContent( 29 | component: TablesListComponent, 30 | modifier: Modifier = Modifier, 31 | ) { 32 | val state by component.model.subscribeAsState() 33 | val screenCloser = screenCloser() 34 | 35 | Column(modifier = modifier) { 36 | TopAppBar( 37 | title = { Text("List") }, 38 | navigationIcon = { 39 | IconButton(onClick = screenCloser) { 40 | Icon(imageVector = Icons.Outlined.Close, contentDescription = "Cancel") 41 | } 42 | } 43 | ) 44 | ErrorBox(modifier = Modifier.fillMaxSize(), error = state.error) { 45 | LazyColumn( 46 | state = rememberLazyListState(), 47 | modifier = Modifier.fillMaxSize(), 48 | ) { 49 | items(state.tables) { table -> 50 | Row( 51 | modifier = Modifier 52 | .fillMaxWidth() 53 | .clickable { component.onListItemClicked(table) } 54 | .padding(16.dp) 55 | ) { 56 | Text(table) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/TablesListState.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | internal data class TablesListState( 4 | val tables: List = emptyList(), 5 | val error: String? = null, 6 | ) 7 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/update/presentation/DefaultUpdateComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.update.presentation 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 11 | import ru.bartwell.delightsqlviewer.core.data.Column 12 | import ru.bartwell.delightsqlviewer.core.mapper.SingleStringSqlMapper 13 | 14 | internal class DefaultUpdateComponent( 15 | componentContext: ComponentContext, 16 | table: String, 17 | column: Column, 18 | rowId: Long, 19 | private val onFinished: () -> Unit, 20 | ) : UpdateComponent, ComponentContext by componentContext { 21 | 22 | private val _model = MutableValue( 23 | UpdateState( 24 | table = table, 25 | column = column, 26 | rowId = rowId, 27 | ) 28 | ) 29 | override val model: Value = _model 30 | 31 | init { 32 | updateData() 33 | } 34 | 35 | private fun updateData() { 36 | val table = model.value.table 37 | val columnName = model.value.column.name 38 | val rowId = model.value.rowId 39 | val rowIdColumn = Column.ROW_ID_COLUMN.name 40 | DelightSqlViewer.getDriver() 41 | .querySingle( 42 | "SELECT $columnName FROM $table WHERE $rowIdColumn = $rowId;", 43 | SingleStringSqlMapper(model.value.column) 44 | ) 45 | .onEach { value -> 46 | if (value == null) { 47 | _model.value = _model.value.copy(isNull = true) 48 | } else { 49 | _model.value = _model.value.copy(value = value) 50 | } 51 | } 52 | .catch { _model.value = _model.value.copy(loadError = it.toString()) } 53 | .launchIn(coroutineScope()) 54 | } 55 | 56 | override fun onBackPressed() = onFinished() 57 | 58 | override fun onValueChange(text: String) { 59 | _model.value = _model.value.copy(value = text) 60 | } 61 | 62 | override fun onNullCheckboxClick() { 63 | _model.value = _model.value.copy(isNull = !model.value.isNull) 64 | } 65 | 66 | override fun onSaveClick() { 67 | val newValue = model.value.value.takeIf { !model.value.isNull } 68 | DelightSqlViewer.getDriver() 69 | .updateSingle(model.value.table, model.value.rowId, model.value.column, newValue) 70 | .onEach { 71 | onBackPressed() 72 | } 73 | .catch { _model.value = _model.value.copy(saveError = it.toString()) } 74 | .launchIn(coroutineScope()) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/update/presentation/UpdateComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.update.presentation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import ru.bartwell.delightsqlviewer.core.component.Component 5 | 6 | internal interface UpdateComponent : Component { 7 | val model: Value 8 | 9 | fun onBackPressed() 10 | fun onValueChange(text: String) 11 | fun onNullCheckboxClick() 12 | fun onSaveClick() 13 | } 14 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/update/presentation/UpdateContent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.update.presentation 2 | 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.rememberScrollState 5 | import androidx.compose.foundation.text.KeyboardActions 6 | import androidx.compose.foundation.text.KeyboardOptions 7 | import androidx.compose.foundation.verticalScroll 8 | import androidx.compose.material.icons.Icons 9 | import androidx.compose.material.icons.automirrored.outlined.ArrowBack 10 | import androidx.compose.material3.* 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.runtime.getValue 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.text.input.ImeAction 15 | import androidx.compose.ui.text.input.KeyboardType 16 | import androidx.compose.ui.unit.dp 17 | import com.arkivanov.decompose.extensions.compose.subscribeAsState 18 | import ru.bartwell.delightsqlviewer.core.data.ColumnType 19 | import ru.bartwell.delightsqlviewer.core.presentation.CheckboxWithText 20 | import ru.bartwell.delightsqlviewer.core.presentation.ErrorBox 21 | 22 | @OptIn(ExperimentalMaterial3Api::class) 23 | @Composable 24 | internal fun UpdateContent( 25 | component: UpdateComponent, 26 | modifier: Modifier = Modifier, 27 | ) { 28 | val state by component.model.subscribeAsState() 29 | Column(modifier = modifier) { 30 | TopAppBar( 31 | title = { Text(state.column.name + " (" + state.column.type.name.lowercase() + ")") }, 32 | navigationIcon = { 33 | IconButton(onClick = component::onBackPressed) { 34 | Icon(Icons.AutoMirrored.Outlined.ArrowBack, contentDescription = "Back") 35 | } 36 | }, 37 | ) 38 | 39 | ErrorBox(state.loadError) { 40 | val scrollState = rememberScrollState() 41 | Column( 42 | modifier = Modifier.fillMaxWidth() 43 | .verticalScroll(scrollState), 44 | ) { 45 | ValueTextField( 46 | state = state, 47 | onValueChange = component::onValueChange, 48 | onSaveClick = component::onSaveClick, 49 | ) 50 | if (!state.column.isNotNullable) { 51 | CheckboxWithText( 52 | modifier = Modifier.offset(y = (-8).dp), 53 | text = "null", 54 | isChecked = state.isNull, 55 | onClick = component::onNullCheckboxClick, 56 | ) 57 | } 58 | Spacer(modifier = Modifier.weight(1f)) 59 | Button( 60 | modifier = Modifier.fillMaxWidth() 61 | .padding(horizontal = 16.dp), 62 | onClick = component::onSaveClick, 63 | ) { 64 | Text("Save") 65 | } 66 | Spacer(modifier = Modifier.height(32.dp)) 67 | } 68 | } 69 | } 70 | } 71 | 72 | @Composable 73 | private fun ValueTextField( 74 | state: UpdateState, 75 | onValueChange: (String) -> Unit, 76 | onSaveClick: () -> Unit, 77 | ) { 78 | val keyboardOptions: KeyboardOptions 79 | val keyboardActions: KeyboardActions 80 | val singleLine: Boolean 81 | when (state.column.type) { 82 | ColumnType.INTEGER -> { 83 | keyboardOptions = KeyboardOptions( 84 | keyboardType = KeyboardType.Number, 85 | imeAction = ImeAction.Done, 86 | ) 87 | keyboardActions = KeyboardActions(onDone = { onSaveClick() }) 88 | singleLine = true 89 | } 90 | 91 | ColumnType.REAL -> { 92 | keyboardOptions = KeyboardOptions( 93 | keyboardType = KeyboardType.Decimal, 94 | imeAction = ImeAction.Done, 95 | ) 96 | keyboardActions = KeyboardActions(onDone = { onSaveClick() }) 97 | singleLine = true 98 | } 99 | 100 | ColumnType.TEXT, ColumnType.BLOB -> { 101 | keyboardOptions = KeyboardOptions.Default 102 | keyboardActions = KeyboardActions.Default 103 | singleLine = false 104 | } 105 | } 106 | 107 | OutlinedTextField( 108 | modifier = Modifier.fillMaxWidth() 109 | .padding(horizontal = 16.dp) 110 | .padding(top = 16.dp), 111 | value = state.value.orEmpty(), 112 | onValueChange = onValueChange, 113 | enabled = !state.isNull, 114 | keyboardOptions = keyboardOptions, 115 | keyboardActions = keyboardActions, 116 | singleLine = singleLine, 117 | isError = state.saveError != null, 118 | supportingText = { Text(text = state.saveError.orEmpty()) }, 119 | ) 120 | } 121 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/update/presentation/UpdateState.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.update.presentation 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | 5 | internal data class UpdateState( 6 | val table: String, 7 | val column: Column, 8 | val rowId: Long, 9 | val value: String? = "", 10 | val isNull: Boolean = false, 11 | val loadError: String? = null, 12 | val saveError: String? = null, 13 | ) 14 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/viewer/presentation/DefaultViewerComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.viewer.presentation 2 | 3 | import com.arkivanov.decompose.ComponentContext 4 | import com.arkivanov.decompose.value.MutableValue 5 | import com.arkivanov.decompose.value.Value 6 | import com.arkivanov.essenty.lifecycle.coroutines.coroutineScope 7 | import kotlinx.coroutines.flow.catch 8 | import kotlinx.coroutines.flow.launchIn 9 | import kotlinx.coroutines.flow.onEach 10 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 11 | import ru.bartwell.delightsqlviewer.core.component.Resumable 12 | import ru.bartwell.delightsqlviewer.core.data.Column 13 | import ru.bartwell.delightsqlviewer.core.mapper.ColumnsSqlMapper 14 | import ru.bartwell.delightsqlviewer.core.mapper.RowsSqlMapper 15 | 16 | internal class DefaultViewerComponent( 17 | componentContext: ComponentContext, 18 | table: String, 19 | private val onFinished: () -> Unit, 20 | private val cellClick: (table: String, column: Column, rowId: Long) -> Unit, 21 | private val structureClick: (table: String) -> Unit, 22 | private val insertClick: (table: String, columns: List) -> Unit, 23 | ) : ViewerComponent, ComponentContext by componentContext, Resumable { 24 | 25 | private val _model = MutableValue(ViewerState(table = table)) 26 | override val model: Value = _model 27 | 28 | override fun onResume() { 29 | updateTable() 30 | } 31 | 32 | private fun updateTable() { 33 | loadColumns() 34 | } 35 | 36 | private fun loadColumns() { 37 | val table = model.value.table 38 | DelightSqlViewer.getDriver() 39 | .query("PRAGMA table_info($table);", ColumnsSqlMapper()) 40 | .onEach { columns -> 41 | _model.value = _model.value.copy(columns = columns + listOf(Column.ROW_ID_COLUMN)) 42 | loadRows() 43 | } 44 | .catch { _model.value = _model.value.copy(loadError = it.toString()) } 45 | .launchIn(coroutineScope()) 46 | } 47 | 48 | private fun loadRows() { 49 | val table = model.value.table 50 | val columns = model.value.columns.joinToString(",") { it.name } 51 | DelightSqlViewer.getDriver() 52 | .query("SELECT $columns FROM $table;", RowsSqlMapper(model.value.columns)) 53 | .onEach { _model.value = _model.value.copy(rows = it) } 54 | .catch { _model.value = _model.value.copy(loadError = it.toString()) } 55 | .launchIn(coroutineScope()) 56 | } 57 | 58 | override fun onDeleteClick() { 59 | _model.value = model.value.copy( 60 | isDeleteMode = !model.value.isDeleteMode, 61 | selectedRows = if (model.value.isDeleteMode) emptyList() else model.value.selectedRows, 62 | ) 63 | } 64 | 65 | override fun onRowSelected(rowId: Long, isSelected: Boolean) { 66 | val newSelected = if (isSelected) { 67 | model.value.selectedRows + rowId 68 | } else { 69 | model.value.selectedRows - rowId 70 | } 71 | _model.value = model.value.copy(selectedRows = newSelected) 72 | } 73 | 74 | override fun onBackPressed() = onFinished() 75 | 76 | override fun onCancelDeleteClick() { 77 | _model.value = model.value.copy(isDeleteMode = false) 78 | } 79 | 80 | override fun onConfirmDeleteClick() { 81 | DelightSqlViewer.getDriver() 82 | .delete(model.value.table, model.value.selectedRows) 83 | .onEach { 84 | updateTable() 85 | _model.value = model.value.copy(isDeleteMode = false) 86 | } 87 | .catch { _model.value = _model.value.copy(deleteError = it.toString()) } 88 | .launchIn(coroutineScope()) 89 | } 90 | 91 | override fun onStructureClick() = structureClick(model.value.table) 92 | 93 | override fun onInsertClick() = insertClick(model.value.table, model.value.visibleColumns) 94 | 95 | override fun onCellClick(column: Column, rowId: Long) = cellClick(model.value.table, column, rowId) 96 | 97 | override fun onAlertDismiss() { 98 | _model.value = model.value.copy(deleteError = null) 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/viewer/presentation/ViewerComponent.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.viewer.presentation 2 | 3 | import com.arkivanov.decompose.value.Value 4 | import ru.bartwell.delightsqlviewer.core.component.Component 5 | import ru.bartwell.delightsqlviewer.core.data.Column 6 | 7 | internal interface ViewerComponent : Component { 8 | val model: Value 9 | 10 | fun onBackPressed() 11 | fun onStructureClick() 12 | fun onInsertClick() 13 | fun onCellClick(column: Column, rowId: Long) 14 | fun onDeleteClick() 15 | fun onRowSelected(rowId: Long, isSelected: Boolean) 16 | fun onCancelDeleteClick() 17 | fun onConfirmDeleteClick() 18 | fun onAlertDismiss() 19 | } 20 | -------------------------------------------------------------------------------- /runtime/src/commonMain/kotlin/ru/bartwell/delightsqlviewer/feature/viewer/presentation/ViewerState.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.viewer.presentation 2 | 3 | import ru.bartwell.delightsqlviewer.core.data.Column 4 | import ru.bartwell.delightsqlviewer.core.data.Row 5 | 6 | internal data class ViewerState( 7 | val table: String, 8 | val columns: List = emptyList(), 9 | val rows: List = emptyList(), 10 | val isDeleteMode: Boolean = false, 11 | val selectedRows: List = emptyList(), 12 | val deleteError: String? = null, 13 | val loadError: String? = null, 14 | ) { 15 | val visibleColumns: List 16 | get() = columns.filter { !it.isRowId } 17 | } 18 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/DelightSqlViewerSceneDelegate.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import kotlinx.cinterop.BetaInteropApi 4 | import platform.UIKit.UIApplicationShortcutItem 5 | import platform.UIKit.UIScene 6 | import platform.UIKit.UISceneConnectionOptions 7 | import platform.UIKit.UISceneSession 8 | import platform.UIKit.UIWindowScene 9 | import platform.UIKit.UIWindowSceneDelegateProtocol 10 | import platform.darwin.NSObject 11 | import ru.bartwell.delightsqlviewer.core.util.ShortcutManager 12 | import ru.bartwell.delightsqlviewer.core.util.id 13 | 14 | @OptIn(BetaInteropApi::class) 15 | internal class DelightSqlViewerSceneDelegate @OverrideInit constructor() : NSObject(), UIWindowSceneDelegateProtocol { 16 | 17 | override fun scene( 18 | scene: UIScene, 19 | willConnectToSession: UISceneSession, 20 | options: UISceneConnectionOptions 21 | ) { 22 | handleAction(options.shortcutItem) 23 | } 24 | 25 | override fun windowScene( 26 | windowScene: UIWindowScene, 27 | performActionForShortcutItem: UIApplicationShortcutItem, 28 | completionHandler: (Boolean) -> Unit 29 | ) { 30 | val isHandled = handleAction(performActionForShortcutItem) 31 | completionHandler(isHandled) 32 | } 33 | 34 | private fun handleAction(shortcutItem: UIApplicationShortcutItem?): Boolean { 35 | return if (shortcutItem?.type == ShortcutManager.id) { 36 | DelightSqlViewer.launch() 37 | true 38 | } else { 39 | false 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/ShortcutActionHandler.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer 2 | 3 | import kotlinx.cinterop.BetaInteropApi 4 | import platform.UIKit.UISceneConfiguration 5 | import platform.UIKit.UISceneSession 6 | 7 | public object ShortcutActionHandler { 8 | 9 | @OptIn(BetaInteropApi::class) 10 | public fun getConfiguration(session: UISceneSession): UISceneConfiguration { 11 | val configuration = UISceneConfiguration( 12 | name = session.configuration.name, 13 | sessionRole = session.role, 14 | ) 15 | configuration.delegateClass = DelightSqlViewerSceneDelegate().`class`() 16 | return configuration 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/core/util/IosSceneController.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import androidx.compose.ui.window.ComposeUIViewController 4 | import com.arkivanov.decompose.DefaultComponentContext 5 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 6 | import com.arkivanov.essenty.lifecycle.create 7 | import platform.UIKit.UIApplication 8 | import platform.UIKit.UIModalPresentationFullScreen 9 | import platform.UIKit.UINavigationController 10 | import platform.UIKit.UITabBarController 11 | import platform.UIKit.UIViewController 12 | import ru.bartwell.delightsqlviewer.App 13 | import ru.bartwell.delightsqlviewer.core.component.DefaultRootComponent 14 | import kotlin.experimental.ExperimentalNativeApi 15 | import kotlin.native.ref.WeakReference 16 | 17 | @OptIn(ExperimentalNativeApi::class) 18 | internal object IosSceneController { 19 | 20 | private var _viewerViewControllerInstance: WeakReference? = null 21 | private val viewerViewControllerInstance: UIViewController? 22 | get() = _viewerViewControllerInstance?.get() 23 | 24 | fun present() { 25 | if (viewerViewControllerInstance != null) { 26 | return 27 | } 28 | val lifecycle = LifecycleRegistry() 29 | val componentContext = DefaultComponentContext(lifecycle) 30 | val rootComponent = DefaultRootComponent(componentContext) 31 | val uiViewController = ComposeUIViewController(configure = { enforceStrictPlistSanityCheck = false }) { 32 | App(rootComponent) 33 | } 34 | lifecycle.create() 35 | _viewerViewControllerInstance = WeakReference(uiViewController) 36 | uiViewController.modalPresentationStyle = UIModalPresentationFullScreen 37 | getTopMostViewController()?.presentViewController(uiViewController, animated = true, completion = null) 38 | } 39 | 40 | private fun getTopMostViewController( 41 | base: UIViewController? = UIApplication.sharedApplication.keyWindow?.rootViewController 42 | ): UIViewController? { 43 | if (base == null) { 44 | return null 45 | } 46 | return when (base) { 47 | is UINavigationController -> getTopMostViewController(base.visibleViewController) 48 | is UITabBarController -> base.selectedViewController?.let { getTopMostViewController(it) } 49 | else -> if (base.presentedViewController != null) { 50 | getTopMostViewController(base.presentedViewController) 51 | } else { 52 | base 53 | } 54 | } 55 | } 56 | 57 | fun dismiss() { 58 | viewerViewControllerInstance?.dismissViewControllerAnimated(true, completion = null) 59 | _viewerViewControllerInstance = null 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/core/util/LaunchManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 6 | internal actual object LaunchManager { 7 | actual fun launch(environmentProvider: EnvironmentProvider<*>) = IosSceneController.present() 8 | } 9 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/core/util/ShortcutManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import platform.UIKit.UIApplication 4 | import platform.UIKit.UIApplicationShortcutIcon 5 | import platform.UIKit.UIApplicationShortcutIconType 6 | import platform.UIKit.UIApplicationShortcutItem 7 | import platform.UIKit.shortcutItems 8 | import platform.darwin.dispatch_async 9 | import platform.darwin.dispatch_get_main_queue 10 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 11 | 12 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 13 | internal actual object ShortcutManager { 14 | internal actual fun setup(environmentProvider: EnvironmentProvider<*>) { 15 | val shortcutItem = UIApplicationShortcutItem( 16 | type = id, 17 | localizedTitle = title, 18 | localizedSubtitle = subtitle, 19 | icon = UIApplicationShortcutIcon.iconWithType( 20 | UIApplicationShortcutIconType.UIApplicationShortcutIconTypeFavorite 21 | ), 22 | userInfo = null, 23 | ) 24 | dispatch_async(dispatch_get_main_queue()) { 25 | UIApplication.sharedApplication.shortcutItems = listOf(shortcutItem) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /runtime/src/iosMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/ScreenCloser.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import androidx.compose.runtime.Composable 4 | import ru.bartwell.delightsqlviewer.core.util.IosSceneController 5 | 6 | @Composable 7 | internal actual fun screenCloser(): () -> Unit { 8 | return { IosSceneController.dismiss() } 9 | } 10 | -------------------------------------------------------------------------------- /runtime/src/jvmMain/kotlin/ru/bartwell/delightsqlviewer/core/util/LaunchManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import androidx.compose.runtime.CompositionLocalProvider 4 | import androidx.compose.runtime.staticCompositionLocalOf 5 | import androidx.compose.ui.awt.ComposeWindow 6 | import com.arkivanov.decompose.DefaultComponentContext 7 | import com.arkivanov.essenty.lifecycle.LifecycleRegistry 8 | import ru.bartwell.delightsqlviewer.App 9 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 10 | import ru.bartwell.delightsqlviewer.core.component.DefaultRootComponent 11 | import java.awt.Dimension 12 | 13 | private const val WINDOW_WIDTH = 800 14 | private const val WINDOW_HEIGHT = 600 15 | private const val MIN_WINDOW_SIZE = 400 16 | 17 | internal val LocalComposeWindow = staticCompositionLocalOf { null } 18 | 19 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING") 20 | internal actual object LaunchManager { 21 | actual fun launch(environmentProvider: EnvironmentProvider<*>) { 22 | // environmentProvider as DesktopEnvironmentProvider 23 | val lifecycle = LifecycleRegistry() 24 | val componentContext = DefaultComponentContext(lifecycle) 25 | val rootComponent = DefaultRootComponent(componentContext) 26 | 27 | val window = ComposeWindow().apply { 28 | title = "Viewer" 29 | size = Dimension(WINDOW_WIDTH, WINDOW_HEIGHT) 30 | minimumSize = Dimension(MIN_WINDOW_SIZE, MIN_WINDOW_SIZE) 31 | preferredSize = Dimension(WINDOW_WIDTH, WINDOW_HEIGHT) 32 | setLocationRelativeTo(null) 33 | setContent { 34 | CompositionLocalProvider(LocalComposeWindow provides window) { 35 | App(rootComponent) 36 | } 37 | } 38 | } 39 | window.pack() 40 | window.isVisible = true 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /runtime/src/jvmMain/kotlin/ru/bartwell/delightsqlviewer/core/util/ShortcutManager.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.core.util 2 | 3 | import ru.bartwell.delightsqlviewer.core.EnvironmentProvider 4 | 5 | @Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING", "EmptyFunctionBlock") 6 | internal actual object ShortcutManager { 7 | internal actual fun setup(environmentProvider: EnvironmentProvider<*>) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /runtime/src/jvmMain/kotlin/ru/bartwell/delightsqlviewer/feature/table/presentation/ScreenCloser.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.feature.table.presentation 2 | 3 | import androidx.compose.runtime.Composable 4 | import ru.bartwell.delightsqlviewer.core.util.LocalComposeWindow 5 | 6 | @Composable 7 | internal actual fun screenCloser(): () -> Unit { 8 | val window = LocalComposeWindow.current 9 | return { window?.dispose() } 10 | } 11 | -------------------------------------------------------------------------------- /sample/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.kotlinAndroid) 4 | alias(libs.plugins.compose.compiler) 5 | } 6 | 7 | android { 8 | namespace = "ru.bartwell.delightsqlviewer.sample.android" 9 | compileSdk = 35 10 | defaultConfig { 11 | applicationId = "ru.bartwell.delightsqlviewer.sample.android" 12 | minSdk = 24 13 | targetSdk = 35 14 | versionCode = 1 15 | versionName = "1.0" 16 | } 17 | buildFeatures { 18 | compose = true 19 | } 20 | packaging { 21 | resources { 22 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 23 | } 24 | } 25 | buildTypes { 26 | getByName("release") { 27 | isMinifyEnabled = false 28 | } 29 | } 30 | compileOptions { 31 | sourceCompatibility = JavaVersion.VERSION_1_8 32 | targetCompatibility = JavaVersion.VERSION_1_8 33 | } 34 | kotlinOptions { 35 | jvmTarget = "1.8" 36 | } 37 | lint { 38 | lintConfig = file("config/lint/lint.xml") 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation(projects.shared) 44 | implementation(libs.compose.material3) 45 | implementation(libs.androidx.activity.compose) 46 | } -------------------------------------------------------------------------------- /sample/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /sample/android/src/main/java/ru/bartwell/delightsqlviewer/sample/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package ru.bartwell.delightsqlviewer.sample.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import ru.bartwell.delightsqlviewer.DelightSqlViewer 7 | import ru.bartwell.delightsqlviewer.adapter.room.RoomEnvironmentProvider 8 | import ru.bartwell.delightsqlviewer.adapter.sqldelight.SqlDelightEnvironmentProvider 9 | import ru.bartwell.delightsqlviewer.sample.shared.App 10 | import ru.bartwell.delightsqlviewer.sample.shared.DatabaseInitializer 11 | import ru.bartwell.delightsqlviewer.sample.shared.database.room.AppDatabase 12 | import ru.bartwell.delightsqlviewer.sample.shared.database.room.DatabaseBuilder 13 | import ru.bartwell.delightsqlviewer.sample.shared.database.sqldelight.DriverFactory 14 | 15 | class MainActivity : ComponentActivity() { 16 | override fun onCreate(savedInstanceState: Bundle?) { 17 | super.onCreate(savedInstanceState) 18 | val databaseInitializer = object : DatabaseInitializer { 19 | override fun initSqlDelight() { 20 | val sqlDelightDriver = DriverFactory(this@MainActivity).createDriver() 21 | DelightSqlViewer.init(object : SqlDelightEnvironmentProvider() { 22 | override fun getDriver() = sqlDelightDriver 23 | override fun getContext() = this@MainActivity 24 | }) 25 | } 26 | 27 | override fun initRoom() { 28 | val roomDatabase = AppDatabase.create(DatabaseBuilder(this@MainActivity).createBuilder()) 29 | DelightSqlViewer.init(object : RoomEnvironmentProvider() { 30 | override fun getDriver() = roomDatabase 31 | override fun getContext() = this@MainActivity 32 | }) 33 | } 34 | } 35 | // Initialize SqlDelight for shortcut 36 | databaseInitializer.initSqlDelight() 37 | setContent { 38 | App(databaseInitializer = databaseInitializer) 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /sample/android/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 |