Skip to main content

loki_file_access/
api.rs

1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 AppThere
3
4//! Public API surface for presenting file-picker dialogs.
5//!
6//! This module defines [`FilePicker`], [`PickOptions`], and [`SaveOptions`] —
7//! the primary entry points for all file-picker operations.  Platform-specific
8//! behaviour is fully abstracted behind these types.
9
10use crate::error::PickerError;
11use crate::token::FileAccessToken;
12
13/// Options for opening an existing file via a platform file-picker dialog.
14///
15/// # Examples
16///
17/// ```
18/// use loki_file_access::PickOptions;
19///
20/// let opts = PickOptions {
21///     mime_types: vec!["image/png".into(), "image/jpeg".into()],
22///     filter_label: Some("Images".into()),
23///     multi: false,
24/// };
25/// ```
26#[derive(Debug, Clone, Default)]
27pub struct PickOptions {
28    /// MIME types to filter in the picker dialog.
29    ///
30    /// An empty vector means all file types are shown.
31    pub mime_types: Vec<String>,
32
33    /// Display label for the file-type filter in the picker UI.
34    ///
35    /// Not all platforms support this (e.g. Android ignores it).
36    pub filter_label: Option<String>,
37
38    /// Whether the user may select multiple files.
39    ///
40    /// When `false`, at most one file is returned.
41    pub multi: bool,
42}
43
44/// Options for saving a new file or overwriting an existing one.
45///
46/// # Examples
47///
48/// ```
49/// use loki_file_access::SaveOptions;
50///
51/// let opts = SaveOptions {
52///     mime_type: Some("text/plain".into()),
53///     suggested_name: Some("notes.txt".into()),
54/// };
55/// ```
56#[derive(Debug, Clone, Default)]
57pub struct SaveOptions {
58    /// The MIME type of the file being saved.
59    ///
60    /// Used by some platforms (Android SAF) to pre-filter the save location.
61    pub mime_type: Option<String>,
62
63    /// Suggested filename including extension.
64    ///
65    /// The user may change this in the save dialog.
66    pub suggested_name: Option<String>,
67}
68
69/// Frontend-agnostic file picker that delegates to the native platform dialog.
70///
71/// `FilePicker` has no state and is cheap to construct.  All methods return
72/// standard [`Future`] values that can be awaited from any async runtime,
73/// including `pollster::block_on` for synchronous contexts.
74///
75/// # Examples
76///
77/// ```no_run
78/// use loki_file_access::{FilePicker, PickOptions};
79///
80/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
81/// let picker = FilePicker::new();
82/// let token = picker
83///     .pick_file_to_open(PickOptions::default())
84///     .await?;
85///
86/// if let Some(token) = token {
87///     println!("Selected: {}", token.display_name());
88/// }
89/// # Ok(())
90/// # }
91/// ```
92#[derive(Debug, Clone, Default)]
93pub struct FilePicker;
94
95impl FilePicker {
96    /// Create a new `FilePicker` instance.
97    #[must_use]
98    pub fn new() -> Self {
99        Self
100    }
101
102    /// Present a platform dialog for the user to select a single file.
103    ///
104    /// Returns `Ok(Some(token))` if the user selected a file, or `Ok(None)`
105    /// if the user cancelled the dialog.  The `multi` field of `options` is
106    /// ignored — use [`pick_files_to_open`](Self::pick_files_to_open) for
107    /// multi-selection.
108    ///
109    /// # Errors
110    ///
111    /// Returns [`PickerError`] if the platform dialog could not be presented.
112    #[must_use = "this returns a Result that may contain an error"]
113    pub async fn pick_file_to_open(
114        &self,
115        options: PickOptions,
116    ) -> Result<Option<FileAccessToken>, PickerError> {
117        let opts = PickOptions {
118            multi: false,
119            ..options
120        };
121        crate::platform::pick_open_single(opts).await
122    }
123
124    /// Present a platform dialog for the user to select multiple files.
125    ///
126    /// Returns a (possibly empty) vector of tokens.  An empty vector means
127    /// the user cancelled the dialog.  The `multi` field of `options` is
128    /// forced to `true`.
129    ///
130    /// # Errors
131    ///
132    /// Returns [`PickerError`] if the platform dialog could not be presented.
133    #[must_use = "this returns a Result that may contain an error"]
134    pub async fn pick_files_to_open(
135        &self,
136        options: PickOptions,
137    ) -> Result<Vec<FileAccessToken>, PickerError> {
138        let opts = PickOptions {
139            multi: true,
140            ..options
141        };
142        crate::platform::pick_open_multi(opts).await
143    }
144
145    /// Present a platform dialog for the user to choose a save location.
146    ///
147    /// Returns `Ok(Some(token))` if the user confirmed a save location, or
148    /// `Ok(None)` if the user cancelled.
149    ///
150    /// # Platform notes
151    ///
152    /// On WASM, this triggers a browser download via a Blob URL rather than
153    /// presenting a traditional save dialog.  The returned token wraps an
154    /// in-memory buffer.
155    ///
156    /// # Errors
157    ///
158    /// Returns [`PickerError`] if the platform dialog could not be presented.
159    #[must_use = "this returns a Result that may contain an error"]
160    pub async fn pick_file_to_save(
161        &self,
162        options: SaveOptions,
163    ) -> Result<Option<FileAccessToken>, PickerError> {
164        crate::platform::pick_save(options).await
165    }
166}