feat: Notiz-Baum in der Datei-Leiste
Setzt Issue #7 um und schliesst damit die Workspace-Map ab. Die files-bar zeigt nicht mehr zwei Platzhalter-Routen, sondern den rekursiv eingelesenen Inhalt des offenen Workspace. TreeNode ist ein verschachteltes Enum mit einer einzigen File-Variante fuer alle Dateitypen - wer wissen will, ob es eine Notiz ist, fragt path.extension(). Aufgebaut wird ueber std::fs::read_dir mit eigener Rekursion; walkdir liegt zwar im Graph, iteriert aber flach, woraus einen Baum zu bauen mehr Code waere. Eingesammelt wird alles ausser Namen mit fuehrendem Punkt - Dateien wie Ordner, womit .dionotes und .git ohne Ausnahmeliste wegfallen - und ausser Symlinks, deren Verfolgung einen Zyklusschutz erfordern wuerde. Leere Ordner werden angezeigt, sortiert wird Ordner-vor-Dateien und alphabetisch ohne Ruecksicht auf Gross-/Kleinschreibung. Der Aufklapp-Zustand liegt als HashSet<PathBuf> neben dem Baum, nicht darin: Es gibt keinen Filewatcher, der Nutzer liest bei Bedarf neu ein - steckte expanded im Knoten, klappte jeder Refresh saemtliche Ordner zu. Persistiert wird er nicht. Mit dem Baum entfaellt die Tab-Leiste im Editor und damit EDITOR_TABS, der letzte Rest des in #5 dokumentierten Pflegepunkts. strum wird dadurch nirgends mehr gebraucht und faellt samt strum_macros aus dem Graph. Ausserdem entfernt: der Dioxus-Vorlagentext Hero samt header.svg, der nach dem Oeffnen eines Workspace die rechte Haelfte mit ungestylten Werbelinks fuellte. Home ist jetzt ein Empty-State.
This commit is contained in:
@@ -1,38 +1,23 @@
|
||||
use dioxus::prelude::*;
|
||||
use dioxus_code::Theme;
|
||||
use dioxus_code_editor::{CodeEditor, Language};
|
||||
use strum::EnumMessage;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::components::custom_link::custom_link_component::CustomLink;
|
||||
use crate::route::Route;
|
||||
use crate::tree::{read_tree, TreeNode};
|
||||
use crate::workspace::Workspace;
|
||||
|
||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||
|
||||
/// Home page
|
||||
/// Was rechts steht, solange keine Notiz gewählt ist.
|
||||
///
|
||||
/// TODO: Sobald ein Klick im Baum eine Notiz öffnet, ist das der Ort für den
|
||||
/// zuletzt bearbeiteten Stand oder eine Kurzhilfe. Gehört zum Notiz-Handling.
|
||||
#[component]
|
||||
pub fn Home() -> Element {
|
||||
rsx! {
|
||||
Hero {}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Unveränderter Dioxus-Vorlagentext. Ersetzen, sobald klar ist, was
|
||||
// beim Öffnen eines Workspace angezeigt wird, bevor eine Notiz gewählt wurde.
|
||||
#[component]
|
||||
pub fn Hero() -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
id: "hero",
|
||||
img { src: HEADER_SVG, id: "header" }
|
||||
div { id: "links",
|
||||
a { href: "https://dioxuslabs.com/learn/0.7/", "📚 Learn Dioxus" }
|
||||
a { href: "https://dioxuslabs.com/awesome", "🚀 Awesome Dioxus" }
|
||||
a { href: "https://github.com/dioxus-community/", "📡 Community Libraries" }
|
||||
a { href: "https://github.com/DioxusLabs/sdk", "⚙️ Dioxus Development Kit" }
|
||||
a { href: "https://marketplace.visualstudio.com/items?itemName=DioxusLabs.dioxus", "💫 VSCode Extension" }
|
||||
a { href: "https://discord.gg/XgGxMSkvUM", "👋 Community Discord" }
|
||||
}
|
||||
div { class: "empty-state",
|
||||
span { class: "empty-state-title", "Keine Notiz geöffnet" }
|
||||
span { "Wähle links eine Notiz aus." }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +37,7 @@ pub fn Editor() -> Element {
|
||||
}
|
||||
}
|
||||
|
||||
/// Die Editor-Hülle: Datei-Leiste links, Editorfläche rechts.
|
||||
/// Die Editor-Hülle: Notiz-Baum links, Editorfläche rechts.
|
||||
///
|
||||
/// Trägt zugleich den Zustands-Guard — ohne offenen Workspace hat der Editor
|
||||
/// nichts anzuzeigen. Der Fall ist real: Hot-Reload setzt den Context zurück,
|
||||
@@ -68,25 +53,51 @@ pub fn EditorShell() -> Element {
|
||||
}
|
||||
});
|
||||
|
||||
// Offene Ordner liegen **neben** dem Baum, nicht darin: Es gibt keinen
|
||||
// Filewatcher, der Nutzer liest bei Bedarf neu ein — steckte `expanded`
|
||||
// im Knoten, klappte jeder Refresh sämtliche Ordner zu.
|
||||
let expanded = use_signal(HashSet::<PathBuf>::new);
|
||||
|
||||
// Einlesen auf einem eigenen Thread: Ein Workspace auf einem langsamen
|
||||
// Laufwerk würde die Oberfläche sonst blockieren.
|
||||
let mut tree = use_resource(move || {
|
||||
let root = workspace.read().as_ref().map(|ws| ws.root.clone());
|
||||
async move {
|
||||
let Some(root) = root else {
|
||||
return Vec::new();
|
||||
};
|
||||
tokio::task::spawn_blocking(move || read_tree(&root).unwrap_or_default())
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
}
|
||||
});
|
||||
|
||||
if workspace.read().is_none() {
|
||||
return rsx! {};
|
||||
}
|
||||
|
||||
let nodes = tree.read().clone().unwrap_or_default();
|
||||
|
||||
rsx! {
|
||||
div {
|
||||
class: "layout-container",
|
||||
div {
|
||||
class: "files-bar",
|
||||
// TODO: Durch den Notiz-Baum des offenen Workspace ersetzen
|
||||
// (rekursiv eingelesen, aufklappbar). Bis dahin die Routen.
|
||||
for entry in Route::EDITOR_TABS {
|
||||
CustomLink {
|
||||
key: "{entry}",
|
||||
label: entry.get_message().unwrap_or_default(),
|
||||
href: entry.clone(),
|
||||
css: "file-selector"
|
||||
div { class: "files-bar-head",
|
||||
span { class: "field-label", "Notizen" }
|
||||
// Kein Filewatcher — externe Änderungen holt man sich hier.
|
||||
button {
|
||||
class: "btn btn-ghost btn-icon",
|
||||
title: "Neu einlesen",
|
||||
onclick: move |_| tree.restart(),
|
||||
"⟳"
|
||||
}
|
||||
}
|
||||
if nodes.is_empty() {
|
||||
div { class: "files-bar-empty", "Keine Notizen in diesem Workspace." }
|
||||
} else {
|
||||
TreeLevel { nodes: nodes.clone(), expanded, depth: 0 }
|
||||
}
|
||||
}
|
||||
div {
|
||||
class: "editor-container",
|
||||
@@ -95,3 +106,53 @@ pub fn EditorShell() -> Element {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Eine Ebene des Baums. Ruft sich für aufgeklappte Ordner selbst auf.
|
||||
#[component]
|
||||
fn TreeLevel(nodes: Vec<TreeNode>, expanded: Signal<HashSet<PathBuf>>, depth: usize) -> Element {
|
||||
rsx! {
|
||||
for node in nodes {
|
||||
TreeEntry { key: "{node.path().display()}", node: node.clone(), expanded, depth }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn TreeEntry(node: TreeNode, mut expanded: Signal<HashSet<PathBuf>>, depth: usize) -> Element {
|
||||
let path = node.path().to_path_buf();
|
||||
let name = node.name().to_string();
|
||||
let is_open = expanded.read().contains(&path);
|
||||
|
||||
match node {
|
||||
TreeNode::Folder { children, .. } => {
|
||||
let toggle_path = path.clone();
|
||||
rsx! {
|
||||
button {
|
||||
class: "file-selector is-folder",
|
||||
style: "--depth: {depth}",
|
||||
onclick: move |_| {
|
||||
let mut open = expanded.write();
|
||||
if !open.remove(&toggle_path) {
|
||||
open.insert(toggle_path.clone());
|
||||
}
|
||||
},
|
||||
span { class: "tree-caret", if is_open { "▾" } else { "▸" } }
|
||||
"{name}"
|
||||
}
|
||||
if is_open {
|
||||
TreeLevel { nodes: children, expanded, depth: depth + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
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}"
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user