diff --git a/apps/app/src-tauri/Cargo.lock b/apps/app/src-tauri/Cargo.lock index 160ed674..32a35469 100644 --- a/apps/app/src-tauri/Cargo.lock +++ b/apps/app/src-tauri/Cargo.lock @@ -462,6 +462,7 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" name = "cherit" version = "0.0.1" dependencies = [ + "futures", "log", "serde", "serde_json", @@ -473,6 +474,7 @@ dependencies = [ "tauri-plugin-log", "tauri-plugin-os", "tauri-plugin-store", + "urlencoding", ] [[package]] @@ -1047,6 +1049,21 @@ dependencies = [ "new_debug_unreachable", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -1054,6 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -1121,6 +1139,7 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", "futures-macro", @@ -4608,6 +4627,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" diff --git a/apps/app/src-tauri/Cargo.toml b/apps/app/src-tauri/Cargo.toml index 3ce6e515..27906868 100644 --- a/apps/app/src-tauri/Cargo.toml +++ b/apps/app/src-tauri/Cargo.toml @@ -30,3 +30,5 @@ tauri-plugin-fs = "2" tauri-plugin-store = "2" tauri-plugin-dialog = "2" tauri-plugin-os = "2" +urlencoding = "2" +futures = "0.3.31" diff --git a/apps/app/src-tauri/src/lib.rs b/apps/app/src-tauri/src/lib.rs index d415dab1..95fe9873 100644 --- a/apps/app/src-tauri/src/lib.rs +++ b/apps/app/src-tauri/src/lib.rs @@ -1,3 +1,194 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct FileNode { + pub name: String, + pub path: String, + pub is_directory: bool, + pub children: Vec, +} + +fn sort_nodes(nodes: &mut Vec) { + nodes.sort_by(|a, b| { + if a.is_directory != b.is_directory { + return if a.is_directory { + std::cmp::Ordering::Less + } else { + std::cmp::Ordering::Greater + }; + } + a.name.to_lowercase().cmp(&b.name.to_lowercase()) + }); + + for node in nodes { + if !node.children.is_empty() { + sort_nodes(&mut node.children); + } + } +} + +#[cfg(not(target_os = "android"))] +fn build_tree_recursive_desktop(path_str: &str) -> std::io::Result> { + use std::fs; + let mut nodes = Vec::new(); + let entries = fs::read_dir(path_str)?; + + for entry in entries { + if let Ok(entry) = entry { + if let Ok(metadata) = entry.metadata() { + let file_name = entry.file_name().to_string_lossy().to_string(); + + let is_directory = metadata.is_dir(); + let starts_with_dot = file_name.starts_with('.'); + let ends_with_md = file_name.ends_with(".md"); + + if (is_directory && !starts_with_dot) || ends_with_md { + let path = entry.path().to_string_lossy().to_string(); + let mut children = Vec::new(); + + if is_directory { + if let Ok(sub_children) = build_tree_recursive_desktop(&path) { + children = sub_children; + } + } + + nodes.push(FileNode { + name: file_name.trim_end_matches(".md").to_string(), + path, + is_directory, + children, + }); + } + } + } + } + Ok(nodes) +} + +#[cfg(target_os = "android")] +fn build_tree_recursive_android( + app: tauri::AppHandle, + path: String, + document_top_tree_uri: Option, +) -> std::pin::Pin, String>> + Send>> { + Box::pin(async move { + use tauri_plugin_android_fs::AndroidFsExt; + use tauri_plugin_android_fs::FileUri; + use tauri_plugin_android_fs::EntryOptions; + use futures::future::join_all; + + let api = app.android_fs(); + + let json_obj = serde_json::json!({ + "uri": path, + "documentTopTreeUri": document_top_tree_uri + }); + let file_uri = FileUri::from_json_str(&json_obj.to_string()) + .map_err(|e| format!("Failed to create FileUri: {}", e))?; + + let options = EntryOptions { + uri: false, + name: true, + last_modified: false, + len: false, + mime_type: false, + }; + + let entries = api.read_dir_with_options(&file_uri, options) + .map_err(|e| e.to_string())?; + + let mut futures = Vec::new(); + let mut nodes = Vec::new(); + + for entry in entries { + let is_directory = entry.is_dir(); + let name_opt = entry.name(); + + if let Some(name_str) = name_opt { + let name = name_str.to_string(); + let starts_with_dot = name.starts_with('.'); + let ends_with_md = name.ends_with(".md"); + + if (is_directory && !starts_with_dot) || ends_with_md { + let path_uri = format!("{}%2F{}", path, urlencoding::encode(&name)); + + if is_directory { + let app_clone = app.clone(); + let path_clone = path_uri.clone(); + let doc_uri_clone = document_top_tree_uri.clone(); + let name_clone = name.clone(); + + futures.push(async move { + let children_res = build_tree_recursive_android( + app_clone, + path_clone.clone(), + doc_uri_clone + ).await; + + match children_res { + Ok(children) => Some(FileNode { + name: name_clone.trim_end_matches(".md").to_string(), + path: path_clone, + is_directory: true, + children, + }), + Err(_) => None + } + }); + } else { + nodes.push(FileNode { + name: name.trim_end_matches(".md").to_string(), + path: path_uri, + is_directory: false, + children: vec![], + }); + } + } + } + } + + let results = join_all(futures).await; + for res in results { + if let Some(node) = res { + nodes.push(node); + } + } + + Ok(nodes) + }) +} + +#[tauri::command] +async fn build_file_tree( + app: tauri::AppHandle, + path: String, + document_top_tree_uri: Option, +) -> Result, String> { + let nodes; + + #[cfg(target_os = "android")] + { + let mut unsorted_nodes = build_tree_recursive_android(app, path, document_top_tree_uri).await?; + sort_nodes(&mut unsorted_nodes); + nodes = unsorted_nodes; + } + + #[cfg(not(target_os = "android"))] + { + // Suppress unused variable warnings on desktop + let _ = app; + let _ = document_top_tree_uri; + nodes = tauri::async_runtime::spawn_blocking(move || { + let mut n = build_tree_recursive_desktop(&path)?; + sort_nodes(&mut n); + Ok(n) + }).await.map_err(|e| e.to_string())? + .map_err(|e: std::io::Error| e.to_string())?; + } + + Ok(nodes) +} + #[cfg_attr(mobile, tauri::mobile_entry_point)] pub fn run() { tauri::Builder::default() @@ -6,6 +197,7 @@ pub fn run() { .plugin(tauri_plugin_store::Builder::new().build()) .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_android_fs::init()) + .invoke_handler(tauri::generate_handler![build_file_tree]) .setup(|app| { if cfg!(debug_assertions) { app.handle().plugin( diff --git a/apps/app/src/components/sidebar/file_manager/index.svelte b/apps/app/src/components/sidebar/file_manager/index.svelte index 915cde41..4ec568c8 100644 --- a/apps/app/src/components/sidebar/file_manager/index.svelte +++ b/apps/app/src/components/sidebar/file_manager/index.svelte @@ -24,6 +24,7 @@ $effect(() => { if (file_tree) sort_file_tree(file_tree); }); + let collapsed_state: boolean = $state(true); $effect(() => { if (!root_path) return; diff --git a/apps/app/src/components/sidebar/file_manager/items_renderer.svelte b/apps/app/src/components/sidebar/file_manager/items_renderer.svelte index 1b19514a..d0287b4e 100644 --- a/apps/app/src/components/sidebar/file_manager/items_renderer.svelte +++ b/apps/app/src/components/sidebar/file_manager/items_renderer.svelte @@ -257,7 +257,7 @@ ondragstart={(e) => handle_drag_start(e, node)} ondragend={reset_dnd} oncontextmenu={(e) => handle_node_right_click(e, node)} - class="{opened_filenode?.path === node.path ? 'bg-base-content/10' : ''} + class="{opened_filenode?.path === node.path ? 'bg-base-content/10' : ''} {dragged_node?.path === node.path ? 'opacity-50' : ''} py-0.75 w-full hover:text-[color-mix(in_srgb,var(--color-base-content)_85%,black)] truncate block" onclick={(e) => { diff --git a/apps/app/src/lib/file_tree/builder.ts b/apps/app/src/lib/file_tree/builder.ts index c4bede33..52bc83a2 100644 --- a/apps/app/src/lib/file_tree/builder.ts +++ b/apps/app/src/lib/file_tree/builder.ts @@ -1,85 +1,42 @@ -import { readDir, type DirEntry } from '@tauri-apps/plugin-fs'; import { type FileNode, type GenericPath } from '@/types'; -import { - AndroidFs, - type AndroidEntryMetadataWithUri, -} from 'tauri-plugin-android-fs-api'; -import { join } from '@tauri-apps/api/path'; -import { current_platform } from './utils'; +import { invoke } from '@tauri-apps/api/core'; export async function build_file_tree_from_fs({ path, document_top_tree_uri, }: GenericPath): Promise { - let entries: DirEntry[] | AndroidEntryMetadataWithUri[] | undefined; - let base_nodes: FileNode[] | undefined; - - if (current_platform == 'android') { - if (!document_top_tree_uri) - throw new Error('Document top tree URI is not set'); - entries = await AndroidFs.readDir({ - uri: path, - documentTopTreeUri: document_top_tree_uri, + try { + return await invoke('build_file_tree', { + path, + documentTopTreeUri: document_top_tree_uri || null, }); - base_nodes = await transform_android_entries_to_filenode(entries, path); - } else { - entries = await readDir(path); - base_nodes = await transform_entries_to_filenode(entries, path); + } catch (error) { + console.error('Failed to build file tree:', error); + throw error; } +} - const nodes = await Promise.all( - base_nodes.map(async (n) => { - if (!n.is_directory) return n; - const children = await build_file_tree_from_fs({ - path: n.path, - document_top_tree_uri, - }); - return { - ...n, - children, - }; - }) - ); +// These functions are no longer needed but kept if needed for other parts of the app +// or we can remove them if we are sure they are unused. +// Based on the task, we are replacing the logic. +// I'll comment them out or remove them if I'm sure. +// The user said "replace it with the js build file tree function". +// I'll keep the exports but empty or commented if I want to be safe, or just remove them. +// "and replace it with the js build file tree function" -> Replace the implementation of `build_file_tree_from_fs`. +// I'll remove the helper functions as they were only used by `build_file_tree_from_fs`. - return nodes; -} export async function transform_entries_to_filenode( - entries: DirEntry[], + entries: any[], base_dir_path: string ): Promise { - const nodes = await Promise.all( - entries - .filter( - (entry) => - (entry.isDirectory && !entry.name.startsWith('.')) || - entry.name.endsWith('.md') - ) - .map(async (entry) => ({ - name: entry.name.replace(/\.md$/, ''), - path: await join(base_dir_path, entry.name), - is_directory: entry.isDirectory, - children: [], - })) - ); - return nodes; + // Deprecated: logic moved to Rust + return []; } + export async function transform_android_entries_to_filenode( - entries: AndroidEntryMetadataWithUri[], + entries: any[], base_dir_path: string ): Promise { - const nodes = await Promise.all( - entries - .filter( - (entry) => - (entry.type === 'Dir' && !entry.name.startsWith('.')) || - entry.name.endsWith('.md') - ) - .map(async (entry) => ({ - name: entry.name.replace(/\.md$/, ''), - path: `${base_dir_path}%2F${encodeURIComponent(entry.name)}`, - is_directory: entry.type === 'Dir', - children: [], - })) - ); - return nodes; + // Deprecated: logic moved to Rust + return []; } diff --git a/apps/app/src/lib/file_tree/utils/file_tree_utils.ts b/apps/app/src/lib/file_tree/utils/file_tree_utils.ts index ca77ad8e..13e34997 100644 --- a/apps/app/src/lib/file_tree/utils/file_tree_utils.ts +++ b/apps/app/src/lib/file_tree/utils/file_tree_utils.ts @@ -1,4 +1,5 @@ import { type FileNode } from '@/types'; + export function find_unused_name( base_name: string, subtree: FileNode[], @@ -11,6 +12,7 @@ export function find_unused_name( base_name = `Untitled ${++i}`; return base_name; } + export function sort_file_tree(nodes: FileNode[]): FileNode[] { // Sort array in-place nodes.sort((a, b) => {