diff --git a/Cargo.lock b/Cargo.lock index fc2acf3..8cbb79b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -113,7 +113,9 @@ dependencies = [ "tauri-plugin-clipboard-manager", "tauri-plugin-dialog", "tauri-plugin-opener", + "tauri-plugin-store", "tokio", + "whoami", ] [[package]] @@ -3085,6 +3087,7 @@ checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" dependencies = [ "bitflags 2.10.0", "libc", + "redox_syscall", ] [[package]] @@ -6104,6 +6107,22 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-store" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a77036340a97eb5bbe1b3209c31e5f27f75e6f92a52fd9dd4b211ef08bf310" +dependencies = [ + "dunce", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.17", + "tokio", + "tracing", +] + [[package]] name = "tauri-runtime" version = "2.9.1" @@ -6945,6 +6964,12 @@ dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.104" @@ -7227,6 +7252,17 @@ version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a751b3277700db47d3e574514de2eced5e54dc8a5436a3bf7a0b248b2cee16f3" +[[package]] +name = "whoami" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d" +dependencies = [ + "libredox", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.2.1" diff --git a/README.md b/README.md index 366dcd7..26459f4 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ ARK Drop is designed for easy file transfer. You can use QR codes to quickly sen > [!WARNING] > ARK Drop is currently under heavy development and should be used with caution. It has not undergone extensive testing and may contain bugs, vulnerabilities, or unexpected behavior. -## Development +## Tech Stack ARK Drop is built using [Tauri](https://tauri.app/) with [SvelteKit](https://kit.svelte.dev/). @@ -17,70 +17,52 @@ ARK Drop is built using [Tauri](https://tauri.app/) with [SvelteKit](https://kit [SvelteKit](https://kit.svelte.dev/) is an application framework built on Svelte. Unlike traditional frameworks, SvelteKit shifts work to a compile step during the build process, resulting in code that directly updates the DOM when the application's state changes, enhancing performance. -## Running ARK Drop Locally +## Development -You can use either `cargo tauri` CLI or `npm` CLI commands to run ARK Drop locally. +### Prerequisites -### Installing `cargo tauri` +- [Rust](https://rustup.rs/) +- [Node.js](https://nodejs.org/) -To install `cargo tauri`, run: +### Install Dependencies ```sh -cargo install tauri-cli +npm install ``` -### Starting the Tauri Development Window - -To start the Tauri development window, run: +### Run Development Server ```sh npm run tauri dev ``` -or - -```sh -cargo tauri dev -``` - This command builds the Rust code and opens the webview to display your web app. You can make changes to your web app, and if your tooling supports it, the webview will update automatically, similar to a browser. -## Building the Project +## Build -Tauri will detect your operating system and build a corresponding bundle. To build the project, run: +Tauri will detect your operating system and build a corresponding bundle. ```sh npm run tauri build ``` -or - -```sh -cargo tauri build -``` - This process will build your frontend, compile the Rust binary, gather all external binaries and resources, and produce platform-specific bundles and installers. -For more information about Tauri builds, refer to the [Tauri building guide](https://tauri.app/v1/guides/building/). - -## Dependencies +For more information, refer to the [Tauri building guide](https://tauri.app/v1/guides/building/). -Cross compilation building is done easiest via cross library. +## Android Build -- [Cross](https://github.com/cross-rs/cross) +Cross-compilation is easiest using [Cross](https://github.com/cross-rs/cross). Alternatively, you can set up the NDK and build manually. -Alternatively you can setup the NDK and build manually - -### Building for Android - -Make sure you have added the nessecary targets to build for android +### Add Android Targets ```sh -rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android +rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android x86_64-linux-android ``` -Build the cdylib for all the targets +### Build for Android Targets +Using Cross: ```sh cross build -p drop_core --target aarch64-linux-android cross build -p drop_core --target armv7-linux-androideabi @@ -88,8 +70,8 @@ cross build -p drop_core --target i686-linux-android cross build -p drop_core --target x86_64-linux-android ``` -Generate the bindings using uniffi for kotlin - +Or using cargo-ndk: ```sh -cargo run -p uniffi-bingen generate --library target/x86_64-linux-android/debug/libdrop_core.so --language=kotlin --out-dir ./bindings +cargo install cargo-ndk +cargo ndk -o ./target/release/jniLibs --target aarch64-linux-android --target armv7-linux-androideabi --target i686-linux-android --target x86_64-linux-android build -p drop_core --release ``` diff --git a/core/src/lib.rs b/core/src/lib.rs index 8d58b24..0d746ec 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -206,6 +206,7 @@ impl IrohInstance { ticket_str: String, output_dir: PathBuf, handle: Arc, + display_name: Option, ) -> IrohResult { // Parse ticket to extract confirmation let (ticket, confirmation) = TicketWrapper::parse(&ticket_str)?; @@ -229,9 +230,9 @@ impl IrohInstance { IrohError::DownloadError(format!("Failed to create receiving directory: {}", e)) })?; - // Create receiver profile + // Create receiver profile with provided name or default to "Anonymous" let profile = ReceiverProfile { - name: "Anonymous".to_string(), + name: display_name.unwrap_or_else(|| "Anonymous".to_string()), avatar_b64: None, }; diff --git a/package-lock.json b/package-lock.json index d618dd6..1b1f817 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-store": "^2.4.1", "html5-qrcode": "^2.3.8", "qrcode": "^1.5.4" }, @@ -1405,6 +1406,15 @@ "@tauri-apps/api": "^2.8.0" } }, + "node_modules/@tauri-apps/plugin-store": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@tauri-apps/plugin-store/-/plugin-store-2.4.1.tgz", + "integrity": "sha512-ckGSEzZ5Ii4Hf2D5x25Oqnm2Zf9MfDWAzR+volY0z/OOBz6aucPKEY0F649JvQ0Vupku6UJo7ugpGRDOFOunkA==", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@tauri-apps/api": "^2.8.0" + } + }, "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", diff --git a/package.json b/package.json index f772d35..413de00 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "@tauri-apps/plugin-clipboard-manager": "^2.3.0", "@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-opener": "^2.5.0", + "@tauri-apps/plugin-store": "^2.4.1", "html5-qrcode": "^2.3.8", "qrcode": "^1.5.4" }, diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 22f7901..917a713 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -23,8 +23,10 @@ tauri = { version = "2.9.1", features = ["tray-icon"] } tauri-plugin-opener = "2.5.0" tauri-plugin-dialog = "2.4.0" tauri-plugin-clipboard-manager = "2.3.0" +tauri-plugin-store = "2.4.1" dirs = "6.0.0" open = "5.3.2" +whoami = "1.5.2" anyhow = { workspace = true } diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 8228557..7020390 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -9,6 +9,7 @@ "dialog:default", "clipboard-manager:default", "clipboard-manager:allow-read-text", - "clipboard-manager:allow-write-text" + "clipboard-manager:allow-write-text", + "store:default" ] } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index dc097ca..c1be0b2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,11 +13,18 @@ use tauri::{ use tokio::sync::mpsc; use tokio::sync::Mutex; +/// Application state shared across all Tauri commands. struct AppState { + /// Iroh instance for peer-to-peer file transfers pub iroh: IrohInstance, + /// Channel sender for internal event communication inner: Mutex>, - // Store active send bubble to keep it alive + /// Active send bubble to keep it alive during transfers active_send_bubble: Arc>>, + /// Custom download directory set by user + custom_download_dir: Mutex>, + /// User display name for file transfer identification + user_display_name: Mutex>, } enum Event { @@ -30,6 +37,8 @@ impl AppState { iroh, inner: Mutex::new(async_proc_input_tx), active_send_bubble: Arc::new(Mutex::new(None)), + custom_download_dir: Mutex::new(None), + user_display_name: Mutex::new(None), } } } @@ -51,6 +60,7 @@ pub fn run() { let (async_proc_output_tx, mut async_proc_output_rx) = mpsc::channel(1); tauri::Builder::default() + .plugin(tauri_plugin_store::Builder::default().build()) .plugin(tauri_plugin_clipboard_manager::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_dialog::init()) @@ -84,6 +94,10 @@ pub fn run() { .invoke_handler(generate_handler![ generate_ticket, receive_files, + set_download_directory, + get_download_directory, + set_display_name, + get_display_name, open_directory, is_valid_ticket, get_env @@ -112,11 +126,28 @@ fn event_handler(message: Event, manager: &AppHandle) { } } +/// Gets an environment variable value. +/// +/// # Arguments +/// * `key` - The environment variable name +/// +/// # Returns +/// The value of the environment variable, or an empty string if not found #[tauri::command] fn get_env(key: &str) -> String { std::env::var(String::from(key)).unwrap_or(String::from("")) } +/// Generates a ticket for sending files. +/// +/// Creates a ticket that encodes the file paths and connection information, +/// which can be shared with a receiver to initiate a file transfer. +/// +/// # Arguments +/// * `paths` - List of file paths to include in the transfer +/// +/// # Returns +/// A BlobTicket containing the transfer information #[tauri::command] async fn generate_ticket( state: tauri::State<'_, AppState>, @@ -171,6 +202,16 @@ async fn generate_ticket( Ok(ticket) } +/// Receives files using a transfer ticket. +/// +/// Downloads files from a sender using the provided ticket and saves them to +/// the configured download directory. +/// +/// # Arguments +/// * `ticket` - The transfer ticket string from the sender +/// +/// # Returns +/// The path to the directory where files were saved #[tauri::command] async fn receive_files( state: tauri::State<'_, AppState>, @@ -187,18 +228,30 @@ async fn receive_files( } }); - // Determine output directory - let output_dir = if let Some(path) = dirs::download_dir() { - path - } else { - // Android download path - PathBuf::from("/storage/emulated/0/Download/") + let output_dir = { + let custom_dir = state.custom_download_dir.lock().await; + custom_dir + .clone() + .or_else(|| dirs::download_dir()) + .unwrap_or_else(|| PathBuf::from("/storage/emulated/0/Download/")) + }; + + // Get display name with fallback chain: custom → system username → None + let display_name = { + let custom_name = state.user_display_name.lock().await; + custom_name.clone() + .or_else(|| Some(whoami::username())) }; // Receive files with proper file writing let _collection = state .iroh - .receive_files(ticket, output_dir.clone(), Arc::new(FileTransferHandle(tx))) + .receive_files( + ticket, + output_dir.clone(), + Arc::new(FileTransferHandle(tx)), + display_name + ) .await .map_err(|e| InvokeError::from_anyhow(anyhow!(e)))?; @@ -206,14 +259,125 @@ async fn receive_files( Ok(output_dir) } +/// Sets a custom download directory for received files. +/// +/// # Arguments +/// * `path` - The filesystem path to the directory +/// +/// # Errors +/// Returns an error if the path doesn't exist or is not a directory +#[tauri::command] +async fn set_download_directory( + state: tauri::State<'_, AppState>, + path: String, +) -> Result<(), InvokeError> { + let path_buf = PathBuf::from(&path); + + if !path_buf.exists() { + return Err(InvokeError::from_anyhow(anyhow!( + "Directory does not exist: {}", + path + ))); + } + + if !path_buf.is_dir() { + return Err(InvokeError::from_anyhow(anyhow!( + "Path is not a directory: {}", + path + ))); + } + + let mut custom_dir = state.custom_download_dir.lock().await; + *custom_dir = Some(path_buf); + + Ok(()) +} + +/// Gets the current download directory. +/// +/// Returns the custom directory if set, otherwise returns the system default +/// download directory, or the Android download path as a fallback. +/// +/// # Returns +/// The absolute path to the download directory as a string +#[tauri::command] +async fn get_download_directory(state: tauri::State<'_, AppState>) -> Result { + let custom_dir = state.custom_download_dir.lock().await; + + let output_dir = custom_dir + .clone() + .or_else(|| dirs::download_dir()) + .unwrap_or_else(|| PathBuf::from("/storage/emulated/0/Download/")); + + Ok(output_dir.to_string_lossy().to_string()) +} + +/// Sets a custom display name for the user. +/// +/// # Arguments +/// * `name` - The display name to set +/// +/// # Errors +/// Returns an error if the name is empty or exceeds 50 characters +#[tauri::command] +async fn set_display_name( + state: tauri::State<'_, AppState>, + name: String, +) -> Result<(), InvokeError> { + let trimmed = name.trim(); + + if trimmed.is_empty() { + return Err(InvokeError::from_anyhow(anyhow!("Display name cannot be empty"))); + } + + if trimmed.len() > 50 { + return Err(InvokeError::from_anyhow(anyhow!("Display name cannot exceed 50 characters"))); + } + + let mut display_name = state.user_display_name.lock().await; + *display_name = Some(trimmed.to_string()); + + Ok(()) +} + +/// Gets the current display name. +/// +/// Returns the custom name if set, otherwise returns the system username. +/// +/// # Returns +/// The display name as a string +#[tauri::command] +async fn get_display_name(state: tauri::State<'_, AppState>) -> Result { + let custom_name = state.user_display_name.lock().await; + + let name = custom_name + .clone() + .unwrap_or_else(|| whoami::username()); + + Ok(name) +} + +/// Opens a directory in the system's file manager. +/// +/// # Arguments +/// * `directory` - Path to the directory to open +/// +/// # Errors +/// Returns an error if the directory cannot be opened #[tauri::command] fn open_directory(directory: PathBuf) -> Result<(), InvokeError> { open::that(directory).map_err(|e| InvokeError::from_anyhow(anyhow!(e))) } +/// Validates a transfer ticket format. +/// +/// # Arguments +/// * `ticket` - The ticket string to validate +/// +/// # Returns +/// `true` if the ticket is valid, `false` otherwise #[tauri::command] fn is_valid_ticket(ticket: String) -> Result { - // With ark-core, we can simply try to parse the ticket match BlobTicket::parse(&ticket) { Ok(_) => Ok(true), Err(_) => Ok(false), diff --git a/src/lib/components/icons/FolderDownload.svelte b/src/lib/components/icons/FolderDownload.svelte new file mode 100644 index 0000000..0b8635e --- /dev/null +++ b/src/lib/components/icons/FolderDownload.svelte @@ -0,0 +1,16 @@ + + + diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 5822170..21fbd2c 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -2,10 +2,76 @@ import NavBar from '$lib/components/NavBar.svelte'; import Edit05 from '$lib/components/icons/Edit05.svelte'; import File06 from '$lib/components/icons/File06.svelte'; + import FolderDownload from '$lib/components/icons/FolderDownload.svelte'; import ShieldTick from '$lib/components/icons/ShieldTick.svelte'; import MessageQuestionSquare from '$lib/components/icons/MessageQuestionSquare.svelte'; import Star01 from '$lib/components/icons/Star01.svelte'; import { goto } from '$app/navigation'; + import { onMount } from 'svelte'; + import { invoke } from '@tauri-apps/api/core'; + import { open } from '@tauri-apps/plugin-dialog'; + import { Store } from '@tauri-apps/plugin-store'; + + let currentDirectory = ''; + let displayName = ''; + let store: Store; + + async function loadSettings() { + try { + store = await Store.load('settings.json'); + + // Load download directory + const savedDirectory = await store.get('download_directory'); + if (savedDirectory) { + await invoke('set_download_directory', { path: savedDirectory }); + currentDirectory = savedDirectory; + } else { + currentDirectory = await invoke('get_download_directory'); + } + + // Load display name + const savedName = await store.get('display_name'); + if (savedName) { + await invoke('set_display_name', { name: savedName }); + displayName = savedName; + } else { + displayName = await invoke('get_display_name'); + } + } catch (error) { + console.error('Failed to load settings:', error); + } + } + + onMount(loadSettings); + + // Reload settings when page becomes visible (e.g., returning from edit-profile) + $: if (typeof document !== 'undefined') { + document.addEventListener('visibilitychange', () => { + if (!document.hidden) { + loadSettings(); + } + }); + } + + async function selectDownloadDirectory() { + try { + const selected = await open({ + directory: true, + multiple: false, + title: 'Select Download Directory' + }); + + if (selected && typeof selected === 'string') { + await invoke('set_download_directory', { path: selected }); + currentDirectory = selected; + + await store.set('download_directory', selected); + await store.save(); + } + } catch (error) { + console.error('Failed to set download directory:', error); + } + }
@@ -17,7 +83,7 @@
- Gilbert + {displayName || 'Loading...'} + +
  • Terms of service
  • Privacy Policy
  • diff --git a/src/routes/settings/edit-profile/+page.svelte b/src/routes/settings/edit-profile/+page.svelte index 9c1a467..3218e09 100644 --- a/src/routes/settings/edit-profile/+page.svelte +++ b/src/routes/settings/edit-profile/+page.svelte @@ -1,5 +1,8 @@ -
    @@ -50,14 +84,14 @@ >Change Avatar
    - - + + {#if openAvatars} diff --git a/src/routes/transfers/+page.svelte b/src/routes/transfers/+page.svelte index 6649bff..3fbd8a9 100644 --- a/src/routes/transfers/+page.svelte +++ b/src/routes/transfers/+page.svelte @@ -1,5 +1,8 @@