feat(fs): resolve content URIs on Android (#1658)

* Implemented writeTextFile on Android.

* Added license headers.

* fix fmt checks.

* implement more file APIs

* change file

* cleanup

* refactor dialog plugin to leverage new FS APIs

* implement metadata functions

* fix build

* expose FS rust API

* resolve resources on android

* update pnpm

* update docs

---------

Co-authored-by: Lucas Nogueira <lucas@tauri.app>
This commit is contained in:
mikoto2000
2024-08-24 01:35:53 +09:00
committed by GitHub
parent 3c52f30ea4
commit 448846b834
38 changed files with 1951 additions and 820 deletions
+39 -45
View File
@@ -8,17 +8,17 @@ use serde::{Deserialize, Serialize};
use tauri::{command, Manager, Runtime, State, Window};
use tauri_plugin_fs::FsExt;
use crate::{Dialog, FileDialogBuilder, FileResponse, MessageDialogKind, Result};
use crate::{Dialog, FileDialogBuilder, FilePath, MessageDialogKind, Result};
#[derive(Serialize)]
#[serde(untagged)]
pub enum OpenResponse {
#[cfg(desktop)]
Folders(Option<Vec<PathBuf>>),
Folders(Option<Vec<FilePath>>),
#[cfg(desktop)]
Folder(Option<PathBuf>),
Files(Option<Vec<FileResponse>>),
File(Option<FileResponse>),
Folder(Option<FilePath>),
Files(Option<Vec<FilePath>>),
File(Option<FilePath>),
}
#[allow(dead_code)]
@@ -136,25 +136,26 @@ pub(crate) async fn open<R: Runtime>(
let folders = dialog_builder.blocking_pick_folders();
if let Some(folders) = &folders {
for folder in folders {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(folder, options.recursive);
if let Ok(path) = folder.path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive);
}
}
}
}
OpenResponse::Folders(folders.map(|folders| {
folders
.iter()
.map(|p| dunce::simplified(p).to_path_buf())
.collect()
}))
OpenResponse::Folders(
folders.map(|folders| folders.into_iter().map(|p| p.simplified()).collect()),
)
} else {
let folder = dialog_builder.blocking_pick_folder();
if let Some(path) = &folder {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive);
if let Some(folder) = &folder {
if let Ok(path) = folder.path() {
if let Some(s) = window.try_fs_scope() {
s.allow_directory(path, options.recursive);
}
}
}
OpenResponse::Folder(folder.map(|p| dunce::simplified(&p).to_path_buf()))
OpenResponse::Folder(folder.map(|p| p.simplified()))
}
}
#[cfg(mobile)]
@@ -163,37 +164,28 @@ pub(crate) async fn open<R: Runtime>(
let files = dialog_builder.blocking_pick_files();
if let Some(files) = &files {
for file in files {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&file.path);
if let Ok(path) = file.path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
}
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
}
}
OpenResponse::Files(files.map(|files| {
files
.into_iter()
.map(|mut f| {
f.path = dunce::simplified(&f.path).to_path_buf();
f
})
.collect()
}))
OpenResponse::Files(files.map(|files| files.into_iter().map(|f| f.simplified()).collect()))
} else {
let file = dialog_builder.blocking_pick_file();
if let Some(file) = &file {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&file.path);
if let Ok(path) = file.path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
}
window
.state::<tauri::scope::Scopes>()
.allow_file(&file.path)?;
}
OpenResponse::File(file.map(|mut f| {
f.path = dunce::simplified(&f.path).to_path_buf();
f
}))
OpenResponse::File(file.map(|f| f.simplified()))
};
Ok(res)
}
@@ -204,7 +196,7 @@ pub(crate) async fn save<R: Runtime>(
window: Window<R>,
dialog: State<'_, Dialog<R>>,
options: SaveDialogOptions,
) -> Result<Option<PathBuf>> {
) -> Result<Option<FilePath>> {
#[cfg(target_os = "ios")]
return Err(crate::Error::FileSaveDialogNotImplemented);
#[cfg(any(desktop, target_os = "android"))]
@@ -230,13 +222,15 @@ pub(crate) async fn save<R: Runtime>(
let path = dialog_builder.blocking_save_file();
if let Some(p) = &path {
if let Some(s) = window.try_fs_scope() {
s.allow_file(p);
if let Ok(path) = p.path() {
if let Some(s) = window.try_fs_scope() {
s.allow_file(&path);
}
window.state::<tauri::scope::Scopes>().allow_file(&path)?;
}
window.state::<tauri::scope::Scopes>().allow_file(p)?;
}
Ok(path.map(|p| dunce::simplified(&p).to_path_buf()))
Ok(path.map(|p| p.simplified()))
}
}
+19 -13
View File
@@ -8,14 +8,12 @@
//! to give results back. This is particularly useful when running dialogs from the main thread.
//! When using on asynchronous contexts such as async commands, the [`blocking`] APIs are recommended.
use std::path::PathBuf;
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use rfd::{AsyncFileDialog, AsyncMessageDialog};
use serde::de::DeserializeOwned;
use tauri::{plugin::PluginApi, AppHandle, Runtime};
use crate::{models::*, FileDialogBuilder, MessageDialogBuilder};
use crate::{models::*, FileDialogBuilder, FilePath, MessageDialogBuilder};
const OK: &str = "Ok";
@@ -115,11 +113,11 @@ impl<R: Runtime> From<MessageDialogBuilder<R>> for AsyncMessageDialog {
}
}
pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).pick_file();
@@ -127,12 +125,16 @@ pub fn pick_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
});
}
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let f = |paths: Option<Vec<rfd::FileHandle>>| {
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
f(paths.map(|list| {
list.into_iter()
.map(|p| p.path().to_path_buf().into())
.collect()
}))
};
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
@@ -141,11 +143,11 @@ pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
});
}
pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
pub fn pick_folder<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).pick_folder();
@@ -153,12 +155,16 @@ pub fn pick_folder<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
});
}
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(
pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let f = |paths: Option<Vec<rfd::FileHandle>>| {
f(paths.map(|list| list.into_iter().map(|p| p.path().to_path_buf()).collect()))
f(paths.map(|list| {
list.into_iter()
.map(|p| p.path().to_path_buf().into())
.collect()
}))
};
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
@@ -167,11 +173,11 @@ pub fn pick_folders<R: Runtime, F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static
});
}
pub fn save_file<R: Runtime, F: FnOnce(Option<PathBuf>) + Send + 'static>(
pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf()));
let f = |path: Option<rfd::FileHandle>| f(path.map(|p| p.path().to_path_buf().into()));
let handle = dialog.dialog.app_handle().to_owned();
let _ = handle.run_on_main_thread(move || {
let dialog = AsyncFileDialog::from(dialog).save_file();
+2
View File
@@ -23,6 +23,8 @@ pub enum Error {
FileSaveDialogNotImplemented,
#[error(transparent)]
Fs(#[from] tauri_plugin_fs::Error),
#[error("URL is not a valid path")]
InvalidPathUrl,
}
impl Serialize for Error {
+190 -102
View File
@@ -17,9 +17,6 @@ use tauri::{
Manager, Runtime,
};
#[cfg(any(desktop, target_os = "ios"))]
use std::fs;
use std::{
path::{Path, PathBuf},
sync::mpsc::sync_channel,
@@ -66,6 +63,87 @@ impl<R: Runtime, T: Manager<R>> crate::DialogExt<R> for T {
}
impl<R: Runtime> Dialog<R> {
/// Create a new messaging dialog builder.
/// The dialog can optionally ask the user for confirmation or include an OK button.
///
/// # Examples
///
/// - Message dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app
/// .dialog()
/// .message("Tauri is Awesome!")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// - Ask dialog:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .show(|yes| {
/// println!("user said {}", if yes { "yes" } else { "no" });
/// });
/// Ok(())
/// });
/// ```
///
/// - Message dialog with OK button:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// app.dialog()
/// .message("Job completed successfully")
/// .ok_button_label("Ok")
/// .show(|_| {
/// println!("dialog closed");
/// });
/// Ok(())
/// });
/// ```
///
/// # `show` vs `blocking_show`
///
/// The dialog builder includes two separate APIs for rendering the dialog: `show` and `blocking_show`.
/// The `show` function is asynchronous and takes a closure to be executed when the dialog is closed.
/// To block the current thread until the user acted on the dialog, you can use `blocking_show`,
/// but note that it cannot be executed on the main thread as it will freeze your application.
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
///
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// std::thread::spawn(move || {
/// let yes = handle.dialog()
/// .message("Are you sure?")
/// .ok_button_label("Yes")
/// .cancel_button_label("No")
/// .blocking_show();
/// });
///
/// Ok(())
/// });
/// ```
pub fn message(&self, message: impl Into<String>) -> MessageDialogBuilder<R> {
MessageDialogBuilder::new(
self.clone(),
@@ -74,6 +152,7 @@ impl<R: Runtime> Dialog<R> {
)
}
/// Creates a new builder for dialogs that lets ths user select file(s) or folder(s).
pub fn file(&self) -> FileDialogBuilder<R> {
FileDialogBuilder::new(self.clone())
}
@@ -216,36 +295,52 @@ impl<R: Runtime> MessageDialogBuilder<R> {
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct FileResponse {
pub base64_data: Option<String>,
pub duration: Option<u64>,
pub height: Option<usize>,
pub width: Option<usize>,
pub mime_type: Option<String>,
pub modified_at: Option<u64>,
pub name: Option<String>,
pub path: PathBuf,
pub size: u64,
/// Represents either a filesystem path or a URI pointing to a file
/// such as `file://` URIs or Android `content://` URIs.
#[derive(Debug, Deserialize, Serialize)]
#[serde(untagged)]
pub enum FilePath {
Url(url::Url),
Path(PathBuf),
}
impl FileResponse {
#[cfg(desktop)]
fn new(path: PathBuf) -> Self {
let metadata = fs::metadata(&path);
let metadata = metadata.as_ref();
Self {
base64_data: None,
duration: None,
height: None,
width: None,
mime_type: None,
modified_at: metadata.ok().and_then(|m| to_msec(m.modified())),
name: path.file_name().map(|f| f.to_string_lossy().into_owned()),
path,
size: metadata.map(|m| m.len()).unwrap_or(0),
impl From<PathBuf> for FilePath {
fn from(value: PathBuf) -> Self {
Self::Path(value)
}
}
impl From<url::Url> for FilePath {
fn from(value: url::Url) -> Self {
Self::Url(value)
}
}
impl From<FilePath> for tauri_plugin_fs::FilePath {
fn from(value: FilePath) -> Self {
match value {
FilePath::Path(p) => tauri_plugin_fs::FilePath::Path(p),
FilePath::Url(url) => tauri_plugin_fs::FilePath::Url(url),
}
}
}
impl FilePath {
fn simplified(self) -> Self {
match self {
Self::Url(url) => Self::Url(url),
Self::Path(p) => Self::Path(dunce::simplified(&p).to_path_buf()),
}
}
#[inline]
fn path(&self) -> Result<PathBuf> {
match self {
Self::Url(url) => url
.to_file_path()
.map(PathBuf::from)
.map_err(|_| Error::InvalidPathUrl),
Self::Path(p) => Ok(p.to_owned()),
}
}
}
@@ -363,21 +458,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_file(|file_path| {
/// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn pick_file<F: FnOnce(Option<FileResponse>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |path: Option<PathBuf>| f(path.map(FileResponse::new));
pub fn pick_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_file(self, f)
}
@@ -385,29 +477,44 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// This is not a blocking operation,
/// and should be used when running on the main thread to avoid deadlocks with the event loop.
///
/// # Reading the files
///
/// The file paths cannot be read directly on Android as they are behind a content URI.
/// The recommended way to read the files is using the [`fs`](https://v2.tauri.app/plugin/file-system/) plugin:
///
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// use tauri_plugin_fs::FsExt;
/// tauri::Builder::default()
/// .setup(|app| {
/// let handle = app.handle().clone();
/// app.dialog().file().pick_file(move |file_path| {
/// let Some(path) = file_path else { return };
/// let Ok(contents) = handle.fs().read_to_string(path) else {
/// eprintln!("failed to read file, <todo add error handling!>");
/// return;
/// };
/// });
/// Ok(())
/// });
/// ```
///
/// See <https://developer.android.com/guide/topics/providers/content-provider-basics> for more information.
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_files(|file_paths| {
/// // do something with the optional file paths here
/// // the file paths value is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn pick_files<F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(self, f: F) {
#[cfg(desktop)]
let f = |paths: Option<Vec<PathBuf>>| {
f(paths.map(|p| {
p.into_iter()
.map(FileResponse::new)
.collect::<Vec<FileResponse>>()
}))
};
pub fn pick_files<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_files(self, f)
}
@@ -417,20 +524,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_folder(|folder_path| {
/// // do something with the optional folder path here
/// // the folder path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
#[cfg(desktop)]
pub fn pick_folder<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pub fn pick_folder<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
pick_folder(self, f)
}
@@ -440,20 +546,19 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().pick_folders(|file_paths| {
/// // do something with the optional folder paths here
/// // the folder paths value is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
#[cfg(desktop)]
pub fn pick_folders<F: FnOnce(Option<Vec<PathBuf>>) + Send + 'static>(self, f: F) {
pub fn pick_folders<F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(self, f: F) {
pick_folders(self, f)
}
@@ -464,19 +569,18 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// tauri::Builder::default()
/// .build(tauri::generate_context!("test/tauri.conf.json"))
/// .expect("failed to build tauri app")
/// .run(|app, _event| {
/// .setup(|app| {
/// app.dialog().file().save_file(|file_path| {
/// // do something with the optional file path here
/// // the file path is `None` if the user closed the dialog
/// })
/// })
/// });
/// Ok(())
/// });
/// ```
pub fn save_file<F: FnOnce(Option<PathBuf>) + Send + 'static>(self, f: F) {
pub fn save_file<F: FnOnce(Option<FilePath>) + Send + 'static>(self, f: F) {
save_file(self, f)
}
}
@@ -489,7 +593,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -498,7 +602,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_file(self) -> Option<FileResponse> {
pub fn blocking_pick_file(self) -> Option<FilePath> {
blocking_fn!(self, pick_file)
}
@@ -508,7 +612,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -517,7 +621,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file paths value is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_pick_files(self) -> Option<Vec<FileResponse>> {
pub fn blocking_pick_files(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_files)
}
@@ -527,7 +631,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -537,7 +641,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folder(self) -> Option<PathBuf> {
pub fn blocking_pick_folder(self) -> Option<FilePath> {
blocking_fn!(self, pick_folder)
}
@@ -547,7 +651,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -557,7 +661,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// }
/// ```
#[cfg(desktop)]
pub fn blocking_pick_folders(self) -> Option<Vec<PathBuf>> {
pub fn blocking_pick_folders(self) -> Option<Vec<FilePath>> {
blocking_fn!(self, pick_folders)
}
@@ -567,7 +671,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
///
/// # Examples
///
/// ```rust,no_run
/// ```
/// use tauri_plugin_dialog::DialogExt;
/// #[tauri::command]
/// async fn my_command(app: tauri::AppHandle) {
@@ -576,23 +680,7 @@ impl<R: Runtime> FileDialogBuilder<R> {
/// // the file path is `None` if the user closed the dialog
/// }
/// ```
pub fn blocking_save_file(self) -> Option<PathBuf> {
pub fn blocking_save_file(self) -> Option<FilePath> {
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 {
Ok(time) => {
let msec = time
.duration_since(std::time::UNIX_EPOCH)
.map(|t| t.as_millis() as u64)
.unwrap_or_else(|err| err.duration().as_millis() as u64);
Some(msec)
}
Err(_) => None,
}
}
+6 -7
View File
@@ -1,7 +1,6 @@
// 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::{
@@ -9,7 +8,7 @@ use tauri::{
AppHandle, Runtime,
};
use crate::{FileDialogBuilder, FileResponse, MessageDialogBuilder};
use crate::{FileDialogBuilder, FilePath, MessageDialogBuilder};
#[cfg(target_os = "android")]
const PLUGIN_IDENTIFIER: &str = "app.tauri.dialog";
@@ -47,15 +46,15 @@ impl<R: Runtime> Dialog<R> {
#[derive(Debug, Deserialize)]
struct FilePickerResponse {
files: Vec<FileResponse>,
files: Vec<FilePath>,
}
#[derive(Debug, Deserialize)]
struct SaveFileResponse {
file: PathBuf,
file: FilePath,
}
pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
pub fn pick_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
@@ -72,7 +71,7 @@ pub fn pick_file<R: Runtime, F: FnOnce(Option<FileResponse>) + Send + 'static>(
});
}
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FileResponse>>) + Send + 'static>(
pub fn pick_files<R: Runtime, F: FnOnce(Option<Vec<FilePath>>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {
@@ -89,7 +88,7 @@ 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>(
pub fn save_file<R: Runtime, F: FnOnce(Option<FilePath>) + Send + 'static>(
dialog: FileDialogBuilder<R>,
f: F,
) {