mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-07-24 17:20:51 +02:00
feat(dialog): Implemented android save dialog. (#1657)
* Implemented android save dialog. * small cleanup * lint --------- Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"dialog": patch:feat
|
||||||
|
---
|
||||||
|
|
||||||
|
Implement `save` API on Android.
|
||||||
+2
-1
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
@@ -18,7 +19,7 @@
|
|||||||
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
<option name="modules">
|
<option name="modules">
|
||||||
<set>
|
<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$" />
|
||||||
<option value="$PROJECT_DIR$/app" />
|
<option value="$PROJECT_DIR$/app" />
|
||||||
<option value="$PROJECT_DIR$/buildSrc" />
|
<option value="$PROJECT_DIR$/buildSrc" />
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
<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
|
var cancelButtonLabel: String? = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@InvokeArg
|
||||||
|
class SaveFileDialogOptions {
|
||||||
|
var fileName: String? = null
|
||||||
|
}
|
||||||
|
|
||||||
@TauriPlugin
|
@TauriPlugin
|
||||||
class DialogPlugin(private val activity: Activity): Plugin(activity) {
|
class DialogPlugin(private val activity: Activity): Plugin(activity) {
|
||||||
var filePickerOptions: FilePickerOptions? = null
|
var filePickerOptions: FilePickerOptions? = null
|
||||||
@@ -204,4 +209,46 @@ class DialogPlugin(private val activity: Activity): Plugin(activity) {
|
|||||||
dialog.show()
|
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
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
interface OpenDialogOptions {
|
interface OpenDialogOptions {
|
||||||
/** The title of the dialog window. */
|
/** The title of the dialog window (desktop only). */
|
||||||
title?: string;
|
title?: string;
|
||||||
/** The filters of the dialog. */
|
/** The filters of the dialog. */
|
||||||
filters?: DialogFilter[];
|
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;
|
defaultPath?: string;
|
||||||
/** Whether the dialog allows multiple selection or not. */
|
/** Whether the dialog allows multiple selection or not. */
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
@@ -65,7 +72,7 @@ interface OpenDialogOptions {
|
|||||||
* @since 2.0.0
|
* @since 2.0.0
|
||||||
*/
|
*/
|
||||||
interface SaveDialogOptions {
|
interface SaveDialogOptions {
|
||||||
/** The title of the dialog window. */
|
/** The title of the dialog window (desktop only). */
|
||||||
title?: string;
|
title?: string;
|
||||||
/** The filters of the dialog. */
|
/** The filters of the dialog. */
|
||||||
filters?: DialogFilter[];
|
filters?: DialogFilter[];
|
||||||
@@ -73,6 +80,9 @@ interface SaveDialogOptions {
|
|||||||
* 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 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.
|
* 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;
|
defaultPath?: string;
|
||||||
/** Whether to allow creating directories in the dialog. Enabled by default. **macOS Only** */
|
/** 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>,
|
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>(
|
fn set_default_path<R: Runtime>(
|
||||||
mut dialog_builder: FileDialogBuilder<R>,
|
mut dialog_builder: FileDialogBuilder<R>,
|
||||||
default_path: PathBuf,
|
default_path: PathBuf,
|
||||||
@@ -193,9 +205,9 @@ pub(crate) async fn save<R: Runtime>(
|
|||||||
dialog: State<'_, Dialog<R>>,
|
dialog: State<'_, Dialog<R>>,
|
||||||
options: SaveDialogOptions,
|
options: SaveDialogOptions,
|
||||||
) -> Result<Option<PathBuf>> {
|
) -> Result<Option<PathBuf>> {
|
||||||
#[cfg(mobile)]
|
#[cfg(target_os = "ios")]
|
||||||
return Err(crate::Error::FileSaveDialogNotImplemented);
|
return Err(crate::Error::FileSaveDialogNotImplemented);
|
||||||
#[cfg(desktop)]
|
#[cfg(any(desktop, target_os = "android"))]
|
||||||
{
|
{
|
||||||
let mut dialog_builder = dialog.file();
|
let mut dialog_builder = dialog.file();
|
||||||
#[cfg(any(windows, target_os = "macos"))]
|
#[cfg(any(windows, target_os = "macos"))]
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ pub enum Error {
|
|||||||
#[cfg(mobile)]
|
#[cfg(mobile)]
|
||||||
#[error("Folder picker is not implemented on mobile")]
|
#[error("Folder picker is not implemented on mobile")]
|
||||||
FolderPickerNotImplemented,
|
FolderPickerNotImplemented,
|
||||||
#[cfg(mobile)]
|
#[cfg(target_os = "ios")]
|
||||||
#[error("File save dialog is not implemented on mobile")]
|
#[error("File save dialog is not implemented on iOS")]
|
||||||
FileSaveDialogNotImplemented,
|
FileSaveDialogNotImplemented,
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Fs(#[from] tauri_plugin_fs::Error),
|
Fs(#[from] tauri_plugin_fs::Error),
|
||||||
|
|||||||
@@ -17,8 +17,10 @@ use tauri::{
|
|||||||
Manager, Runtime,
|
Manager, Runtime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(any(desktop, target_os = "ios"))]
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::mpsc::sync_channel,
|
sync::mpsc::sync_channel,
|
||||||
};
|
};
|
||||||
@@ -273,6 +275,7 @@ pub struct FileDialogBuilder<R: Runtime> {
|
|||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub(crate) struct FileDialogPayload<'a> {
|
pub(crate) struct FileDialogPayload<'a> {
|
||||||
|
file_name: &'a Option<String>,
|
||||||
filters: &'a Vec<Filter>,
|
filters: &'a Vec<Filter>,
|
||||||
multiple: bool,
|
multiple: bool,
|
||||||
}
|
}
|
||||||
@@ -298,6 +301,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
|
|||||||
#[cfg(mobile)]
|
#[cfg(mobile)]
|
||||||
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
|
pub(crate) fn payload(&self, multiple: bool) -> FileDialogPayload<'_> {
|
||||||
FileDialogPayload {
|
FileDialogPayload {
|
||||||
|
file_name: &self.file_name,
|
||||||
filters: &self.filters,
|
filters: &self.filters,
|
||||||
multiple,
|
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) {
|
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
|
||||||
save_file(self, 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
|
/// // the file path is `None` if the user closed the dialog
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
#[cfg(desktop)]
|
|
||||||
pub fn blocking_save_file(self) -> Option<PathBuf> {
|
pub fn blocking_save_file(self) -> Option<PathBuf> {
|
||||||
blocking_fn!(self, save_file)
|
blocking_fn!(self, save_file)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
|
// taken from deno source code: https://github.com/denoland/deno/blob/ffffa2f7c44bd26aec5ae1957e0534487d099f48/runtime/ops/fs.rs#L913
|
||||||
|
#[cfg(desktop)]
|
||||||
#[inline]
|
#[inline]
|
||||||
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
|
fn to_msec(maybe_time: std::result::Result<std::time::SystemTime, std::io::Error>) -> Option<u64> {
|
||||||
match maybe_time {
|
match maybe_time {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
|
||||||
// SPDX-License-Identifier: Apache-2.0
|
// SPDX-License-Identifier: Apache-2.0
|
||||||
// SPDX-License-Identifier: MIT
|
// SPDX-License-Identifier: MIT
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use serde::{de::DeserializeOwned, Deserialize};
|
use serde::{de::DeserializeOwned, Deserialize};
|
||||||
use tauri::{
|
use tauri::{
|
||||||
@@ -49,6 +50,11 @@ struct FilePickerResponse {
|
|||||||
files: Vec<FileResponse>,
|
files: Vec<FileResponse>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct SaveFileResponse {
|
||||||
|
file: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
|
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
|
||||||
dialog: FileDialogBuilder<R>,
|
dialog: FileDialogBuilder<R>,
|
||||||
f: F,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ShowMessageDialogResponse {
|
struct ShowMessageDialogResponse {
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|||||||
Reference in New Issue
Block a user