├── .editorconfig ├── .github └── workflows │ ├── publish.yml │ └── run-checks.yml ├── .gitignore ├── .idea └── icon.svg ├── .run ├── Android Example.run.xml ├── Jvm Example.run.xml ├── MacOS Example.run.xml ├── WASM-Web Example.run.xml ├── Web Example.run.xml └── iOS Example.run.xml ├── LICENSE ├── README.md ├── RELEASE.md ├── build.gradle.kts ├── examples ├── android │ ├── build.gradle.kts │ └── src │ │ └── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ └── com │ │ │ └── darkrockstudios │ │ │ └── libraries │ │ │ └── mpfilepicker │ │ │ └── android │ │ │ └── MainActivity.kt │ │ └── res │ │ ├── mipmap-anydpi-v26 │ │ └── ic_launcher.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ ├── ic_launcher_background.png │ │ ├── ic_launcher_foreground.png │ │ └── ic_launcher_monochrome.png │ │ └── values │ │ └── strings.xml ├── ios │ ├── build.gradle.kts │ ├── ios.podspec │ └── src │ │ └── iosMain │ │ └── kotlin │ │ └── main.ios.kt ├── iosApp │ ├── Podfile │ ├── Podfile.lock │ ├── iosApp.xcodeproj │ │ ├── project.pbxproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── iosApp.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── iosApp │ │ ├── Assets.xcassets │ │ ├── AccentColor.colorset │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset │ │ │ └── Contents.json │ │ └── Contents.json │ │ ├── ContentView.swift │ │ ├── Preview Content │ │ └── Preview Assets.xcassets │ │ │ └── Contents.json │ │ └── iosAppApp.swift ├── jvm │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── Main.kt ├── macosX64 │ ├── .gitignore │ ├── build.gradle.kts │ └── src │ │ └── macosX64Main │ │ └── kotlin │ │ └── main.kt ├── web-wasm │ ├── build.gradle.kts │ └── src │ │ └── wasmJsMain │ │ ├── kotlin │ │ └── main.kt │ │ └── resources │ │ └── index.html └── web │ ├── build.gradle.kts │ └── src │ └── jsMain │ ├── kotlin │ └── main.kt │ └── resources │ └── index.html ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kotlin-js-store └── yarn.lock ├── mpfilepicker ├── build.gradle.kts └── src │ ├── androidMain │ ├── AndroidManifest.xml │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ └── FilePicker.android.kt │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ └── FilePicker.kt │ ├── iosMain │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ ├── FilePicker.ios.kt │ │ └── IosFilePickerLauncher.kt │ ├── jsMain │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ └── FilePicker.js.kt │ ├── jvmMain │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ ├── FileChooser.kt │ │ └── FilePicker.desktop.kt │ ├── macosX64Main │ └── kotlin │ │ └── com │ │ └── darkrockstudios │ │ └── libraries │ │ └── mpfilepicker │ │ └── FilePicker.macos.kt │ └── wasmJsMain │ └── kotlin │ └── com │ └── darkrockstudios │ └── libraries │ └── mpfilepicker │ └── FilePicker.wasm.kt ├── renovate.json ├── screenshot-android.png ├── screenshot-desktop-windows.jpg └── settings.gradle.kts /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = tab 3 | indent_size = 4 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | max_line_length = 100 8 | trim_trailing_whitespace = true 9 | 10 | [*.yml] 11 | indent_style = unset 12 | indent_size = unset -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish packages to Repositories 2 | env: 3 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 4 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 5 | SIGNING_KEY: ${{ secrets.SIGNING_KEY }} 6 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 7 | SIGNING_USER: ${{ secrets.SIGNING_USER }} 8 | on: 9 | release: 10 | types: [ created ] 11 | 12 | jobs: 13 | publish: 14 | runs-on: macos-latest 15 | environment: Publish 16 | permissions: 17 | contents: read 18 | packages: write 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-java@v4 22 | with: 23 | java-version: '17' 24 | distribution: 'adopt' 25 | - name: Validate Gradle wrapper 26 | uses: gradle/wrapper-validation-action@v2.1.1 27 | - name: Publish package 28 | uses: gradle/gradle-build-action@v3.1.0 29 | with: 30 | arguments: mpfilepicker:publishAllPublicationsToMavenRepository -------------------------------------------------------------------------------- /.github/workflows/run-checks.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | runs-on: macos-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | 18 | - name: Validate Gradle Wrapper 19 | uses: gradle/wrapper-validation-action@v2 20 | 21 | - name: Configure JDK 22 | uses: actions/setup-java@v4 23 | with: 24 | java-version: '17' 25 | distribution: 'adopt' 26 | 27 | - name: Setup Gradle 28 | uses: gradle/actions/setup-gradle@v3 29 | 30 | - name: Build project 31 | run: ./gradlew build 32 | 33 | lint: 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout 37 | uses: actions/checkout@v4 38 | 39 | - name: Validate Gradle Wrapper 40 | uses: gradle/wrapper-validation-action@v2 41 | 42 | - name: Configure JDK 43 | uses: actions/setup-java@v4 44 | with: 45 | java-version: '17' 46 | distribution: 'adopt' 47 | 48 | - name: Setup Gradle 49 | uses: gradle/actions/setup-gradle@v3 50 | 51 | - name: Lint project 52 | run: ./gradlew lint 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/* 9 | .idea/modules.xml 10 | .idea/jarRepositories.xml 11 | .idea/compiler.xml 12 | .idea/libraries/ 13 | *.iws 14 | *.iml 15 | *.ipr 16 | out/ 17 | !**/src/main/**/out/ 18 | !**/src/test/**/out/ 19 | 20 | ### Eclipse ### 21 | .apt_generated 22 | .classpath 23 | .factorypath 24 | .project 25 | .settings 26 | .springBeans 27 | .sts4-cache 28 | bin/ 29 | !**/src/main/**/bin/ 30 | !**/src/test/**/bin/ 31 | 32 | ### NetBeans ### 33 | /nbproject/private/ 34 | /nbbuild/ 35 | /dist/ 36 | /nbdist/ 37 | /.nb-gradle/ 38 | 39 | ### VS Code ### 40 | .vscode/ 41 | 42 | ### Mac OS ### 43 | .DS_Store 44 | /.idea/codeStyles/codeStyleConfig.xml 45 | /.idea/artifacts/common_desktop_1_0_SNAPSHOT.xml 46 | /.idea/deploymentTargetDropDown.xml 47 | /.idea/artifacts/desktop_jvm_1_0_SNAPSHOT.xml 48 | /local.properties 49 | 50 | # Created by https://www.toptal.com/developers/gitignore/api/xcode,cocoapods,swift 51 | # Edit at https://www.toptal.com/developers/gitignore?templates=xcode,cocoapods,swift 52 | 53 | ### CocoaPods ### 54 | ## CocoaPods GitIgnore Template 55 | 56 | # CocoaPods - Only use to conserve bandwidth / Save time on Pushing 57 | # - Also handy if you have a large number of dependant pods 58 | # - AS PER https://guides.cocoapods.org/using/using-cocoapods.html NEVER IGNORE THE LOCK FILE 59 | Pods/ 60 | 61 | ### Swift ### 62 | # Xcode 63 | # 64 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 65 | 66 | ## User settings 67 | xcuserdata/ 68 | 69 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 70 | *.xcscmblueprint 71 | *.xccheckout 72 | 73 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 74 | DerivedData/ 75 | *.moved-aside 76 | *.pbxuser 77 | !default.pbxuser 78 | *.mode1v3 79 | !default.mode1v3 80 | *.mode2v3 81 | !default.mode2v3 82 | *.perspectivev3 83 | !default.perspectivev3 84 | 85 | ## Obj-C/Swift specific 86 | *.hmap 87 | 88 | ## App packaging 89 | *.ipa 90 | *.dSYM.zip 91 | *.dSYM 92 | 93 | ## Playgrounds 94 | timeline.xctimeline 95 | playground.xcworkspace 96 | 97 | # Swift Package Manager 98 | # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. 99 | # Packages/ 100 | # Package.pins 101 | # Package.resolved 102 | # *.xcodeproj 103 | # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata 104 | # hence it is not needed unless you have added a package configuration file to your project 105 | # .swiftpm 106 | 107 | .build/ 108 | 109 | # CocoaPods 110 | # We recommend against adding the Pods directory to your .gitignore. However 111 | # you should judge for yourself, the pros and cons are mentioned at: 112 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 113 | # Pods/ 114 | # Add this line if you want to avoid checking in source code from the Xcode workspace 115 | # *.xcworkspace 116 | 117 | # Carthage 118 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 119 | # Carthage/Checkouts 120 | 121 | Carthage/Build/ 122 | 123 | # Accio dependency management 124 | Dependencies/ 125 | .accio/ 126 | 127 | # fastlane 128 | # It is recommended to not store the screenshots in the git repo. 129 | # Instead, use fastlane to re-generate the screenshots whenever they are needed. 130 | # For more information about the recommended setup visit: 131 | # https://docs.fastlane.tools/best-practices/source-control/#source-control 132 | 133 | fastlane/report.xml 134 | fastlane/Preview.html 135 | fastlane/screenshots/**/*.png 136 | fastlane/test_output 137 | 138 | # Code Injection 139 | # After new code Injection tools there's a generated folder /iOSInjectionProject 140 | # https://github.com/johnno1962/injectionforxcode 141 | 142 | iOSInjectionProject/ 143 | 144 | ### Xcode ### 145 | 146 | ## Xcode 8 and earlier 147 | 148 | ### Xcode Patch ### 149 | *.xcodeproj/* 150 | !*.xcodeproj/project.pbxproj 151 | !*.xcodeproj/xcshareddata/ 152 | !*.xcodeproj/project.xcworkspace/ 153 | !*.xcworkspace/contents.xcworkspacedata 154 | /*.gcno 155 | **/xcshareddata/WorkspaceSettings.xcsettings 156 | 157 | # End of https://www.toptal.com/developers/gitignore/api/xcode,cocoapods,swift 158 | -------------------------------------------------------------------------------- /.idea/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.run/Android Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 68 | -------------------------------------------------------------------------------- /.run/Jvm Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10 | -------------------------------------------------------------------------------- /.run/MacOS Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /.run/WASM-Web Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /.run/Web Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 16 | 18 | true 19 | true 20 | false 21 | 22 | 23 | -------------------------------------------------------------------------------- /.run/iOS Example.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023-2023 Adam Brown 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Thanks for all the contribtuions 2 | Currently this project is unmaintained. If you want an alternative I will recommend [FileKit](https://github.com/vinceglb/FileKit) 3 | 4 | # Compose Multiplatform File Picker 5 | 6 | ![MIT License](https://img.shields.io/github/license/Wavesonics/compose-multiplatform-file-picker) ![Latest Release](https://img.shields.io/github/v/release/Wavesonics/compose-multiplatform-file-picker?include_prereleases) 7 | 8 | ![badge-platform-windows] ![badge-platform-linux] ![badge-platform-macos] ![badge-platform-android] ![badge-platform-js] ![badge-platform-macosX64] 9 | 10 | ![badge-kotlin] 11 | 12 | A multiplatform compose widget for picking files with each platform's Native File Picker Dialog. 13 | 14 | ## Include in your project: 15 | 16 | ```kts 17 | implementation("com.darkrockstudios:mpfilepicker:3.1.0") 18 | ``` 19 | 20 | ## How to use 21 | 22 | In your shared jetbrains compose multiplatform code, add one of the following. 23 | 24 | To show the dialog, simply set the boolean state to true via a button or what ever you want. 25 | 26 | ### Pick a file with a filter: 27 | 28 | ````kotlin 29 | var showFilePicker by remember { mutableStateOf(false) } 30 | 31 | val fileType = listOf("jpg", "png") 32 | FilePicker(show = showFilePicker, fileExtensions = fileType) { platformFile -> 33 | showFilePicker = false 34 | // do something with the file 35 | } 36 | ```` 37 | 38 | ### Pick multiple files with a filter: 39 | 40 | ````kotlin 41 | var showFilePicker by remember { mutableStateOf(false) } 42 | 43 | val fileType = listOf("jpg", "png") 44 | MultipleFilePicker(show = showFilePicker, fileExtensions = fileType) { file -> 45 | showFilePicker = false 46 | // do something with the file 47 | } 48 | ```` 49 | 50 | 51 | ### Pick a directory: 52 | 53 | ````kotlin 54 | var showDirPicker by remember { mutableStateOf(false) } 55 | 56 | DirectoryPicker(showDirPicker) { path -> 57 | showDirPicker = false 58 | // do something with path 59 | } 60 | ```` 61 | 62 | On each supported platform, it will update the platform native file picker dialog. On desktop, it will fall back to the 63 | Swing file picker if the native one can't be used for some reason. 64 | 65 |
66 | 67 | Screenshots 68 | 69 | ## Windows 70 | 71 | ![Windows native file picker](screenshot-desktop-windows.jpg "Windows native file picker") 72 | 73 | ## Android 74 | 75 | ![Android native file picker](screenshot-android.png "Android native file picker") 76 | 77 |
78 | 79 | ## Desktop/JVM Implementation 80 | 81 | The native desktop dialog implementation is derived from the [Pacmc project](https://github.com/jakobkmar/pacmc) 82 | but uses [TinyFileDialogs](https://github.com/LWJGL/lwjgl3/blob/master/modules/lwjgl/tinyfd/src/generated/java/org/lwjgl/util/tinyfd/TinyFileDialogs.java) 83 | 84 | See `FileChooser.kt` as well as the `lwjgl` gradle filter. 85 | 86 | ## Building 87 | 88 | Intellij IDEA should be able to build the project except Android variant. 89 | To build and run Android examples, use Android Studio. 90 | 91 | ### JS 92 | 93 | run `examples:web:jsBrowserDevelopmentRun` via Gradle, it will build a JS example and open it in a browser. 94 | 95 | ### MacOS and JVM 96 | 97 | Click on a green button next to the main function in `examples/jvm/.../Main.kt` or `examples/macosX64/.../main.kt`. 98 | 99 | ### Android 100 | 101 | Open the project in Android Studio. A run configuration for Android should be added automatically. 102 | Clicking on it will run it on an emulator. 103 | 104 | ### iOS 105 | 106 | Requirements: 107 | - MacOS 108 | - Xcode 109 | - Android Studio 110 | 111 | Setup your environment by following the [Official Multiplatform Mobile Guide](https://kotlinlang.org/docs/multiplatform-mobile-setup.html). 112 | To run iOS example app you also need to follow the [Set-up an environment to work with CocoaPods Guide](https://kotlinlang.org/docs/native-cocoapods.html#set-up-an-environment-to-work-with-cocoapods). 113 | 114 | Open the project in Android Studio, install `Kotlin Multiplatform Mobile` plugin. Create an `iOS Application` run config 115 | and enter the following: 116 | - Xcode Project File: `/examples/iosApp/iosApp.xcworkspace` 117 | - Xcode Project Scheme: `iosApp` 118 | - Xcode Project Cinfiguration: `Debug` 119 | - Execution Target: choose any simulator you have already created 120 | 121 | Use it to launch a simulator and run/debug the example iOS App, 122 | 123 | [badge-kotlin]: https://img.shields.io/badge/kotlin-1.8.20-blue.svg?logo=kotlin 124 | 125 | 126 | 127 | [badge-platform-linux]: http://img.shields.io/badge/platform-jvm/linux-2D3F6C.svg?style=flat 128 | 129 | [badge-platform-android]: http://img.shields.io/badge/platform-android-6EDB8D.svg?style=flat 130 | 131 | [badge-platform-ios]: http://img.shields.io/badge/platform-ios-CDCDCD.svg?style=flat 132 | 133 | [badge-platform-windows]: http://img.shields.io/badge/platform-jvm/windows-4D76CD.svg?style=flat 134 | 135 | [badge-platform-macos]: http://img.shields.io/badge/platform-jvm/macos-111111.svg?style=flat 136 | 137 | [badge-platform-js]: http://img.shields.io/badge/platform-js-34913c.svg?style=flat 138 | 139 | [badge-platform-macosX64]: http://img.shields.io/badge/platform-macosX64-34913c.svg?style=flat 140 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to Release 2 | 3 | 1. increase version number in following files 4 | - examples/ios/ios.podspec 5 | - gradle/libs.versions.toml 6 | - examples/iosApp/Podfile.lock 7 | - README.md - update this file after library is released 8 | 2. Create a new Release & Tag on Github, but set it as a pre-release. This is to avoid confusion for 9 | users when trying to install a library that is uploaded but not yet available for download. 10 | 3. Wait for build and upload 11 | 4. [SonaType Repo Manager](https://s01.oss.sonatype.org/) 12 | 5. Login 13 | 6. Go to Staging Repository 14 | 7. Close the library 15 | 8. Release the library 16 | 9. After library is available for download then update README.md 17 | and set the Github Release as the latest release -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) apply false 3 | alias(libs.plugins.androidLibrary) apply false 4 | alias(libs.plugins.jetbrainsCompose) apply false 5 | alias(libs.plugins.kotlinMultiplatform) apply false 6 | } 7 | -------------------------------------------------------------------------------- /examples/android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.androidApplication) 3 | alias(libs.plugins.jetbrainsCompose) 4 | kotlin("android") 5 | } 6 | 7 | dependencies { 8 | implementation(project(":mpfilepicker")) 9 | implementation(libs.compose.activity) 10 | } 11 | 12 | android { 13 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 14 | defaultConfig { 15 | applicationId = "com.darkrockstudios.libraries.mpfilepicker.android" 16 | minSdk = libs.versions.android.min.sdk.get().toInt() 17 | targetSdk = libs.versions.android.target.sdk.get().toInt() 18 | } 19 | compileOptions { 20 | sourceCompatibility = JavaVersion.VERSION_17 21 | targetCompatibility = JavaVersion.VERSION_17 22 | } 23 | buildTypes { 24 | getByName("release") { 25 | isMinifyEnabled = false 26 | } 27 | } 28 | namespace = "com.darkrockstudios.libraries.mpfilepicker.android" 29 | } 30 | -------------------------------------------------------------------------------- /examples/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 12 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /examples/android/src/main/java/com/darkrockstudios/libraries/mpfilepicker/android/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker.android 2 | 3 | import android.os.Bundle 4 | import androidx.activity.compose.setContent 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.MaterialTheme 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.getValue 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.runtime.setValue 14 | import com.darkrockstudios.libraries.mpfilepicker.DirectoryPicker 15 | import com.darkrockstudios.libraries.mpfilepicker.FilePicker 16 | import com.darkrockstudios.libraries.mpfilepicker.MultipleFilePicker 17 | 18 | class MainActivity : AppCompatActivity() { 19 | override fun onCreate(savedInstanceState: Bundle?) { 20 | super.onCreate(savedInstanceState) 21 | setContent { 22 | MaterialTheme { 23 | val fileType = listOf("jpg", "png") 24 | 25 | Column { 26 | var showSingleFilePicker by remember { mutableStateOf(false) } 27 | var pathSingleChosen by remember { mutableStateOf("") } 28 | 29 | Button(onClick = { 30 | showSingleFilePicker = true 31 | }) { 32 | Text("Choose File") 33 | } 34 | Text("File Chosen: $pathSingleChosen") 35 | 36 | FilePicker(showSingleFilePicker, fileExtensions = fileType) { platformFile -> 37 | if (platformFile != null) { 38 | pathSingleChosen = platformFile.uri.path ?: "none selected" 39 | } 40 | showSingleFilePicker = false 41 | } 42 | 43 | ///////////////////////////////////////////////////////////////// 44 | 45 | var showMultipleFilePicker by remember { mutableStateOf(false) } 46 | var pathMultipleChosen by remember { mutableStateOf(listOf("")) } 47 | 48 | Button(onClick = { 49 | showMultipleFilePicker = true 50 | }) { 51 | Text("Multiple Choose File") 52 | } 53 | Text("Multiple File Chosen: $pathMultipleChosen") 54 | 55 | MultipleFilePicker(showMultipleFilePicker, fileExtensions = fileType) { platformFiles -> 56 | if (platformFiles != null) { 57 | pathMultipleChosen = platformFiles.map { it.uri.path + "\n" } 58 | } 59 | showMultipleFilePicker = false 60 | } 61 | 62 | ///////////////////////////////////////////////////////////////// 63 | 64 | var showDirPicker by remember { mutableStateOf(false) } 65 | var dirChosen by remember { mutableStateOf("") } 66 | 67 | Button(onClick = { 68 | showDirPicker = true 69 | }) { 70 | Text("Choose Directory") 71 | } 72 | Text("Directory Chosen: $dirChosen") 73 | 74 | DirectoryPicker(showDirPicker) { path -> 75 | dirChosen = path ?: "none selected" 76 | showDirPicker = false 77 | } 78 | } 79 | } 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/examples/android/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /examples/android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | File Picker Example 4 | -------------------------------------------------------------------------------- /examples/ios/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("native.cocoapods") 3 | alias(libs.plugins.kotlinMultiplatform) 4 | alias(libs.plugins.jetbrainsCompose) 5 | } 6 | 7 | kotlin { 8 | iosX64() 9 | iosArm64() 10 | iosSimulatorArm64() 11 | 12 | sourceSets { 13 | iosMain.dependencies { 14 | implementation(compose.ui) 15 | implementation(compose.foundation) 16 | implementation(compose.material) 17 | implementation(compose.runtime) 18 | 19 | implementation(project(":mpfilepicker")) 20 | } 21 | } 22 | } 23 | 24 | 25 | kotlin.cocoapods { 26 | name = "ios" 27 | version = libs.versions.library.get() 28 | summary = 29 | "A multiplatform compose widget for picking files with each platform''s Native File Picker Dialog." 30 | homepage = "https://github.com/Wavesonics/compose-multiplatform-file-picker" 31 | ios.deploymentTarget = "14.1" 32 | 33 | framework { 34 | baseName = "ios" 35 | } 36 | 37 | podfile = project.file("../iosApp/Podfile") 38 | } 39 | -------------------------------------------------------------------------------- /examples/ios/ios.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |spec| 2 | spec.name = 'ios' 3 | spec.version = '3.1.0' 4 | spec.homepage = 'https://github.com/Wavesonics/compose-multiplatform-file-picker' 5 | spec.source = { :http=> ''} 6 | spec.authors = '' 7 | spec.license = '' 8 | spec.summary = 'A multiplatform compose widget for picking files with each platform''s Native File Picker Dialog.' 9 | spec.vendored_frameworks = 'build/cocoapods/framework/ios.framework' 10 | spec.libraries = 'c++' 11 | spec.ios.deployment_target = '14.1' 12 | 13 | 14 | if !Dir.exist?('build/cocoapods/framework/ios.framework') || Dir.empty?('build/cocoapods/framework/ios.framework') 15 | raise " 16 | 17 | Kotlin framework 'ios' doesn't exist yet, so a proper Xcode project can't be generated. 18 | 'pod install' should be executed after running ':generateDummyFramework' Gradle task: 19 | 20 | ./gradlew :examples:ios:generateDummyFramework 21 | 22 | Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)" 23 | end 24 | 25 | spec.pod_target_xcconfig = { 26 | 'KOTLIN_PROJECT_PATH' => ':examples:ios', 27 | 'PRODUCT_MODULE_NAME' => 'ios', 28 | } 29 | 30 | spec.script_phases = [ 31 | { 32 | :name => 'Build ios', 33 | :execution_position => :before_compile, 34 | :shell_path => '/bin/sh', 35 | :script => <<-SCRIPT 36 | if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then 37 | echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\"" 38 | exit 0 39 | fi 40 | set -ev 41 | REPO_ROOT="$PODS_TARGET_SRCROOT" 42 | "$REPO_ROOT/../../gradlew" -p "$REPO_ROOT" $KOTLIN_PROJECT_PATH:syncFramework \ 43 | -Pkotlin.native.cocoapods.platform=$PLATFORM_NAME \ 44 | -Pkotlin.native.cocoapods.archs="$ARCHS" \ 45 | -Pkotlin.native.cocoapods.configuration="$CONFIGURATION" 46 | SCRIPT 47 | } 48 | ] 49 | spec.resources = ['build/compose/ios/ios/compose-resources'] 50 | end -------------------------------------------------------------------------------- /examples/ios/src/iosMain/kotlin/main.ios.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Column 2 | import androidx.compose.material.Button 3 | import androidx.compose.material.MaterialTheme 4 | import androidx.compose.material.Text 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.mutableStateOf 7 | import androidx.compose.runtime.remember 8 | import androidx.compose.runtime.setValue 9 | import androidx.compose.ui.window.ComposeUIViewController 10 | import com.darkrockstudios.libraries.mpfilepicker.DirectoryPicker 11 | import com.darkrockstudios.libraries.mpfilepicker.FilePicker 12 | import com.darkrockstudios.libraries.mpfilepicker.MultipleFilePicker 13 | import com.darkrockstudios.libraries.mpfilepicker.launchDirectoryPicker 14 | import com.darkrockstudios.libraries.mpfilepicker.launchFilePicker 15 | import kotlinx.coroutines.MainScope 16 | import kotlinx.coroutines.launch 17 | import platform.UIKit.UIViewController 18 | 19 | @Suppress("Unused", "FunctionName") 20 | fun MainViewController(): UIViewController = ComposeUIViewController { 21 | val fileType = listOf("jpg", "png", "md") 22 | 23 | MaterialTheme { 24 | Column { 25 | var showSingleFilePicker by remember { mutableStateOf(false) } 26 | var singlePathChosen by remember { mutableStateOf("") } 27 | 28 | Button(onClick = { 29 | showSingleFilePicker = true 30 | }) { 31 | Text("Choose File") 32 | } 33 | Text("File Chosen: $singlePathChosen") 34 | 35 | FilePicker(showSingleFilePicker, fileExtensions = fileType) { platformFile -> 36 | singlePathChosen = platformFile?.nsUrl?.path ?: "none selected" 37 | showSingleFilePicker = false 38 | } 39 | 40 | ///////////////////////////////////////////////////////////////// 41 | 42 | var showMultipleFilePicker by remember { mutableStateOf(false) } 43 | var multiplePathChosen by remember { mutableStateOf(listOf("")) } 44 | 45 | Button(onClick = { 46 | showMultipleFilePicker = true 47 | }) { 48 | Text("Choose Multiple Files") 49 | } 50 | Text("Files Chosen: $multiplePathChosen") 51 | 52 | MultipleFilePicker(showMultipleFilePicker, fileExtensions = fileType) { platformFiles -> 53 | multiplePathChosen = platformFiles?.map { it.nsUrl.path + "\n" } ?: emptyList() 54 | showMultipleFilePicker = false 55 | } 56 | 57 | ///////////////////////////////////////////////////////////////// 58 | 59 | var nonComposeFileChosen by remember { mutableStateOf("") } 60 | 61 | Button(onClick = { 62 | MainScope().launch { 63 | nonComposeFileChosen = launchFilePicker(fileExtensions = fileType) 64 | .firstOrNull()?.nsUrl?.path ?: "none selected" 65 | } 66 | }) { 67 | 68 | Text("Choose File Non-Compose") 69 | } 70 | Text("File Chosen: $nonComposeFileChosen") 71 | 72 | ///////////////////////////////////////////////////////////////// 73 | 74 | var nonComposeMultipleFileChosen by remember { mutableStateOf(listOf("")) } 75 | 76 | Button(onClick = { 77 | MainScope().launch { 78 | nonComposeMultipleFileChosen = launchFilePicker(fileExtensions = fileType, allowMultiple = true) 79 | .map { it.nsUrl.path + "\n" } 80 | } 81 | }) { 82 | 83 | Text("Choose Multiple Files Non-Compose") 84 | } 85 | Text("Multiple File Chosen: $nonComposeMultipleFileChosen") 86 | 87 | ///////////////////////////////////////////////////////////////// 88 | 89 | var showDirPicker by remember { mutableStateOf(false) } 90 | var dirChosen by remember { mutableStateOf("") } 91 | 92 | Button(onClick = { 93 | showDirPicker = true 94 | }) { 95 | Text("Choose Directory") 96 | } 97 | Text("Directory Chosen: $dirChosen") 98 | 99 | DirectoryPicker(showDirPicker) { path -> 100 | dirChosen = path ?: "none selected" 101 | showDirPicker = false 102 | } 103 | 104 | ///////////////////////////////////////////////////////////////// 105 | 106 | var nonComposeDirChosen by remember { mutableStateOf("") } 107 | 108 | Button(onClick = { 109 | MainScope().launch { 110 | nonComposeDirChosen = launchDirectoryPicker() 111 | .firstOrNull()?.nsUrl?.path ?: "none selected" 112 | } 113 | }) { 114 | 115 | Text("Choose Directory Non-Compose") 116 | } 117 | Text("Directory Chosen: $nonComposeDirChosen") 118 | 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/iosApp/Podfile: -------------------------------------------------------------------------------- 1 | 2 | target 'iosApp' do 3 | use_frameworks! 4 | platform :ios, '14.1' 5 | pod 'ios', :path => '../ios' 6 | end -------------------------------------------------------------------------------- /examples/iosApp/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - ios (3.1.0) 3 | 4 | DEPENDENCIES: 5 | - ios (from `../ios`) 6 | 7 | EXTERNAL SOURCES: 8 | ios: 9 | :path: "../ios" 10 | 11 | SPEC CHECKSUMS: 12 | ios: 5991262a52ccfe5cd22b68bb8d83e5dd5be98dce 13 | 14 | PODFILE CHECKSUM: d0f1a8cda67b334342153a01dcff5e0cb3dfeab9 15 | 16 | COCOAPODS: 1.14.2 17 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 56; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 126C2CC92AD2E4A7002BDF98 /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 126C2CC82AD2E4A7002BDF98 /* SwiftUI.framework */; }; 11 | 12A0AD4D2ABB0E4D00FACB56 /* iosAppApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A0AD4C2ABB0E4D00FACB56 /* iosAppApp.swift */; }; 12 | 12A0AD4F2ABB0E4D00FACB56 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12A0AD4E2ABB0E4D00FACB56 /* ContentView.swift */; }; 13 | 12A0AD512ABB0E4F00FACB56 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 12A0AD502ABB0E4F00FACB56 /* Assets.xcassets */; }; 14 | 12A0AD542ABB0E4F00FACB56 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 12A0AD532ABB0E4F00FACB56 /* Preview Assets.xcassets */; }; 15 | E5E7F953E9121AAD5305461D /* Pods_iosApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8B97AEAAB675D969AA113448 /* Pods_iosApp.framework */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXFileReference section */ 19 | 126C2CC82AD2E4A7002BDF98 /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; 20 | 12A0AD492ABB0E4D00FACB56 /* iosApp.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = iosApp.app; sourceTree = BUILT_PRODUCTS_DIR; }; 21 | 12A0AD4C2ABB0E4D00FACB56 /* iosAppApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = iosAppApp.swift; sourceTree = ""; }; 22 | 12A0AD4E2ABB0E4D00FACB56 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; 23 | 12A0AD502ABB0E4F00FACB56 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 24 | 12A0AD532ABB0E4F00FACB56 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 25 | 8B97AEAAB675D969AA113448 /* Pods_iosApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_iosApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 26 | AF61615137E0065B1C6266B3 /* Pods-iosApp.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.release.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.release.xcconfig"; sourceTree = ""; }; 27 | F4AAE304375D66D253BA8BFE /* Pods-iosApp.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-iosApp.debug.xcconfig"; path = "Target Support Files/Pods-iosApp/Pods-iosApp.debug.xcconfig"; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | 12A0AD462ABB0E4D00FACB56 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | 126C2CC92AD2E4A7002BDF98 /* SwiftUI.framework in Frameworks */, 36 | E5E7F953E9121AAD5305461D /* Pods_iosApp.framework in Frameworks */, 37 | ); 38 | runOnlyForDeploymentPostprocessing = 0; 39 | }; 40 | /* End PBXFrameworksBuildPhase section */ 41 | 42 | /* Begin PBXGroup section */ 43 | 12A0AD402ABB0E4D00FACB56 = { 44 | isa = PBXGroup; 45 | children = ( 46 | 12A0AD4B2ABB0E4D00FACB56 /* iosApp */, 47 | 12A0AD4A2ABB0E4D00FACB56 /* Products */, 48 | 5303203EC96310C194849FA5 /* Pods */, 49 | E4CC1E4E25BFD3CE640B0CD0 /* Frameworks */, 50 | ); 51 | sourceTree = ""; 52 | }; 53 | 12A0AD4A2ABB0E4D00FACB56 /* Products */ = { 54 | isa = PBXGroup; 55 | children = ( 56 | 12A0AD492ABB0E4D00FACB56 /* iosApp.app */, 57 | ); 58 | name = Products; 59 | sourceTree = ""; 60 | }; 61 | 12A0AD4B2ABB0E4D00FACB56 /* iosApp */ = { 62 | isa = PBXGroup; 63 | children = ( 64 | 12A0AD4C2ABB0E4D00FACB56 /* iosAppApp.swift */, 65 | 12A0AD4E2ABB0E4D00FACB56 /* ContentView.swift */, 66 | 12A0AD502ABB0E4F00FACB56 /* Assets.xcassets */, 67 | 12A0AD522ABB0E4F00FACB56 /* Preview Content */, 68 | ); 69 | path = iosApp; 70 | sourceTree = ""; 71 | }; 72 | 12A0AD522ABB0E4F00FACB56 /* Preview Content */ = { 73 | isa = PBXGroup; 74 | children = ( 75 | 12A0AD532ABB0E4F00FACB56 /* Preview Assets.xcassets */, 76 | ); 77 | path = "Preview Content"; 78 | sourceTree = ""; 79 | }; 80 | 5303203EC96310C194849FA5 /* Pods */ = { 81 | isa = PBXGroup; 82 | children = ( 83 | F4AAE304375D66D253BA8BFE /* Pods-iosApp.debug.xcconfig */, 84 | AF61615137E0065B1C6266B3 /* Pods-iosApp.release.xcconfig */, 85 | ); 86 | path = Pods; 87 | sourceTree = ""; 88 | }; 89 | E4CC1E4E25BFD3CE640B0CD0 /* Frameworks */ = { 90 | isa = PBXGroup; 91 | children = ( 92 | 126C2CC82AD2E4A7002BDF98 /* SwiftUI.framework */, 93 | 8B97AEAAB675D969AA113448 /* Pods_iosApp.framework */, 94 | ); 95 | name = Frameworks; 96 | sourceTree = ""; 97 | }; 98 | /* End PBXGroup section */ 99 | 100 | /* Begin PBXNativeTarget section */ 101 | 12A0AD482ABB0E4D00FACB56 /* iosApp */ = { 102 | isa = PBXNativeTarget; 103 | buildConfigurationList = 12A0AD572ABB0E4F00FACB56 /* Build configuration list for PBXNativeTarget "iosApp" */; 104 | buildPhases = ( 105 | 5B99BB9D861309F7E66EED8C /* [CP] Check Pods Manifest.lock */, 106 | 12A0AD452ABB0E4D00FACB56 /* Sources */, 107 | 12A0AD462ABB0E4D00FACB56 /* Frameworks */, 108 | 12A0AD472ABB0E4D00FACB56 /* Resources */, 109 | 4AC3DEDD3EFC0720AD077483 /* [CP] Embed Pods Frameworks */, 110 | D576410C2082D329D1FB9C9F /* [CP] Copy Pods Resources */, 111 | ); 112 | buildRules = ( 113 | ); 114 | dependencies = ( 115 | ); 116 | name = iosApp; 117 | productName = iosApp; 118 | productReference = 12A0AD492ABB0E4D00FACB56 /* iosApp.app */; 119 | productType = "com.apple.product-type.application"; 120 | }; 121 | /* End PBXNativeTarget section */ 122 | 123 | /* Begin PBXProject section */ 124 | 12A0AD412ABB0E4D00FACB56 /* Project object */ = { 125 | isa = PBXProject; 126 | attributes = { 127 | BuildIndependentTargetsInParallel = 1; 128 | LastSwiftUpdateCheck = 1500; 129 | LastUpgradeCheck = 1500; 130 | TargetAttributes = { 131 | 12A0AD482ABB0E4D00FACB56 = { 132 | CreatedOnToolsVersion = 15.0; 133 | }; 134 | }; 135 | }; 136 | buildConfigurationList = 12A0AD442ABB0E4D00FACB56 /* Build configuration list for PBXProject "iosApp" */; 137 | compatibilityVersion = "Xcode 14.0"; 138 | developmentRegion = en; 139 | hasScannedForEncodings = 0; 140 | knownRegions = ( 141 | en, 142 | Base, 143 | ); 144 | mainGroup = 12A0AD402ABB0E4D00FACB56; 145 | productRefGroup = 12A0AD4A2ABB0E4D00FACB56 /* Products */; 146 | projectDirPath = ""; 147 | projectRoot = ""; 148 | targets = ( 149 | 12A0AD482ABB0E4D00FACB56 /* iosApp */, 150 | ); 151 | }; 152 | /* End PBXProject section */ 153 | 154 | /* Begin PBXResourcesBuildPhase section */ 155 | 12A0AD472ABB0E4D00FACB56 /* Resources */ = { 156 | isa = PBXResourcesBuildPhase; 157 | buildActionMask = 2147483647; 158 | files = ( 159 | 12A0AD542ABB0E4F00FACB56 /* Preview Assets.xcassets in Resources */, 160 | 12A0AD512ABB0E4F00FACB56 /* Assets.xcassets in Resources */, 161 | ); 162 | runOnlyForDeploymentPostprocessing = 0; 163 | }; 164 | /* End PBXResourcesBuildPhase section */ 165 | 166 | /* Begin PBXShellScriptBuildPhase section */ 167 | 4AC3DEDD3EFC0720AD077483 /* [CP] Embed Pods Frameworks */ = { 168 | isa = PBXShellScriptBuildPhase; 169 | buildActionMask = 2147483647; 170 | files = ( 171 | ); 172 | inputFileListPaths = ( 173 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-input-files.xcfilelist", 174 | ); 175 | name = "[CP] Embed Pods Frameworks"; 176 | outputFileListPaths = ( 177 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks-${CONFIGURATION}-output-files.xcfilelist", 178 | ); 179 | runOnlyForDeploymentPostprocessing = 0; 180 | shellPath = /bin/sh; 181 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-frameworks.sh\"\n"; 182 | showEnvVarsInLog = 0; 183 | }; 184 | 5B99BB9D861309F7E66EED8C /* [CP] Check Pods Manifest.lock */ = { 185 | isa = PBXShellScriptBuildPhase; 186 | buildActionMask = 2147483647; 187 | files = ( 188 | ); 189 | inputFileListPaths = ( 190 | ); 191 | inputPaths = ( 192 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 193 | "${PODS_ROOT}/Manifest.lock", 194 | ); 195 | name = "[CP] Check Pods Manifest.lock"; 196 | outputFileListPaths = ( 197 | ); 198 | outputPaths = ( 199 | "$(DERIVED_FILE_DIR)/Pods-iosApp-checkManifestLockResult.txt", 200 | ); 201 | runOnlyForDeploymentPostprocessing = 0; 202 | shellPath = /bin/sh; 203 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 204 | showEnvVarsInLog = 0; 205 | }; 206 | D576410C2082D329D1FB9C9F /* [CP] Copy Pods Resources */ = { 207 | isa = PBXShellScriptBuildPhase; 208 | buildActionMask = 2147483647; 209 | files = ( 210 | ); 211 | inputFileListPaths = ( 212 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-input-files.xcfilelist", 213 | ); 214 | name = "[CP] Copy Pods Resources"; 215 | outputFileListPaths = ( 216 | "${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources-${CONFIGURATION}-output-files.xcfilelist", 217 | ); 218 | runOnlyForDeploymentPostprocessing = 0; 219 | shellPath = /bin/sh; 220 | shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-iosApp/Pods-iosApp-resources.sh\"\n"; 221 | showEnvVarsInLog = 0; 222 | }; 223 | /* End PBXShellScriptBuildPhase section */ 224 | 225 | /* Begin PBXSourcesBuildPhase section */ 226 | 12A0AD452ABB0E4D00FACB56 /* Sources */ = { 227 | isa = PBXSourcesBuildPhase; 228 | buildActionMask = 2147483647; 229 | files = ( 230 | 12A0AD4F2ABB0E4D00FACB56 /* ContentView.swift in Sources */, 231 | 12A0AD4D2ABB0E4D00FACB56 /* iosAppApp.swift in Sources */, 232 | ); 233 | runOnlyForDeploymentPostprocessing = 0; 234 | }; 235 | /* End PBXSourcesBuildPhase section */ 236 | 237 | /* Begin XCBuildConfiguration section */ 238 | 12A0AD552ABB0E4F00FACB56 /* Debug */ = { 239 | isa = XCBuildConfiguration; 240 | buildSettings = { 241 | ALWAYS_SEARCH_USER_PATHS = NO; 242 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 243 | CLANG_ANALYZER_NONNULL = YES; 244 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 245 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 246 | CLANG_ENABLE_MODULES = YES; 247 | CLANG_ENABLE_OBJC_ARC = YES; 248 | CLANG_ENABLE_OBJC_WEAK = YES; 249 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 250 | CLANG_WARN_BOOL_CONVERSION = YES; 251 | CLANG_WARN_COMMA = YES; 252 | CLANG_WARN_CONSTANT_CONVERSION = YES; 253 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 254 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 255 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 256 | CLANG_WARN_EMPTY_BODY = YES; 257 | CLANG_WARN_ENUM_CONVERSION = YES; 258 | CLANG_WARN_INFINITE_RECURSION = YES; 259 | CLANG_WARN_INT_CONVERSION = YES; 260 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 261 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 262 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 263 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 264 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 265 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 266 | CLANG_WARN_STRICT_PROTOTYPES = YES; 267 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 268 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 269 | CLANG_WARN_UNREACHABLE_CODE = YES; 270 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 271 | COPY_PHASE_STRIP = NO; 272 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 273 | ENABLE_STRICT_OBJC_MSGSEND = YES; 274 | ENABLE_TESTABILITY = YES; 275 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 276 | GCC_C_LANGUAGE_STANDARD = gnu17; 277 | GCC_DYNAMIC_NO_PIC = NO; 278 | GCC_NO_COMMON_BLOCKS = YES; 279 | GCC_OPTIMIZATION_LEVEL = 0; 280 | GCC_PREPROCESSOR_DEFINITIONS = ( 281 | "DEBUG=1", 282 | "$(inherited)", 283 | ); 284 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 285 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 286 | GCC_WARN_UNDECLARED_SELECTOR = YES; 287 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 288 | GCC_WARN_UNUSED_FUNCTION = YES; 289 | GCC_WARN_UNUSED_VARIABLE = YES; 290 | INFOPLIST_KEY_UISupportsDocumentBrowser = YES; 291 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 292 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 293 | MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; 294 | MTL_FAST_MATH = YES; 295 | ONLY_ACTIVE_ARCH = YES; 296 | SDKROOT = iphoneos; 297 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; 298 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 299 | }; 300 | name = Debug; 301 | }; 302 | 12A0AD562ABB0E4F00FACB56 /* Release */ = { 303 | isa = XCBuildConfiguration; 304 | buildSettings = { 305 | ALWAYS_SEARCH_USER_PATHS = NO; 306 | ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; 307 | CLANG_ANALYZER_NONNULL = YES; 308 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 309 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; 310 | CLANG_ENABLE_MODULES = YES; 311 | CLANG_ENABLE_OBJC_ARC = YES; 312 | CLANG_ENABLE_OBJC_WEAK = YES; 313 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 314 | CLANG_WARN_BOOL_CONVERSION = YES; 315 | CLANG_WARN_COMMA = YES; 316 | CLANG_WARN_CONSTANT_CONVERSION = YES; 317 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 318 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 319 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 320 | CLANG_WARN_EMPTY_BODY = YES; 321 | CLANG_WARN_ENUM_CONVERSION = YES; 322 | CLANG_WARN_INFINITE_RECURSION = YES; 323 | CLANG_WARN_INT_CONVERSION = YES; 324 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 325 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 326 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 327 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 328 | CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; 329 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 330 | CLANG_WARN_STRICT_PROTOTYPES = YES; 331 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 332 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 333 | CLANG_WARN_UNREACHABLE_CODE = YES; 334 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 335 | COPY_PHASE_STRIP = NO; 336 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 337 | ENABLE_NS_ASSERTIONS = NO; 338 | ENABLE_STRICT_OBJC_MSGSEND = YES; 339 | ENABLE_USER_SCRIPT_SANDBOXING = NO; 340 | GCC_C_LANGUAGE_STANDARD = gnu17; 341 | GCC_NO_COMMON_BLOCKS = YES; 342 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 343 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 344 | GCC_WARN_UNDECLARED_SELECTOR = YES; 345 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 346 | GCC_WARN_UNUSED_FUNCTION = YES; 347 | GCC_WARN_UNUSED_VARIABLE = YES; 348 | INFOPLIST_KEY_UISupportsDocumentBrowser = YES; 349 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 350 | LOCALIZATION_PREFERS_STRING_CATALOGS = YES; 351 | MTL_ENABLE_DEBUG_INFO = NO; 352 | MTL_FAST_MATH = YES; 353 | SDKROOT = iphoneos; 354 | SWIFT_COMPILATION_MODE = wholemodule; 355 | VALIDATE_PRODUCT = YES; 356 | }; 357 | name = Release; 358 | }; 359 | 12A0AD582ABB0E4F00FACB56 /* Debug */ = { 360 | isa = XCBuildConfiguration; 361 | baseConfigurationReference = F4AAE304375D66D253BA8BFE /* Pods-iosApp.debug.xcconfig */; 362 | buildSettings = { 363 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 364 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 365 | CODE_SIGN_STYLE = Automatic; 366 | CURRENT_PROJECT_VERSION = 1; 367 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 368 | DEVELOPMENT_TEAM = ""; 369 | ENABLE_PREVIEWS = YES; 370 | GENERATE_INFOPLIST_FILE = YES; 371 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 372 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 373 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 374 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 375 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 376 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 377 | LD_RUNPATH_SEARCH_PATHS = ( 378 | "$(inherited)", 379 | "@executable_path/Frameworks", 380 | ); 381 | MARKETING_VERSION = 1.0; 382 | PRODUCT_BUNDLE_IDENTIFIER = com.darkrockstudios.libraries.mpfilepicker.iosApp; 383 | PRODUCT_NAME = "$(TARGET_NAME)"; 384 | SWIFT_EMIT_LOC_STRINGS = YES; 385 | SWIFT_VERSION = 5.0; 386 | TARGETED_DEVICE_FAMILY = "1,2"; 387 | }; 388 | name = Debug; 389 | }; 390 | 12A0AD592ABB0E4F00FACB56 /* Release */ = { 391 | isa = XCBuildConfiguration; 392 | baseConfigurationReference = AF61615137E0065B1C6266B3 /* Pods-iosApp.release.xcconfig */; 393 | buildSettings = { 394 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 395 | ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; 396 | CODE_SIGN_STYLE = Automatic; 397 | CURRENT_PROJECT_VERSION = 1; 398 | DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; 399 | DEVELOPMENT_TEAM = ""; 400 | ENABLE_PREVIEWS = YES; 401 | GENERATE_INFOPLIST_FILE = YES; 402 | INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; 403 | INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; 404 | INFOPLIST_KEY_UILaunchScreen_Generation = YES; 405 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 406 | INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; 407 | IPHONEOS_DEPLOYMENT_TARGET = 14.1; 408 | LD_RUNPATH_SEARCH_PATHS = ( 409 | "$(inherited)", 410 | "@executable_path/Frameworks", 411 | ); 412 | MARKETING_VERSION = 1.0; 413 | PRODUCT_BUNDLE_IDENTIFIER = com.darkrockstudios.libraries.mpfilepicker.iosApp; 414 | PRODUCT_NAME = "$(TARGET_NAME)"; 415 | SWIFT_EMIT_LOC_STRINGS = YES; 416 | SWIFT_VERSION = 5.0; 417 | TARGETED_DEVICE_FAMILY = "1,2"; 418 | }; 419 | name = Release; 420 | }; 421 | /* End XCBuildConfiguration section */ 422 | 423 | /* Begin XCConfigurationList section */ 424 | 12A0AD442ABB0E4D00FACB56 /* Build configuration list for PBXProject "iosApp" */ = { 425 | isa = XCConfigurationList; 426 | buildConfigurations = ( 427 | 12A0AD552ABB0E4F00FACB56 /* Debug */, 428 | 12A0AD562ABB0E4F00FACB56 /* Release */, 429 | ); 430 | defaultConfigurationIsVisible = 0; 431 | defaultConfigurationName = Release; 432 | }; 433 | 12A0AD572ABB0E4F00FACB56 /* Build configuration list for PBXNativeTarget "iosApp" */ = { 434 | isa = XCConfigurationList; 435 | buildConfigurations = ( 436 | 12A0AD582ABB0E4F00FACB56 /* Debug */, 437 | 12A0AD592ABB0E4F00FACB56 /* Release */, 438 | ); 439 | defaultConfigurationIsVisible = 0; 440 | defaultConfigurationName = Release; 441 | }; 442 | /* End XCConfigurationList section */ 443 | }; 444 | rootObject = 12A0AD412ABB0E4D00FACB56 /* Project object */; 445 | } 446 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | } 8 | ], 9 | "info" : { 10 | "author" : "xcode", 11 | "version" : 1 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/ContentView.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ContentView.swift 3 | // iosApp 4 | // 5 | // Created by Edward James Remo on 9/20/23. 6 | // 7 | 8 | import SwiftUI 9 | import ios 10 | 11 | struct ContentView: View { 12 | var body: some View { 13 | ZStack { 14 | ComposeView() 15 | }.listRowInsets(EdgeInsets()) 16 | } 17 | } 18 | 19 | 20 | struct ComposeView: UIViewControllerRepresentable { 21 | func makeUIViewController(context: Context) -> UIViewController { 22 | Main_iosKt.MainViewController() 23 | } 24 | 25 | func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} 26 | } 27 | 28 | #Preview { 29 | ContentView() 30 | } 31 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /examples/iosApp/iosApp/iosAppApp.swift: -------------------------------------------------------------------------------- 1 | // 2 | // iosAppApp.swift 3 | // iosApp 4 | // 5 | // Created by Edward James Remo on 9/20/23. 6 | // 7 | 8 | import SwiftUI 9 | 10 | @main 11 | struct iosAppApp: App { 12 | var body: some Scene { 13 | WindowGroup { 14 | ContentView() 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /examples/jvm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("jvm") 3 | alias(libs.plugins.jetbrainsCompose) 4 | } 5 | 6 | dependencies { 7 | implementation(project(":mpfilepicker")) 8 | implementation(compose.desktop.currentOs) 9 | } 10 | -------------------------------------------------------------------------------- /examples/jvm/src/main/kotlin/Main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.layout.Column 2 | import androidx.compose.material.Button 3 | import androidx.compose.material.Text 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.setValue 8 | import androidx.compose.ui.window.Window 9 | import androidx.compose.ui.window.application 10 | import com.darkrockstudios.libraries.mpfilepicker.DirectoryPicker 11 | import com.darkrockstudios.libraries.mpfilepicker.FilePicker 12 | import com.darkrockstudios.libraries.mpfilepicker.MultipleFilePicker 13 | 14 | fun main() = application { 15 | var showSingleFile by remember { mutableStateOf(false) } 16 | var pathSingleChosen by remember { mutableStateOf("") } 17 | 18 | var showMultiFile by remember { mutableStateOf(false) } 19 | var pathMultiChosen by remember { mutableStateOf(listOf("")) } 20 | 21 | var showDirPicker by remember { mutableStateOf(false) } 22 | var dirChosen by remember { mutableStateOf("") } 23 | 24 | Window(onCloseRequest = ::exitApplication) { 25 | Column { 26 | Button(onClick = { 27 | showSingleFile = true 28 | }) { 29 | Text("Choose File") 30 | } 31 | Text("File Chosen: $pathSingleChosen") 32 | 33 | ///////////////////////////////////////////////////////////////// 34 | 35 | Button(onClick = { 36 | showMultiFile = true 37 | }) { 38 | Text("Choose Multiple File") 39 | } 40 | Text("Files Chosen: $pathMultiChosen") 41 | 42 | ///////////////////////////////////////////////////////////////// 43 | 44 | 45 | Button(onClick = { 46 | showDirPicker = true 47 | }) { 48 | Text("Choose Directory") 49 | } 50 | Text("Directory Chosen: $dirChosen") 51 | } 52 | } 53 | 54 | FilePicker(showSingleFile, fileExtensions = listOf("jpg", "png")) { file -> 55 | pathSingleChosen = file?.file?.path ?: "none selected" 56 | showSingleFile = false 57 | } 58 | 59 | MultipleFilePicker(showMultiFile, fileExtensions = listOf("jpg", "png")) { files -> 60 | pathMultiChosen = files?.map { it.file.path + "\n" } ?: emptyList() 61 | showMultiFile = false 62 | } 63 | 64 | DirectoryPicker(showDirPicker) { path -> 65 | dirChosen = path ?: "none selected" 66 | showDirPicker = false 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/macosX64/.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea/ 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store -------------------------------------------------------------------------------- /examples/macosX64/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.jetbrainsCompose) 4 | } 5 | 6 | kotlin { 7 | macosX64 { 8 | binaries { 9 | executable { 10 | entryPoint = "main" 11 | } 12 | } 13 | } 14 | 15 | sourceSets { 16 | val macosX64Main by getting { 17 | dependencies { 18 | implementation(compose.ui) 19 | implementation(compose.foundation) 20 | implementation(compose.material3) 21 | implementation(compose.runtime) 22 | implementation(project(":mpfilepicker")) 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/macosX64/src/macosX64Main/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.foundation.isSystemInDarkTheme 2 | import androidx.compose.foundation.layout.Column 3 | import androidx.compose.material3.* 4 | import androidx.compose.runtime.getValue 5 | import androidx.compose.runtime.mutableStateOf 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.runtime.setValue 8 | import androidx.compose.ui.window.Window 9 | import com.darkrockstudios.libraries.mpfilepicker.DirectoryPicker 10 | import com.darkrockstudios.libraries.mpfilepicker.FilePicker 11 | import com.darkrockstudios.libraries.mpfilepicker.MultipleFilePicker 12 | import platform.AppKit.NSApp 13 | import platform.AppKit.NSApplication 14 | 15 | fun main() { 16 | NSApplication.sharedApplication() 17 | Window(title = "Youtube history") { 18 | MaterialTheme(colorScheme = if (isSystemInDarkTheme()) darkColorScheme() else lightColorScheme()) { 19 | Scaffold { 20 | var showSingleFile by remember { mutableStateOf(false) } 21 | var showMultipleFile by remember { mutableStateOf(false) } 22 | var singleFilePathChosen by remember { mutableStateOf("") } 23 | var multipleFilesPathsChosen by remember { mutableStateOf(listOf("")) } 24 | 25 | var showDirPicker by remember { mutableStateOf(false) } 26 | var dirChosen by remember { mutableStateOf("") } 27 | 28 | Column { 29 | Button(onClick = { 30 | showSingleFile = true 31 | }) { 32 | Text("Choose File") 33 | } 34 | Text("File Chosen: $singleFilePathChosen") 35 | 36 | ///////////////////////////////////////////////////////////////// 37 | 38 | 39 | Button(onClick = { 40 | showMultipleFile = true 41 | }) { 42 | Text("Choose Multiple Files") 43 | } 44 | Text("File Chosen: $multipleFilesPathsChosen") 45 | 46 | ///////////////////////////////////////////////////////////////// 47 | 48 | Button(onClick = { 49 | showDirPicker = true 50 | }) { 51 | Text("Choose Directory") 52 | } 53 | Text("Directory Chosen: $dirChosen") 54 | } 55 | 56 | FilePicker(showSingleFile, fileExtensions = listOf("jpg", "png", "plist")) { file -> 57 | singleFilePathChosen = file?.nsUrl?.path ?: "none selected" 58 | showSingleFile = false 59 | } 60 | 61 | MultipleFilePicker(showMultipleFile, fileExtensions = listOf("jpg", "png", "plist")) { files -> 62 | multipleFilesPathsChosen = files?.map { it.nsUrl.path + "\n" } ?: listOf() 63 | showMultipleFile = false 64 | } 65 | 66 | DirectoryPicker(showDirPicker) { path -> 67 | dirChosen = path ?: "none selected" 68 | showDirPicker = false 69 | } 70 | } 71 | 72 | } 73 | } 74 | NSApp?.run() 75 | } 76 | -------------------------------------------------------------------------------- /examples/web-wasm/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 2 | 3 | plugins { 4 | alias(libs.plugins.kotlinMultiplatform) 5 | alias(libs.plugins.jetbrainsCompose) 6 | } 7 | 8 | kotlin { 9 | @OptIn(ExperimentalWasmDsl::class) 10 | wasmJs { 11 | browser { 12 | commonWebpackConfig { 13 | outputFileName = "composeApp.js" 14 | } 15 | } 16 | binaries.executable() 17 | } 18 | sourceSets { 19 | val wasmJsMain by getting { 20 | dependencies { 21 | implementation(compose.runtime) 22 | implementation(compose.foundation) 23 | implementation(compose.material3) 24 | implementation(project(":mpfilepicker")) 25 | } 26 | } 27 | } 28 | } 29 | 30 | compose.experimental { 31 | web.application {} 32 | } 33 | -------------------------------------------------------------------------------- /examples/web-wasm/src/wasmJsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.animation.AnimatedVisibility 2 | import androidx.compose.foundation.background 3 | import androidx.compose.foundation.layout.* 4 | import androidx.compose.foundation.lazy.LazyColumn 5 | import androidx.compose.material3.* 6 | import androidx.compose.runtime.* 7 | import androidx.compose.ui.ExperimentalComposeUiApi 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.text.font.FontFamily 11 | import androidx.compose.ui.text.style.TextOverflow 12 | import androidx.compose.ui.unit.dp 13 | import androidx.compose.ui.window.CanvasBasedWindow 14 | import com.darkrockstudios.libraries.mpfilepicker.* 15 | import kotlinx.coroutines.launch 16 | import kotlin.io.encoding.Base64 17 | import kotlin.io.encoding.ExperimentalEncodingApi 18 | 19 | @OptIn(ExperimentalComposeUiApi::class) 20 | fun main() { 21 | CanvasBasedWindow(canvasElementId = "ComposeTarget", title = "") { ExampleView() } 22 | } 23 | 24 | @OptIn(ExperimentalEncodingApi::class) 25 | @Composable 26 | fun ExampleView() { 27 | val scope = rememberCoroutineScope() 28 | 29 | var pickerVisible by remember { mutableStateOf(false) } 30 | var multipickerVisible by remember { mutableStateOf(false) } 31 | 32 | var fileSelected by remember { mutableStateOf(false) } 33 | val fileNames = remember { mutableStateListOf() } 34 | val fileContents = remember { mutableStateListOf() } 35 | val fileContentsRaw = remember { mutableStateListOf() } 36 | 37 | suspend fun addFile( 38 | file: PlatformFile, 39 | ) { 40 | val content = readFileAsByteArray(file.file) 41 | 42 | fileNames.add(file.file.name) 43 | fileContents.add(content.decodeToString()) 44 | fileContentsRaw.add(Base64.encode(content)) 45 | } 46 | 47 | FilePicker( 48 | pickerVisible, 49 | ) { 50 | pickerVisible = false 51 | fileNames.clear() 52 | fileContents.clear() 53 | 54 | if (it != null) { 55 | fileSelected = true 56 | 57 | scope.launch { 58 | addFile(it) 59 | } 60 | } else { 61 | fileSelected = false 62 | } 63 | } 64 | 65 | MultipleFilePicker( 66 | multipickerVisible, 67 | ) { 68 | multipickerVisible = false 69 | fileNames.clear() 70 | fileContents.clear() 71 | 72 | if (it?.isNotEmpty() == true) { 73 | fileSelected = true 74 | 75 | scope.launch { 76 | it.forEach { file -> 77 | addFile(file) 78 | } 79 | } 80 | } else { 81 | fileSelected = false 82 | } 83 | } 84 | 85 | MaterialTheme { 86 | Column(modifier = Modifier.fillMaxSize()) { 87 | Row( 88 | modifier = Modifier.padding(10.dp), 89 | horizontalArrangement = Arrangement.spacedBy(10.dp) 90 | ) { 91 | // Button to open the file picker 92 | Button(onClick = { 93 | pickerVisible = true 94 | multipickerVisible = false 95 | }) { 96 | Text("Open File Picker") 97 | } 98 | 99 | // Button to open the multiple file picker 100 | Button(onClick = { 101 | pickerVisible = false 102 | multipickerVisible = true 103 | }) { 104 | Text("Open Multi-File Picker") 105 | } 106 | } 107 | 108 | AnimatedVisibility(fileSelected) { 109 | Text("Selected Files:") 110 | } 111 | AnimatedVisibility(!fileSelected) { 112 | Text("No files selected") 113 | } 114 | 115 | LazyColumn(contentPadding = PaddingValues(10.dp)) { 116 | fileNames.forEachIndexed { idx, fileName -> 117 | item { 118 | Card { 119 | Column { 120 | Text("File: $fileName") 121 | if (fileContents.getOrNull(idx)?.contains('\uFFFD') == false) { 122 | Text("Content:") 123 | FileContent(fileContents.getOrNull(idx)) 124 | } 125 | 126 | Text("Raw Content (Base64):") 127 | FileContent(fileContentsRaw.getOrNull(idx)) 128 | } 129 | } 130 | Spacer(modifier = Modifier.height(10.dp)) 131 | } 132 | } 133 | } 134 | } 135 | } 136 | } 137 | 138 | @Composable 139 | private fun FileContent(fileContent: String?) { 140 | Text( 141 | fileContent ?: "N/A", 142 | modifier = Modifier 143 | .background( 144 | Color.LightGray.copy(alpha = 0.5f), 145 | MaterialTheme.shapes.medium 146 | ) 147 | .width(1200.dp) 148 | .padding(5.dp), 149 | fontFamily = FontFamily.Monospace, 150 | color = Color.Black, 151 | overflow = TextOverflow.Ellipsis, 152 | softWrap = true 153 | ) 154 | } 155 | 156 | -------------------------------------------------------------------------------- /examples/web-wasm/src/wasmJsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Compose App 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/web/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.kotlinMultiplatform) 3 | alias(libs.plugins.jetbrainsCompose) 4 | } 5 | 6 | kotlin { 7 | js(IR) { 8 | browser() 9 | binaries.executable() 10 | } 11 | sourceSets { 12 | val jsMain by getting { 13 | dependencies { 14 | implementation(libs.kotlinx.html) 15 | implementation(kotlin("stdlib-js")) 16 | implementation(compose.html.core) 17 | implementation(compose.runtime) 18 | implementation(project(":mpfilepicker")) 19 | } 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/web/src/jsMain/kotlin/main.kt: -------------------------------------------------------------------------------- 1 | import androidx.compose.runtime.* 2 | import com.darkrockstudios.libraries.mpfilepicker.FilePicker 3 | import com.darkrockstudios.libraries.mpfilepicker.MultipleFilePicker 4 | import com.darkrockstudios.libraries.mpfilepicker.readFileAsText 5 | import kotlinx.coroutines.launch 6 | import org.jetbrains.compose.web.dom.Br 7 | import org.jetbrains.compose.web.dom.Button 8 | import org.jetbrains.compose.web.dom.Text 9 | import org.jetbrains.compose.web.renderComposable 10 | 11 | fun main() { 12 | renderComposable(rootElementId = "root") { 13 | val scope = rememberCoroutineScope() 14 | 15 | var showSingleFile by remember { mutableStateOf(false) } 16 | var fileName by remember { mutableStateOf("No file chosen") } 17 | var fileContents by remember { mutableStateOf("") } 18 | Button(attrs = { 19 | onClick { 20 | showSingleFile = true 21 | } 22 | }) { 23 | Text("Pick a text file") 24 | } 25 | Br() 26 | Text("File name: $fileName") 27 | Br() 28 | Text("File content: $fileContents") 29 | 30 | FilePicker(showSingleFile, fileExtensions = listOf("txt", "md")) { file -> 31 | if (file != null) { 32 | fileName = file.file.name 33 | scope.launch { 34 | fileContents = readFileAsText(file.file) 35 | } 36 | } 37 | showSingleFile = false 38 | } 39 | 40 | Br() 41 | Br() 42 | Br() 43 | Br() 44 | 45 | var showMultipleFile by remember { mutableStateOf(false) } 46 | var filesNames by remember { mutableStateOf(emptyList()) } 47 | Button(attrs = { 48 | onClick { 49 | showMultipleFile = true 50 | } 51 | }) { 52 | Text("Pick multiple image files") 53 | } 54 | Br() 55 | Text("Files names: $filesNames") 56 | Br() 57 | MultipleFilePicker(showMultipleFile, fileExtensions = listOf("png", "jpeg", "jpg"), initialDirectory = null) { files -> 58 | filesNames = files?.map { it.file.name } ?: listOf() 59 | showMultipleFile = false 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/web/src/jsMain/resources/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello, Kotlin/JS! 6 | 7 | 8 |
9 | 10 | 11 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | android.useAndroidX=true 3 | org.jetbrains.compose.experimental.jscanvas.enabled=true 4 | org.jetbrains.compose.experimental.macos.enabled=true 5 | org.jetbrains.compose.experimental.uikit.enabled=true 6 | org.jetbrains.compose.experimental.wasm.enabled=true 7 | org.gradle.jvmargs=-Xmx4096m 8 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | agp = "8.3.0" 3 | androidx-appcompat = "1.6.1" 4 | androidx-core = "1.12.0" 5 | compose-android = "1.8.2" 6 | compose-plugin = "1.6.1" 7 | junit = "4.13.2" 8 | kotlin = "1.9.22" 9 | kotlinx-coroutines = "1.8.0" 10 | kotlinx-html = "0.11.0" 11 | library = "3.1.0" 12 | android-compile-sdk = "34" 13 | android-target-sdk = "34" 14 | android-min-sdk = "21" 15 | 16 | [libraries] 17 | androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } 18 | androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } 19 | compose-activity = { module = "androidx.activity:activity-compose", version.ref = "compose-android" } 20 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } 21 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" } 22 | kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } 23 | kotlinx-html = { module = "org.jetbrains.kotlinx:kotlinx-html", version.ref = "kotlinx-html" } 24 | junit = { module = "junit:junit", version.ref = "junit" } 25 | 26 | [plugins] 27 | androidApplication = { id = "com.android.application", version.ref = "agp" } 28 | androidLibrary = { id = "com.android.library", version.ref = "agp" } 29 | jetbrainsCompose = { id = "org.jetbrains.compose", version.ref = "compose-plugin" } 30 | kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } 31 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 87 | APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit 88 | 89 | # Use the maximum available, or set MAX_FD != -1 to use that value. 90 | MAX_FD=maximum 91 | 92 | warn () { 93 | echo "$*" 94 | } >&2 95 | 96 | die () { 97 | echo 98 | echo "$*" 99 | echo 100 | exit 1 101 | } >&2 102 | 103 | # OS specific support (must be 'true' or 'false'). 104 | cygwin=false 105 | msys=false 106 | darwin=false 107 | nonstop=false 108 | case "$( uname )" in #( 109 | CYGWIN* ) cygwin=true ;; #( 110 | Darwin* ) darwin=true ;; #( 111 | MSYS* | MINGW* ) msys=true ;; #( 112 | NONSTOP* ) nonstop=true ;; 113 | esac 114 | 115 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 116 | 117 | 118 | # Determine the Java command to use to start the JVM. 119 | if [ -n "$JAVA_HOME" ] ; then 120 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 121 | # IBM's JDK on AIX uses strange locations for the executables 122 | JAVACMD=$JAVA_HOME/jre/sh/java 123 | else 124 | JAVACMD=$JAVA_HOME/bin/java 125 | fi 126 | if [ ! -x "$JAVACMD" ] ; then 127 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 128 | 129 | Please set the JAVA_HOME variable in your environment to match the 130 | location of your Java installation." 131 | fi 132 | else 133 | JAVACMD=java 134 | if ! command -v java >/dev/null 2>&1 135 | then 136 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | fi 142 | 143 | # Increase the maximum file descriptors if we can. 144 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 145 | case $MAX_FD in #( 146 | max*) 147 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 148 | # shellcheck disable=SC3045 149 | MAX_FD=$( ulimit -H -n ) || 150 | warn "Could not query maximum file descriptor limit" 151 | esac 152 | case $MAX_FD in #( 153 | '' | soft) :;; #( 154 | *) 155 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 156 | # shellcheck disable=SC3045 157 | ulimit -n "$MAX_FD" || 158 | warn "Could not set maximum file descriptor limit to $MAX_FD" 159 | esac 160 | fi 161 | 162 | # Collect all arguments for the java command, stacking in reverse order: 163 | # * args from the command line 164 | # * the main class name 165 | # * -classpath 166 | # * -D...appname settings 167 | # * --module-path (only if needed) 168 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 169 | 170 | # For Cygwin or MSYS, switch paths to Windows format before running java 171 | if "$cygwin" || "$msys" ; then 172 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 173 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command; 206 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 207 | # shell script including quotes and variable substitutions, so put them in 208 | # double quotes to make sure that they get re-expanded; and 209 | # * put everything else in single quotes, so that it's not re-expanded. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -classpath "$CLASSPATH" \ 214 | org.gradle.wrapper.GradleWrapperMain \ 215 | "$@" 216 | 217 | # Stop when "xargs" is not available. 218 | if ! command -v xargs >/dev/null 2>&1 219 | then 220 | die "xargs is not available" 221 | fi 222 | 223 | # Use "xargs" to parse quoted args. 224 | # 225 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 226 | # 227 | # In Bash we could simply go: 228 | # 229 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 230 | # set -- "${ARGS[@]}" "$@" 231 | # 232 | # but POSIX shell has neither arrays nor command substitution, so instead we 233 | # post-process each arg (as a line of input to sed) to backslash-escape any 234 | # character that might be a shell metacharacter, then use eval to reverse 235 | # that process (while maintaining the separation between arguments), and wrap 236 | # the whole thing up as a single "set" statement. 237 | # 238 | # This will of course break if any of these variables contains a newline or 239 | # an unmatched quote. 240 | # 241 | 242 | eval "set -- $( 243 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 244 | xargs -n1 | 245 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 246 | tr '\n' ' ' 247 | )" '"$@"' 248 | 249 | exec "$JAVACMD" "$@" 250 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /mpfilepicker/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl 2 | import java.net.URI 3 | 4 | plugins { 5 | alias(libs.plugins.kotlinMultiplatform) 6 | alias(libs.plugins.androidLibrary) 7 | alias(libs.plugins.jetbrainsCompose) 8 | id("maven-publish") 9 | id("signing") 10 | } 11 | 12 | val readableName = "Multiplatform File Picker" 13 | val repoUrl = "https://github.com/Wavesonics/compose-multiplatform-file-picker" 14 | group = "com.darkrockstudios" 15 | description = "A multiplatform compose widget for picking files" 16 | version = libs.versions.library.get() 17 | 18 | extra.apply { 19 | set("isReleaseVersion", !(version as String).endsWith("SNAPSHOT")) 20 | } 21 | 22 | kotlin { 23 | androidTarget { 24 | publishLibraryVariants("release") 25 | } 26 | 27 | jvm { 28 | compilations.all { 29 | kotlinOptions.jvmTarget = "17" 30 | } 31 | } 32 | 33 | js(IR) { 34 | browser() 35 | binaries.executable() 36 | } 37 | 38 | @OptIn(ExperimentalWasmDsl::class) 39 | wasmJs { 40 | moduleName = "mpfilepicker" 41 | browser() 42 | binaries.executable() 43 | } 44 | 45 | macosX64() 46 | 47 | listOf( 48 | iosX64(), 49 | iosArm64(), 50 | iosSimulatorArm64(), 51 | ).forEach { 52 | it.binaries.framework { 53 | baseName = "MPFilePicker" 54 | } 55 | } 56 | 57 | sourceSets { 58 | commonMain.dependencies { 59 | api(compose.runtime) 60 | api(compose.foundation) 61 | } 62 | 63 | commonTest.dependencies { 64 | implementation(kotlin("test")) 65 | } 66 | 67 | androidMain.dependencies { 68 | api(compose.uiTooling) 69 | api(compose.preview) 70 | api(compose.material) 71 | api(libs.androidx.appcompat) 72 | api(libs.androidx.core.ktx) 73 | api(libs.compose.activity) 74 | api(libs.kotlinx.coroutines.android) 75 | } 76 | 77 | jvmMain.dependencies { 78 | api(compose.uiTooling) 79 | api(compose.preview) 80 | api(compose.material) 81 | 82 | val lwjglVersion = "3.3.1" 83 | listOf("lwjgl", "lwjgl-tinyfd").forEach { lwjglDep -> 84 | implementation("org.lwjgl:${lwjglDep}:${lwjglVersion}") 85 | listOf( 86 | "natives-windows", 87 | "natives-windows-x86", 88 | "natives-windows-arm64", 89 | "natives-macos", 90 | "natives-macos-arm64", 91 | "natives-linux", 92 | "natives-linux-arm64", 93 | "natives-linux-arm32" 94 | ).forEach { native -> 95 | runtimeOnly("org.lwjgl:${lwjglDep}:${lwjglVersion}:${native}") 96 | } 97 | } 98 | } 99 | val jvmTest by getting 100 | val jsMain by getting 101 | val wasmJsMain by getting 102 | } 103 | 104 | @Suppress("OPT_IN_USAGE") 105 | compilerOptions { 106 | freeCompilerArgs = listOf("-Xexpect-actual-classes") 107 | } 108 | 109 | val javadocJar by tasks.registering(Jar::class) { 110 | archiveClassifier.set("javadoc") 111 | } 112 | 113 | publishing { 114 | repositories { 115 | maven { 116 | val releaseRepo = 117 | URI("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/") 118 | val snapshotRepo = 119 | URI("https://s01.oss.sonatype.org/content/repositories/snapshots/") 120 | url = if (extra["isReleaseVersion"] == true) releaseRepo else snapshotRepo 121 | credentials { 122 | username = System.getenv("OSSRH_USERNAME") ?: "Unknown user" 123 | password = System.getenv("OSSRH_PASSWORD") ?: "Unknown password" 124 | } 125 | } 126 | } 127 | publications { 128 | publications.withType { 129 | artifact(javadocJar.get()) 130 | 131 | pom { 132 | name.set(readableName) 133 | description.set(project.description) 134 | inceptionYear.set("2023") 135 | url.set(repoUrl) 136 | developers { 137 | developer { 138 | name.set("Adam Brown") 139 | id.set("Wavesonics") 140 | } 141 | } 142 | licenses { 143 | license { 144 | name.set("MIT") 145 | url.set("https://opensource.org/licenses/MIT") 146 | } 147 | } 148 | scm { 149 | connection.set("scm:git:git://github.com/Wavesonics/compose-multiplatform-file-picker.git") 150 | developerConnection.set("scm:git:ssh://git@github.com/Wavesonics/compose-multiplatform-file-picker.git") 151 | url.set("https://github.com/Wavesonics/compose-multiplatform-file-picker") 152 | } 153 | } 154 | } 155 | } 156 | } 157 | } 158 | 159 | tasks.withType().configureEach { 160 | val signingTasks = tasks.withType() 161 | mustRunAfter(signingTasks) 162 | } 163 | 164 | signing { 165 | val signingKey: String? = System.getenv("SIGNING_KEY") 166 | val signingPassword: String? = System.getenv("SIGNING_PASSWORD") 167 | if (signingKey != null && signingPassword != null) { 168 | useInMemoryPgpKeys(null, signingKey, signingPassword) 169 | sign(publishing.publications) 170 | } else { 171 | println("No signing credentials provided. Skipping Signing.") 172 | } 173 | } 174 | 175 | android { 176 | namespace = "com.darkrockstudios.libraries.mpfilepicker" 177 | compileSdk = libs.versions.android.compile.sdk.get().toInt() 178 | sourceSets["main"].manifest.srcFile("src/androidMain/AndroidManifest.xml") 179 | defaultConfig { 180 | minSdk = libs.versions.android.min.sdk.get().toInt() 181 | } 182 | compileOptions { 183 | sourceCompatibility = JavaVersion.VERSION_17 184 | targetCompatibility = JavaVersion.VERSION_17 185 | } 186 | } 187 | 188 | compose.experimental { 189 | web.application {} 190 | } 191 | -------------------------------------------------------------------------------- /mpfilepicker/src/androidMain/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /mpfilepicker/src/androidMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.android.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import android.net.Uri 4 | import android.webkit.MimeTypeMap 5 | import androidx.activity.compose.rememberLauncherForActivityResult 6 | import androidx.activity.result.contract.ActivityResultContracts 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.runtime.LaunchedEffect 9 | 10 | public actual data class PlatformFile( 11 | val uri: Uri, 12 | ) 13 | 14 | @Composable 15 | public actual fun FilePicker( 16 | show: Boolean, 17 | initialDirectory: String?, 18 | fileExtensions: List, 19 | title: String?, 20 | onFileSelected: FileSelected 21 | ) { 22 | val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocument()) { result -> 23 | if (result != null) { 24 | val platformFile = PlatformFile(result) 25 | onFileSelected(platformFile) 26 | } else { 27 | onFileSelected(null) 28 | } 29 | } 30 | 31 | val mimeTypeMap = MimeTypeMap.getSingleton() 32 | val mimeTypes = if (fileExtensions.isNotEmpty()) { 33 | fileExtensions.mapNotNull { ext -> 34 | mimeTypeMap.getMimeTypeFromExtension(ext) 35 | }.toTypedArray() 36 | } else { 37 | emptyArray() 38 | } 39 | 40 | LaunchedEffect(show) { 41 | if (show) { 42 | launcher.launch(mimeTypes) 43 | } 44 | } 45 | } 46 | 47 | @Composable 48 | public actual fun MultipleFilePicker( 49 | show: Boolean, 50 | initialDirectory: String?, 51 | fileExtensions: List, 52 | title: String?, 53 | onFileSelected: FilesSelected 54 | ) { 55 | val launcher = rememberLauncherForActivityResult( 56 | contract = ActivityResultContracts.OpenMultipleDocuments() 57 | ) { result -> 58 | val files = result.map { uri -> 59 | PlatformFile(uri) 60 | } 61 | 62 | if (files.isNotEmpty()) { 63 | onFileSelected(files) 64 | } else { 65 | onFileSelected(null) 66 | } 67 | } 68 | 69 | val mimeTypeMap = MimeTypeMap.getSingleton() 70 | val mimeTypes = if (fileExtensions.isNotEmpty()) { 71 | fileExtensions.mapNotNull { ext -> 72 | mimeTypeMap.getMimeTypeFromExtension(ext) 73 | }.toTypedArray() 74 | } else { 75 | emptyArray() 76 | } 77 | 78 | LaunchedEffect(show) { 79 | if (show) { 80 | launcher.launch(mimeTypes) 81 | } 82 | } 83 | } 84 | 85 | @Composable 86 | public actual fun DirectoryPicker( 87 | show: Boolean, 88 | initialDirectory: String?, 89 | title: String?, 90 | onFileSelected: (String?) -> Unit 91 | ) { 92 | val launcher = rememberLauncherForActivityResult(contract = ActivityResultContracts.OpenDocumentTree()) { result -> 93 | onFileSelected(result?.toString()) 94 | } 95 | 96 | LaunchedEffect(show) { 97 | if (show) { 98 | launcher.launch(null) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /mpfilepicker/src/commonMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | 5 | public expect class PlatformFile 6 | 7 | public typealias FileSelected = (PlatformFile?) -> Unit 8 | 9 | public typealias FilesSelected = (List?) -> Unit 10 | 11 | @Composable 12 | public expect fun FilePicker( 13 | show: Boolean, 14 | initialDirectory: String? = null, 15 | fileExtensions: List = emptyList(), 16 | title: String? = null, 17 | onFileSelected: FileSelected, 18 | ) 19 | 20 | @Composable 21 | public expect fun MultipleFilePicker( 22 | show: Boolean, 23 | initialDirectory: String? = null, 24 | fileExtensions: List = emptyList(), 25 | title: String? = null, 26 | onFileSelected: FilesSelected 27 | ) 28 | 29 | @Composable 30 | public expect fun DirectoryPicker( 31 | show: Boolean, 32 | initialDirectory: String? = null, 33 | title: String? = null, 34 | onFileSelected: (String?) -> Unit, 35 | ) 36 | -------------------------------------------------------------------------------- /mpfilepicker/src/iosMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.ios.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import androidx.compose.runtime.remember 6 | import kotlinx.cinterop.ExperimentalForeignApi 7 | import kotlinx.cinterop.addressOf 8 | import kotlinx.cinterop.usePinned 9 | import platform.Foundation.NSData 10 | import platform.Foundation.NSURL 11 | import platform.posix.memcpy 12 | 13 | public actual data class PlatformFile( 14 | val nsUrl: NSURL, 15 | ) { 16 | public val bytes: ByteArray = 17 | nsUrl.dataRepresentation.toByteArray() 18 | 19 | @OptIn(ExperimentalForeignApi::class) 20 | private fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length.toInt()).apply { 21 | usePinned { 22 | memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) 23 | } 24 | } 25 | } 26 | 27 | @Composable 28 | public actual fun FilePicker( 29 | show: Boolean, 30 | initialDirectory: String?, 31 | fileExtensions: List, 32 | title: String?, 33 | onFileSelected: FileSelected, 34 | ) { 35 | val launcher = remember { 36 | FilePickerLauncher( 37 | initialDirectory = initialDirectory, 38 | pickerMode = FilePickerLauncher.Mode.File(fileExtensions), 39 | onFileSelected = { 40 | onFileSelected(it?.firstOrNull()) 41 | }, 42 | ) 43 | } 44 | 45 | LaunchedEffect(show) { 46 | if (show) { 47 | launcher.launchFilePicker() 48 | } 49 | } 50 | } 51 | 52 | @Composable 53 | public actual fun MultipleFilePicker( 54 | show: Boolean, 55 | initialDirectory: String?, 56 | fileExtensions: List, 57 | title: String?, 58 | onFileSelected: FilesSelected 59 | ) { 60 | val launcher = remember { 61 | FilePickerLauncher( 62 | initialDirectory = initialDirectory, 63 | pickerMode = FilePickerLauncher.Mode.MultipleFiles(fileExtensions), 64 | onFileSelected = onFileSelected, 65 | ) 66 | } 67 | 68 | LaunchedEffect(show) { 69 | if (show) { 70 | launcher.launchFilePicker() 71 | } 72 | } 73 | } 74 | 75 | @Composable 76 | public actual fun DirectoryPicker( 77 | show: Boolean, 78 | initialDirectory: String?, 79 | title: String?, 80 | onFileSelected: (String?) -> Unit, 81 | ) { 82 | val launcher = remember { 83 | FilePickerLauncher( 84 | initialDirectory = initialDirectory, 85 | pickerMode = FilePickerLauncher.Mode.Directory, 86 | onFileSelected = { onFileSelected(it?.firstOrNull()?.nsUrl?.path) }, 87 | ) 88 | } 89 | 90 | LaunchedEffect(show) { 91 | if (show) { 92 | launcher.launchFilePicker() 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /mpfilepicker/src/iosMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/IosFilePickerLauncher.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import com.darkrockstudios.libraries.mpfilepicker.FilePickerLauncher.Mode 4 | import com.darkrockstudios.libraries.mpfilepicker.FilePickerLauncher.Mode.Directory 5 | import com.darkrockstudios.libraries.mpfilepicker.FilePickerLauncher.Mode.File 6 | import com.darkrockstudios.libraries.mpfilepicker.FilePickerLauncher.Mode.MultipleFiles 7 | import platform.Foundation.NSURL 8 | import platform.UIKit.UIAdaptivePresentationControllerDelegateProtocol 9 | import platform.UIKit.UIApplication 10 | import platform.UIKit.UIDocumentPickerDelegateProtocol 11 | import platform.UIKit.UIDocumentPickerMode 12 | import platform.UIKit.UIDocumentPickerViewController 13 | import platform.UIKit.UIPresentationController 14 | import platform.UniformTypeIdentifiers.UTType 15 | import platform.UniformTypeIdentifiers.UTTypeContent 16 | import platform.UniformTypeIdentifiers.UTTypeFolder 17 | import platform.darwin.NSObject 18 | import kotlin.coroutines.resume 19 | import kotlin.coroutines.resumeWithException 20 | import kotlin.coroutines.suspendCoroutine 21 | import kotlin.native.concurrent.ThreadLocal 22 | 23 | /** 24 | * Wraps platform specific implementation for launching a 25 | * File Picker. 26 | * 27 | * @param initialDirectory Initial directory that the 28 | * file picker should open to. 29 | * @param pickerMode [Mode] to open the picker with. 30 | * 31 | */ 32 | public class FilePickerLauncher( 33 | private val initialDirectory: String?, 34 | private val pickerMode: Mode, 35 | private val onFileSelected: FilesSelected, 36 | ) { 37 | 38 | @ThreadLocal 39 | public companion object { 40 | /** 41 | * For use only with launching plain (no compose dependencies) 42 | * file picker. When a function completes iOS deallocates 43 | * unreferenced objects created within it, so we need to 44 | * keep a reference of the active launcher. 45 | */ 46 | internal var activeLauncher: FilePickerLauncher? = null 47 | } 48 | 49 | /** 50 | * Identifies the kind of file picker to open. Either 51 | * [Directory] or [File]. 52 | */ 53 | public sealed interface Mode { 54 | /** 55 | * Use this mode to open a [FilePickerLauncher] for selecting 56 | * folders/directories. 57 | */ 58 | public data object Directory : Mode 59 | 60 | /** 61 | * Use this mode to open a [FilePickerLauncher] for selecting 62 | * multiple files. 63 | * 64 | * @param extensions List of file extensions that can be 65 | * selected on this file picker. 66 | */ 67 | public data class MultipleFiles(val extensions: List) : Mode 68 | 69 | /** 70 | * Use this mode to open a [FilePickerLauncher] for selecting 71 | * a single file. 72 | * 73 | * @param extensions List of file extensions that can be 74 | * selected on this file picker. 75 | */ 76 | public data class File(val extensions: List) : Mode 77 | } 78 | 79 | private val pickerDelegate = object : NSObject(), 80 | UIDocumentPickerDelegateProtocol, 81 | UIAdaptivePresentationControllerDelegateProtocol { 82 | 83 | override fun documentPicker( 84 | controller: UIDocumentPickerViewController, didPickDocumentsAtURLs: List<*> 85 | ) { 86 | 87 | (didPickDocumentsAtURLs as? List<*>)?.let { list -> 88 | val files = list.map { file -> 89 | (file as? NSURL)?.let { nsUrl -> 90 | PlatformFile(nsUrl) 91 | } ?: return@let listOf() 92 | } 93 | 94 | onFileSelected(files) 95 | } 96 | } 97 | 98 | override fun documentPickerWasCancelled( 99 | controller: UIDocumentPickerViewController 100 | ) { 101 | onFileSelected(null) 102 | } 103 | 104 | override fun presentationControllerWillDismiss( 105 | presentationController: UIPresentationController 106 | ) { 107 | (presentationController.presentedViewController as? UIDocumentPickerViewController) 108 | ?.let { documentPickerWasCancelled(it) } 109 | } 110 | } 111 | 112 | private val contentTypes: List 113 | get() = when (pickerMode) { 114 | is Directory -> listOf(UTTypeFolder) 115 | is File -> pickerMode.extensions 116 | .mapNotNull { UTType.typeWithFilenameExtension(it) } 117 | .ifEmpty { listOf(UTTypeContent) } 118 | is MultipleFiles -> pickerMode.extensions 119 | .mapNotNull { UTType.typeWithFilenameExtension(it) } 120 | .ifEmpty { listOf(UTTypeContent) } 121 | } 122 | 123 | private fun createPicker() = UIDocumentPickerViewController( 124 | forOpeningContentTypes = contentTypes 125 | ).apply { 126 | delegate = pickerDelegate 127 | initialDirectory?.let { directoryURL = NSURL(string = it) } 128 | } 129 | 130 | 131 | public fun launchFilePicker() { 132 | activeLauncher = this 133 | val picker = createPicker() 134 | UIApplication.sharedApplication.keyWindow?.rootViewController?.presentViewController( 135 | // Reusing a closed/dismissed picker causes problems with 136 | // triggering delegate functions, launch with a new one. 137 | picker, 138 | animated = true, 139 | completion = { 140 | (picker as? UIDocumentPickerViewController) 141 | ?.allowsMultipleSelection = pickerMode is MultipleFiles 142 | }, 143 | ) 144 | } 145 | } 146 | 147 | public suspend fun launchFilePicker( 148 | initialDirectory: String? = null, 149 | fileExtensions: List, 150 | allowMultiple: Boolean? = false, 151 | ): List = suspendCoroutine { cont -> 152 | try { 153 | FilePickerLauncher( 154 | initialDirectory = initialDirectory, 155 | pickerMode = if (allowMultiple == true) MultipleFiles(fileExtensions) else File(fileExtensions), 156 | onFileSelected = { selected -> 157 | // File selection has ended, no launcher is active anymore 158 | // dereference it 159 | FilePickerLauncher.activeLauncher = null 160 | cont.resume(selected.orEmpty()) 161 | } 162 | ).also { launcher -> 163 | // We're showing the file picker at this time so we set 164 | // the activeLauncher here. This might be the last time 165 | // there's an outside reference to the file picker. 166 | FilePickerLauncher.activeLauncher = launcher 167 | launcher.launchFilePicker() 168 | } 169 | } catch (e: Throwable) { 170 | // don't swallow errors 171 | cont.resumeWithException(e) 172 | } 173 | } 174 | 175 | public suspend fun launchDirectoryPicker( 176 | initialDirectory: String? = null, 177 | ): List = suspendCoroutine { cont -> 178 | try { 179 | FilePickerLauncher( 180 | initialDirectory = initialDirectory, 181 | pickerMode = Directory, 182 | onFileSelected = { selected -> 183 | // File selection has ended, no launcher is active anymore 184 | // dereference it 185 | FilePickerLauncher.activeLauncher = null 186 | cont.resume(selected.orEmpty()) 187 | }, 188 | ).also { launcher -> 189 | // We're showing the file picker at this time so we set 190 | // the activeLauncher here. This might be the last time 191 | // there's an outside reference to the file picker. 192 | FilePickerLauncher.activeLauncher = launcher 193 | launcher.launchFilePicker() 194 | } 195 | } catch (e: Throwable) { 196 | cont.resumeWithException(e) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /mpfilepicker/src/jsMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.js.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import kotlinx.browser.document 6 | import org.khronos.webgl.ArrayBuffer 7 | import org.khronos.webgl.Uint8Array 8 | import org.khronos.webgl.get 9 | import org.w3c.dom.Document 10 | import org.w3c.dom.HTMLInputElement 11 | import org.w3c.dom.ItemArrayLike 12 | import org.w3c.dom.asList 13 | import org.w3c.files.File 14 | import org.w3c.files.FileReader 15 | import kotlin.coroutines.resume 16 | import kotlin.coroutines.suspendCoroutine 17 | 18 | public actual data class PlatformFile( 19 | val file: File, 20 | ) 21 | 22 | @Composable 23 | public actual fun FilePicker( 24 | show: Boolean, 25 | initialDirectory: String?, 26 | fileExtensions: List, 27 | title: String?, 28 | onFileSelected: FileSelected, 29 | ) { 30 | LaunchedEffect(show) { 31 | if (show) { 32 | val fixedExtensions = fileExtensions.map { ".$it" } 33 | val file: List = document.selectFilesFromDisk(fixedExtensions.joinToString(","), false) 34 | val platformFile = PlatformFile(file.first()) 35 | onFileSelected(platformFile) 36 | } 37 | } 38 | } 39 | 40 | @Composable 41 | public actual fun MultipleFilePicker( 42 | show: Boolean, 43 | initialDirectory: String?, 44 | fileExtensions: List, 45 | title: String?, 46 | onFileSelected: FilesSelected 47 | ) { 48 | LaunchedEffect(show) { 49 | if (show) { 50 | val fixedExtensions = fileExtensions.map { ".$it" } 51 | val files: List = document.selectFilesFromDisk(fixedExtensions.joinToString(","), true) 52 | val webFiles = files.map { PlatformFile(it) } 53 | onFileSelected(webFiles) 54 | } 55 | } 56 | } 57 | 58 | @Composable 59 | public actual fun DirectoryPicker( 60 | show: Boolean, 61 | initialDirectory: String?, 62 | title: String?, 63 | onFileSelected: (String?) -> Unit, 64 | ) { 65 | // in a browser we can not pick directories 66 | throw NotImplementedError("DirectoryPicker is not supported on the web") 67 | } 68 | 69 | private suspend fun Document.selectFilesFromDisk( 70 | accept: String, 71 | isMultiple: Boolean 72 | ): List = suspendCoroutine { 73 | val tempInput = (createElement("input") as HTMLInputElement).apply { 74 | type = "file" 75 | style.display = "none" 76 | this.accept = accept 77 | multiple = isMultiple 78 | } 79 | 80 | tempInput.onchange = { changeEvt -> 81 | val files = (changeEvt.target.asDynamic().files as ItemArrayLike).asList() 82 | it.resume(files) 83 | } 84 | 85 | body!!.append(tempInput) 86 | tempInput.click() 87 | tempInput.remove() 88 | } 89 | 90 | public suspend fun readFileAsText(file: File): String = suspendCoroutine { 91 | val reader = FileReader() 92 | reader.onload = { loadEvt -> 93 | val content = loadEvt.target.asDynamic().result as String 94 | it.resumeWith(Result.success(content)) 95 | } 96 | reader.readAsText(file, "UTF-8") 97 | } 98 | 99 | public suspend fun readFileAsByteArray(file: File): ByteArray = suspendCoroutine { 100 | val reader = FileReader() 101 | reader.onload = {loadEvt -> 102 | val content = loadEvt.target.asDynamic().result as ArrayBuffer 103 | val array = Uint8Array(content) 104 | val fileByteArray = ByteArray(array.length) 105 | for (i in 0 until array.length) { 106 | fileByteArray[i] = array[i] 107 | } 108 | it.resumeWith(Result.success(fileByteArray)) 109 | } 110 | reader.readAsArrayBuffer(file) 111 | } 112 | -------------------------------------------------------------------------------- /mpfilepicker/src/jvmMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FileChooser.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import org.lwjgl.system.MemoryStack 4 | import org.lwjgl.util.tinyfd.TinyFileDialogs 5 | import org.lwjgl.util.tinyfd.TinyFileDialogs.tinyfd_selectFolderDialog 6 | 7 | internal fun chooseFile( 8 | initialDirectory: String, 9 | fileExtension: String, 10 | title: String? 11 | ): String? = MemoryStack.stackPush().use { stack -> 12 | val filters = if (fileExtension.isNotEmpty()) fileExtension.split(",") else emptyList() 13 | val aFilterPatterns = stack.mallocPointer(filters.size) 14 | filters.forEach { 15 | aFilterPatterns.put(stack.UTF8("*.$it")) 16 | } 17 | aFilterPatterns.flip() 18 | TinyFileDialogs.tinyfd_openFileDialog( 19 | title, 20 | initialDirectory, 21 | aFilterPatterns, 22 | null, 23 | false 24 | ) 25 | } 26 | 27 | internal fun chooseFiles( 28 | initialDirectory: String, 29 | fileExtension: String, 30 | title: String?, 31 | ): List? = MemoryStack.stackPush().use { stack -> 32 | val filters = if (fileExtension.isNotEmpty()) fileExtension.split(",") else emptyList() 33 | val aFilterPatterns = stack.mallocPointer(filters.size) 34 | filters.forEach { 35 | aFilterPatterns.put(stack.UTF8("*.$it")) 36 | } 37 | aFilterPatterns.flip() 38 | val t = TinyFileDialogs.tinyfd_openFileDialog( 39 | /* aTitle = */ title, 40 | /* aDefaultPathAndFile = */ initialDirectory, 41 | /* aFilterPatterns = */ aFilterPatterns, 42 | /* aSingleFilterDescription = */ null, 43 | /* aAllowMultipleSelects = */ true, 44 | ) 45 | t?.split("|") 46 | } 47 | 48 | internal fun chooseDirectory( 49 | initialDirectory: String, 50 | title: String? 51 | ): String? = tinyfd_selectFolderDialog( 52 | title, 53 | initialDirectory 54 | ) 55 | -------------------------------------------------------------------------------- /mpfilepicker/src/jvmMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.desktop.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import java.io.File 6 | 7 | public actual data class PlatformFile( 8 | val file: File, 9 | ) 10 | 11 | @Composable 12 | public actual fun FilePicker( 13 | show: Boolean, 14 | initialDirectory: String?, 15 | fileExtensions: List, 16 | title: String?, 17 | onFileSelected: FileSelected, 18 | ) { 19 | LaunchedEffect(show) { 20 | if (show) { 21 | val fileFilter = if (fileExtensions.isNotEmpty()) { 22 | fileExtensions.joinToString(",") 23 | } else { 24 | "" 25 | } 26 | 27 | val initialDir = initialDirectory ?: System.getProperty("user.dir") 28 | val filePath = chooseFile( 29 | initialDirectory = initialDir, 30 | fileExtension = fileFilter, 31 | title = title 32 | ) 33 | if (filePath != null) { 34 | val file = File(filePath) 35 | val platformFile = PlatformFile(file) 36 | onFileSelected(platformFile) 37 | } else { 38 | onFileSelected(null) 39 | } 40 | 41 | } 42 | } 43 | } 44 | 45 | @Composable 46 | public actual fun MultipleFilePicker( 47 | show: Boolean, 48 | initialDirectory: String?, 49 | fileExtensions: List, 50 | title: String?, 51 | onFileSelected: FilesSelected 52 | ) { 53 | LaunchedEffect(show) { 54 | if (show) { 55 | val fileFilter = if (fileExtensions.isNotEmpty()) { 56 | fileExtensions.joinToString(",") 57 | } else { 58 | "" 59 | } 60 | 61 | val initialDir = initialDirectory ?: System.getProperty("user.dir") 62 | val filePaths = chooseFiles( 63 | initialDirectory = initialDir, 64 | fileExtension = fileFilter, 65 | title = title 66 | ) 67 | if (filePaths != null) { 68 | onFileSelected(filePaths.map { PlatformFile(File(it)) }) 69 | } else { 70 | onFileSelected(null) 71 | } 72 | 73 | } 74 | } 75 | } 76 | 77 | @Composable 78 | public actual fun DirectoryPicker( 79 | show: Boolean, 80 | initialDirectory: String?, 81 | title: String?, 82 | onFileSelected: (String?) -> Unit, 83 | ) { 84 | LaunchedEffect(show) { 85 | if (show) { 86 | val initialDir = initialDirectory ?: System.getProperty("user.dir") 87 | val fileChosen = chooseDirectory(initialDir, title) 88 | onFileSelected(fileChosen) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /mpfilepicker/src/macosX64Main/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.macos.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import kotlinx.cinterop.ExperimentalForeignApi 6 | import kotlinx.cinterop.addressOf 7 | import kotlinx.cinterop.usePinned 8 | import platform.AppKit.NSOpenPanel 9 | import platform.AppKit.setAllowedFileTypes 10 | import platform.Foundation.NSData 11 | import platform.Foundation.NSURL 12 | import platform.posix.memcpy 13 | 14 | public actual data class PlatformFile( 15 | val nsUrl: NSURL, 16 | ) { 17 | public val bytes: ByteArray = 18 | nsUrl.dataRepresentation.toByteArray() 19 | 20 | @OptIn(ExperimentalForeignApi::class) 21 | private fun NSData.toByteArray(): ByteArray = ByteArray(this@toByteArray.length.toInt()).apply { 22 | usePinned { 23 | memcpy(it.addressOf(0), this@toByteArray.bytes, this@toByteArray.length) 24 | } 25 | } 26 | } 27 | 28 | @Composable 29 | public actual fun FilePicker( 30 | show: Boolean, 31 | initialDirectory: String?, 32 | fileExtensions: List, 33 | title: String?, 34 | onFileSelected: FileSelected, 35 | ) { 36 | LaunchedEffect(show) { 37 | if (show) { 38 | with(NSOpenPanel()) { 39 | if (initialDirectory != null) directoryURL = 40 | NSURL.fileURLWithPath(initialDirectory, true) 41 | allowsMultipleSelection = false 42 | setAllowedFileTypes(fileExtensions) 43 | allowsOtherFileTypes = true 44 | canChooseDirectories = false 45 | canChooseFiles = true 46 | if (title != null) message = title 47 | 48 | runModal() 49 | 50 | val fileURL = URL 51 | val filePath = fileURL?.path 52 | if (filePath != null) { 53 | val platformFile = PlatformFile(fileURL) 54 | onFileSelected(platformFile) 55 | } else { 56 | onFileSelected(null) 57 | } 58 | } 59 | } 60 | } 61 | } 62 | 63 | @Composable 64 | public actual fun MultipleFilePicker( 65 | show: Boolean, 66 | initialDirectory: String?, 67 | fileExtensions: List, 68 | title: String?, 69 | onFileSelected: FilesSelected, 70 | ) { 71 | LaunchedEffect(show) { 72 | if (show) { 73 | with(NSOpenPanel()) { 74 | if (initialDirectory != null) directoryURL = NSURL.fileURLWithPath(initialDirectory, true) 75 | allowsMultipleSelection = true 76 | setAllowedFileTypes(fileExtensions) 77 | allowsOtherFileTypes = true 78 | canChooseDirectories = false 79 | canChooseFiles = true 80 | if (title != null) message = title 81 | runModal() 82 | 83 | val filesUrls = URLs 84 | 85 | val files: List = filesUrls.mapNotNull { file -> 86 | (file as? NSURL)?.let { nsUrl -> PlatformFile(nsUrl) } 87 | } 88 | 89 | if (files.isEmpty()) onFileSelected(null) 90 | else onFileSelected(files) 91 | } 92 | } 93 | } 94 | } 95 | 96 | @Composable 97 | public actual fun DirectoryPicker( 98 | show: Boolean, 99 | initialDirectory: String?, 100 | title: String?, 101 | onFileSelected: (String?) -> Unit 102 | ) { 103 | LaunchedEffect(show) { 104 | if (show) { 105 | with(NSOpenPanel()) { 106 | if (initialDirectory != null) directoryURL = NSURL.fileURLWithPath(initialDirectory, true) 107 | allowsMultipleSelection = false 108 | canChooseDirectories = true 109 | canChooseFiles = false 110 | canCreateDirectories = true 111 | if (title != null) message = title 112 | runModal() 113 | 114 | val fileURL = URL 115 | val filePath = fileURL?.path 116 | onFileSelected(filePath) 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /mpfilepicker/src/wasmJsMain/kotlin/com/darkrockstudios/libraries/mpfilepicker/FilePicker.wasm.kt: -------------------------------------------------------------------------------- 1 | package com.darkrockstudios.libraries.mpfilepicker 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.LaunchedEffect 5 | import kotlinx.browser.document 6 | import org.khronos.webgl.ArrayBuffer 7 | import org.khronos.webgl.Uint8Array 8 | import org.khronos.webgl.get 9 | import org.w3c.dom.Document 10 | import org.w3c.dom.HTMLInputElement 11 | import org.w3c.dom.asList 12 | import org.w3c.files.File 13 | import org.w3c.files.FileReader 14 | import kotlin.coroutines.* 15 | 16 | public actual data class PlatformFile( 17 | val file: File, 18 | ) 19 | 20 | @Composable 21 | public actual fun FilePicker( 22 | show: Boolean, 23 | initialDirectory: String?, 24 | fileExtensions: List, 25 | title: String?, 26 | onFileSelected: FileSelected, 27 | ) { 28 | LaunchedEffect(show) { 29 | if (show) { 30 | val fixedExtensions = fileExtensions.map { ".$it" } 31 | val file: List = document.selectFilesFromDisk(fixedExtensions.joinToString(","), false) 32 | val platformFile = PlatformFile(file.first()) 33 | onFileSelected(platformFile) 34 | } 35 | } 36 | } 37 | 38 | @Composable 39 | public actual fun MultipleFilePicker( 40 | show: Boolean, 41 | initialDirectory: String?, 42 | fileExtensions: List, 43 | title: String?, 44 | onFileSelected: FilesSelected 45 | ) { 46 | LaunchedEffect(show) { 47 | if (show) { 48 | val fixedExtensions = fileExtensions.map { ".$it" } 49 | val files: List = document.selectFilesFromDisk(fixedExtensions.joinToString(","), true) 50 | val webFiles = files.map { PlatformFile(it) } 51 | onFileSelected(webFiles) 52 | } 53 | } 54 | } 55 | 56 | @Composable 57 | public actual fun DirectoryPicker( 58 | show: Boolean, 59 | initialDirectory: String?, 60 | title: String?, 61 | onFileSelected: (String?) -> Unit, 62 | ) { 63 | // in a browser we can not pick directories 64 | throw NotImplementedError("DirectoryPicker is not supported on the web") 65 | } 66 | 67 | private suspend fun Document.selectFilesFromDisk( 68 | accept: String, 69 | isMultiple: Boolean 70 | ): List = suspendCoroutine { 71 | val tempInput = (createElement("input") as HTMLInputElement).apply { 72 | type = "file" 73 | style.display = "none" 74 | this.accept = accept 75 | multiple = isMultiple 76 | } 77 | 78 | tempInput.onchange = { changeEvt -> 79 | try { 80 | val inputElement = changeEvt.target as HTMLInputElement 81 | val files = inputElement.files?.asList() ?: emptyList() 82 | it.resume(files) 83 | } catch (e: Throwable) { 84 | it.resumeWithException(e) 85 | } 86 | } 87 | 88 | body!!.append(tempInput) 89 | tempInput.click() 90 | tempInput.remove() 91 | } 92 | 93 | public suspend fun readFileAsText(file: File): String = suspendCoroutine { 94 | val reader = FileReader() 95 | reader.onload = { loadEvt -> 96 | try { 97 | val eventFileReader = loadEvt.target?.let { it as FileReader } 98 | val content = eventFileReader!!.result?.unsafeCast()!! 99 | it.resume(content.toString()) 100 | } catch (e: Throwable) { 101 | it.resumeWithException(e) 102 | } 103 | } 104 | reader.readAsText(file, "UTF-8") 105 | } 106 | 107 | public suspend fun readFileAsByteArray(file: File): ByteArray = suspendCoroutine { 108 | val reader = FileReader() 109 | reader.onload = {loadEvt -> 110 | try { 111 | val eventFileReader = loadEvt.target?.let { it as FileReader }!! 112 | val content = eventFileReader.result as ArrayBuffer 113 | val array = Uint8Array(content) 114 | 115 | val fileByteArray = ByteArray(array.length) 116 | for (i in 0 until array.length) { 117 | fileByteArray[i] = array[i] 118 | } 119 | it.resumeWith(Result.success(fileByteArray)) 120 | } catch (e: Throwable) { 121 | it.resumeWithException(e) 122 | } 123 | } 124 | reader.readAsArrayBuffer(file) 125 | } 126 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /screenshot-android.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/screenshot-android.png -------------------------------------------------------------------------------- /screenshot-desktop-windows.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Wavesonics/compose-multiplatform-file-picker/e6664ee69c03d0adb8eba04c1c67fe7718469adb/screenshot-desktop-windows.jpg -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 4 | google() 5 | gradlePluginPortal() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | dependencyResolutionManagement { 11 | repositories { 12 | google() 13 | mavenCentral() 14 | maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") 15 | mavenLocal() 16 | } 17 | } 18 | 19 | rootProject.name = "MultiplatformFilePicker" 20 | 21 | include(":mpfilepicker") 22 | include(":examples:android") 23 | include(":examples:jvm") 24 | include(":examples:web-wasm") 25 | include(":examples:web") 26 | include(":examples:macosX64") 27 | include(":examples:ios") 28 | --------------------------------------------------------------------------------