feat(dialog): Implemented android save dialog. (#1657)

* Implemented android save dialog.

* small cleanup

* lint

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
pull/1655/head
mikoto2000 10 months ago committed by GitHub
parent 5081f30daf
commit bc7eecf420
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"dialog": patch:feat
---
Implement `save` API on Android.

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GradleMigrationSettings" migrationVersion="1" />
<component name="GradleSettings">
<option name="linkedExternalProjectsSettings">
<GradleProjectSettings>
@ -18,7 +19,7 @@
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
<option name="modules">
<set>
<option value="$USER_HOME$/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.0-beta.22/mobile/android" />
<option value="$USER_HOME$/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.0-rc.2/mobile/android" />
<option value="$PROJECT_DIR$" />
<option value="$PROJECT_DIR$/app" />
<option value="$PROJECT_DIR$/buildSrc" />

@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">

@ -41,6 +41,11 @@ class MessageOptions {
var cancelButtonLabel: String? = null
}
@InvokeArg
class SaveFileDialogOptions {
var fileName: String? = null
}
@TauriPlugin
class DialogPlugin(private val activity: Activity): Plugin(activity) {
var filePickerOptions: FilePickerOptions? = null
@ -204,4 +209,46 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
dialog.show()
}
}
@Command
fun saveFileDialog(invoke: Invoke) {
try {
val args = invoke.parseArgs(SaveFileDialogOptions::class.java)
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
intent.addCategory(Intent.CATEGORY_OPENABLE)
intent.setType("text/plain")
intent.putExtra(Intent.EXTRA_TITLE, args.fileName ?: "")
startActivityForResult(invoke, intent, "saveFileDialogResult")
} catch (ex: Exception) {
val message = ex.message ?: "Failed to pick save file"
Logger.error(message)
invoke.reject(message)
}
}
@ActivityCallback
fun saveFileDialogResult(invoke: Invoke, result: ActivityResult) {
try {
when (result.resultCode) {
Activity.RESULT_OK -> {
val callResult = JSObject()
val intent: Intent? = result.data
if (intent != null) {
val uri = intent.data
if (uri != null) {
callResult.put("file", uri.toString())
}
}
invoke.resolve(callResult)
}
Activity.RESULT_CANCELED -> invoke.reject("File picker cancelled")
else -> invoke.reject("Failed to pick files")
}
} catch (ex: java.lang.Exception) {
val message = ex.message ?: "Failed to read file pick result"
Logger.error(message)
invoke.reject(message)
}
}
}

@ -40,11 +40,18 @@ interface DialogFilter {
* @since 2.0.0
*/
interface OpenDialogOptions {
/** The title of the dialog window. */
/** The title of the dialog window (desktop only). */
title?: string;
/** The filters of the dialog. */
filters?: DialogFilter[];
/** Initial directory or file path. */
/**
* Initial directory or file path.
* If it's a directory path, the dialog interface will change to that folder.
* 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.
*
* On mobile the file name is always used on the dialog's file name input.
* If not provided, Android uses `(invalid).txt` as default file name.
*/
defaultPath?: string;
/** Whether the dialog allows multiple selection or not. */
multiple?: boolean;
@ -65,7 +72,7 @@ interface OpenDialogOptions {
* @since 2.0.0
*/
interface SaveDialogOptions {
/** The title of the dialog window. */
/** The title of the dialog window (desktop only). */
title?: string;
/** The filters of the dialog. */
filters?: DialogFilter[];
@ -73,6 +80,9 @@ interface SaveDialogOptions {
* Initial directory or file path.
* If it's a directory path, the dialog interface will change to that folder.
* 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.
*
* On mobile the file name is always used on the dialog's file name input.
* If not provided, Android uses `(invalid).txt` as default file name.
*/
defaultPath?: string;
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */

@ -71,6 +71,18 @@ pub struct SaveDialogOptions {
can_create_directories: Option<bool>,
}
#[cfg(mobile)]
fn set_default_path<R: Runtime>(
mut dialog_builder: FileDialogBuilder<R>,
default_path: PathBuf,
) -> FileDialogBuilder<R> {
if let Some(file_name) = default_path.file_name() {
dialog_builder = dialog_builder.set_file_name(file_name.to_string_lossy());
}
dialog_builder
}
#[cfg(desktop)]
fn set_default_path<R: Runtime>(
mut dialog_builder: FileDialogBuilder<R>,
default_path: PathBuf,
@ -193,9 +205,9 @@ pub(crate) async fn save<R: Runtime>(
dialog: State<'_, Dialog<R>>,
options: SaveDialogOptions,
) -> Result<Option<PathBuf>> {
#[cfg(mobile)]
#[cfg(target_os = "ios")]
return Err(crate::Error::FileSaveDialogNotImplemented);
#[cfg(desktop)]
#[cfg(any(desktop, target_os = "android"))]
{
let mut dialog_builder = dialog.file();
#[cfg(any(windows, target_os = "macos"))]

@ -18,8 +18,8 @@ pub enum Error {
#[cfg(mobile)]
#[error("Folder picker is not implemented on mobile")]
FolderPickerNotImplemented,
#[cfg(mobile)]
#[error("File save dialog is not implemented on mobile")]
#[cfg(target_os = "ios")]
#[error("File save dialog is not implemented on iOS")]
FileSaveDialogNotImplemented,
#[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error),

@ -17,8 +17,10 @@ use tauri::{
Manager, Runtime,
};
#[cfg(any(desktop, target_os = "ios"))]
use std::fs;
use std::{
fs,
path::{Path, PathBuf},
sync::mpsc::sync_channel,
};
@ -273,6 +275,7 @@ pub struct FileDialogBuilder<R: Runtime> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct FileDialogPayload<'a> {
file_name: &'a Option<String>,
filters: &'a Vec<Filter>,
multiple: bool,
}
@ -298,6 +301,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
#[cfg(mobile)]
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
FileDialogPayload {
file_name: &self.file_name,
filters: &self.filters,
multiple,
}
@ -471,7 +475,6 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// })
/// })
/// ```
#[cfg(desktop)]
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
@ -572,13 +575,13 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_save_file(self) -> Option<PathBuf> {
blocking_fn!(self, save_file)
}
}
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
#[cfg(desktop)]
#[inline]
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
match maybe_time {

@ -1,6 +1,7 @@
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
// SPDX-License-Identifier: Apache-2.0
// SPDX-License-Identifier: MIT
use std::path::PathBuf;
use serde::{de::DeserializeOwned, Deserialize};
use tauri::{
@ -49,6 +50,11 @@ struct FilePickerResponse {
files: Vec<FileResponse>,
}
#[derive(Debug, Deserialize)]
struct SaveFileResponse {
file: PathBuf,
}
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
@ -83,6 +89,23 @@ pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'sta
});
}
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
std::thread::spawn(move || {
let res = dialog
.dialog
.0
.run_mobile_plugin::<SaveFileResponse>("saveFileDialog", dialog.payload(false));
if let Ok(response) = res {
f(Some(response.file))
} else {
f(None)
}
});
}
#[derive(Debug, Deserialize)]
struct ShowMessageDialogResponse {
#[allow(dead_code)]

Loading…
Cancel
Save