Panel: Clobber-Schutz, Content-Editor, Archiv-Reveal/Guard, Rechtschreib-Sprache
- register_mcp_server schreibt .claude.json nur bei fehlendem/abweichendem Eintrag (claudes Live-State nicht unnötig ueberschreiben). - Content-Editor im Panel (Rohtext-Textarea): Cmd/Ctrl+Enter speichert via panel_set, Esc verwirft; eingehende Updates werden waehrend der Bearbeitung gepuffert (Button "geaendert"), Speichern gewinnt. - Archivieren zeigt die Datei danach im Dateimanager (reveal_path_cmd, cfg pro OS). Breadth-Guard: Archiv-Ordner darf kein Home-Vorfahre sein, muss absolut sein; expand_home loest bare ~ auf. - Rechtschreibpruefung: Default in settings.json (spellcheckLang "de"), pro Text per Selector ueberschreibbar.
This commit is contained in:
+69
-3
@@ -76,6 +76,51 @@
|
|||||||
.panel-btn.copied {
|
.panel-btn.copied {
|
||||||
color: #a6e3a1;
|
color: #a6e3a1;
|
||||||
}
|
}
|
||||||
|
.panel-btn.error {
|
||||||
|
color: #f38ba8;
|
||||||
|
}
|
||||||
|
.panel-btn svg {
|
||||||
|
display: block;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.3;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
.panel-btn.active {
|
||||||
|
background: #313244;
|
||||||
|
color: #cdd6f4;
|
||||||
|
}
|
||||||
|
.panel-btn.changed {
|
||||||
|
color: #f9e2af;
|
||||||
|
}
|
||||||
|
.panel-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 16px 20px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.panel-editor[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.panel-lang {
|
||||||
|
margin-right: auto;
|
||||||
|
background: #313244;
|
||||||
|
color: #a6adc8;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
#panel-content {
|
#panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -159,10 +204,31 @@
|
|||||||
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
|
<select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
|
||||||
|
<option value="de">DE</option>
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="en-GB">EN-GB</option>
|
||||||
|
<option value="fr">FR</option>
|
||||||
|
<option value="es">ES</option>
|
||||||
|
<option value="it">IT</option>
|
||||||
|
<option value="nl">NL</option>
|
||||||
|
</select>
|
||||||
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||||
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
|
<button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||||
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||||
<button class="panel-btn" id="panel-dock" title="Wieder andocken">Andocken</button>
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-dock" title="Wieder andocken">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="11" rx="1.5" /><path d="M9.5 2.5v11" /><path d="M4 8h3.4M6 6.2 7.8 8 6 9.8" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-close" title="Panel schließen">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="panel-content" class="md"></div>
|
<div id="panel-content" class="md"></div>
|
||||||
|
|||||||
+68
-14
@@ -88,6 +88,17 @@ pub(crate) fn project_archive_dir(project: &str) -> Option<PathBuf> {
|
|||||||
pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
|
pub(crate) fn set_project_archive_dir(project: &str, dir: &str) -> Result<(), String> {
|
||||||
let paths = Paths::real();
|
let paths = Paths::real();
|
||||||
let expanded = expand_home(&paths, dir);
|
let expanded = expand_home(&paths, dir);
|
||||||
|
if !expanded.is_absolute() {
|
||||||
|
return Err(format!("Archiv-Ordner muss ein absoluter Pfad sein: {}", expanded.display()));
|
||||||
|
}
|
||||||
|
// Zu weit gefasst? Ein Vorfahre des Home (inkl. Home selbst und Root) würde
|
||||||
|
// claude über additionalDirectories weiten Zugriff geben — das lehnen wir ab.
|
||||||
|
if paths.home.starts_with(&expanded) {
|
||||||
|
return Err(format!(
|
||||||
|
"Archiv-Ordner zu weit gefasst: {}. Bitte einen spezifischen Unterordner wählen.",
|
||||||
|
expanded.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;
|
fs::create_dir_all(&expanded).map_err(|e| format!("{}: {e}", expanded.display()))?;
|
||||||
let contracted = contract_home(&paths, &expanded);
|
let contracted = contract_home(&paths, &expanded);
|
||||||
let mut cfg = read_project_config_in(&paths, project)?;
|
let mut cfg = read_project_config_in(&paths, project)?;
|
||||||
@@ -638,8 +649,11 @@ fn set_todo_in(paths: &Paths, name: &str, enabled: bool) -> Result<(), String> {
|
|||||||
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
fs::write(&sp, raw + "\n").map_err(|e| format!("{}: {e}", sp.display()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// "~/x" relativ zum Home auflösen; alles andere unverändert.
|
/// "~" bzw. "~/x" relativ zum Home auflösen; alles andere unverändert.
|
||||||
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
|
fn expand_home(paths: &Paths, p: &str) -> PathBuf {
|
||||||
|
if p == "~" {
|
||||||
|
return paths.home.clone();
|
||||||
|
}
|
||||||
match p.strip_prefix("~/") {
|
match p.strip_prefix("~/") {
|
||||||
Some(rest) => paths.home.join(rest),
|
Some(rest) => paths.home.join(rest),
|
||||||
None => PathBuf::from(p),
|
None => PathBuf::from(p),
|
||||||
@@ -1460,6 +1474,14 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
|
|||||||
let Ok(exe) = std::env::current_exe() else {
|
let Ok(exe) = std::env::current_exe() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und schreibt
|
||||||
|
// den Entwurf in den Chat statt ins Panel.
|
||||||
|
let desired = serde_json::json!({
|
||||||
|
"type": "stdio",
|
||||||
|
"command": exe.to_string_lossy(),
|
||||||
|
"args": ["--mcp-panel"],
|
||||||
|
"alwaysLoad": true,
|
||||||
|
});
|
||||||
let cfg = pool_dir.join(".claude.json");
|
let cfg = pool_dir.join(".claude.json");
|
||||||
let mut v = fs::read_to_string(&cfg)
|
let mut v = fs::read_to_string(&cfg)
|
||||||
.ok()
|
.ok()
|
||||||
@@ -1473,18 +1495,13 @@ fn register_mcp_server(pool_dir: &std::path::Path) {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
// Schon korrekt und kein Altkey -> nichts schreiben. `.claude.json` ist
|
||||||
|
// claudes Live-State; wir fassen sie nur an, wenn der Eintrag fehlt/abweicht.
|
||||||
|
if servers.get("aicontrol").is_none() && servers.get(PANEL_MCP_SERVER) == Some(&desired) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
servers.remove("aicontrol"); // alter Key vor Umbenennung
|
servers.remove("aicontrol"); // alter Key vor Umbenennung
|
||||||
servers.insert(
|
servers.insert(PANEL_MCP_SERVER.into(), desired);
|
||||||
PANEL_MCP_SERVER.into(),
|
|
||||||
serde_json::json!({
|
|
||||||
"type": "stdio",
|
|
||||||
"command": exe.to_string_lossy(),
|
|
||||||
"args": ["--mcp-panel"],
|
|
||||||
// Tool nicht deferren, sonst sieht das Modell write_panel nicht und
|
|
||||||
// schreibt den Entwurf in den Chat statt ins Panel.
|
|
||||||
"alwaysLoad": true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
if let Ok(out) = serde_json::to_string_pretty(&v) {
|
if let Ok(out) = serde_json::to_string_pretty(&v) {
|
||||||
let _ = fs::write(&cfg, out + "\n");
|
let _ = fs::write(&cfg, out + "\n");
|
||||||
}
|
}
|
||||||
@@ -2137,6 +2154,17 @@ fn terminal_font_size() -> u32 {
|
|||||||
.unwrap_or(13)
|
.unwrap_or(13)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Standard-Sprache der Rechtschreibprüfung (settings.json: spellcheckLang),
|
||||||
|
/// Default „de". Pro Text im Panel überschreibbar.
|
||||||
|
#[tauri::command]
|
||||||
|
fn spellcheck_lang() -> String {
|
||||||
|
fs::read_to_string(Paths::real().config_dir().join(APP_SETTINGS_FILE))
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| serde_json::from_str::<serde_json::Value>(&s).ok())
|
||||||
|
.and_then(|v| v["spellcheckLang"].as_str().map(str::to_string))
|
||||||
|
.unwrap_or_else(|| "de".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
/// Setzt die Schriftgröße; erhält übrige App-settings.
|
/// Setzt die Schriftgröße; erhält übrige App-settings.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
fn set_terminal_font_size(size: u32) -> Result<(), String> {
|
fn set_terminal_font_size(size: u32) -> Result<(), String> {
|
||||||
@@ -2269,6 +2297,30 @@ fn panel_archive_cmd(project: String, dir: Option<String>) -> Result<String, Str
|
|||||||
archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
|
archive_panel_content(&project, dir.as_deref()).map(|p| p.display().to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Zeigt einen Pfad im Dateimanager (nach dem Archivieren „im Finder zeigen").
|
||||||
|
#[tauri::command]
|
||||||
|
fn reveal_path_cmd(path: String) {
|
||||||
|
reveal_path(std::path::Path::new(&path));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn reveal_path(path: &std::path::Path) {
|
||||||
|
let _ = Command::new("open").arg("-R").arg(path).spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
fn reveal_path(path: &std::path::Path) {
|
||||||
|
let target = path.parent().unwrap_or(path);
|
||||||
|
let _ = Command::new("xdg-open").arg(target).spawn();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "windows")]
|
||||||
|
fn reveal_path(path: &std::path::Path) {
|
||||||
|
let _ = Command::new("explorer")
|
||||||
|
.arg(format!("/select,{}", path.display()))
|
||||||
|
.spawn();
|
||||||
|
}
|
||||||
|
|
||||||
/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.
|
/// D-Bus-Relay für die GNOME-Extension: Show()/Hide() steuern das Popup-Fenster.
|
||||||
/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.
|
/// Die Extension ruft Show() beim Panel-Klick und positioniert das Fenster selbst.
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@@ -2513,7 +2565,7 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
|||||||
if let tauri::WindowEvent::Destroyed = event {
|
if let tauri::WindowEvent::Destroyed = event {
|
||||||
if window.label().starts_with("panel-") {
|
if window.label().starts_with("panel-") {
|
||||||
use tauri::{Emitter, Manager};
|
use tauri::{Emitter, Manager};
|
||||||
let _ = window.app_handle().emit("panel-attached", ());
|
let _ = window.app_handle().emit("panel-window-closed", ());
|
||||||
} else {
|
} else {
|
||||||
terminal::close(window);
|
terminal::close(window);
|
||||||
}
|
}
|
||||||
@@ -2529,12 +2581,14 @@ fn terminal_builder(project: String) -> tauri::Builder<tauri::Wry> {
|
|||||||
terminal::open_panel_window,
|
terminal::open_panel_window,
|
||||||
panel_archive_dir_cmd,
|
panel_archive_dir_cmd,
|
||||||
panel_archive_cmd,
|
panel_archive_cmd,
|
||||||
|
reveal_path_cmd,
|
||||||
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
// Header im Terminal-Prozess: Projektliste, Icon und Pool-Name.
|
||||||
list_projects,
|
list_projects,
|
||||||
project_icon,
|
project_icon,
|
||||||
pool_label,
|
pool_label,
|
||||||
terminal_font_size,
|
terminal_font_size,
|
||||||
set_terminal_font_size
|
set_terminal_font_size,
|
||||||
|
spellcheck_lang
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+92
-10
@@ -58,11 +58,20 @@ export function initPanelView(opts: {
|
|||||||
modeBtn: HTMLElement;
|
modeBtn: HTMLElement;
|
||||||
titleEl?: HTMLElement;
|
titleEl?: HTMLElement;
|
||||||
editBtn?: HTMLElement;
|
editBtn?: HTMLElement;
|
||||||
/// Wird mit dem neuen Rohtext aufgerufen, wenn der Titel geändert wurde.
|
editContentBtn?: HTMLElement;
|
||||||
|
langSelect?: HTMLSelectElement;
|
||||||
|
/// Standard-Sprache der Rechtschreibprüfung (aus den App-Settings).
|
||||||
|
defaultLang?: string;
|
||||||
|
/// Wird mit dem neuen Rohtext aufgerufen, wenn Titel oder Inhalt geändert
|
||||||
|
/// wurden.
|
||||||
onCommit?: (text: string) => void;
|
onCommit?: (text: string) => void;
|
||||||
}): PanelView {
|
}): PanelView {
|
||||||
let rawText = "";
|
let rawText = "";
|
||||||
let rendered = true;
|
let rendered = true;
|
||||||
|
// Content-Edit-Zustand: während des Editierens werden eingehende Updates
|
||||||
|
// gepuffert statt angewandt.
|
||||||
|
let editing = false;
|
||||||
|
let pending: string | null = null;
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
if (rendered) {
|
if (rendered) {
|
||||||
@@ -83,12 +92,7 @@ export function initPanelView(opts: {
|
|||||||
opts.copyBtn.addEventListener("click", async () => {
|
opts.copyBtn.addEventListener("click", async () => {
|
||||||
await navigator.clipboard.writeText(rawText);
|
await navigator.clipboard.writeText(rawText);
|
||||||
opts.copyBtn.classList.add("copied");
|
opts.copyBtn.classList.add("copied");
|
||||||
const label = opts.copyBtn.textContent;
|
setTimeout(() => opts.copyBtn.classList.remove("copied"), 1200);
|
||||||
opts.copyBtn.textContent = "Kopiert";
|
|
||||||
setTimeout(() => {
|
|
||||||
opts.copyBtn.classList.remove("copied");
|
|
||||||
opts.copyBtn.textContent = label;
|
|
||||||
}, 1200);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
|
// Titel-Edit: Edit-Button macht den Titel editierbar, Enter/Blur schreibt die
|
||||||
@@ -124,14 +128,92 @@ export function initPanelView(opts: {
|
|||||||
titleEl.addEventListener("blur", commit);
|
titleEl.addEventListener("blur", commit);
|
||||||
}
|
}
|
||||||
|
|
||||||
draw();
|
// Content-Edit: Button schaltet den Inhalt auf eine Rohtext-Textarea. Speichern
|
||||||
return {
|
// (Button erneut oder Cmd/Ctrl+Enter) schreibt zurück, Escape verwirft.
|
||||||
set(text: string) {
|
// Eingehende Updates während des Editierens werden gepuffert.
|
||||||
|
if (opts.editContentBtn && opts.onCommit) {
|
||||||
|
const editBtn = opts.editContentBtn;
|
||||||
|
const editor = document.createElement("textarea");
|
||||||
|
editor.className = "panel-editor";
|
||||||
|
editor.spellcheck = true;
|
||||||
|
editor.hidden = true;
|
||||||
|
opts.content.after(editor);
|
||||||
|
|
||||||
|
// Sprache der Rechtschreibprüfung: Default aus den Settings, per Selector
|
||||||
|
// pro Text überschreibbar.
|
||||||
|
let lang = opts.defaultLang || "de";
|
||||||
|
const sel = opts.langSelect;
|
||||||
|
if (sel) {
|
||||||
|
if ([...sel.options].some((o) => o.value === lang)) sel.value = lang;
|
||||||
|
else lang = sel.value;
|
||||||
|
sel.addEventListener("change", () => {
|
||||||
|
lang = sel.value;
|
||||||
|
editor.lang = lang;
|
||||||
|
// Neuprüfung erzwingen.
|
||||||
|
editor.spellcheck = false;
|
||||||
|
editor.spellcheck = true;
|
||||||
|
if (!editor.hidden) editor.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
editor.lang = lang;
|
||||||
|
|
||||||
|
const leave = () => {
|
||||||
|
editing = false;
|
||||||
|
editor.hidden = true;
|
||||||
|
opts.content.hidden = false;
|
||||||
|
editBtn.classList.remove("active", "changed");
|
||||||
|
};
|
||||||
|
const enter = () => {
|
||||||
|
editing = true;
|
||||||
|
pending = null;
|
||||||
|
editor.value = rawText;
|
||||||
|
opts.content.hidden = true;
|
||||||
|
editor.hidden = false;
|
||||||
|
editBtn.classList.add("active");
|
||||||
|
editor.focus();
|
||||||
|
};
|
||||||
|
const save = () => {
|
||||||
|
const v = editor.value;
|
||||||
|
leave();
|
||||||
|
opts.onCommit!(v); // schreibt Datei -> panel-update -> set() rendert
|
||||||
|
};
|
||||||
|
const cancel = () => {
|
||||||
|
const p = pending;
|
||||||
|
leave();
|
||||||
|
if (p !== null) applyText(p);
|
||||||
|
else draw();
|
||||||
|
};
|
||||||
|
editBtn.addEventListener("click", () => (editing ? save() : enter()));
|
||||||
|
editor.addEventListener("keydown", (e) => {
|
||||||
|
if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {
|
||||||
|
e.preventDefault();
|
||||||
|
save();
|
||||||
|
} else if (e.key === "Escape") {
|
||||||
|
e.preventDefault();
|
||||||
|
cancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyText(text: string) {
|
||||||
rawText = text;
|
rawText = text;
|
||||||
if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
|
if (titleEl && titleEl.getAttribute("contenteditable") !== "true") {
|
||||||
titleEl.textContent = firstLine(text);
|
titleEl.textContent = firstLine(text);
|
||||||
}
|
}
|
||||||
draw();
|
draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
draw();
|
||||||
|
return {
|
||||||
|
set(text: string) {
|
||||||
|
// Während einer Inhalts-Bearbeitung nicht überschreiben — puffern und den
|
||||||
|
// Edit-Button als „geändert" markieren.
|
||||||
|
if (editing) {
|
||||||
|
pending = text;
|
||||||
|
opts.editContentBtn?.classList.add("changed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyText(text);
|
||||||
},
|
},
|
||||||
raw: () => rawText,
|
raw: () => rawText,
|
||||||
};
|
};
|
||||||
|
|||||||
+21
-15
@@ -2,7 +2,7 @@ import "@fontsource/jetbrains-mono/400.css";
|
|||||||
import "@fontsource/jetbrains-mono/500.css";
|
import "@fontsource/jetbrains-mono/500.css";
|
||||||
import { invoke } from "@tauri-apps/api/core";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
import { getCurrentWebviewWindow } from "@tauri-apps/api/webviewWindow";
|
||||||
import { listen } from "@tauri-apps/api/event";
|
import { listen, emit } from "@tauri-apps/api/event";
|
||||||
import { initPanelView } from "./panel-view";
|
import { initPanelView } from "./panel-view";
|
||||||
|
|
||||||
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
// Abgelöstes Panel-Fenster: liest den aktuellen Entwurf einmal ein und folgt
|
||||||
@@ -16,6 +16,9 @@ const view = initPanelView({
|
|||||||
modeBtn: document.getElementById("panel-mode")!,
|
modeBtn: document.getElementById("panel-mode")!,
|
||||||
titleEl: document.querySelector(".panel-title") as HTMLElement,
|
titleEl: document.querySelector(".panel-title") as HTMLElement,
|
||||||
editBtn: document.getElementById("panel-title-edit")!,
|
editBtn: document.getElementById("panel-title-edit")!,
|
||||||
|
editContentBtn: document.getElementById("panel-content-edit")!,
|
||||||
|
langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
|
||||||
|
defaultLang: await invoke<string>("spellcheck_lang"),
|
||||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -25,8 +28,11 @@ await listen<string>("panel-update", (e) => view.set(e.payload));
|
|||||||
|
|
||||||
// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
|
// Archivieren: wie im angedockten Panel — Ordner nehmen oder per Dialog wählen.
|
||||||
const archiveBtn = document.getElementById("panel-archive")!;
|
const archiveBtn = document.getElementById("panel-archive")!;
|
||||||
|
function flashBtn(btn: HTMLElement, cls: string) {
|
||||||
|
btn.classList.add(cls);
|
||||||
|
setTimeout(() => btn.classList.remove(cls), 1400);
|
||||||
|
}
|
||||||
archiveBtn.addEventListener("click", async () => {
|
archiveBtn.addEventListener("click", async () => {
|
||||||
const orig = archiveBtn.textContent;
|
|
||||||
try {
|
try {
|
||||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
project,
|
project,
|
||||||
@@ -38,20 +44,20 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
if (!chosen) return;
|
if (!chosen) return;
|
||||||
dir = chosen as string;
|
dir = chosen as string;
|
||||||
}
|
}
|
||||||
await invoke<string>("panel_archive_cmd", { project, dir });
|
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
||||||
archiveBtn.textContent = "Archiviert";
|
flashBtn(archiveBtn, "copied");
|
||||||
archiveBtn.classList.add("copied");
|
invoke("reveal_path_cmd", { path });
|
||||||
} catch {
|
} catch {
|
||||||
archiveBtn.textContent = "Fehler";
|
flashBtn(archiveBtn, "error");
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
archiveBtn.textContent = orig;
|
|
||||||
archiveBtn.classList.remove("copied");
|
|
||||||
}, 1400);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// „Andocken": Fenster schließen — terminal.rs meldet `panel-attached`, das
|
// „Andocken": angedocktes Panel wieder einblenden, dann dieses Fenster schließen.
|
||||||
// angedockte Panel im Terminal-Fenster erscheint wieder.
|
document.getElementById("panel-dock")!.addEventListener("click", async () => {
|
||||||
document
|
await emit("panel-attached");
|
||||||
.getElementById("panel-dock")!
|
win.close();
|
||||||
.addEventListener("click", () => win.close());
|
});
|
||||||
|
|
||||||
|
// „Schließen": Fenster zu, ohne wieder anzudocken (Panel bleibt aus, bis ein
|
||||||
|
// neuer Entwurf kommt).
|
||||||
|
document.getElementById("panel-close")!.addEventListener("click", () => win.close());
|
||||||
|
|||||||
+16
-9
@@ -553,6 +553,9 @@ const view = initPanelView({
|
|||||||
modeBtn: document.getElementById("panel-mode")!,
|
modeBtn: document.getElementById("panel-mode")!,
|
||||||
titleEl: panel.querySelector(".panel-title") as HTMLElement,
|
titleEl: panel.querySelector(".panel-title") as HTMLElement,
|
||||||
editBtn: document.getElementById("panel-title-edit")!,
|
editBtn: document.getElementById("panel-title-edit")!,
|
||||||
|
editContentBtn: document.getElementById("panel-content-edit")!,
|
||||||
|
langSelect: document.getElementById("panel-lang") as HTMLSelectElement,
|
||||||
|
defaultLang: await invoke<string>("spellcheck_lang"),
|
||||||
onCommit: (text) => invoke("panel_set", { project, text }),
|
onCommit: (text) => invoke("panel_set", { project, text }),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -583,6 +586,11 @@ await listen("panel-attached", () => {
|
|||||||
detached = false;
|
detached = false;
|
||||||
if (hasContent) showPanel();
|
if (hasContent) showPanel();
|
||||||
});
|
});
|
||||||
|
// Abgelöstes Fenster geschlossen (per ✕ oder OS): angedocktes Panel bleibt
|
||||||
|
// ausgeblendet, aber der nächste Entwurf darf es wieder einblenden.
|
||||||
|
await listen("panel-window-closed", () => {
|
||||||
|
detached = false;
|
||||||
|
});
|
||||||
|
|
||||||
document
|
document
|
||||||
.getElementById("panel-detach")!
|
.getElementById("panel-detach")!
|
||||||
@@ -591,8 +599,11 @@ document.getElementById("panel-hide")!.addEventListener("click", hidePanel);
|
|||||||
|
|
||||||
// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
|
// Archivieren: konfigurierten Ordner nehmen, sonst per Dialog wählen (setzt ihn).
|
||||||
const archiveBtn = document.getElementById("panel-archive")!;
|
const archiveBtn = document.getElementById("panel-archive")!;
|
||||||
|
function flashBtn(btn: HTMLElement, cls: string) {
|
||||||
|
btn.classList.add(cls);
|
||||||
|
setTimeout(() => btn.classList.remove(cls), 1400);
|
||||||
|
}
|
||||||
archiveBtn.addEventListener("click", async () => {
|
archiveBtn.addEventListener("click", async () => {
|
||||||
const orig = archiveBtn.textContent;
|
|
||||||
try {
|
try {
|
||||||
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
const configured = await invoke<string | null>("panel_archive_dir_cmd", {
|
||||||
project,
|
project,
|
||||||
@@ -604,17 +615,13 @@ archiveBtn.addEventListener("click", async () => {
|
|||||||
if (!chosen) return;
|
if (!chosen) return;
|
||||||
dir = chosen as string;
|
dir = chosen as string;
|
||||||
}
|
}
|
||||||
await invoke<string>("panel_archive_cmd", { project, dir });
|
const path = await invoke<string>("panel_archive_cmd", { project, dir });
|
||||||
archiveBtn.textContent = "Archiviert";
|
flashBtn(archiveBtn, "copied");
|
||||||
archiveBtn.classList.add("copied");
|
invoke("reveal_path_cmd", { path });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
archiveBtn.textContent = "Fehler";
|
flashBtn(archiveBtn, "error");
|
||||||
invoke("term_log", { msg: `archive: ${e}` });
|
invoke("term_log", { msg: `archive: ${e}` });
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
archiveBtn.textContent = orig;
|
|
||||||
archiveBtn.classList.remove("copied");
|
|
||||||
}, 1400);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Splitter: Panelbreite ziehen.
|
// Splitter: Panelbreite ziehen.
|
||||||
|
|||||||
+69
-4
@@ -269,6 +269,51 @@
|
|||||||
.panel-btn.copied {
|
.panel-btn.copied {
|
||||||
color: #a6e3a1;
|
color: #a6e3a1;
|
||||||
}
|
}
|
||||||
|
.panel-btn.error {
|
||||||
|
color: #f38ba8;
|
||||||
|
}
|
||||||
|
.panel-btn svg {
|
||||||
|
display: block;
|
||||||
|
stroke: currentColor;
|
||||||
|
stroke-width: 1.3;
|
||||||
|
fill: none;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-linejoin: round;
|
||||||
|
}
|
||||||
|
.panel-btn.active {
|
||||||
|
background: #313244;
|
||||||
|
color: #cdd6f4;
|
||||||
|
}
|
||||||
|
.panel-btn.changed {
|
||||||
|
color: #f9e2af;
|
||||||
|
}
|
||||||
|
.panel-editor {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
resize: none;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
padding: 14px 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
font-family: "JetBrains Mono", Menlo, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.55;
|
||||||
|
}
|
||||||
|
.panel-editor[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.panel-lang {
|
||||||
|
margin-right: auto;
|
||||||
|
background: #313244;
|
||||||
|
color: #a6adc8;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 3px 4px;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
#panel-content {
|
#panel-content {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -372,11 +417,31 @@
|
|||||||
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
<button class="panel-btn panel-edit" id="panel-title-edit" title="Titel bearbeiten">✎</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
|
<select class="panel-lang" id="panel-lang" title="Sprache der Rechtschreibprüfung">
|
||||||
|
<option value="de">DE</option>
|
||||||
|
<option value="en">EN</option>
|
||||||
|
<option value="en-GB">EN-GB</option>
|
||||||
|
<option value="fr">FR</option>
|
||||||
|
<option value="es">ES</option>
|
||||||
|
<option value="it">IT</option>
|
||||||
|
<option value="nl">NL</option>
|
||||||
|
</select>
|
||||||
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
<button class="panel-btn" id="panel-mode" title="Rohtext / gerendert">MD</button>
|
||||||
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage">Kopieren</button>
|
<button class="panel-btn" id="panel-content-edit" title="Entwurf bearbeiten (Cmd/Ctrl+Enter speichert, Esc verwirft)">
|
||||||
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">Archivieren</button>
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M10.8 2.6 13.4 5.2 6 12.6l-3.1.5.5-3.1z" /><path d="M9.7 3.7 12.3 6.3" /></svg>
|
||||||
<button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">⧉</button>
|
</button>
|
||||||
<button class="panel-btn" id="panel-hide" title="Panel ausblenden">✕</button>
|
<button class="panel-btn" id="panel-copy" title="In die Zwischenablage kopieren">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="5.5" y="5.5" width="8" height="8" rx="1.5" /><path d="M10.5 3.2V3A1.5 1.5 0 0 0 9 1.5H3A1.5 1.5 0 0 0 1.5 3v6A1.5 1.5 0 0 0 3 10.5h.2" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-archive" title="In den Archiv-Ordner speichern">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><rect x="1.5" y="2.5" width="13" height="3.5" rx="1" /><path d="M2.8 6v6.5a1 1 0 0 0 1 1h8.4a1 1 0 0 0 1-1V6" /><path d="M6.3 9h3.4" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-detach" title="In eigenes Fenster ablösen">
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16"><path d="M12.5 8.5V13a1 1 0 0 1-1 1H3a1 1 0 0 1-1-1V4.5a1 1 0 0 1 1-1h4.5" /><path d="M10 2h4v4" /><path d="M14 2 8 8" /></svg>
|
||||||
|
</button>
|
||||||
|
<button class="panel-btn" id="panel-hide" title="Panel ausblenden">
|
||||||
|
<svg width="12" height="12" viewBox="0 0 12 12"><path d="M3 3l6 6M9 3l-6 6" /></svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="panel-content" class="md"></div>
|
<div id="panel-content" class="md"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user