├── .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 |
22 |
--------------------------------------------------------------------------------
/.run/Android Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/.run/Jvm Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.run/MacOS Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
11 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.run/WASM-Web Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 | false
22 |
23 |
24 |
--------------------------------------------------------------------------------
/.run/Web Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | true
19 | true
20 | false
21 |
22 |
23 |
--------------------------------------------------------------------------------
/.run/iOS Example.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
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 |  
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 | 
72 |
73 | ## Android
74 |
75 | 
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 |
--------------------------------------------------------------------------------