Skip to content

Commit f6a6734

Browse files
timabellclaude
andcommitted
feat: Add change folder button to Dioxus sidebar (#20)
Add a 📂 button in the sidebar header that opens the OS folder picker, allowing users to change their notes folder after initial setup. When a new folder is selected: - Config is saved to disk - File tree is rebuilt with new folder contents - Current file/document state is cleared - Folder picker opens at current notes folder location Implementation changes: - Convert notes_path from prop to Signal for runtime updates - Add sidebar-header div with flex layout for button placement - Add CSS styling for change-folder-btn with hover/active states Prompts: - "dioxus needs the ability to choose another folder after first run (like kotlin app has)" - "way too complex, think we just want a button that pops the folder choose, if the user wants to create a folder they can do that in the filesystem gui themselves. reusing the setup is the wrong approach" - "can we pre-fill the folder dialog with the currently selected folder?" - "also rename the button 📂" - "works great, commit" Fixes #20 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b383ea6 commit f6a6734

2 files changed

Lines changed: 115 additions & 18 deletions

File tree

crates/markdown-neuraxis-dioxus/src/assets/solarized-light.css

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,39 @@ body {
4040

4141
.sidebar h2 {
4242
margin-top: 0;
43+
margin-bottom: 0;
4344
color: var(--base01);
4445
}
4546

47+
.sidebar-header {
48+
display: flex;
49+
justify-content: space-between;
50+
align-items: center;
51+
margin-bottom: 8px;
52+
}
53+
54+
.change-folder-btn {
55+
background: var(--base3);
56+
border: 1px solid var(--base1);
57+
border-radius: 4px;
58+
padding: 4px 8px;
59+
font-size: 12px;
60+
color: var(--base01);
61+
cursor: pointer;
62+
transition: all 0.2s;
63+
}
64+
65+
.change-folder-btn:hover {
66+
background: var(--base2);
67+
border-color: var(--blue);
68+
color: var(--blue);
69+
}
70+
71+
.change-folder-btn:active {
72+
background: var(--blue);
73+
color: var(--base3);
74+
}
75+
4676
.file-list {
4777
margin-top: 12px;
4878
}

crates/markdown-neuraxis-dioxus/src/ui/app.rs

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
use crate::platform::pick_folder;
12
use dioxus::prelude::*;
3+
use markdown_neuraxis_config::Config;
24
use markdown_neuraxis_engine::{
35
Document, FileTree, MarkdownFile, Snapshot, editing::commands::Cmd, io,
46
};
@@ -38,20 +40,24 @@ pub fn App(notes_path: PathBuf) -> Element {
3840
notes_path.display()
3941
);
4042

43+
// Notes path as a signal so it can be changed at runtime
44+
let notes_path = use_signal(|| notes_path);
45+
4146
// Error state for runtime errors
4247
let mut error_state = use_signal(|| None::<RuntimeError>);
4348

4449
// Build file tree
4550
let mut file_tree = use_signal(|| {
46-
log::info!("Building file tree for: {}", notes_path.display());
47-
match io::build_file_tree(&notes_path) {
51+
let path = notes_path.read();
52+
log::info!("Building file tree for: {}", path.display());
53+
match io::build_file_tree(&path) {
4854
Ok(tree) => {
4955
log::info!("File tree built successfully");
5056
tree
5157
}
5258
Err(e) => {
5359
log::error!("Error building file tree: {e}");
54-
FileTree::new(notes_path.clone())
60+
FileTree::new(path.clone())
5561
}
5662
}
5763
});
@@ -66,17 +72,17 @@ pub fn App(notes_path: PathBuf) -> Element {
6672

6773
// Create callbacks outside the rsx! block for cleaner code
6874
let on_sidebar_file_select = {
69-
let notes_path = notes_path.clone();
7075
let mut selected_file = selected_file;
7176
let mut current_document = current_document;
7277
let mut current_snapshot = current_snapshot;
7378
let mut error_state = error_state;
7479
let mut mobile_nav_open = mobile_nav_open;
7580
let mut focused_folder = focused_folder;
7681
move |markdown_file: MarkdownFile| {
82+
let path = notes_path.read();
7783
load_existing_document(
7884
&markdown_file,
79-
&notes_path,
85+
&path,
8086
&mut selected_file,
8187
&mut current_document,
8288
&mut current_snapshot,
@@ -90,15 +96,15 @@ pub fn App(notes_path: PathBuf) -> Element {
9096
};
9197

9298
let on_file_navigate = {
93-
let notes_path = notes_path.clone();
9499
let mut selected_file = selected_file;
95100
let mut current_document = current_document;
96101
let mut current_snapshot = current_snapshot;
97102
let mut error_state = error_state;
98103
move |file_path: PathBuf| {
104+
let path = notes_path.read();
99105
navigate_to_path(
100106
file_path,
101-
&notes_path,
107+
&path,
102108
&mut selected_file,
103109
&mut current_document,
104110
&mut current_snapshot,
@@ -108,14 +114,14 @@ pub fn App(notes_path: PathBuf) -> Element {
108114
};
109115

110116
let on_wikilink_navigate = {
111-
let notes_path = notes_path.clone();
112117
let mut selected_file = selected_file;
113118
let mut current_document = current_document;
114119
let mut current_snapshot = current_snapshot;
115120
let mut error_state = error_state;
116121
let mut file_tree = file_tree;
117122
let mut focused_folder = focused_folder;
118123
move |target: String| {
124+
let path = notes_path.read();
119125
// First check if target matches a folder
120126
let folder_path = file_tree.read().find_folder(&target);
121127
if let Some(folder_path) = folder_path {
@@ -131,7 +137,7 @@ pub fn App(notes_path: PathBuf) -> Element {
131137

132138
// Not a folder, resolve as file - clear any folder focus
133139
focused_folder.set(None);
134-
let markdown_file = resolve_wikilink(&target, &notes_path);
140+
let markdown_file = resolve_wikilink(&target, &path);
135141
// Expand parent folders so the file is visible in the tree
136142
if let Some(parent) = markdown_file.relative_path().parent()
137143
&& !parent.as_str().is_empty()
@@ -142,7 +148,7 @@ pub fn App(notes_path: PathBuf) -> Element {
142148
}
143149
load_document(
144150
markdown_file,
145-
&notes_path,
151+
&path,
146152
&mut selected_file,
147153
&mut current_document,
148154
&mut current_snapshot,
@@ -152,7 +158,7 @@ pub fn App(notes_path: PathBuf) -> Element {
152158
};
153159

154160
let on_command = create_command_callback(
155-
notes_path.clone(),
161+
notes_path,
156162
selected_file,
157163
current_document,
158164
current_snapshot,
@@ -182,7 +188,67 @@ pub fn App(notes_path: PathBuf) -> Element {
182188
}
183189
div {
184190
class: if *mobile_nav_open.read() { "sidebar mobile-visible" } else { "sidebar" },
185-
h2 { "Files" }
191+
div {
192+
class: "sidebar-header",
193+
h2 { "Files" }
194+
button {
195+
class: "change-folder-btn",
196+
title: "Change notes folder",
197+
onclick: move |_| {
198+
let mut notes_path = notes_path;
199+
let mut file_tree = file_tree;
200+
let mut selected_file = selected_file;
201+
let mut current_document = current_document;
202+
let mut current_snapshot = current_snapshot;
203+
let mut focused_folder = focused_folder;
204+
let mut error_state = error_state;
205+
206+
let current_path = notes_path.read().clone();
207+
spawn(async move {
208+
if let Some(new_path) = pick_folder(Some(&current_path)).await {
209+
// Save the new path to config
210+
let config = Config { notes_path: new_path.clone() };
211+
match config.save() {
212+
Ok(()) => {
213+
log::info!("Config saved with new notes path: {}", new_path.display());
214+
215+
// Update notes_path signal
216+
notes_path.set(new_path.clone());
217+
218+
// Rebuild file tree
219+
match io::build_file_tree(&new_path) {
220+
Ok(tree) => {
221+
log::info!("File tree rebuilt successfully");
222+
file_tree.set(tree);
223+
}
224+
Err(e) => {
225+
log::error!("Error building file tree: {e}");
226+
file_tree.set(FileTree::new(new_path));
227+
}
228+
}
229+
230+
// Clear current file state
231+
selected_file.set(None);
232+
current_document.set(None);
233+
current_snapshot.set(None);
234+
focused_folder.set(None);
235+
error_state.set(None);
236+
}
237+
Err(e) => {
238+
RuntimeError::log_and_set(
239+
&mut error_state,
240+
"Failed to save config".to_string(),
241+
e,
242+
);
243+
}
244+
}
245+
}
246+
// If None (cancelled), do nothing
247+
});
248+
},
249+
"📂"
250+
}
251+
}
186252
super::components::TreeView {
187253
tree: ReadSignal::from(file_tree),
188254
selected_file: selected_file.read().clone(),
@@ -203,7 +269,7 @@ pub fn App(notes_path: PathBuf) -> Element {
203269
super::components::MainPanel {
204270
file: file.clone(),
205271
snapshot: snapshot.clone(),
206-
notes_path: notes_path.clone(),
272+
notes_path: notes_path.read().clone(),
207273
document: document.clone(),
208274
on_file_select: Some(Callback::new(on_file_navigate)),
209275
on_command,
@@ -368,14 +434,15 @@ pub fn resolve_wikilink(target: &str, _notes_path: &Path) -> MarkdownFile {
368434

369435
/// Create a command callback for document editing
370436
fn create_command_callback(
371-
notes_path: PathBuf,
437+
notes_path: Signal<PathBuf>,
372438
selected_file: Signal<Option<MarkdownFile>>,
373439
mut current_document: Signal<Option<Arc<Document>>>,
374440
mut current_snapshot: Signal<Option<Snapshot>>,
375441
mut file_tree: Signal<FileTree>,
376442
mut error_state: Signal<Option<RuntimeError>>,
377443
) -> impl FnMut(Cmd) + 'static {
378444
move |cmd: Cmd| {
445+
let path = notes_path.read();
379446
let document_arc = current_document.read().clone();
380447
if let Some(mut document_arc) = document_arc {
381448
// Use Arc::make_mut for efficient copy-on-write
@@ -388,13 +455,13 @@ fn create_command_callback(
388455
let content = document.text();
389456

390457
// Check if file exists before writing
391-
let file_existed = io::read_file(file.relative_path(), &notes_path).is_ok();
458+
let file_existed = io::read_file(file.relative_path(), &path).is_ok();
392459

393-
match io::write_file(file.relative_path(), &notes_path, &content) {
460+
match io::write_file(file.relative_path(), &path, &content) {
394461
Ok(()) => {
395462
if !file_existed {
396-
let absolute_path = file.relative_path().to_path(&notes_path);
397-
file_tree.write().add_file(&absolute_path, &notes_path);
463+
let absolute_path = file.relative_path().to_path(&*path);
464+
file_tree.write().add_file(&absolute_path, &path);
398465
log::info!(
399466
"New file created and auto-saved: {:?}",
400467
file.relative_path()

0 commit comments

Comments
 (0)