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}