Skip to content

Commit 9e0ec57

Browse files
committed
Add FolderDownload icon and implement download directory selection in settings
- Introduced a new FolderDownload icon component for UI. - Enhanced settings page to allow users to select and display a custom download directory. - Implemented Tauri commands to get and set the download directory, improving file management capabilities. Signed-off-by: Pushkar Mishra <pushkarmishra029@gmail.com>
1 parent 42e299f commit 9e0ec57

3 files changed

Lines changed: 170 additions & 9 deletions

File tree

src-tauri/src/lib.rs

Lines changed: 109 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,16 @@ use tauri::{
1313
use tokio::sync::mpsc;
1414
use tokio::sync::Mutex;
1515

16+
/// Application state shared across all Tauri commands.
1617
struct AppState {
18+
/// Iroh instance for peer-to-peer file transfers
1719
pub iroh: IrohInstance,
20+
/// Channel sender for internal event communication
1821
inner: Mutex<mpsc::Sender<Event>>,
19-
// Store active send bubble to keep it alive
22+
/// Active send bubble to keep it alive during transfers
2023
active_send_bubble: Arc<Mutex<Option<SendFilesBubble>>>,
24+
/// Custom download directory set by user
25+
custom_download_dir: Mutex<Option<PathBuf>>,
2126
}
2227

2328
enum Event {
@@ -30,6 +35,7 @@ impl AppState {
3035
iroh,
3136
inner: Mutex::new(async_proc_input_tx),
3237
active_send_bubble: Arc::new(Mutex::new(None)),
38+
custom_download_dir: Mutex::new(None),
3339
}
3440
}
3541
}
@@ -84,6 +90,8 @@ pub fn run() {
8490
.invoke_handler(generate_handler![
8591
generate_ticket,
8692
receive_files,
93+
set_download_directory,
94+
get_download_directory,
8795
open_directory,
8896
is_valid_ticket,
8997
get_env
@@ -112,11 +120,28 @@ fn event_handler(message: Event, manager: &AppHandle) {
112120
}
113121
}
114122

123+
/// Gets an environment variable value.
124+
///
125+
/// # Arguments
126+
/// * `key` - The environment variable name
127+
///
128+
/// # Returns
129+
/// The value of the environment variable, or an empty string if not found
115130
#[tauri::command]
116131
fn get_env(key: &str) -> String {
117132
std::env::var(String::from(key)).unwrap_or(String::from(""))
118133
}
119134

135+
/// Generates a ticket for sending files.
136+
///
137+
/// Creates a ticket that encodes the file paths and connection information,
138+
/// which can be shared with a receiver to initiate a file transfer.
139+
///
140+
/// # Arguments
141+
/// * `paths` - List of file paths to include in the transfer
142+
///
143+
/// # Returns
144+
/// A BlobTicket containing the transfer information
120145
#[tauri::command]
121146
async fn generate_ticket(
122147
state: tauri::State<'_, AppState>,
@@ -171,6 +196,16 @@ async fn generate_ticket(
171196
Ok(ticket)
172197
}
173198

199+
/// Receives files using a transfer ticket.
200+
///
201+
/// Downloads files from a sender using the provided ticket and saves them to
202+
/// the configured download directory.
203+
///
204+
/// # Arguments
205+
/// * `ticket` - The transfer ticket string from the sender
206+
///
207+
/// # Returns
208+
/// The path to the directory where files were saved
174209
#[tauri::command]
175210
async fn receive_files(
176211
state: tauri::State<'_, AppState>,
@@ -187,12 +222,12 @@ async fn receive_files(
187222
}
188223
});
189224

190-
// Determine output directory
191-
let output_dir = if let Some(path) = dirs::download_dir() {
192-
path
193-
} else {
194-
// Android download path
195-
PathBuf::from("/storage/emulated/0/Download/")
225+
let output_dir = {
226+
let custom_dir = state.custom_download_dir.lock().await;
227+
custom_dir
228+
.clone()
229+
.or_else(|| dirs::download_dir())
230+
.unwrap_or_else(|| PathBuf::from("/storage/emulated/0/Download/"))
196231
};
197232

198233
// Receive files with proper file writing
@@ -206,14 +241,80 @@ async fn receive_files(
206241
Ok(output_dir)
207242
}
208243

244+
/// Sets a custom download directory for received files.
245+
///
246+
/// # Arguments
247+
/// * `path` - The filesystem path to the directory
248+
///
249+
/// # Errors
250+
/// Returns an error if the path doesn't exist or is not a directory
251+
#[tauri::command]
252+
async fn set_download_directory(
253+
state: tauri::State<'_, AppState>,
254+
path: String,
255+
) -> Result<(), InvokeError> {
256+
let path_buf = PathBuf::from(&path);
257+
258+
if !path_buf.exists() {
259+
return Err(InvokeError::from_anyhow(anyhow!(
260+
"Directory does not exist: {}",
261+
path
262+
)));
263+
}
264+
265+
if !path_buf.is_dir() {
266+
return Err(InvokeError::from_anyhow(anyhow!(
267+
"Path is not a directory: {}",
268+
path
269+
)));
270+
}
271+
272+
let mut custom_dir = state.custom_download_dir.lock().await;
273+
*custom_dir = Some(path_buf);
274+
275+
Ok(())
276+
}
277+
278+
/// Gets the current download directory.
279+
///
280+
/// Returns the custom directory if set, otherwise returns the system default
281+
/// download directory, or the Android download path as a fallback.
282+
///
283+
/// # Returns
284+
/// The absolute path to the download directory as a string
285+
#[tauri::command]
286+
async fn get_download_directory(state: tauri::State<'_, AppState>) -> Result<String, InvokeError> {
287+
let custom_dir = state.custom_download_dir.lock().await;
288+
289+
let output_dir = custom_dir
290+
.clone()
291+
.or_else(|| dirs::download_dir())
292+
.unwrap_or_else(|| PathBuf::from("/storage/emulated/0/Download/"));
293+
294+
Ok(output_dir.to_string_lossy().to_string())
295+
}
296+
297+
/// Opens a directory in the system's file manager.
298+
///
299+
/// # Arguments
300+
/// * `directory` - Path to the directory to open
301+
///
302+
/// # Errors
303+
/// Returns an error if the directory cannot be opened
209304
#[tauri::command]
210305
fn open_directory(directory: PathBuf) -> Result<(), InvokeError> {
211306
open::that(directory).map_err(|e| InvokeError::from_anyhow(anyhow!(e)))
212307
}
213308

309+
/// Validates a transfer ticket format.
310+
///
311+
/// # Arguments
312+
/// * `ticket` - The ticket string to validate
313+
///
314+
/// # Returns
315+
/// `true` if the ticket is valid, `false` otherwise
214316
#[tauri::command]
215317
fn is_valid_ticket(ticket: String) -> Result<bool, InvokeError> {
216-
// With ark-core, we can simply try to parse the ticket
217318
match BlobTicket::parse(&ticket) {
218319
Ok(_) => Ok(true),
219320
Err(_) => Ok(false),
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<svg
2+
{...$$props}
3+
width="100%"
4+
height="100%"
5+
viewBox="0 0 24 24"
6+
fill="none"
7+
xmlns="http://www.w3.org/2000/svg"
8+
>
9+
<path
10+
d="M13 10L12 11M12 11L11 10M12 11V7M22 11V17.2C22 18.8802 22 19.7202 21.673 20.362C21.3854 20.9265 20.9265 21.3854 20.362 21.673C19.7202 22 18.8802 22 17.2 22H6.8C5.11984 22 4.27976 22 3.63803 21.673C3.07354 21.3854 2.6146 20.9265 2.32698 20.362C2 19.7202 2 18.8802 2 17.2V6.8C2 5.11984 2 4.27976 2.32698 3.63803C2.6146 3.07354 3.07354 2.6146 3.63803 2.32698C4.27976 2 5.11984 2 6.8 2H8.34315C9.16065 2 9.5694 2 9.93694 2.15224C10.2619 2.28624 10.5497 2.49827 10.7764 2.77091C11.0337 3.07908 11.1816 3.49013 11.4775 4.31224L12.5225 7.18776C12.8184 8.00987 12.9663 8.42092 13.2236 8.72909C13.4503 9.00173 13.7381 9.21376 14.0631 9.34776C14.4306 9.5 14.8394 9.5 15.6569 9.5H17.2C18.8802 9.5 19.7202 9.5 20.362 9.82698C20.9265 10.1146 21.3854 10.5735 21.673 11.138C21.8657 11.5 21.9523 11.9561 21.9827 12.5"
11+
stroke-width="2"
12+
stroke-linecap="round"
13+
stroke-linejoin="round"
14+
/>
15+
</svg>

src/routes/settings/+page.svelte

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,41 @@
22
import NavBar from '$lib/components/NavBar.svelte';
33
import Edit05 from '$lib/components/icons/Edit05.svelte';
44
import File06 from '$lib/components/icons/File06.svelte';
5+
import FolderDownload from '$lib/components/icons/FolderDownload.svelte';
56
import ShieldTick from '$lib/components/icons/ShieldTick.svelte';
67
import MessageQuestionSquare from '$lib/components/icons/MessageQuestionSquare.svelte';
78
import Star01 from '$lib/components/icons/Star01.svelte';
89
import { goto } from '$app/navigation';
10+
import { onMount } from 'svelte';
11+
import { invoke } from '@tauri-apps/api/core';
12+
import { open } from '@tauri-apps/plugin-dialog';
13+
14+
let currentDirectory = '';
15+
16+
onMount(async () => {
17+
try {
18+
currentDirectory = await invoke('get_download_directory');
19+
} catch (error) {
20+
console.error('Failed to get download directory:', error);
21+
}
22+
});
23+
24+
async function selectDownloadDirectory() {
25+
try {
26+
const selected = await open({
27+
directory: true,
28+
multiple: false,
29+
title: 'Select Download Directory'
30+
});
31+
32+
if (selected && typeof selected === 'string') {
33+
await invoke('set_download_directory', { path: selected });
34+
currentDirectory = selected;
35+
}
36+
} catch (error) {
37+
console.error('Failed to set download directory:', error);
38+
}
39+
}
940
</script>
1041

1142
<div class="flex w-full flex-col bg-blue-dark-500 p-4">
@@ -32,7 +63,21 @@
3263
<ul
3364
class="my-3 flex flex-col gap-3 stroke-nav-item-icon-fg p-4 font-semibold text-text-secondary-700"
3465
>
35-
<li class="flex flex-row gap-3 px-3 py-2"><File06 class=" h-6 w-6" />Tems of service</li>
66+
<li>
67+
<button
68+
on:click={selectDownloadDirectory}
69+
class="flex w-full flex-row items-center gap-3 rounded-lg px-3 py-2 hover:bg-gray-modern-100"
70+
>
71+
<FolderDownload class="h-6 w-6 stroke-nav-item-icon-fg" />
72+
<div class="flex flex-1 flex-col items-start">
73+
<span>Download Directory</span>
74+
{#if currentDirectory}
75+
<span class="text-xs font-normal text-gray-modern-500">{currentDirectory}</span>
76+
{/if}
77+
</div>
78+
</button>
79+
</li>
80+
<li class="flex flex-row gap-3 px-3 py-2"><File06 class=" h-6 w-6" />Terms of service</li>
3681
<li class="flex flex-row gap-3 px-3 py-2">
3782
<ShieldTick class="h-6 w-6 stroke-nav-item-icon-fg" />Privacy Policy
3883
</li>

0 commit comments

Comments
 (0)