refactor: Router zusammenfuehren und offenen Workspace im Context fuehren
Die App hatte zwei Routable-Enums, zwischen denen ein Signal<bool> umschaltete.
Der Schalter wurde nie gesetzt - beim ersten Umlegen waere aufgefallen, dass
sich beide Router dieselbe History teilen und beide "/" belegen: Wer im
Selector auf "Open Workspace" klickt, steht auf /ws_open, und der
Editor-Router kennt nur / und /editor.
Jetzt ein Enum, der Editor-Teil unter #[nest("/workspace")]. An die Stelle des
bool tritt Signal<Option<Workspace>> in einem Context-Provider - Props
scheiden aus, weil sich durch ein Outlet keine durchreichen lassen. Fehlt der
Workspace, schickt ein Guard im EditorShell-Layout zurueck zum Selector; der
Fall tritt real bei Hot-Reload auf, wo der Context faellt und die Route bleibt.
Der Selector verliert seine Reiter und wird eine einzige Route. Das folgt aus
dem UI-Prototyp in Issue #8: Oeffnen ist der Alltagsfall, Hinzufuegen passiert
pro Workspace einmal - zwei gleichrangige Reiter verlangten bei jedem Start
eine Entscheidung, die fast immer dieselbe ist. Damit entfallen der leere
Stub create_new_workspace.rs, die Tab-Konstante fuer den Selector und die
zugehoerigen CSS-Klassen.
Weil EnumIter nicht mehr gebraucht wird, faellt strum_macros als direkte
Abhaengigkeit weg; strum re-exportiert die Derives bereits.
Entscheidungen dazu in den Issues #5, #6 und #8.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2330,7 +2330,6 @@ dependencies = [
|
|||||||
"dioxus-code",
|
"dioxus-code",
|
||||||
"dioxus-code-editor",
|
"dioxus-code-editor",
|
||||||
"strum",
|
"strum",
|
||||||
"strum_macros",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ edition = "2021"
|
|||||||
dioxus = { version = "0.7.1", features = ["router"] }
|
dioxus = { version = "0.7.1", features = ["router"] }
|
||||||
dioxus-code = "0.1.2"
|
dioxus-code = "0.1.2"
|
||||||
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
|
dioxus-code-editor = { version = "0.1.2", features = ["all-languages"] }
|
||||||
strum = { version = "0.28.0", features = ["strum_macros", "derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
strum_macros = "0.28.0"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["desktop"]
|
default = ["desktop"]
|
||||||
|
|||||||
@@ -49,30 +49,6 @@
|
|||||||
transform: scaleY(1);
|
transform: scaleY(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Workspace selector tab ------------------------------------------------- */
|
|
||||||
|
|
||||||
.workspace-selector {
|
|
||||||
padding: var(--dn-space-2) var(--dn-space-4);
|
|
||||||
border-radius: var(--dn-radius);
|
|
||||||
color: var(--dn-fg-muted);
|
|
||||||
font-size: var(--dn-text-base);
|
|
||||||
font-weight: 500;
|
|
||||||
text-decoration: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
transition: color var(--dn-dur-fast) var(--dn-ease),
|
|
||||||
background-color var(--dn-dur-fast) var(--dn-ease);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-selector:hover {
|
|
||||||
color: var(--dn-fg);
|
|
||||||
background: var(--dn-bg-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-selector.active {
|
|
||||||
color: var(--dn-fg-on-accent);
|
|
||||||
background: var(--dn-accent);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Buttons ---------------------------------------------------------------- */
|
/* --- Buttons ---------------------------------------------------------------- */
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|||||||
@@ -25,15 +25,6 @@
|
|||||||
padding: var(--dn-space-6);
|
padding: var(--dn-space-6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-selector-links {
|
|
||||||
display: flex;
|
|
||||||
gap: var(--dn-space-1);
|
|
||||||
padding: var(--dn-space-1);
|
|
||||||
background: var(--dn-bg-sunken);
|
|
||||||
border: 1px solid var(--dn-border);
|
|
||||||
border-radius: var(--dn-radius-lg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.workspace-selector-view {
|
.workspace-selector-view {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 34rem;
|
max-width: 34rem;
|
||||||
|
|||||||
@@ -198,9 +198,18 @@
|
|||||||
.flex {
|
.flex {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.table {
|
||||||
|
display: table;
|
||||||
|
}
|
||||||
|
.border-collapse {
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
.transform {
|
.transform {
|
||||||
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
transform: var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,);
|
||||||
}
|
}
|
||||||
|
.resize {
|
||||||
|
resize: both;
|
||||||
|
}
|
||||||
.border {
|
.border {
|
||||||
border-style: var(--tw-border-style);
|
border-style: var(--tw-border-style);
|
||||||
border-width: 1px;
|
border-width: 1px;
|
||||||
@@ -208,6 +217,9 @@
|
|||||||
.text-wrap {
|
.text-wrap {
|
||||||
text-wrap: wrap;
|
text-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
.underline {
|
||||||
|
text-decoration-line: underline;
|
||||||
|
}
|
||||||
.outline {
|
.outline {
|
||||||
outline-style: var(--tw-outline-style);
|
outline-style: var(--tw-outline-style);
|
||||||
outline-width: 1px;
|
outline-width: 1px;
|
||||||
|
|||||||
23
src/main.rs
23
src/main.rs
@@ -1,9 +1,12 @@
|
|||||||
mod views;
|
|
||||||
mod components;
|
mod components;
|
||||||
|
mod route;
|
||||||
|
mod views;
|
||||||
|
mod workspace;
|
||||||
|
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use crate::views::editor::dionotes_editor::EditorRoute;
|
|
||||||
use crate::views::workspace_selector::selector::WorkspaceRoute;
|
use crate::route::Route;
|
||||||
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
const FAVICON: Asset = asset!("/assets/favicon.ico");
|
||||||
|
|
||||||
@@ -22,7 +25,11 @@ fn main() {
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn App() -> Element {
|
fn App() -> Element {
|
||||||
let workspace_selected: Signal<bool> = use_signal(|| false);
|
// Der offene Workspace als Context, nicht als Prop: Er wird im Selector
|
||||||
|
// gesetzt und in der Datei-Leiste gelesen — beide hängen unter einem
|
||||||
|
// Outlet, durch das sich keine Props durchreichen lassen.
|
||||||
|
use_context_provider(|| Signal::new(None::<Workspace>));
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
document::Link { rel: "icon", href: FAVICON }
|
document::Link { rel: "icon", href: FAVICON }
|
||||||
// Tailwind first: its preflight lives in @layer base, our unlayered
|
// Tailwind first: its preflight lives in @layer base, our unlayered
|
||||||
@@ -32,12 +39,6 @@ fn App() -> Element {
|
|||||||
document::Link { rel: "stylesheet", href: BASE_CSS }
|
document::Link { rel: "stylesheet", href: BASE_CSS }
|
||||||
document::Link { rel: "stylesheet", href: LAYOUT_CSS }
|
document::Link { rel: "stylesheet", href: LAYOUT_CSS }
|
||||||
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
|
document::Link { rel: "stylesheet", href: COMPONENTS_CSS }
|
||||||
if workspace_selected() {
|
Router::<Route> {}
|
||||||
Router::<EditorRoute> {}
|
|
||||||
} else {
|
|
||||||
Router::<WorkspaceRoute> {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
36
src/route.rs
Normal file
36
src/route.rs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
use dioxus::prelude::*;
|
||||||
|
use strum::EnumMessage;
|
||||||
|
|
||||||
|
use crate::views::editor::dionotes_editor::{Editor, EditorShell, Home};
|
||||||
|
use crate::views::workspace_selector::selector::Selector;
|
||||||
|
|
||||||
|
/// Alle Routen der App in einem Enum.
|
||||||
|
///
|
||||||
|
/// Bewusst *ein* Router statt zweier: Zwei Routable-Enums teilen sich dieselbe
|
||||||
|
/// History, und beide hatten `/` belegt — beim Umschalten hätte der Editor eine
|
||||||
|
/// URL zu sehen bekommen, die er nicht kennt.
|
||||||
|
#[derive(Debug, Clone, Routable, PartialEq, EnumMessage)]
|
||||||
|
#[rustfmt::skip]
|
||||||
|
pub enum Route {
|
||||||
|
#[nest("/workspace")]
|
||||||
|
#[layout(EditorShell)]
|
||||||
|
#[route("/")]
|
||||||
|
#[strum(message = "Home")]
|
||||||
|
Home {},
|
||||||
|
#[route("/editor")]
|
||||||
|
#[strum(message = "Editor")]
|
||||||
|
Editor {},
|
||||||
|
#[end_layout]
|
||||||
|
#[end_nest]
|
||||||
|
#[route("/")]
|
||||||
|
Selector {},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Route {
|
||||||
|
/// Die Einträge der Datei-Leiste im Editor.
|
||||||
|
///
|
||||||
|
/// Explizit statt über `EnumIter`, weil das Enum jetzt auch den Selector
|
||||||
|
/// enthält und der in dieser Leiste nichts zu suchen hat. Wermutstropfen:
|
||||||
|
/// eine neue Editor-Route hier einzutragen erzwingt der Compiler nicht.
|
||||||
|
pub const EDITOR_TABS: [Self; 2] = [Self::Home {}, Self::Editor {}];
|
||||||
|
}
|
||||||
@@ -1,32 +1,24 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use dioxus_code::Theme;
|
use dioxus_code::Theme;
|
||||||
use dioxus_code_editor::{CodeEditor, Language};
|
use dioxus_code_editor::{CodeEditor, Language};
|
||||||
|
use strum::EnumMessage;
|
||||||
|
|
||||||
use crate::components::custom_link::custom_link_component::CustomLink;
|
use crate::components::custom_link::custom_link_component::CustomLink;
|
||||||
use strum::{EnumMessage, IntoEnumIterator};
|
use crate::route::Route;
|
||||||
use strum_macros::EnumIter;
|
use crate::workspace::Workspace;
|
||||||
|
|
||||||
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
const HEADER_SVG: Asset = asset!("/assets/header.svg");
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
|
|
||||||
#[rustfmt::skip]
|
|
||||||
pub enum EditorRoute {
|
|
||||||
#[layout(Navbar)]
|
|
||||||
#[route("/")]
|
|
||||||
#[strum(message = "Home")]
|
|
||||||
Home {},
|
|
||||||
#[route("/editor")]
|
|
||||||
#[strum(message = "Editor")]
|
|
||||||
Editor {},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Home page
|
/// Home page
|
||||||
#[component]
|
#[component]
|
||||||
fn Home() -> Element {
|
pub fn Home() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
Hero {}
|
Hero {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Unveränderter Dioxus-Vorlagentext. Ersetzen, sobald klar ist, was
|
||||||
|
// beim Öffnen eines Workspace angezeigt wird, bevor eine Notiz gewählt wurde.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Hero() -> Element {
|
pub fn Hero() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -46,7 +38,7 @@ pub fn Hero() -> Element {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
fn Editor() -> Element {
|
pub fn Editor() -> Element {
|
||||||
let mut source = use_signal(String::new);
|
let mut source = use_signal(String::new);
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
@@ -60,15 +52,34 @@ fn Editor() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shared navbar component.
|
/// Die Editor-Hülle: Datei-Leiste 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,
|
||||||
|
/// während die Route bestehen bleibt.
|
||||||
#[component]
|
#[component]
|
||||||
fn Navbar() -> Element {
|
pub fn EditorShell() -> Element {
|
||||||
|
let workspace = use_context::<Signal<Option<Workspace>>>();
|
||||||
|
let navigator = use_navigator();
|
||||||
|
|
||||||
|
use_effect(move || {
|
||||||
|
if workspace.read().is_none() {
|
||||||
|
navigator.replace(Route::Selector {});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if workspace.read().is_none() {
|
||||||
|
return rsx! {};
|
||||||
|
}
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: "layout-container",
|
class: "layout-container",
|
||||||
div {
|
div {
|
||||||
class: "files-bar",
|
class: "files-bar",
|
||||||
for entry in EditorRoute::iter() {
|
// TODO: Durch den Notiz-Baum des offenen Workspace ersetzen
|
||||||
|
// (rekursiv eingelesen, aufklappbar). Bis dahin die Routen.
|
||||||
|
for entry in Route::EDITOR_TABS {
|
||||||
CustomLink {
|
CustomLink {
|
||||||
key: "{entry}",
|
key: "{entry}",
|
||||||
label: entry.get_message().unwrap_or_default(),
|
label: entry.get_message().unwrap_or_default(),
|
||||||
@@ -79,7 +90,7 @@ fn Navbar() -> Element {
|
|||||||
}
|
}
|
||||||
div {
|
div {
|
||||||
class: "editor-container",
|
class: "editor-container",
|
||||||
Outlet::<EditorRoute> { }
|
Outlet::<Route> { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
|
|
||||||
/// Create new Workspace Layout
|
|
||||||
// TODO: Platzhalter — Formular zum Anlegen eines Workspace (Name, Pfad) und
|
|
||||||
// Verdrahtung als Ziel von WorkspaceRoute::NewWorkspace fehlen noch.
|
|
||||||
#[component]
|
|
||||||
fn NewWorkspaceLayout() -> Element {
|
|
||||||
rsx! {
|
|
||||||
div {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1 @@
|
|||||||
pub mod selector;
|
pub mod selector;
|
||||||
pub mod create_new_workspace;
|
|
||||||
@@ -1,60 +1,25 @@
|
|||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use strum::{EnumMessage, IntoEnumIterator};
|
|
||||||
use strum_macros::EnumIter;
|
|
||||||
use crate::components::custom_link::custom_link_component::CustomLink;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Routable, PartialEq, EnumIter, EnumMessage)]
|
/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen
|
||||||
#[rustfmt::skip]
|
/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt,
|
||||||
pub enum WorkspaceRoute {
|
/// das Hinzufügen ist Nebenaktion.
|
||||||
#[layout(Workspace)]
|
|
||||||
#[route("/")]
|
|
||||||
#[strum(message = "Create new Workspace")]
|
|
||||||
NewWorkspace {},
|
|
||||||
#[route("/ws_open")]
|
|
||||||
#[strum(message = "Open Workspace")]
|
|
||||||
OpenWorkspace {},
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Page to create new Workspace
|
|
||||||
#[component]
|
#[component]
|
||||||
fn NewWorkspace() -> Element {
|
pub fn Selector() -> Element {
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div { class: "workspace-selector-layout",
|
||||||
"New Workspace",
|
div { class: "workspace-selector-view",
|
||||||
}
|
// TODO: Workspace-Liste aus workspaces.ron laden und darstellen
|
||||||
}
|
// (Ordnername, Pfad darunter, Statusabzeichen). Bis dahin nur
|
||||||
}
|
// der leere Zustand.
|
||||||
|
div { class: "empty-state",
|
||||||
/// Page to open available Workspaces
|
span { class: "empty-state-title", "Noch kein Workspace" }
|
||||||
#[component]
|
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
|
||||||
fn OpenWorkspace() -> Element {
|
// TODO: Nativen Ordner-Dialog anbinden, Marker anlegen und
|
||||||
rsx! {
|
// den Workspace in den Context schreiben.
|
||||||
div {
|
button { class: "btn btn-primary", disabled: true,
|
||||||
"Open Workspace",
|
"Workspace hinzufügen…"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Workspace Layout
|
|
||||||
#[component]
|
|
||||||
fn Workspace() -> Element {
|
|
||||||
rsx! {
|
|
||||||
div {
|
|
||||||
class: "workspace-selector-layout",
|
|
||||||
div {
|
|
||||||
class: "workspace-selector-links",
|
|
||||||
for route in WorkspaceRoute::iter() {
|
|
||||||
CustomLink {
|
|
||||||
key: "{route}",
|
|
||||||
label: route.get_message().unwrap_or_default(),
|
|
||||||
href: route.clone(),
|
|
||||||
css: "workspace-selector",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
class: "workspace-selector-view",
|
|
||||||
Outlet::<WorkspaceRoute> { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/workspace.rs
Normal file
34
src/workspace.rs
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// Ein geöffneter Workspace — der Ordner auf der Platte, in dem die Notizen liegen.
|
||||||
|
///
|
||||||
|
/// Heute genau ein Feld. Der Typ existiert trotzdem als eigener Name, weil
|
||||||
|
/// `CONTEXT.md` „Workspace" als Domänenbegriff führt und die Einstellungen aus
|
||||||
|
/// `.dionotes/workspace.ron` hier andocken werden.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct Workspace {
|
||||||
|
pub root: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Workspace {
|
||||||
|
/// Anzeigename: der Ordnername. Bewusst abgeleitet statt gespeichert —
|
||||||
|
/// ein eigenes Namensfeld wäre nach dem ersten Umbenennen des Ordners falsch.
|
||||||
|
pub fn name(&self) -> String {
|
||||||
|
self.root
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| self.root.to_string_lossy().into_owned())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::Workspace;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn name_is_the_folder_name() {
|
||||||
|
let ws = Workspace { root: PathBuf::from("C:/Users/marvin/Notizen") };
|
||||||
|
assert_eq!(ws.name(), "Notizen");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user