Merge pull request 'Selector-UI: Liste, Zustandsabzeichen, nebenlaeufige Pruefung' (#13) from feat/selector-ui into main
Reviewed-on: #13
This commit was merged in pull request #13.
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -2334,6 +2334,7 @@ dependencies = [
|
|||||||
"ron",
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"strum",
|
"strum",
|
||||||
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"]
|
|||||||
ron = "0.12.2"
|
ron = "0.12.2"
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
strum = { version = "0.28.0", features = ["derive"] }
|
strum = { version = "0.28.0", features = ["derive"] }
|
||||||
|
tokio = { version = "1.53.1", features = ["rt"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["desktop"]
|
default = ["desktop"]
|
||||||
|
|||||||
@@ -167,6 +167,92 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Workspace-Listeneintrag ---------------------------------------------------
|
||||||
|
Ordnername, darunter der Pfad, rechts bei Bedarf ein Statusabzeichen. Ein
|
||||||
|
defekter Eintrag wird gedämpft, bleibt aber anklickbar: Ein Klick versucht
|
||||||
|
es erneut, womit „Stick wieder eingesteckt" ohne eigenen Knopf abgedeckt ist.
|
||||||
|
-------------------------------------------------------------------------- */
|
||||||
|
|
||||||
|
.ws-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--dn-space-3);
|
||||||
|
width: 100%;
|
||||||
|
padding: var(--dn-space-3);
|
||||||
|
border: 0;
|
||||||
|
border-radius: var(--dn-radius);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font: inherit;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color var(--dn-dur-fast) var(--dn-ease);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item:hover {
|
||||||
|
background: var(--dn-bg-raised);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item-body {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item-name {
|
||||||
|
color: var(--dn-fg);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bei Überlänge vorne kürzen: Das Ende eines Pfades sagt mehr als sein Anfang. */
|
||||||
|
.ws-item-path {
|
||||||
|
direction: rtl;
|
||||||
|
text-align: left;
|
||||||
|
color: var(--dn-fg-faint);
|
||||||
|
font-family: var(--dn-font-code);
|
||||||
|
font-size: var(--dn-text-xs);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-item.is-broken .ws-item-name {
|
||||||
|
color: var(--dn-fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-badge {
|
||||||
|
flex: none;
|
||||||
|
padding: 2px var(--dn-space-2);
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: var(--dn-text-xs);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Drei Zustände, drei Farben — sie führen zu verschiedenen Handlungen:
|
||||||
|
Laufwerk einhängen, Ordner wiederherstellen, Datei reparieren. */
|
||||||
|
.ws-badge.unreachable {
|
||||||
|
background: color-mix(in oklab, var(--dn-warning) 18%, transparent);
|
||||||
|
color: var(--dn-warning);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-badge.no-marker {
|
||||||
|
background: color-mix(in oklab, var(--dn-fg-faint) 22%, transparent);
|
||||||
|
color: var(--dn-fg-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ws-badge.damaged {
|
||||||
|
background: color-mix(in oklab, var(--dn-danger) 18%, transparent);
|
||||||
|
color: var(--dn-danger);
|
||||||
|
}
|
||||||
|
|
||||||
/* --- Dialog --------------------------------------------------------------------
|
/* --- Dialog --------------------------------------------------------------------
|
||||||
Für Unterbrechungen einer laufenden Aktion. Zustände gehören dagegen inline
|
Für Unterbrechungen einer laufenden Aktion. Zustände gehören dagegen inline
|
||||||
an ihren Ort, nicht in ein zweites Fenster.
|
an ihren Ort, nicht in ein zweites Fenster.
|
||||||
|
|||||||
@@ -10,29 +10,50 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* --- Workspace selector ----------------------------------------------------
|
/* --- Workspace selector ----------------------------------------------------
|
||||||
The pre-editor screen: pick or create a workspace. Centered, deliberately
|
The pre-editor screen. One view, no tabs: the list of known workspaces
|
||||||
sparse — it has exactly one job.
|
leads, adding one is the secondary action in the footer. Opening is the
|
||||||
|
everyday case; adding happens once per workspace.
|
||||||
-------------------------------------------------------------------------- */
|
-------------------------------------------------------------------------- */
|
||||||
|
|
||||||
.workspace-selector-layout {
|
.selector-shell {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 40rem;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: var(--dn-space-7) var(--dn-space-5);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
gap: var(--dn-space-5);
|
||||||
justify-content: center;
|
|
||||||
gap: var(--dn-space-6);
|
|
||||||
padding: var(--dn-space-6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace-selector-view {
|
.selector-head {
|
||||||
width: 100%;
|
display: flex;
|
||||||
max-width: 34rem;
|
align-items: baseline;
|
||||||
padding: var(--dn-space-6);
|
justify-content: space-between;
|
||||||
background: var(--dn-bg-sunken);
|
gap: var(--dn-space-4);
|
||||||
border: 1px solid var(--dn-border);
|
}
|
||||||
border-radius: var(--dn-radius-lg);
|
|
||||||
box-shadow: var(--dn-shadow);
|
.selector-title {
|
||||||
|
font-size: var(--dn-text-xl);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nur die Liste scrollt — Kopf und Fuß bleiben stehen. */
|
||||||
|
.selector-list-wrap {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selector-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--dn-space-3);
|
||||||
|
padding-top: var(--dn-space-4);
|
||||||
|
border-top: 1px solid var(--dn-border);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Editor shell ----------------------------------------------------------
|
/* --- Editor shell ----------------------------------------------------------
|
||||||
|
|||||||
@@ -2,40 +2,69 @@ use dioxus::prelude::*;
|
|||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
use crate::workspace::{dir_is_empty, Workspace};
|
use crate::workspace::{dir_is_empty, OpenError, Workspace};
|
||||||
use crate::workspace_list::WorkspaceList;
|
use crate::workspace_list::WorkspaceList;
|
||||||
|
|
||||||
/// Der Bildschirm vor dem Editor: einen bekannten Workspace öffnen oder einen
|
/// Der Bildschirm vor dem Editor. Eine Ansicht, keine Reiter: Die Liste der
|
||||||
/// hinzufügen. Eine einzige Route, keine Reiter — die Workspace-Liste führt,
|
/// bekannten Workspaces führt, das Hinzufügen ist Nebenaktion in der Fußzeile.
|
||||||
/// das Hinzufügen ist Nebenaktion.
|
/// Öffnen ist der Alltagsfall, Hinzufügen passiert pro Workspace einmal.
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Selector() -> Element {
|
pub fn Selector() -> Element {
|
||||||
let mut workspace = use_context::<Signal<Option<Workspace>>>();
|
let mut workspace = use_context::<Signal<Option<Workspace>>>();
|
||||||
let mut list = use_signal(WorkspaceList::load);
|
let mut list = use_signal(WorkspaceList::load);
|
||||||
let navigator = use_navigator();
|
let navigator = use_navigator();
|
||||||
|
|
||||||
// Ordner, der auf die Bestätigung „ist nicht leer" wartet.
|
|
||||||
let mut pending = use_signal(|| None::<PathBuf>);
|
let mut pending = use_signal(|| None::<PathBuf>);
|
||||||
let mut error = use_signal(|| None::<String>);
|
let mut error = use_signal(|| None::<String>);
|
||||||
|
|
||||||
// Übernimmt einen Workspace: Liste voranstellen und sichern, in den
|
// Zustandsprüfung der Einträge, nebenläufig und auf einem eigenen Thread:
|
||||||
// Context legen, in den Editor wechseln.
|
// Auf einem nicht eingehängten Netzlaufwerk läuft schon `is_dir()` in ein
|
||||||
|
// Timeout von Sekunden, und das würde sonst den Selector einfrieren —
|
||||||
|
// ausgerechnet den Bildschirm, den jeder Start zuerst zeigt.
|
||||||
|
//
|
||||||
|
// spawn_blocking setzt die Tokio-Runtime voraus, die dioxus-desktop
|
||||||
|
// mitbringt. Für die web-/mobile-Features müsste das anders laufen —
|
||||||
|
// die sind laut Entscheidung in Issue #1 aber ausdrücklich nicht Ziel.
|
||||||
|
let probed = use_resource(move || {
|
||||||
|
let paths = list.read().entries().to_vec();
|
||||||
|
async move {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
paths
|
||||||
|
.into_iter()
|
||||||
|
.map(|path| {
|
||||||
|
let state = Workspace::open(path.clone()).err();
|
||||||
|
(path, state)
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut accept = move |opened: Workspace| {
|
let mut accept = move |opened: Workspace| {
|
||||||
list.write().promote(opened.root.clone());
|
list.write().promote(opened.root.clone());
|
||||||
if let Err(err) = list.read().save() {
|
if let Err(err) = list.read().save() {
|
||||||
// Kein Abbruch: Der Workspace ist offen, nur das Merken schlug fehl.
|
error.set(Some(format!(
|
||||||
error.set(Some(format!("Der Workspace ist offen, aber die Liste ließ sich nicht speichern: {err}")));
|
"Der Workspace ist offen, aber die Liste ließ sich nicht speichern: {err}"
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
workspace.set(Some(opened));
|
workspace.set(Some(opened));
|
||||||
navigator.push(Route::Home {});
|
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) {
|
let mut create_at = move |root: PathBuf| match Workspace::create(root) {
|
||||||
Ok(opened) => accept(opened),
|
Ok(opened) => accept(opened),
|
||||||
Err(err) => error.set(Some(format!("Anlegen fehlgeschlagen: {err}"))),
|
Err(err) => error.set(Some(format!("Anlegen fehlgeschlagen: {err}"))),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ein Klick versucht es erneut — damit ist „Stick wieder eingesteckt"
|
||||||
|
// ohne eigenen Knopf abgedeckt.
|
||||||
|
let mut open_entry = move |root: PathBuf| match Workspace::open(root) {
|
||||||
|
Ok(opened) => accept(opened),
|
||||||
|
Err(err) => error.set(Some(format!("Öffnen fehlgeschlagen: {err}"))),
|
||||||
|
};
|
||||||
|
|
||||||
let choose = move |_| async move {
|
let choose = move |_| async move {
|
||||||
// Abbruch liefert None und ist kein Fehler — keine Meldung.
|
// Abbruch liefert None und ist kein Fehler — keine Meldung.
|
||||||
let Some(handle) = rfd::AsyncFileDialog::new().pick_folder().await else {
|
let Some(handle) = rfd::AsyncFileDialog::new().pick_folder().await else {
|
||||||
@@ -44,43 +73,75 @@ pub fn Selector() -> Element {
|
|||||||
let root = handle.path().to_path_buf();
|
let root = handle.path().to_path_buf();
|
||||||
error.set(None);
|
error.set(None);
|
||||||
|
|
||||||
// Ein bereits vorhandener Workspace wird geöffnet, nicht abgelehnt:
|
// Ein vorhandener Workspace wird geöffnet, nicht abgelehnt: derselbe
|
||||||
// derselbe Knopf nimmt neue und wiedergefundene Workspaces auf.
|
// Knopf nimmt neue und wiedergefundene Workspaces auf.
|
||||||
if Workspace::has_marker(&root) {
|
if Workspace::has_marker(&root) {
|
||||||
match Workspace::open(root) {
|
open_entry(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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
match dir_is_empty(&root) {
|
match dir_is_empty(&root) {
|
||||||
Ok(true) => create_at(root),
|
Ok(true) => create_at(root),
|
||||||
// Nicht leer ist der Normalfall beim Import — deshalb eine
|
// Nicht leer ist beim Import der Normalfall, keine Ausnahme.
|
||||||
// Rückfrage, keine Ablehnung.
|
|
||||||
Ok(false) => pending.set(Some(root)),
|
Ok(false) => pending.set(Some(root)),
|
||||||
Err(err) => error.set(Some(format!("Ordner nicht lesbar: {err}"))),
|
Err(err) => error.set(Some(format!("Ordner nicht lesbar: {err}"))),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let paths = list.read().entries().to_vec();
|
||||||
|
let states = probed.read().clone();
|
||||||
|
// Solange die Prüfung läuft, bleiben die Einträge unmarkiert. Die lineare
|
||||||
|
// Suche ist bei einer Workspace-Liste billiger als eine Map aufzubauen.
|
||||||
|
let state_of = |path: &PathBuf| -> Option<OpenError> {
|
||||||
|
states
|
||||||
|
.as_ref()?
|
||||||
|
.iter()
|
||||||
|
.find(|(known, _)| known == path)
|
||||||
|
.and_then(|(_, state)| state.clone())
|
||||||
|
};
|
||||||
|
let is_empty = paths.is_empty();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div { class: "workspace-selector-layout",
|
div { class: "selector-shell",
|
||||||
div { class: "workspace-selector-view",
|
div { class: "selector-head",
|
||||||
// TODO: Workspace-Liste darstellen (Ordnername, Pfad, Statusabzeichen)
|
span { class: "selector-title", "Workspace öffnen" }
|
||||||
// statt des leeren Zustands, sobald Einträge vorhanden sind.
|
if !is_empty {
|
||||||
div { class: "empty-state",
|
span { class: "field-hint", "{paths.len()} bekannt" }
|
||||||
span { class: "empty-state-title", "Noch kein Workspace" }
|
}
|
||||||
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
|
}
|
||||||
button { class: "btn btn-primary", onclick: choose,
|
|
||||||
"Workspace hinzufügen…"
|
div { class: "selector-list-wrap",
|
||||||
|
if is_empty {
|
||||||
|
div { class: "empty-state",
|
||||||
|
span { class: "empty-state-title", "Hier ist noch nichts" }
|
||||||
|
span { "Der erste Workspace ist ein Ordner deiner Wahl." }
|
||||||
}
|
}
|
||||||
if let Some(message) = error() {
|
} else {
|
||||||
span { class: "field-error", "{message}" }
|
div { class: "ws-list",
|
||||||
|
for path in paths.iter() {
|
||||||
|
WorkspaceEntry {
|
||||||
|
key: "{path.display()}",
|
||||||
|
path: path.clone(),
|
||||||
|
state: state_of(path),
|
||||||
|
onopen: move |path| open_entry(path),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
div { class: "selector-foot",
|
||||||
|
span { class: "field-hint", "Ein Workspace ist ein Ordner mit Notizen." }
|
||||||
|
button {
|
||||||
|
class: if is_empty { "btn btn-primary" } else { "btn" },
|
||||||
|
onclick: choose,
|
||||||
|
"Workspace hinzufügen…"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(message) = error() {
|
||||||
|
span { class: "field-error", "{message}" }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(root) = pending() {
|
if let Some(root) = pending() {
|
||||||
@@ -96,6 +157,37 @@ pub fn Selector() -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Ein Eintrag der Workspace-Liste: Ordnername, darunter der Pfad, rechts bei
|
||||||
|
/// Bedarf das Statusabzeichen. Ein defekter Eintrag bleibt anklickbar und wird
|
||||||
|
/// nie automatisch entfernt — meist fehlt nur gerade das Laufwerk.
|
||||||
|
#[component]
|
||||||
|
fn WorkspaceEntry(path: PathBuf, state: Option<OpenError>, onopen: EventHandler<PathBuf>) -> Element {
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|n| n.to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| path.to_string_lossy().into_owned());
|
||||||
|
let full = path.display().to_string();
|
||||||
|
let clicked = path.clone();
|
||||||
|
// Klasse und Beschriftung des Abzeichens vorab, damit rsx! den Zustand
|
||||||
|
// nicht zweimal gleichzeitig braucht.
|
||||||
|
let broken = state.is_some();
|
||||||
|
let badge = state.as_ref().map(|state| (state.badge_class(), state.to_string()));
|
||||||
|
|
||||||
|
rsx! {
|
||||||
|
button {
|
||||||
|
class: if broken { "ws-item is-broken" } else { "ws-item" },
|
||||||
|
onclick: move |_| onopen.call(clicked.clone()),
|
||||||
|
span { class: "ws-item-body",
|
||||||
|
span { class: "ws-item-name", "{name}" }
|
||||||
|
span { class: "ws-item-path", "{full}" }
|
||||||
|
}
|
||||||
|
if let Some((class, label)) = badge {
|
||||||
|
span { class: "ws-badge {class}", "{label}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Rückfrage vor dem Anlegen in einem Ordner, der bereits Dateien enthält.
|
/// Rückfrage vor dem Anlegen in einem Ordner, der bereits Dateien enthält.
|
||||||
/// Sie soll informieren, nicht beunruhigen — deshalb steht ausdrücklich da,
|
/// Sie soll informieren, nicht beunruhigen — deshalb steht ausdrücklich da,
|
||||||
/// dass nichts Vorhandenes verändert wird.
|
/// dass nichts Vorhandenes verändert wird.
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ pub struct Workspace {
|
|||||||
/// Die ersten drei Varianten sind genau die Zustände, mit denen ein
|
/// Die ersten drei Varianten sind genau die Zustände, mit denen ein
|
||||||
/// Listeneintrag markiert wird — sie führen zu verschiedenen Handlungen:
|
/// Listeneintrag markiert wird — sie führen zu verschiedenen Handlungen:
|
||||||
/// Laufwerk einhängen, Ordner wiederherstellen, Datei reparieren.
|
/// Laufwerk einhängen, Ordner wiederherstellen, Datei reparieren.
|
||||||
#[derive(Debug)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum OpenError {
|
pub enum OpenError {
|
||||||
/// Der Ordner existiert nicht. Meist ein abgezogener Stick oder ein nicht
|
/// Der Ordner existiert nicht. Meist ein abgezogener Stick oder ein nicht
|
||||||
/// eingehängtes Netzlaufwerk — deshalb wird der Eintrag nie automatisch
|
/// eingehängtes Netzlaufwerk — deshalb wird der Eintrag nie automatisch
|
||||||
@@ -52,7 +52,7 @@ pub enum OpenError {
|
|||||||
Damaged,
|
Damaged,
|
||||||
/// Der Marker stammt aus einer neueren Fassung der App.
|
/// Der Marker stammt aus einer neueren Fassung der App.
|
||||||
TooNew { found: u32 },
|
TooNew { found: u32 },
|
||||||
Io(io::Error),
|
Io(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for OpenError {
|
impl fmt::Display for OpenError {
|
||||||
@@ -72,7 +72,18 @@ impl fmt::Display for OpenError {
|
|||||||
|
|
||||||
impl From<io::Error> for OpenError {
|
impl From<io::Error> for OpenError {
|
||||||
fn from(err: io::Error) -> Self {
|
fn from(err: io::Error) -> Self {
|
||||||
Self::Io(err)
|
Self::Io(err.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OpenError {
|
||||||
|
/// CSS-Klasse fuer das Statusabzeichen am Listeneintrag.
|
||||||
|
pub fn badge_class(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
Self::Unreachable => "unreachable",
|
||||||
|
Self::NotAWorkspace => "no-marker",
|
||||||
|
Self::Damaged | Self::TooNew { .. } | Self::Io(_) => "damaged",
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user