feat: Ordner-Dialog und Anlegen-Flow
Setzt die Entscheidungen aus #2, #4 und #6 um: Der Knopf "Workspace hinzufuegen..." oeffnet den nativen Ordner-Dialog, legt den Marker an und uebernimmt den Workspace in Context, Liste und Editor. Der Marker ist .dionotes/workspace.ron mit genau (schema_version: 1). Er wird additiv angelegt, vorhandene Dateien bleiben unberuehrt - ein Test haelt das fest. Ein beschaedigter Marker fuehrt dazu, dass der Workspace nicht geoeffnet und der Marker nicht neu geschrieben wird; auch das ist getestet, denn sobald dort ein Passwort-Hash liegt, waere stillschweigendes Neuanlegen ein Weg, den Schutz auszuhebeln. OpenError unterscheidet die drei Zustaende aus #6 - nicht erreichbar, kein Workspace mehr, beschaedigt - plus den Fall eines zu neuen Schemas. Die Display-Impl liefert genau die Formulierungen, mit denen spaeter die Listeneintraege markiert werden. Der Flow folgt der Regel aus #6: Ein abgebrochener Dialog ist kein Fehler und erzeugt keine Meldung. Ein Ordner, der bereits Workspace ist, wird geoeffnet statt abgelehnt - derselbe Knopf nimmt neue und wiedergefundene Workspaces auf, was seine Beschriftung erst rechtfertigt. Ein nicht leerer Ordner loest eine Rueckfrage aus, die ausdruecklich sagt, dass nichts Vorhandenes veraendert wird; das ist beim Import der Normalfall, keine Ausnahme. rfd wird mit denselben Features eingebunden, die dioxus-desktop ohnehin zieht (xdg-portal statt default). Mit den Default-Features waeren neun Wayland-Pakete dazugekommen, die das Portal nicht braucht - so kostet die explizite Abhaengigkeit keinen einzigen zusaetzlichen Eintrag.
This commit is contained in:
@@ -1,24 +1,126 @@
|
||||
use dioxus::prelude::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::route::Route;
|
||||
use crate::workspace::{dir_is_empty, Workspace};
|
||||
use crate::workspace_list::WorkspaceList;
|
||||
|
||||
/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen
|
||||
/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt,
|
||||
/// das Hinzufügen ist Nebenaktion.
|
||||
#[component]
|
||||
pub fn Selector() -> Element {
|
||||
let mut workspace = use_context::<Signal<Option<Workspace>>>();
|
||||
let mut list = use_signal(WorkspaceList::load);
|
||||
let navigator = use_navigator();
|
||||
|
||||
// Ordner, der auf die Bestätigung „ist nicht leer" wartet.
|
||||
let mut pending = use_signal(|| None::<PathBuf>);
|
||||
let mut error = use_signal(|| None::<String>);
|
||||
|
||||
// Übernimmt einen Workspace: Liste voranstellen und sichern, in den
|
||||
// Context legen, in den Editor wechseln.
|
||||
let mut accept = move |opened: Workspace| {
|
||||
list.write().promote(opened.root.clone());
|
||||
if let Err(err) = list.read().save() {
|
||||
// Kein Abbruch: Der Workspace ist offen, nur das Merken schlug fehl.
|
||||
error.set(Some(format!("Der Workspace ist offen, aber die Liste ließ sich nicht speichern: {err}")));
|
||||
}
|
||||
workspace.set(Some(opened));
|
||||
navigator.push(Route::Home {});
|
||||
};
|
||||
|
||||
// Wird sowohl nach dem Ordner-Dialog als auch nach der Rückfrage gebraucht.
|
||||
let mut create_at = move |root: PathBuf| match Workspace::create(root) {
|
||||
Ok(opened) => accept(opened),
|
||||
Err(err) => error.set(Some(format!("Anlegen fehlgeschlagen: {err}"))),
|
||||
};
|
||||
|
||||
let choose = move |_| async move {
|
||||
// Abbruch liefert None und ist kein Fehler — keine Meldung.
|
||||
let Some(handle) = rfd::AsyncFileDialog::new().pick_folder().await else {
|
||||
return;
|
||||
};
|
||||
let root = handle.path().to_path_buf();
|
||||
error.set(None);
|
||||
|
||||
// Ein bereits vorhandener Workspace wird geöffnet, nicht abgelehnt:
|
||||
// derselbe Knopf nimmt neue und wiedergefundene Workspaces auf.
|
||||
if Workspace::has_marker(&root) {
|
||||
match Workspace::open(root) {
|
||||
Ok(opened) => accept(opened),
|
||||
// Die Display-Impl benennt den Zustand bereits: „beschädigt",
|
||||
// „kein Workspace mehr", „nicht erreichbar".
|
||||
Err(err) => error.set(Some(format!("Öffnen fehlgeschlagen: {err}"))),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
match dir_is_empty(&root) {
|
||||
Ok(true) => create_at(root),
|
||||
// Nicht leer ist der Normalfall beim Import — deshalb eine
|
||||
// Rückfrage, keine Ablehnung.
|
||||
Ok(false) => pending.set(Some(root)),
|
||||
Err(err) => error.set(Some(format!("Ordner nicht lesbar: {err}"))),
|
||||
}
|
||||
};
|
||||
|
||||
rsx! {
|
||||
div { class: "workspace-selector-layout",
|
||||
div { class: "workspace-selector-view",
|
||||
// TODO: Workspace-Liste aus workspaces.ron laden und darstellen
|
||||
// (Ordnername, Pfad darunter, Statusabzeichen). Bis dahin nur
|
||||
// der leere Zustand.
|
||||
// TODO: Workspace-Liste darstellen (Ordnername, Pfad, Statusabzeichen)
|
||||
// statt des leeren Zustands, sobald Einträge vorhanden sind.
|
||||
div { class: "empty-state",
|
||||
span { class: "empty-state-title", "Noch kein Workspace" }
|
||||
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
|
||||
// TODO: Nativen Ordner-Dialog anbinden, Marker anlegen und
|
||||
// den Workspace in den Context schreiben.
|
||||
button { class: "btn btn-primary", disabled: true,
|
||||
button { class: "btn btn-primary", onclick: choose,
|
||||
"Workspace hinzufügen…"
|
||||
}
|
||||
if let Some(message) = error() {
|
||||
span { class: "field-error", "{message}" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(root) = pending() {
|
||||
NotEmptyDialog {
|
||||
root: root.clone(),
|
||||
onconfirm: move |root| {
|
||||
pending.set(None);
|
||||
create_at(root);
|
||||
},
|
||||
oncancel: move |_| pending.set(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Rückfrage vor dem Anlegen in einem Ordner, der bereits Dateien enthält.
|
||||
/// Sie soll informieren, nicht beunruhigen — deshalb steht ausdrücklich da,
|
||||
/// dass nichts Vorhandenes verändert wird.
|
||||
#[component]
|
||||
fn NotEmptyDialog(root: PathBuf, onconfirm: EventHandler<PathBuf>, oncancel: EventHandler<()>) -> Element {
|
||||
let shown = root.display().to_string();
|
||||
let confirmed = root.clone();
|
||||
|
||||
rsx! {
|
||||
div { class: "dialog-scrim",
|
||||
div { class: "dialog",
|
||||
h2 { "Ordner ist nicht leer" }
|
||||
p {
|
||||
code { "{shown}" }
|
||||
" enthält bereits Dateien. DioNotes legt darin nur den Ordner "
|
||||
code { ".dionotes" }
|
||||
" an und verändert nichts Vorhandenes."
|
||||
}
|
||||
div { class: "dialog-actions",
|
||||
button { class: "btn btn-ghost", onclick: move |_| oncancel.call(()), "Abbrechen" }
|
||||
button {
|
||||
class: "btn btn-primary",
|
||||
onclick: move |_| onconfirm.call(confirmed.clone()),
|
||||
"Workspace anlegen"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user