Skip to content

Commit 93ff28a

Browse files
mikoto2000lucasfernog
authored andcommitted
feat(dialog): Implemented android save dialog. (tauri-apps#1657)
* Implemented android save dialog. * small cleanup * lint --------- Co-authored-by: Lucas Nogueira <[email protected]>
1 parent d4f1d99 commit 93ff28a

File tree

9 files changed

+112
-12
lines changed

9 files changed

+112
-12
lines changed

.changes/android-dialog-save.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"dialog": patch:feat
3+
---
4+
5+
Implement `save` API on Android.

examples/api/src-tauri/gen/android/.idea/gradle.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/api/src-tauri/gen/android/.idea/misc.xml

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

plugins/dialog/android/src/main/java/DialogPlugin.kt

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,11 @@ class MessageOptions {
4141
var cancelButtonLabel: String? = null
4242
}
4343

44+
@InvokeArg
45+
class SaveFileDialogOptions {
46+
var fileName: String? = null
47+
}
48+
4449
@TauriPlugin
4550
class DialogPlugin(private val activity: Activity): Plugin(activity) {
4651
var filePickerOptions: FilePickerOptions? = null
@@ -204,4 +209,46 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
204209
dialog.show()
205210
}
206211
}
212+
213+
@Command
214+
fun saveFileDialog(invoke: Invoke) {
215+
try {
216+
val args = invoke.parseArgs(SaveFileDialogOptions::class.java)
217+
218+
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
219+
intent.addCategory(Intent.CATEGORY_OPENABLE)
220+
intent.setType("text/plain")
221+
intent.putExtra(Intent.EXTRA_TITLE, args.fileName ?: "")
222+
startActivityForResult(invoke, intent, "saveFileDialogResult")
223+
} catch (ex: Exception) {
224+
val message = ex.message ?: "Failed to pick save file"
225+
Logger.error(message)
226+
invoke.reject(message)
227+
}
228+
}
229+
230+
@ActivityCallback
231+
fun saveFileDialogResult(invoke: Invoke, result: ActivityResult) {
232+
try {
233+
when (result.resultCode) {
234+
Activity.RESULT_OK -> {
235+
val callResult = JSObject()
236+
val intent: Intent? = result.data
237+
if (intent != null) {
238+
val uri = intent.data
239+
if (uri != null) {
240+
callResult.put("file", uri.toString())
241+
}
242+
}
243+
invoke.resolve(callResult)
244+
}
245+
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
246+
else -> invoke.reject("Failed to pick files")
247+
}
248+
} catch (ex: java.lang.Exception) {
249+
val message = ex.message ?: "Failed to read file pick result"
250+
Logger.error(message)
251+
invoke.reject(message)
252+
}
253+
}
207254
}

plugins/dialog/guest-js/index.ts

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,18 @@ interface DialogFilter {
4040
* @since 2.0.0
4141
*/
4242
interface OpenDialogOptions {
43-
/** The title of the dialog window. */
43+
/** The title of the dialog window (desktop only). */
4444
title?: string;
4545
/** The filters of the dialog. */
4646
filters?: DialogFilter[];
47-
/** Initial directory or file path. */
47+
/**
48+
* Initial directory or file path.
49+
* If it's a directory path, the dialog interface will change to that folder.
50+
* If it's not an existing directory, the file name will be set to the dialog's file name input and the dialog will be set to the parent folder.
51+
*
52+
* On mobile the file name is always used on the dialog's file name input.
53+
* If not provided, Android uses `(invalid).txt` as default file name.
54+
*/
4855
defaultPath?: string;
4956
/** Whether the dialog allows multiple selection or not. */
5057
multiple?: boolean;
@@ -65,14 +72,17 @@ interface OpenDialogOptions {
6572
* @since 2.0.0
6673
*/
6774
interface SaveDialogOptions {
68-
/** The title of the dialog window. */
75+
/** The title of the dialog window (desktop only). */
6976
title?: string;
7077
/** The filters of the dialog. */
7178
filters?: DialogFilter[];
7279
/**
7380
* Initial directory or file path.
7481
* If it's a directory path, the dialog interface will change to that folder.
7582
* If it's not an existing directory, the file name will be set to the dialog's file name input and the dialog will be set to the parent folder.
83+
*
84+
* On mobile the file name is always used on the dialog's file name input.
85+
* If not provided, Android uses `(invalid).txt` as default file name.
7686
*/
7787
defaultPath?: string;
7888
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */

plugins/dialog/src/commands.rs

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,18 @@ pub struct SaveDialogOptions {
7171
can_create_directories: Option<bool>,
7272
}
7373

74+
#[cfg(mobile)]
75+
fn set_default_path<R: Runtime>(
76+
mut dialog_builder: FileDialogBuilder<R>,
77+
default_path: PathBuf,
78+
) -> FileDialogBuilder<R> {
79+
if let Some(file_name) = default_path.file_name() {
80+
dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy());
81+
}
82+
dialog_builder
83+
}
84+
85+
#[cfg(desktop)]
7486
fn set_default_path<R: Runtime>(
7587
mut dialog_builder: FileDialogBuilder<R>,
7688
default_path: PathBuf,
@@ -193,9 +205,9 @@ pub(crate) async fn save<R: Runtime>(
193205
dialog: State<'_, Dialog<R>>,
194206
options: SaveDialogOptions,
195207
) -> Result<Option<PathBuf>> {
196-
#[cfg(mobile)]
208+
#[cfg(target_os = "ios")]
197209
return Err(crate::Error::FileSaveDialogNotImplemented);
198-
#[cfg(desktop)]
210+
#[cfg(any(desktop, target_os = "android"))]
199211
{
200212
let mut dialog_builder = dialog.file();
201213
#[cfg(any(windows, target_os = "macos"))]

plugins/dialog/src/error.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@ pub enum Error {
1818
#[cfg(mobile)]
1919
#[error("Folder picker is not implemented on mobile")]
2020
FolderPickerNotImplemented,
21-
#[cfg(mobile)]
22-
#[error("File save dialog is not implemented on mobile")]
21+
#[cfg(target_os = "ios")]
22+
#[error("File save dialog is not implemented on iOS")]
2323
FileSaveDialogNotImplemented,
2424
#[error(transparent)]
2525
Fs(#[from] tauri_plugin_fs::Error),

plugins/dialog/src/lib.rs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@ use tauri::{
1717
Manager, Runtime,
1818
};
1919

20+
#[cfg(any(desktop, target_os = "ios"))]
21+
use std::fs;
22+
2023
use std::{
21-
fs,
2224
path::{Path, PathBuf},
2325
sync::mpsc::sync_channel,
2426
};
@@ -273,6 +275,7 @@ pub struct FileDialogBuilder<R: Runtime> {
273275
#[derive(Serialize)]
274276
#[serde(rename_all = "camelCase")]
275277
pub(crate) struct FileDialogPayload<'a> {
278+
file_name: &'a Option<String>,
276279
filters: &'a Vec<Filter>,
277280
multiple: bool,
278281
}
@@ -298,6 +301,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
298301
#[cfg(mobile)]
299302
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
300303
FileDialogPayload {
304+
file_name: &self.file_name,
301305
filters: &self.filters,
302306
multiple,
303307
}
@@ -471,7 +475,6 @@ impl<R: Runtime> FileDialogBuilder<R> {
471475
/// })
472476
/// })
473477
/// ```
474-
#[cfg(desktop)]
475478
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
476479
save_file(self, f)
477480
}
@@ -572,13 +575,13 @@ impl<R: Runtime> FileDialogBuilder<R> {
572575
/// // the file path is `None` if the user closed the dialog
573576
/// }
574577
/// ```
575-
#[cfg(desktop)]
576578
pub fn blocking_save_file(self) -> Option<PathBuf> {
577579
blocking_fn!(self, save_file)
578580
}
579581
}
580582

581583
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
584+
#[cfg(desktop)]
582585
#[inline]
583586
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
584587
match maybe_time {

plugins/dialog/src/mobile.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
22
// SPDX-License-Identifier: Apache-2.0
33
// SPDX-License-Identifier: MIT
4+
use std::path::PathBuf;
45

56
use serde::{de::DeserializeOwned, Deserialize};
67
use tauri::{
@@ -49,6 +50,11 @@ struct FilePickerResponse {
4950
files: Vec<FileResponse>,
5051
}
5152

53+
#[derive(Debug, Deserialize)]
54+
struct SaveFileResponse {
55+
file: PathBuf,
56+
}
57+
5258
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
5359
dialog: FileDialogBuilder<R>,
5460
f: F,
@@ -83,6 +89,23 @@ pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'sta
8389
});
8490
}
8591

92+
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
93+
dialog: FileDialogBuilder<R>,
94+
f: F,
95+
) {
96+
std::thread::spawn(move || {
97+
let res = dialog
98+
.dialog
99+
.0
100+
.run_mobile_plugin::<SaveFileResponse>("saveFileDialog", dialog.payload(false));
101+
if let Ok(response) = res {
102+
f(Some(response.file))
103+
} else {
104+
f(None)
105+
}
106+
});
107+
}
108+
86109
#[derive(Debug, Deserialize)]
87110
struct ShowMessageDialogResponse {
88111
#[allow(dead_code)]

0 commit comments

Comments
 (0)