feat: Willkommensnotiz fuer frisch angelegte Workspaces

Ein neu angelegter Workspace bekommt eine Willkommen.md mit einer
Kurzanleitung: was ein Workspace ist, dass Unterordner funktionieren, wozu
.dionotes da ist und wo der Knopf zum Neu-Einlesen sitzt. Sie wird direkt im
Editor geoeffnet, statt den Nutzer vor eine leere Flaeche zu setzen.

Nur bei einem leeren Ordner. Wer einen Ordner voller Notizen importiert, will
keine fremde Datei dazwischen - er kennt das Programm dann meist schon. Die
Pruefung liegt vor dem Anlegen von .dionotes, denn danach ist der Ordner nie
mehr leer; genau diesen Fehler hat der Test beim ersten Anlauf gefunden.

Damit die Notiz angezeigt werden kann, gibt es jetzt die geoeffnete Notiz als
eigenen Context - getrennt vom Workspace, weil sie sich aendert, ohne dass der
Workspace wechselt. Der Editor laedt ihren Inhalt beim Wechsel nach, auf einem
eigenen Thread.

Dadurch schrumpft das TODO im Notiz-Baum auf eine Zeile, die gleich mit
erledigt ist: Ein Klick auf eine Datei oeffnet sie und markiert sie in der
Leiste. Die dafuer vorhandene CSS-Regel .file-selector.active hat damit
wieder einen Nutzer.

Ausdruecklich nicht dabei: Speichern, Autosave, der Umgang mit ungesicherten
Aenderungen. Der Editor zeigt den Inhalt, Aenderungen landen bisher nirgends -
das gehoert zum Notiz-Handling und steht als TODO im Code.
This commit is contained in:
2026-08-28 10:57:17 +02:00
parent 7917fa08cc
commit c8cf525ec5
4 changed files with 128 additions and 12 deletions

View File

@@ -24,14 +24,35 @@ pub fn Home() -> Element {
#[component]
pub fn Editor() -> Element {
let note = use_context::<Signal<Option<PathBuf>>>();
let mut source = use_signal(String::new);
// Beim Wechsel der Notiz den Inhalt nachladen — auf einem eigenen Thread,
// damit eine große Datei auf langsamem Laufwerk nicht die Oberfläche hält.
use_effect(move || {
let path = note.read().clone();
spawn(async move {
let text = match path {
Some(path) => tokio::task::spawn_blocking(move || {
std::fs::read_to_string(path).unwrap_or_default()
})
.await
.unwrap_or_default(),
None => String::new(),
};
source.set(text);
});
});
rsx! {
CodeEditor {
class: "editor-class",
value: source(),
language: Language::Markdown,
theme: Theme::CATPPUCCIN_MOCHA,
// TODO: Änderungen landen bisher nirgends. Speichern, Autosave und
// der Umgang mit ungesicherten Änderungen gehören zum
// Notiz-Handling und damit in den nächsten Effort.
oninput: move |value| source.set(value),
}
}
@@ -119,9 +140,12 @@ fn TreeLevel(nodes: Vec<TreeNode>, expanded: Signal<HashSet<PathBuf>>, depth: us
#[component]
fn TreeEntry(node: TreeNode, mut expanded: Signal<HashSet<PathBuf>>, depth: usize) -> Element {
let mut note = use_context::<Signal<Option<PathBuf>>>();
let navigator = use_navigator();
let path = node.path().to_path_buf();
let name = node.name().to_string();
let is_open = expanded.read().contains(&path);
let is_current = note.read().as_deref() == Some(path.as_path());
match node {
TreeNode::Folder { children, .. } => {
@@ -144,15 +168,20 @@ fn TreeEntry(node: TreeNode, mut expanded: Signal<HashSet<PathBuf>>, depth: usiz
}
}
}
TreeNode::File { .. } => rsx! {
// TODO: Klick öffnet die Notiz im Editor. Gehört zum Notiz-Handling
// und damit in den nächsten Effort, nicht in diese Map.
button {
class: "file-selector",
style: "--depth: {depth}",
span { class: "tree-caret" }
"{name}"
TreeNode::File { .. } => {
let opened = path.clone();
rsx! {
button {
class: if is_current { "file-selector active" } else { "file-selector" },
style: "--depth: {depth}",
onclick: move |_| {
note.set(Some(opened.clone()));
navigator.push(Route::Editor {});
},
span { class: "tree-caret" }
"{name}"
}
}
},
}
}
}