mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-01-31 00:45:24 +01: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:
5
.changes/android-dialog-save.md
Normal file
5
.changes/android-dialog-save.md
Normal file
@@ -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)]
|
||||
|
||||
Reference in New Issue
Block a user