Fügt CLI-Toolverwaltung und erweiterte UI-Funktionen hinzu

Ermöglicht die Verwaltung (Erstellen, Auflisten, Ausführen) von Tools über eine Befehlszeilenschnittstelle für Automatisierung und Integration. Die Benutzeroberfläche wurde mit dedizierten Ansichten für Toolerstellung und -bearbeitung, In-App-Lua-Hilfe sowie kontextsensitiven Menüs erweitert. Neue Abhängigkeiten (CLI11, rang) wurden integriert und eine umfassende Testsuite hinzugefügt. Die Lizenzierung wurde auf AGPL-3.0 umgestellt und ein Kontributionsleitfaden bereitgestellt.
This commit is contained in:
2026-07-17 17:22:15 +02:00
parent 074fa73992
commit c611b008af
45 changed files with 5501 additions and 709 deletions
+347
View File
@@ -0,0 +1,347 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Tool erstellen</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
<div id="shell" class="settings-shell">
<div class="resize-handle resize-top" data-webview-resize="t"></div>
<div class="resize-handle resize-right" data-webview-resize="r"></div>
<div class="resize-handle resize-bottom" data-webview-resize="b"></div>
<div class="resize-handle resize-left" data-webview-resize="l"></div>
<div class="resize-handle resize-top-left" data-webview-resize="tl"></div>
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<button class="titlebar-btn" id="back-button" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Neues Tool</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
<button class="titlebar-btn close" data-webview-close data-webview-ignore>×</button>
</div>
</div>
<main class="add-tool-page">
<header class="add-tool-heading">
<div>
<span class="add-tool-eyebrow">Toolbox Builder</span>
<h1>Tool erstellen</h1>
<p>Erstellt einen vollständigen Ordner in <code>.Toolbox</code>.</p>
</div>
<div class="add-tool-icon-preview" id="icon-preview" aria-label="Icon-Vorschau">+</div>
</header>
<form id="add-tool-form" class="add-tool-form">
<section class="add-tool-section add-tool-template-section">
<div class="add-tool-section-heading">
<div>
<h2>Vorlage auswählen</h2>
<p>Ein Ziel auswählen Toolbox erzeugt Name und Startskript automatisch.</p>
</div>
<span class="add-tool-template-badge">Einfach</span>
</div>
<div class="add-tool-template-grid" role="group" aria-label="Tool-Vorlage">
<button type="button" class="add-tool-template" data-template="program">
<span class="add-tool-template-icon"></span>
<span><strong>Programm</strong><small>Eine .exe starten</small></span>
</button>
<button type="button" class="add-tool-template" data-template="folder">
<span class="add-tool-template-icon"></span>
<span><strong>Ordner</strong><small>Im Explorer öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="website">
<span class="add-tool-template-icon"></span>
<span><strong>Website</strong><small>Eine URL öffnen</small></span>
</button>
<button type="button" class="add-tool-template is-active" data-template="lua" aria-pressed="true">
<span class="add-tool-template-icon">λ</span>
<span><strong>Freies Lua</strong><small>Eigenes Skript</small></span>
</button>
</div>
<div id="website-template-controls" class="add-tool-template-controls" hidden>
<input id="website-url" class="settings-input" type="url" autocomplete="url"
placeholder="https://example.com">
<button type="button" id="apply-website-template" class="add-tool-secondary">Übernehmen</button>
</div>
</section>
<section class="add-tool-section">
<h2>Allgemein</h2>
<label for="tool-name">Name <span aria-hidden="true">*</span></label>
<input id="tool-name" class="settings-input" maxlength="80" autocomplete="off" required
placeholder="Zum Beispiel: Projekt öffnen">
<label for="tool-description">Beschreibung</label>
<textarea id="tool-description" class="settings-input add-tool-description" maxlength="8192"
placeholder="Was erledigt dieses Tool?"></textarea>
<div class="add-tool-grid">
<div>
<label for="tool-timeout">Lua-Timeout</label>
<div class="add-tool-input-suffix">
<input id="tool-timeout" class="settings-input" type="number" min="0" max="86400" value="30">
<span>Sek.</span>
</div>
</div>
<div>
<label for="tool-icon">Icon (PNG)</label>
<input id="tool-icon" class="settings-input add-tool-file" type="file" accept="image/png">
</div>
</div>
</section>
<details id="advanced-script" class="add-tool-section add-tool-script-section" open>
<summary>
<span>Erweitert: start.lua bearbeiten</span>
<small>optional</small>
</summary>
<div class="add-tool-section-heading">
<h2>start.lua</h2>
<div class="add-tool-script-meta">
<span>Wird beim Klick auf die Karte ausgeführt</span>
<button type="button" class="lua-help-trigger" id="lua-help-button">Lua-Cheatsheet</button>
</div>
</div>
<div class="add-tool-code-editor">
<pre id="tool-script-highlight" aria-hidden="true"><code class="language-lua"></code></pre>
<textarea id="tool-script" class="settings-input add-tool-script" spellcheck="false" wrap="off"
aria-label="Lua-Skript">-- Dein Toolbox-Skript
print("Tool gestartet")
</textarea>
</div>
</details>
<div class="add-tool-actions">
<div id="create-status" class="settings-status" role="status" aria-live="polite"></div>
<button type="button" id="cancel-button" class="add-tool-secondary">Abbrechen</button>
<button type="submit" id="create-button" class="back-button settings-save">Tool erstellen</button>
</div>
</form>
</main>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
const form = document.getElementById('add-tool-form');
const nameInput = document.getElementById('tool-name');
const descriptionInput = document.getElementById('tool-description');
const timeoutInput = document.getElementById('tool-timeout');
const scriptInput = document.getElementById('tool-script');
const scriptHighlight = document.querySelector('#tool-script-highlight code');
const scriptHighlightContainer = document.getElementById('tool-script-highlight');
const iconInput = document.getElementById('tool-icon');
const iconPreview = document.getElementById('icon-preview');
const status = document.getElementById('create-status');
const createButton = document.getElementById('create-button');
const advancedScript = document.getElementById('advanced-script');
const templateButtons = [...document.querySelectorAll('[data-template]')];
const websiteControls = document.getElementById('website-template-controls');
const websiteUrl = document.getElementById('website-url');
const applyWebsiteButton = document.getElementById('apply-website-template');
let iconBase64 = '';
let templateType = 'lua';
let templateTarget = '';
function highlightLua() {
scriptHighlight.textContent = scriptInput.value + (scriptInput.value.endsWith('\n') ? ' ' : '');
Prism.highlightElement(scriptHighlight);
}
scriptInput.addEventListener('input', () => {
if (templateType !== 'lua') {
selectTemplate('lua');
advancedScript.open = true;
}
highlightLua();
});
scriptInput.addEventListener('scroll', () => {
scriptHighlightContainer.scrollTop = scriptInput.scrollTop;
scriptHighlightContainer.scrollLeft = scriptInput.scrollLeft;
});
function selectTemplate(kind) {
if (kind !== templateType) {
templateTarget = '';
}
templateType = kind;
templateButtons.forEach(button => {
const selected = button.dataset.template === kind;
button.classList.toggle('is-active', selected);
button.setAttribute('aria-pressed', String(selected));
});
websiteControls.hidden = kind !== 'website';
}
function applyTemplateResponse(responseText, kind) {
const response = JSON.parse(responseText);
if (response.cancelled) {
status.textContent = '';
delete status.dataset.kind;
return;
}
if (!response.success || !response.draft) {
status.textContent = response.message || 'Vorlage konnte nicht erstellt werden.';
status.dataset.kind = 'error';
return;
}
nameInput.value = response.draft.name;
descriptionInput.value = response.draft.description;
scriptInput.value = response.draft.luaScript;
templateType = response.draft.templateType;
templateTarget = response.draft.templateTarget;
highlightLua();
advancedScript.open = false;
status.textContent = `${kind} ist bereit. Nur noch „Tool erstellen“ klicken.`;
status.dataset.kind = 'success';
nameInput.focus();
nameInput.select();
}
async function chooseNativeTemplate(kind) {
selectTemplate(kind === 'Programm' ? 'program' : 'folder');
status.textContent = kind === 'Programm' ? 'Programm auswählen…' : 'Ordner auswählen…';
status.dataset.kind = 'pending';
try {
const responseText = kind === 'Programm'
? await saucer.exposed.selectProgramTemplate()
: await saucer.exposed.selectFolderTemplate();
applyTemplateResponse(responseText, kind);
} catch (error) {
console.error('Vorlage konnte nicht erstellt werden:', error);
status.textContent = 'Der Auswahldialog konnte nicht geöffnet werden.';
status.dataset.kind = 'error';
}
}
templateButtons.forEach(button => {
button.addEventListener('click', () => {
const kind = button.dataset.template;
if (kind === 'program') {
void chooseNativeTemplate('Programm');
} else if (kind === 'folder') {
void chooseNativeTemplate('Ordner');
} else if (kind === 'website') {
selectTemplate('website');
websiteUrl.focus();
} else {
selectTemplate('lua');
advancedScript.open = true;
scriptInput.focus();
}
});
});
async function applyWebsiteTemplate() {
selectTemplate('website');
status.textContent = 'Website-Vorlage wird erstellt…';
status.dataset.kind = 'pending';
try {
applyTemplateResponse(
await saucer.exposed.createWebsiteTemplate(websiteUrl.value.trim()),
'Website'
);
} catch (error) {
console.error('Website-Vorlage konnte nicht erstellt werden:', error);
status.textContent = 'Website-Vorlage konnte nicht erstellt werden.';
status.dataset.kind = 'error';
}
}
applyWebsiteButton.addEventListener('click', () => void applyWebsiteTemplate());
websiteUrl.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.preventDefault();
void applyWebsiteTemplate();
}
});
function returnToToolbox() {
saucer.exposed.openIndex();
}
document.getElementById('back-button').addEventListener('click', returnToToolbox);
document.getElementById('cancel-button').addEventListener('click', returnToToolbox);
document.getElementById('lua-help-button').addEventListener('click', () => {
saucer.exposed.openLuaHelp();
});
iconInput.addEventListener('change', () => {
const file = iconInput.files[0];
iconBase64 = '';
iconPreview.replaceChildren('+');
if (!file) {
return;
}
if (file.type !== 'image/png' || file.size > 2 * 1024 * 1024) {
iconInput.value = '';
status.textContent = 'Bitte ein PNG mit maximal 2 MB auswählen.';
status.dataset.kind = 'error';
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const dataUrl = String(reader.result || '');
iconBase64 = dataUrl.includes(',') ? dataUrl.split(',', 2)[1] : '';
const image = document.createElement('img');
image.src = dataUrl;
image.alt = 'Gewähltes Tool-Icon';
iconPreview.replaceChildren(image);
status.textContent = '';
delete status.dataset.kind;
});
reader.readAsDataURL(file);
});
form.addEventListener('submit', async event => {
event.preventDefault();
const name = nameInput.value.trim();
if (!name) {
status.textContent = 'Bitte einen Namen eingeben.';
status.dataset.kind = 'error';
nameInput.focus();
return;
}
createButton.disabled = true;
status.textContent = 'Tool wird erstellt…';
status.dataset.kind = 'pending';
try {
const responseText = await saucer.exposed.createTool(JSON.stringify({
name,
description: descriptionInput.value,
timeoutSeconds: Number.parseInt(timeoutInput.value, 10) || 0,
luaScript: scriptInput.value,
iconBase64,
templateType,
templateTarget,
}));
const response = JSON.parse(responseText);
status.textContent = response.message || (response.success ? 'Tool wurde erstellt.' : 'Tool konnte nicht erstellt werden.');
status.dataset.kind = response.success ? 'success' : 'error';
if (response.success) {
window.setTimeout(returnToToolbox, 700);
return;
}
} catch (error) {
console.error('Tool konnte nicht erstellt werden:', error);
status.textContent = 'Tool konnte nicht erstellt werden.';
status.dataset.kind = 'error';
}
createButton.disabled = false;
});
highlightLua();
nameInput.focus();
</script>
</body>
</html>
-165
View File
@@ -1,165 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
<div id="shell">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
<ul>
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
<li><a href="#" onclick="showContent('none')">Option 3</a></li>
<li><a href="#" onclick="showContent('none')">Option 4</a></li>
</ul>
</div>
<!-- Hauptcontainer -->
<div class="main-container">
<!-- Header mit Zurück-Button -->
<div class="header">
<h1>Einstellungen</h1>
<button class="back-button" onclick="goBack()"></button>
</div>
<!-- Zentraler Editor (Standardmäßig versteckt) -->
<div class="editor" id="editor">
<label for="code-editor"></label>
<textarea id="code-editor" placeholder="Write Lua code here..."></textarea>
<pre><code id="highlightedOutput" class="language-lua"></code></pre>
</div>
<div class="general" id="general">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" class="settings-tool-name">-</div>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
</div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
<script>
function goBack() {
saucer.exposed.openIndex();
}
function showContent(option) {
const general = document.getElementById('general');
const editor = document.getElementById('editor');
if (option === 'editor') {
editor.style.display = 'flex';
general.style.display = 'none';
} else if (option === 'general') {
general.style.display = 'flex';
editor.style.display = 'none';
} else {
editor.style.display = 'none';
general.style.display = 'none';
}
}
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('code-editor');
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = highlightLua(editor.value);
});
const toolName = document.getElementById('tool-name');
const timeoutInput = document.getElementById('lua-timeout');
const saveBtn = document.getElementById('save-settings');
const saveStatus = document.getElementById('save-status');
saucer.exposed.getActiveToolName().then(name => {
toolName.textContent = name || '-';
});
saucer.exposed.getLuaTimeoutSeconds().then(value => {
timeoutInput.value = value;
});
saveBtn.addEventListener('click', () => {
const seconds = parseInt(timeoutInput.value, 10);
saucer.exposed.setLuaTimeoutSeconds(Number.isFinite(seconds) ? seconds : 0);
saveStatus.textContent = 'Gespeichert';
setTimeout(() => {
saveStatus.textContent = '';
}, 1500);
});
});
</script>
</body>
</html>
+420 -123
View File
@@ -1,9 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Toolbox</title>
<title>Tool bearbeiten</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page">
@@ -16,10 +16,11 @@
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<button class="titlebar-btn" onclick="goBack()" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Einstellungen</span>
<button class="titlebar-btn" id="back-button" data-webview-ignore></button>
<span class="titlebar-title" data-webview-drag>Tool bearbeiten</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
@@ -27,149 +28,445 @@
</div>
</div>
<div class="settings-body">
<!-- Sidebar für Optionen -->
<div class="sidebar">
<h2>Einstellungen</h2>
<ul>
<li><a href="#" onclick="showContent('general')">Einstellungen</a></li>
<li><a href="#" onclick="showContent('editor')">Skript</a></li>
<li><a href="#" onclick="showContent('none')">Option 3</a></li>
<li><a href="#" onclick="showContent('none')">Option 4</a></li>
</ul>
</div>
<!-- Hauptcontainer -->
<div class="main-container">
<!-- Zentraler Editor (Standardmäßig versteckt) -->
<div class="editor" id="editor">
<label for="code-editor"></label>
<textarea id="code-editor" placeholder="Write Lua code here..."></textarea>
<pre><code id="highlightedOutput" class="language-lua"></code></pre>
<main class="tool-settings-page">
<header class="tool-settings-heading">
<div id="settings-icon-preview" class="add-tool-icon-preview" aria-label="Tool-Icon">λ</div>
<div>
<span class="add-tool-eyebrow">Toolbox</span>
<h1 id="settings-heading">Tool bearbeiten</h1>
<p>Eigenschaften und Startverhalten dieses Tools.</p>
</div>
</header>
<div class="general" id="general">
<div class="settings-card">
<h3 style="margin: 0 0 10px 0;">Tool</h3>
<div id="tool-name" class="settings-tool-name">-</div>
<nav class="tool-settings-tabs" aria-label="Einstellungsbereiche">
<button type="button" class="tool-settings-tab is-active" data-panel="general">Allgemein</button>
<button type="button" class="tool-settings-tab" data-panel="launch">Startverhalten</button>
<button type="button" class="tool-settings-tab" data-panel="actions">Aktionen</button>
</nav>
<label for="lua-timeout">Lua Timeout (Sekunden)</label>
<input id="lua-timeout" class="settings-input" type="number" min="0" step="1" value="30">
<div class="settings-help">
0 = kein Timeout
<form id="tool-settings-form" class="tool-settings-form">
<section class="tool-settings-panel is-active" data-panel-content="general">
<div class="add-tool-section">
<h2>Allgemein</h2>
<label for="tool-name">Name</label>
<input id="tool-name" class="settings-input" maxlength="80" autocomplete="off" required>
<label for="tool-description">Beschreibung</label>
<textarea id="tool-description" class="settings-input add-tool-description" maxlength="8192"
placeholder="Was erledigt dieses Tool?"></textarea>
<div class="add-tool-grid">
<div>
<label for="tool-timeout">Lua-Timeout</label>
<div class="add-tool-input-suffix">
<input id="tool-timeout" class="settings-input" type="number" min="0" max="86400" value="30">
<span>Sek.</span>
</div>
<small class="tool-settings-help">0 deaktiviert den Timeout.</small>
</div>
<div>
<label for="tool-icon">Icon (PNG)</label>
<input id="tool-icon" class="settings-input add-tool-file" type="file" accept="image/png">
<button type="button" id="remove-icon" class="tool-settings-link">Icon entfernen</button>
</div>
</div>
</div>
</section>
<section class="tool-settings-panel" data-panel-content="launch">
<div class="add-tool-section add-tool-template-section">
<div class="add-tool-section-heading">
<div>
<h2>Starttyp</h2>
<p>Standardvorlagen bleiben ohne Lua-Kenntnisse bearbeitbar.</p>
</div>
<span id="template-badge" class="add-tool-template-badge">Lua</span>
</div>
<div class="add-tool-template-grid" role="group" aria-label="Starttyp">
<button type="button" class="add-tool-template" data-template="program">
<span class="add-tool-template-icon"></span>
<span><strong>Programm</strong><small>Eine .exe starten</small></span>
</button>
<button type="button" class="add-tool-template" data-template="folder">
<span class="add-tool-template-icon"></span>
<span><strong>Ordner</strong><small>Im Explorer öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="website">
<span class="add-tool-template-icon"></span>
<span><strong>Website</strong><small>Eine URL öffnen</small></span>
</button>
<button type="button" class="add-tool-template" data-template="lua">
<span class="add-tool-template-icon">λ</span>
<span><strong>Freies Lua</strong><small>Eigenes Skript</small></span>
</button>
</div>
<button id="save-settings" class="back-button settings-save">Speichern</button>
<div id="save-status" class="settings-status"></div>
<div id="native-target-controls" class="tool-settings-target" hidden>
<label for="template-target">Ausgewähltes Ziel</label>
<div class="add-tool-template-controls">
<input id="template-target" class="settings-input" readonly>
<button type="button" id="select-target" class="add-tool-secondary">Neu auswählen</button>
</div>
</div>
<div id="website-target-controls" class="tool-settings-target" hidden>
<label for="website-url">Website-Adresse</label>
<div class="add-tool-template-controls">
<input id="website-url" class="settings-input" type="url" autocomplete="url"
placeholder="https://example.com">
<button type="button" id="apply-website" class="add-tool-secondary">Übernehmen</button>
</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="footer">
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
</div>
<div id="lua-section" class="add-tool-section">
<div class="add-tool-section-heading">
<h2>start.lua</h2>
<button type="button" class="lua-help-trigger" id="lua-help-button">Lua-Cheatsheet</button>
</div>
<p class="tool-settings-help">Wird beim Klick auf die Toolkarte ausgeführt.</p>
<div class="add-tool-code-editor tool-settings-editor">
<pre id="tool-script-highlight" aria-hidden="true"><code class="language-lua"></code></pre>
<textarea id="tool-script" class="settings-input add-tool-script" spellcheck="false" wrap="off"
aria-label="Lua-Skript"></textarea>
</div>
</div>
<div id="generated-script-note" class="add-tool-section tool-settings-generated" hidden>
<span class="tool-settings-generated-icon"></span>
<div>
<h2>Startskript wird automatisch gepflegt</h2>
<p>Toolbox erzeugt <code>start.lua</code> beim Speichern sicher aus dem ausgewählten Ziel.</p>
</div>
</div>
</section>
<section class="tool-settings-panel" data-panel-content="actions">
<div class="add-tool-section tool-settings-action-card">
<div>
<h2>Toolordner</h2>
<p>Öffnet den Ordner mit <code>start.lua</code>, <code>config.ini</code> und weiteren Tooldateien.</p>
</div>
<button type="button" id="open-tool-folder" class="add-tool-secondary">Im Explorer öffnen</button>
</div>
<div class="add-tool-section tool-settings-action-card">
<div>
<h2 style="color:#ff6b6b">Tool löschen</h2>
<p>Entfernt dieses Tool unwiderruflich inklusive aller Dateien.</p>
</div>
<button type="button" id="delete-tool" class="add-tool-secondary" style="color:#ff6b6b;border-color:rgba(255,107,107,.4)">Löschen…</button>
</div>
</section>
</form>
</main>
<footer class="tool-settings-footer">
<div id="settings-status" class="settings-status" role="status" aria-live="polite"></div>
<button type="button" id="cancel-button" class="add-tool-secondary">Zurück</button>
<button type="submit" form="tool-settings-form" id="save-button" class="back-button settings-save">Speichern</button>
</footer>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
function goBack() {
const form = document.getElementById('tool-settings-form');
const nameInput = document.getElementById('tool-name');
const descriptionInput = document.getElementById('tool-description');
const timeoutInput = document.getElementById('tool-timeout');
const iconInput = document.getElementById('tool-icon');
const iconPreview = document.getElementById('settings-icon-preview');
const heading = document.getElementById('settings-heading');
const scriptInput = document.getElementById('tool-script');
const scriptHighlight = document.querySelector('#tool-script-highlight code');
const scriptHighlightContainer = document.getElementById('tool-script-highlight');
const nativeTargetControls = document.getElementById('native-target-controls');
const websiteTargetControls = document.getElementById('website-target-controls');
const targetInput = document.getElementById('template-target');
const websiteInput = document.getElementById('website-url');
const luaSection = document.getElementById('lua-section');
const generatedNote = document.getElementById('generated-script-note');
const templateBadge = document.getElementById('template-badge');
const templateButtons = [...document.querySelectorAll('[data-template]')];
const status = document.getElementById('settings-status');
const saveButton = document.getElementById('save-button');
let templateType = 'lua';
let templateTarget = '';
let iconBase64 = '';
let iconChanged = false;
function setStatus(message = '', kind = '') {
status.textContent = message;
if (kind) {
status.dataset.kind = kind;
} else {
delete status.dataset.kind;
}
}
function returnToToolbox() {
saucer.exposed.openIndex();
}
function showContent(option) {
const general = document.getElementById('general');
const editor = document.getElementById('editor');
function highlightLua() {
scriptHighlight.textContent = scriptInput.value + (scriptInput.value.endsWith('\n') ? ' ' : '');
Prism.highlightElement(scriptHighlight);
}
if (option === 'editor') {
editor.style.display = 'flex';
general.style.display = 'none';
} else if (option === 'general') {
general.style.display = 'flex';
editor.style.display = 'none';
} else {
editor.style.display = 'none';
general.style.display = 'none';
function renderIcon() {
iconPreview.replaceChildren();
if (!iconBase64) {
iconPreview.textContent = 'λ';
return;
}
const image = document.createElement('img');
image.src = `data:image/png;base64,${iconBase64}`;
image.alt = 'Tool-Icon';
iconPreview.appendChild(image);
}
function selectTemplate(kind) {
templateType = kind;
templateButtons.forEach(button => {
const selected = button.dataset.template === kind;
button.classList.toggle('is-active', selected);
button.setAttribute('aria-pressed', String(selected));
});
const labels = {program: 'Programm', folder: 'Ordner', website: 'Website', lua: 'Lua'};
templateBadge.textContent = labels[kind] || 'Lua';
nativeTargetControls.hidden = kind !== 'program' && kind !== 'folder';
websiteTargetControls.hidden = kind !== 'website';
luaSection.hidden = kind !== 'lua';
generatedNote.hidden = kind === 'lua';
targetInput.value = kind === 'program' || kind === 'folder' ? templateTarget : '';
websiteInput.value = kind === 'website' ? templateTarget : '';
}
function applyTemplateResponse(responseText) {
const response = JSON.parse(responseText);
if (response.cancelled) {
setStatus();
return false;
}
if (!response.success || !response.draft) {
setStatus(response.message || 'Das Ziel konnte nicht übernommen werden.', 'error');
return false;
}
templateType = response.draft.templateType;
templateTarget = response.draft.templateTarget;
scriptInput.value = response.draft.luaScript;
selectTemplate(templateType);
highlightLua();
setStatus('Startziel wurde übernommen. Zum Anwenden noch speichern.', 'success');
return true;
}
async function chooseNativeTarget(kind) {
setStatus(kind === 'program' ? 'Programm auswählen…' : 'Ordner auswählen…', 'pending');
try {
const response = kind === 'program'
? await saucer.exposed.selectProgramTemplate()
: await saucer.exposed.selectFolderTemplate();
applyTemplateResponse(response);
} catch (error) {
console.error('Zielauswahl fehlgeschlagen:', error);
setStatus('Der Auswahldialog konnte nicht geöffnet werden.', 'error');
}
}
document.addEventListener('DOMContentLoaded', () => {
const editor = document.getElementById('code-editor');
const output = document.getElementById('highlightedOutput');
showContent('general');
function escapeHtml(text) {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
async function applyWebsite() {
setStatus('Website wird geprüft…', 'pending');
try {
applyTemplateResponse(await saucer.exposed.createWebsiteTemplate(websiteInput.value.trim()));
} catch (error) {
console.error('Website-Prüfung fehlgeschlagen:', error);
setStatus('Die Website konnte nicht übernommen werden.', 'error');
}
}
function highlightLua(code) {
let escaped = escapeHtml(code);
const tokens = [];
function stash(pattern, className, input) {
return input.replace(pattern, (match) => {
const key = `@@TOKEN${tokens.length}@@`;
tokens.push(`<span class="${className}">${match}</span>`);
return key;
});
}
escaped = stash(/--\[\[[\s\S]*?\]\]|--[^\n]*/g, 'code-comment', escaped);
escaped = stash(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|\[\[[\s\S]*?\]\]/g, 'code-string', escaped);
escaped = escaped.replace(/\b(\d+(\.\d+)?)\b/g, '<span class="code-number">$1</span>');
const keywords = [
'and','break','do','else','elseif','end','false','for','function',
'if','in','local','nil','not','or','repeat','return','then','true',
'until','while'
];
const keywordRegex = new RegExp(`\\b(${keywords.join('|')})\\b`, 'g');
escaped = escaped.replace(keywordRegex, '<span class="code-keyword">$1</span>');
tokens.forEach((value, index) => {
escaped = escaped.replace(`@@TOKEN${index}@@`, value);
});
return escaped;
}
// Synchronisiere den Inhalt von textarea mit dem Codeblock
editor.addEventListener('input', () => {
output.innerHTML = highlightLua(editor.value);
function showPanel(name) {
document.querySelectorAll('[data-panel-content]').forEach(panel => {
panel.classList.toggle('is-active', panel.dataset.panelContent === name);
});
const toolName = document.getElementById('tool-name');
const timeoutInput = document.getElementById('lua-timeout');
const saveBtn = document.getElementById('save-settings');
const saveStatus = document.getElementById('save-status');
saucer.exposed.getActiveToolName().then(name => {
toolName.textContent = name || '-';
});
saucer.exposed.getLuaTimeoutSeconds().then(value => {
timeoutInput.value = value;
});
saveBtn.addEventListener('click', () => {
const seconds = parseInt(timeoutInput.value, 10);
saucer.exposed.setLuaTimeoutSeconds(Number.isFinite(seconds) ? seconds : 0);
saveStatus.textContent = 'Gespeichert';
setTimeout(() => {
saveStatus.textContent = '';
}, 1500);
document.querySelectorAll('[data-panel]').forEach(tab => {
tab.classList.toggle('is-active', tab.dataset.panel === name);
});
}
document.querySelectorAll('[data-panel]').forEach(tab => {
tab.addEventListener('click', () => showPanel(tab.dataset.panel));
});
</script>
templateButtons.forEach(button => {
button.addEventListener('click', () => {
const kind = button.dataset.template;
if (kind === 'program' || kind === 'folder') {
void chooseNativeTarget(kind);
return;
}
if (kind === 'website') {
templateTarget = '';
selectTemplate('website');
websiteInput.focus();
return;
}
templateTarget = '';
selectTemplate('lua');
scriptInput.focus();
});
});
document.getElementById('select-target').addEventListener('click', () => void chooseNativeTarget(templateType));
document.getElementById('apply-website').addEventListener('click', () => void applyWebsite());
websiteInput.addEventListener('keydown', event => {
if (event.key === 'Enter') {
event.preventDefault();
void applyWebsite();
}
});
scriptInput.addEventListener('input', highlightLua);
scriptInput.addEventListener('scroll', () => {
scriptHighlightContainer.scrollTop = scriptInput.scrollTop;
scriptHighlightContainer.scrollLeft = scriptInput.scrollLeft;
});
iconInput.addEventListener('change', () => {
const file = iconInput.files[0];
if (!file) {
return;
}
if (file.type !== 'image/png' || file.size > 2 * 1024 * 1024) {
iconInput.value = '';
setStatus('Bitte ein PNG mit maximal 2 MB auswählen.', 'error');
return;
}
const reader = new FileReader();
reader.addEventListener('load', () => {
const dataUrl = String(reader.result || '');
iconBase64 = dataUrl.includes(',') ? dataUrl.split(',', 2)[1] : '';
iconChanged = true;
renderIcon();
setStatus('Neues Icon ausgewählt. Zum Anwenden noch speichern.', 'success');
});
reader.readAsDataURL(file);
});
document.getElementById('remove-icon').addEventListener('click', () => {
iconBase64 = '';
iconChanged = true;
iconInput.value = '';
renderIcon();
setStatus('Icon wird beim Speichern entfernt.', 'success');
});
document.getElementById('open-tool-folder').addEventListener('click', async () => {
try {
const response = JSON.parse(await saucer.exposed.openToolFolder());
setStatus(response.message, response.success ? 'success' : 'error');
} catch (error) {
console.error('Toolordner konnte nicht geöffnet werden:', error);
setStatus('Der Toolordner konnte nicht geöffnet werden.', 'error');
}
});
document.getElementById('lua-help-button').addEventListener('click', () => saucer.exposed.openLuaHelp());
document.getElementById('delete-tool')?.addEventListener('click', async () => {
const toolName = nameInput.value.trim() || 'dieses Tool';
if (!confirm(`"${toolName}" unwiderruflich löschen?`)) {
return;
}
try {
const details = JSON.parse(await saucer.exposed.getToolDetails());
const toolPath = details.success ? (details.details?.path || details.path || '') : '';
if (!toolPath) {
setStatus('Tool-Pfad konnte nicht ermittelt werden.', 'error');
return;
}
const response = JSON.parse(await saucer.exposed.deleteTool(toolPath));
if (response.success) {
returnToToolbox();
} else {
setStatus(response.message || 'Löschen fehlgeschlagen.', 'error');
}
} catch (error) {
console.error('Löschen fehlgeschlagen:', error);
setStatus('Löschen fehlgeschlagen.', 'error');
}
});
document.getElementById('back-button').addEventListener('click', returnToToolbox);
document.getElementById('cancel-button').addEventListener('click', returnToToolbox);
form.addEventListener('submit', async event => {
event.preventDefault();
if (!nameInput.value.trim()) {
showPanel('general');
nameInput.focus();
setStatus('Bitte einen Toolnamen eingeben.', 'error');
return;
}
if (templateType === 'website') {
templateTarget = websiteInput.value.trim();
}
saveButton.disabled = true;
setStatus('Änderungen werden gespeichert…', 'pending');
const payload = {
name: nameInput.value.trim(),
description: descriptionInput.value,
timeoutSeconds: Number.parseInt(timeoutInput.value, 10) || 0,
luaScript: scriptInput.value,
templateType,
templateTarget,
};
if (iconChanged) {
payload.iconBase64 = iconBase64;
}
try {
const response = JSON.parse(await saucer.exposed.saveToolDetails(JSON.stringify(payload)));
setStatus(response.message || (response.success ? 'Tool wurde gespeichert.' : 'Speichern fehlgeschlagen.'),
response.success ? 'success' : 'error');
if (response.success) {
heading.textContent = nameInput.value.trim();
iconChanged = false;
}
} catch (error) {
console.error('Tool konnte nicht gespeichert werden:', error);
setStatus('Tool konnte nicht gespeichert werden.', 'error');
}
saveButton.disabled = false;
});
async function loadTool() {
setStatus('Tool wird geladen…', 'pending');
try {
const response = JSON.parse(await saucer.exposed.getToolDetails());
if (!response.success || !response.details) {
setStatus(response.message || 'Tool konnte nicht geladen werden.', 'error');
saveButton.disabled = true;
return;
}
const details = response.details;
nameInput.value = details.name || '';
descriptionInput.value = details.description || '';
timeoutInput.value = details.timeoutSeconds ?? 30;
scriptInput.value = details.luaScript || '';
templateType = details.templateType || 'lua';
templateTarget = details.templateTarget || '';
iconBase64 = details.iconBase64 || '';
iconChanged = false;
heading.textContent = details.name || 'Tool bearbeiten';
renderIcon();
selectTemplate(templateType);
highlightLua();
setStatus();
} catch (error) {
console.error('Tool konnte nicht geladen werden:', error);
setStatus('Tool konnte nicht geladen werden.', 'error');
saveButton.disabled = true;
}
}
void loadTool();
</script>
</body>
</html>
+203 -131
View File
@@ -20,6 +20,7 @@
<button onclick="openFileSystem()" id="file-system-button" class="button icon-button">&#x1f4c1;</button>
</div>
<button onclick="openEinstFirst()" class="button icon-button" id="option-button" style="display: none;">&#9881;</button>
<button onclick="toggleGeneralSettings()" class="button icon-button" id="general-settings-button" title="Einstellungen">&#9881;</button>
</div>
</div>
<div class="main" id="toolbox">
@@ -32,36 +33,54 @@
<!--Schmidti.Digital &copy; 2024-->
</div>
</div>
<div id="settings-overlay" class="settings-overlay" hidden>
<div class="settings-panel">
<div class="settings-panel-header">
<h2>Einstellungen</h2>
<button type="button" class="settings-panel-close" onclick="toggleGeneralSettings()">&times;</button>
</div>
<div class="settings-panel-body">
<div class="settings-row">
<div class="settings-row-label">
<span class="settings-row-title">Fenster-Deckkraft</span>
<span class="settings-row-desc">70% bis 100% bei 100% ist das Fenster vollstaendig deckend.</span>
</div>
<div class="settings-row-control">
<input type="range" id="opacity-slider" min="70" max="100" value="100" step="1">
<span id="opacity-value" class="settings-row-value">100%</span>
</div>
</div>
</div>
</div>
</div>
<script src="saucer://embedded/html/tool-runtime.js"></script>
<script>
function getNewContent(){
saucer.exposed.getNewContent();
}
function runLuaWithId(event) {
async function runLuaWithId(event) {
event.preventDefault();
if (event.target.tagName === 'BUTTON') {
// Falls der Button gedrückt wurde, brich die Funktion ab
if (event.target.closest('button')) {
return false;
}
const button = event.target;
const toolCard = button.closest('.tool-card');
const toolCard = event.currentTarget;
if (toolCard) {
const toolId = toolCard.getAttribute('data-tool-id');
const toolKey = toolCard.dataset.toolKey;
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) !== 'true') {
console.log(`Disabling button for tool ID: ${toolId}`);
saucer.exposed.run_lua(parseInt(toolId, 10));
// Button deaktivieren
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
// Speichern des Zustands im sessionStorage
sessionStorage.setItem(`toolButtonDisabled_${toolId}`, 'true');
} else {
if (ToolRuntime.isCardRunBlocked(toolCard)) {
console.log(`Button is already disabled for tool ID: ${toolId}`);
return false;
}
try {
const response = JSON.parse(await saucer.exposed.run_lua(toolKey));
if (response.singleInstance === true) {
await loadToolboxItems();
}
} catch (error) {
console.error('Fehler beim Starten des Tools:', error);
}
}
}
@@ -79,83 +98,93 @@
}
function openAddTool() {
openFileSystem();
saucer.exposed.openAddTool();
}
function createToolCard(item) {
const toolId = String(item.id);
const card = document.createElement('div');
card.className = 'tool-card';
card.dataset.url = String(item.url || '');
card.dataset.toolId = toolId;
card.dataset.toolKey = String(item.path || '');
card.addEventListener('click', runLuaWithId);
card.addEventListener('contextmenu', showContextMenu);
const icon = document.createElement('img');
const iconData = typeof item.icon === 'string' ? item.icon : '';
icon.src = iconData.startsWith('data:image/png;base64,')
? iconData
: `data:image/png;base64,${iconData}`;
icon.alt = String(item.name || '');
const info = document.createElement('div');
info.className = 'tool-info';
const name = document.createElement('h3');
name.textContent = String(item.name || '');
const description = document.createElement('p');
description.textContent = String(item.description || '');
const action = document.createElement('div');
action.className = 'tool-action';
const settingsButton = document.createElement('button');
settingsButton.type = 'button';
settingsButton.textContent = '⚙';
settingsButton.title = 'Tool bearbeiten';
settingsButton.setAttribute('aria-label', 'Tool bearbeiten');
settingsButton.addEventListener('click', event => {
openEinst(event, card.dataset.toolKey);
});
const runningStatus = document.createElement('span');
runningStatus.className = 'tool-running-status';
runningStatus.textContent = 'Läuft…';
runningStatus.hidden = true;
info.append(name, description);
action.append(runningStatus, settingsButton);
card.append(icon, info, action);
ToolRuntime.applyCardState(card, ToolRuntime.initialRunning(item));
return card;
}
function renderToolboxItems(items) {
const toolbox = document.getElementById('toolbox');
toolbox.querySelectorAll('.tool-card').forEach(card => card.remove());
const cards = document.createDocumentFragment();
items.forEach(item => cards.appendChild(createToolCard(item)));
toolbox.insertBefore(cards, toolbox.querySelector('.add-tool-card'));
}
function loadToolboxItems() {
return saucer.exposed.getToolboxItems()
.then(jsonString => {
const items = JSON.parse(jsonString);
renderToolboxItems(Array.isArray(items) ? items : []);
})
.catch(error => console.error('Fehler beim Abrufen der Toolbox-Elemente:', error));
}
function reloadContent() {
const toolbox = document.getElementById('toolbox');
// Toolbox leeren, aber das add-tool-card Element behalten
const addToolCard = toolbox.querySelector('.add-tool-card');
toolbox.innerHTML = '';
// Toolbox-Daten neu abrufen
saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString);
m_toolboxItems.forEach(item => {
const iconSrc = item.icon.startsWith('data:image/png;base64,')
? item.icon
: `data:image/png;base64,${item.icon}`;
const toolCardHTML = `
<div class="tool-card" data-url="${item.url}"
data-tool-id="${item.id}"
onclick="runLuaWithId(event)">
<img src="${iconSrc}" alt="${item.name}">
<div class="tool-info">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
<div class="tool-action">
<button onclick="openEinst(event, ${item.id})">Einst</button>
</div>
</div>
`;
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands der Buttons
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');
const button = card.querySelector('button');
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) === 'true') {
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
}
});
}).catch(error => console.error('Fehler beim Abrufen der Toolbox-Elemente:', error));
loadToolboxItems();
}
function enableButtonByToolId(toolId) {
saucer.exposed.debug_print();
// Finden Sie den Button mit dem entsprechenden data-tool-id
const button = document.querySelector(`.tool-card[data-tool-id='${toolId}']`);
if (button) {
button.disabled = false;
button.classList.remove('running');
button.textContent = 'Einst';
// Speichern des Zustands im sessionStorage
sessionStorage.setItem(`toolButtonDisabled_${toolId}`, 'false');
} else {
console.error('Button mit Tool-ID ' + toolId + ' nicht gefunden.');
}
function setToolRunningByPath(toolKey, running) {
const card = Array.from(document.querySelectorAll('.tool-card'))
.find(candidate => candidate.dataset.toolKey === toolKey);
ToolRuntime.applyCardState(card, running);
}
function openEinst(event, toolId){
function openEinst(event, toolPath){
if (event) {
event.stopPropagation();
event.preventDefault();
}
console.log('Einst Button geklickt!');
saucer.exposed.openEinst(toolId);
saucer.exposed.openEinst(toolPath);
}
function openEinstFirst(){
@@ -163,11 +192,100 @@
if (!firstCard) {
return;
}
const toolId = firstCard.getAttribute('data-tool-id');
saucer.exposed.openEinst(parseInt(toolId));
saucer.exposed.openEinst(firstCard.dataset.toolKey);
}
let activeContextMenu = null;
function closeContextMenu() {
if (activeContextMenu) {
activeContextMenu.remove();
activeContextMenu = null;
}
}
function showContextMenu(event) {
event.preventDefault();
event.stopPropagation();
closeContextMenu();
const card = event.currentTarget;
const toolKey = card.dataset.toolKey;
const toolName = card.querySelector('h3')?.textContent || 'dieses Tool';
const menu = document.createElement('div');
menu.className = 'context-menu';
const editBtn = document.createElement('button');
editBtn.type = 'button';
editBtn.textContent = 'Bearbeiten';
editBtn.addEventListener('click', () => {
closeContextMenu();
openEinst(null, toolKey);
});
const deleteBtn = document.createElement('button');
deleteBtn.type = 'button';
deleteBtn.textContent = 'Löschen';
deleteBtn.className = 'context-menu-danger';
deleteBtn.addEventListener('click', () => {
closeContextMenu();
confirmDeleteTool(toolKey, toolName);
});
menu.append(editBtn, deleteBtn);
document.body.append(menu);
activeContextMenu = menu;
const mx = Math.min(event.clientX, window.innerWidth - 180);
const my = Math.min(event.clientY, window.innerHeight - 90);
menu.style.left = mx + 'px';
menu.style.top = my + 'px';
}
async function confirmDeleteTool(toolKey, toolName) {
if (!confirm(`"${toolName}" wirklich löschen?`)) {
return;
}
try {
const response = JSON.parse(await saucer.exposed.deleteTool(toolKey));
if (response.success) {
await loadToolboxItems();
} else {
alert(response.message || 'Löschen fehlgeschlagen.');
}
} catch (error) {
console.error('Fehler beim Löschen:', error);
alert('Löschen fehlgeschlagen.');
}
}
document.addEventListener('click', closeContextMenu);
let settingsOverlayVisible = false;
let currentOpacity = 100;
function toggleGeneralSettings() {
const overlay = document.getElementById('settings-overlay');
settingsOverlayVisible = !settingsOverlayVisible;
overlay.hidden = !settingsOverlayVisible;
if (settingsOverlayVisible) {
const slider = document.getElementById('opacity-slider');
const valueLabel = document.getElementById('opacity-value');
slider.value = currentOpacity;
valueLabel.textContent = currentOpacity + '%';
}
}
document.addEventListener('DOMContentLoaded', () => {
const slider = document.getElementById('opacity-slider');
const valueLabel = document.getElementById('opacity-value');
slider.addEventListener('input', () => {
const val = parseInt(slider.value, 10);
valueLabel.textContent = val + '%';
currentOpacity = val;
document.getElementById('shell').style.opacity = (val / 100).toString();
});
document.addEventListener('click', (e) => {
const link = e.target.closest('a');
if (link) {
@@ -175,53 +293,7 @@
}
}, true);
const toolbox = document.getElementById('toolbox');
// Das add-tool-card Element entfernen und später wieder hinzufügen
const addToolCard = toolbox.querySelector('.add-tool-card');
saucer.exposed.getToolboxItems().then(jsonString => {
const m_toolboxItems = JSON.parse(jsonString);
m_toolboxItems.forEach(item => {
const iconSrc = item.icon.startsWith('data:image/png;base64,')
? item.icon
: `data:image/png;base64,${item.icon}`;
const toolCardHTML = `
<div class="tool-card" data-url="${item.url}"
data-tool-id="${item.id}"
onclick="runLuaWithId(event)">
<img src="${iconSrc}" alt="${item.name}">
<div class="tool-info">
<h3>${item.name}</h3>
<p>${item.description}</p>
</div>
<div class="tool-action">
<button onclick="openEinst(event, ${item.id})">Einst</button>
</div>
</div>
`;
toolbox.innerHTML += toolCardHTML;
});
// Das add-tool-card Element wieder hinzufügen
if (addToolCard) {
toolbox.appendChild(addToolCard);
}
// Überprüfen und Wiederherstellen des Zustands beim Laden der Seite
document.querySelectorAll('.tool-card').forEach(card => {
const toolId = card.getAttribute('data-tool-id');
const button = card.querySelector('button');
if (sessionStorage.getItem(`toolButtonDisabled_${toolId}`) === 'true') {
button.disabled = true;
button.classList.add('running');
button.textContent = 'Wird ausgeführt...';
}
});
}).catch(error => console.error('Error fetching toolbox items:', error));
loadToolboxItems();
});
</script>
</body>
+347
View File
@@ -0,0 +1,347 @@
/<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lua-Cheatsheet</title>
<link rel="stylesheet" href="saucer://embedded/html/styles.css">
</head>
<body class="settings-page lua-help-page">
<div id="shell" class="settings-shell">
<div class="resize-handle resize-top" data-webview-resize="t"></div>
<div class="resize-handle resize-right" data-webview-resize="r"></div>
<div class="resize-handle resize-bottom" data-webview-resize="b"></div>
<div class="resize-handle resize-left" data-webview-resize="l"></div>
<div class="resize-handle resize-top-left" data-webview-resize="tl"></div>
<div class="resize-handle resize-top-right" data-webview-resize="tr"></div>
<div class="resize-handle resize-bottom-left" data-webview-resize="bl"></div>
<div class="resize-handle resize-bottom-right" data-webview-resize="br"></div>
<div class="titlebar" data-webview-drag>
<div class="titlebar-left" data-webview-drag>
<span class="lua-help-mark" aria-hidden="true">λ</span>
<span class="titlebar-title" data-webview-drag>Lua-Hilfe</span>
</div>
<div class="titlebar-actions" data-webview-drag>
<button class="titlebar-btn" data-webview-minimize data-webview-ignore></button>
<button class="titlebar-btn close" data-webview-close data-webview-ignore>×</button>
</div>
</div>
<nav class="lua-help-tabs" aria-label="Lua-Hilfe" role="tablist">
<button class="lua-help-tab active" id="cheatsheet-tab" type="button" role="tab"
aria-selected="true" aria-controls="cheatsheet-panel">Cheatsheet</button>
<button class="lua-help-tab" id="official-docs-tab" type="button" role="tab"
aria-selected="false">
Offizielle Lua-5.4-Doku
</button>
<span class="lua-help-doc-status" id="docs-status" role="status" aria-live="polite">Online · lua.org</span>
</nav>
<div class="lua-help-panels">
<main class="lua-help-content" id="cheatsheet-panel" role="tabpanel" aria-labelledby="cheatsheet-tab">
<header class="lua-help-hero">
<div>
<span class="add-tool-eyebrow">Toolbox · Lua 5.4.1</span>
<h1>Lua auf einen Blick</h1>
<p>Die wichtigsten Muster für <code>start.lua</code> lokal verfügbar, kurz und kopierbar.</p>
</div>
<div class="lua-help-badge">start.lua</div>
</header>
<section class="lua-help-notice">
<strong>Toolbox-Kontext</strong>
<span>Das Skript startet beim Klick auf eine Tool-Karte. Timeout <code>0</code> bedeutet unbegrenzt. Tools sind vertrauenswürdig und dürfen über <code>os</code> und <code>io</code> auf das System zugreifen.</span>
</section>
<div class="lua-help-grid">
<article class="lua-help-card">
<h2>Nur eine laufende Instanz</h2>
<pre><code class="language-lua">-- toolbox: single-instance
-- Während dieses Skript läuft, wird seine
-- Tool-Karte rot angezeigt und gesperrt.
-- Ohne die Direktive sind parallele Starts erlaubt.</code></pre>
</article>
<article class="lua-help-card">
<h2>Variablen &amp; Typen</h2>
<pre><code class="language-lua">local name = "Toolbox"
local count = 3
local enabled = true
local nothing = nil
-- Typen prüfen
type(name) -- "string"
tostring(count) -- "3"
tonumber("42") -- 42</code></pre>
</article>
<article class="lua-help-card">
<h2>Operatoren</h2>
<pre><code class="language-lua">-- Rechnen: + - * / // % ^
local half = 7 // 2 -- 3
local text = "Lua " .. "5.4"
local length = #text
-- Vergleich: == ~= &lt; &gt; &lt;= &gt;=
-- Logik: and, or, not</code></pre>
</article>
<article class="lua-help-card">
<h2>Bedingungen</h2>
<pre><code class="language-lua">if count &gt; 10 then
print("groß")
elseif count &gt; 0 then
print("positiv")
else
print("leer")
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Schleifen</h2>
<pre><code class="language-lua">for i = 1, 5 do
print(i)
end
local i = 1
while i &lt;= 3 do
i = i + 1
end
repeat
i = i - 1
until i == 0</code></pre>
</article>
<article class="lua-help-card">
<h2>Funktionen</h2>
<pre><code class="language-lua">local function greet(name)
return "Hallo " .. name
end
local function position()
return 10, 20
end
local x, y = position()
print(greet("Toolbox"), x, y)</code></pre>
</article>
<article class="lua-help-card">
<h2>Tabellen</h2>
<pre><code class="language-lua">local tool = {
name = "Backup",
tags = { "files", "daily" }
}
print(tool.name)
print(tool.tags[1]) -- ab 1 indiziert
for key, value in pairs(tool) do
print(key, value)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Strings</h2>
<pre><code class="language-lua">local path = [[C:\Projekte\Toolbox]]
local upper = string.upper("lua")
local found = string.find("start.lua", "lua")
local formatted = string.format("%s: %d", "Dateien", 4)</code></pre>
</article>
<article class="lua-help-card">
<h2>Dateien</h2>
<pre><code class="language-lua">local file, err = io.open("status.txt", "w")
if not file then
error(err)
end
file:write("fertig\n")
file:close()</code></pre>
</article>
<article class="lua-help-card">
<h2>Fehler behandeln</h2>
<pre><code class="language-lua">local ok, result = pcall(function()
assert(false, "etwas ging schief")
end)
if not ok then
print("Fehler:", result)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Module</h2>
<pre><code class="language-lua">-- helper.lua
local M = {}
function M.run()
return "fertig"
end
return M
-- start.lua
local helper = require("helper")
print(helper.run())</code></pre>
</article>
<article class="lua-help-card">
<h2>Gültigkeitsbereiche</h2>
<pre><code class="language-lua">local outer = "sichtbar"
do
local inner = "nur im Block"
print(outer, inner)
end
-- Ohne local wird eine globale Variable erzeugt.
-- Globale Namen möglichst vermeiden.</code></pre>
</article>
<article class="lua-help-card">
<h2>Listen &amp; <code>ipairs</code></h2>
<pre><code class="language-lua">local files = { "a.txt", "b.txt" }
table.insert(files, "c.txt")
local last = table.remove(files)
for index, file in ipairs(files) do
print(index, file)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Mehrfachwerte &amp; Varargs</h2>
<pre><code class="language-lua">local function bounds()
return 10, 20
end
local function join(...)
return table.concat({ ... }, ", ")
end
local min, max = bounds()
print(join("Lua", min, max))</code></pre>
</article>
<article class="lua-help-card">
<h2>String-Muster</h2>
<pre><code class="language-lua">local text = "build-2026.log"
local year = text:match("build%-(%d+)%.log")
local clean = text:gsub("[^%w%.%-]", "_")
for word in string.gmatch("a,b,c", "[^,]+") do
print(word)
end</code></pre>
</article>
<article class="lua-help-card">
<h2>Zeit &amp; Datum</h2>
<pre><code class="language-lua">local now = os.time()
local stamp = os.date("%Y-%m-%d %H:%M:%S", now)
print(stamp)
local started = os.clock()
-- Arbeit ...
print("CPU-Sekunden", os.clock() - started)</code></pre>
</article>
<article class="lua-help-card">
<h2>Coroutinen</h2>
<pre><code class="language-lua">local worker = coroutine.create(function()
coroutine.yield("Schritt 1")
return "fertig"
end)
local ok, value = coroutine.resume(worker)
assert(ok, value)
print(value)</code></pre>
</article>
<article class="lua-help-card">
<h2>Metatables</h2>
<pre><code class="language-lua">local defaults = { timeout = 30 }
local config = setmetatable({}, {
__index = defaults
})
print(config.timeout) -- 30
config.timeout = 10</code></pre>
</article>
<article class="lua-help-card">
<h2>Defensiv aufräumen</h2>
<pre><code class="language-lua">local file &lt;close&gt; = assert(io.open("data.txt", "r"))
local content = file:read("*a")
-- Lua 5.4 schließt file beim Verlassen
-- des Gültigkeitsbereichs automatisch.
assert(#content &gt; 0, "Datei ist leer")</code></pre>
</article>
</div>
<section class="lua-help-libraries">
<h2>In Toolbox verfügbare Bibliotheken</h2>
<div class="lua-help-chips">
<code>base</code><code>os</code><code>string</code><code>coroutine</code><code>package</code>
<code>table</code><code>math</code><code>utf8</code><code>io</code>
</div>
</section>
<section class="lua-help-libraries">
<h2>Toolbox-Rezepte unter Windows</h2>
<div class="lua-help-grid">
<article class="lua-help-card">
<h2>Programm starten</h2>
<pre><code class="language-lua">-- start benötigt einen leeren Fenstertitel vor dem Pfad
local command = [[start "" "C:\Program Files\App\app.exe"]]
local ok, kind, code = os.execute(command)
assert(ok, string.format("Start fehlgeschlagen: %s %s", kind, code))</code></pre>
</article>
<article class="lua-help-card">
<h2>Ordner öffnen</h2>
<pre><code class="language-lua">local folder = [[C:\Projekte]]
os.execute('explorer "' .. folder .. '"')
-- Relative Pfade beziehen sich auf das
-- Arbeitsverzeichnis von Toolbox.</code></pre>
</article>
<article class="lua-help-card">
<h2>Umgebungsvariablen</h2>
<pre><code class="language-lua">local profile = assert(os.getenv("USERPROFILE"))
local temp = os.getenv("TEMP") or profile
print(profile, temp)</code></pre>
</article>
<article class="lua-help-card">
<h2>Sicheres Kommando-Quoting</h2>
<pre><code class="language-lua">local function quote(value)
assert(not value:find('"'), "Ungültiges Anführungszeichen")
return '"' .. value .. '"'
end
-- Keine ungeprüften Eingaben an os.execute übergeben.</code></pre>
</article>
</div>
</section>
<section class="lua-help-notice lua-help-footer-note">
<strong>Timeout &amp; Vertrauen</strong>
<span>Toolbox prüft den Timeout zwischen Lua-Instruktionen. Blockierende native Aufrufe wie <code>os.execute()</code> können erst nach ihrer Rückkehr unterbrochen werden. Führe daher nur überprüfte Skripte und Programme aus.</span>
</section>
</main>
</div>
</div>
<script src="saucer://embedded/vendor/prism/prism-core.min.js"></script>
<script src="saucer://embedded/vendor/prism/prism-lua.min.js"></script>
<script>
const docsTab = document.getElementById('official-docs-tab');
docsTab.addEventListener('click', () => {
saucer.exposed.showOfficialLuaDocs();
});
Prism.highlightAll();
</script>
</body>
</html>
+939 -15
View File
@@ -15,6 +15,32 @@
--accent: #4fc3ff;
--accent-deep: #1e74c9;
}
/* Globale Scrollbar-Stile im Toolbox-Design */
* {
scrollbar-width: thin;
scrollbar-color: rgba(60, 150, 220, 0.95) rgba(20, 45, 70, 0.4);
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: rgba(20, 45, 70, 0.3);
border-radius: 999px;
}
::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(120, 210, 255, 0.9), rgba(60, 150, 220, 0.95));
border-radius: 999px;
border: 2px solid rgba(20, 45, 70, 0.4);
}
::-webkit-scrollbar-thumb:hover {
background: linear-gradient(180deg, rgba(210, 245, 255, 0.85), rgba(120, 210, 255, 0.9));
}
::-webkit-scrollbar-corner {
background: transparent;
}
html {
background: transparent;
border-radius: 18px;
@@ -33,7 +59,7 @@
display: flex;
flex-direction: column;
height: 100vh;
background: rgba(10, 26, 40, 0.80);
background: rgba(10, 26, 40, 1.0);
border-radius: 18px;
overflow: hidden;
}
@@ -127,20 +153,6 @@
flex: 1;
padding: 16px;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.main::-webkit-scrollbar {
width: 10px;
}
.main::-webkit-scrollbar-track {
background: rgba(20, 45, 70, 0.45);
border-radius: 999px;
}
.main::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, rgba(120, 210, 255, 0.8), rgba(60, 150, 220, 0.9));
border-radius: 999px;
border: 2px solid rgba(20, 45, 70, 0.4);
}
.tool-card {
display: flex;
@@ -161,6 +173,14 @@
background: var(--glass-strong);
box-shadow: 0 16px 26px var(--shadow), inset 0 1px 0 var(--shine);
}
.tool-card.tool-running,
.tool-card.tool-running:hover {
transform: none;
cursor: not-allowed;
background: linear-gradient(135deg, rgba(125, 24, 35, 0.82), rgba(68, 18, 29, 0.9));
border-color: rgba(255, 102, 112, 0.85);
box-shadow: 0 12px 24px rgba(75, 5, 15, 0.48), inset 0 1px 0 rgba(255, 190, 195, 0.18);
}
.tool-card img {
width: 62px;
height: 62px;
@@ -185,8 +205,16 @@
color: #9bc4dc;
}
.tool-action {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.tool-running-status {
color: #ffb7bd;
font-size: 11px;
font-weight: 650;
}
.tool-action button {
background: linear-gradient(180deg, rgba(130, 220, 255, 0.9), rgba(40, 120, 190, 0.95));
border: 1px solid rgba(140, 210, 255, 0.6);
@@ -521,3 +549,899 @@
.settings-page .settings-save {
margin-top: 16px;
}
.add-tool-page {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 22px;
position: relative;
z-index: 1;
}
.add-tool-heading {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
margin-bottom: 18px;
}
.add-tool-heading h1 {
margin: 3px 0 5px;
color: var(--ink);
font-size: 24px;
}
.add-tool-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
}
.add-tool-heading code {
color: var(--accent);
}
.add-tool-eyebrow {
color: var(--accent);
font-size: 11px;
font-weight: 700;
letter-spacing: 1.4px;
text-transform: uppercase;
}
.add-tool-icon-preview {
width: 58px;
height: 58px;
flex: 0 0 58px;
display: grid;
place-items: center;
overflow: hidden;
border: 1px solid var(--glass-border);
border-radius: 16px;
background: var(--glass-strong);
color: var(--accent);
font-size: 28px;
box-shadow: 0 10px 20px var(--shadow), inset 0 1px 0 var(--shine);
}
.add-tool-icon-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}
.add-tool-form {
display: grid;
gap: 14px;
}
.add-tool-section {
padding: 16px;
border: 1px solid var(--glass-border);
border-radius: 14px;
background: var(--glass);
box-shadow: 0 12px 22px var(--shadow), inset 0 1px 0 var(--shine);
}
.add-tool-section h2 {
margin: 0 0 14px;
color: var(--ink);
font-size: 15px;
}
.add-tool-section label {
display: block;
margin: 11px 0 6px;
color: var(--ink-soft);
font-size: 12px;
font-weight: 600;
}
.add-tool-description {
min-height: 64px;
resize: vertical;
font-family: inherit;
}
.add-tool-grid {
display: grid;
grid-template-columns: minmax(130px, 0.7fr) minmax(180px, 1.3fr);
gap: 14px;
}
.add-tool-input-suffix {
display: flex;
align-items: center;
gap: 8px;
}
.add-tool-input-suffix span {
color: var(--ink-soft);
font-size: 12px;
}
.add-tool-file {
padding: 7px;
font-size: 12px;
}
.add-tool-section-heading {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
}
.add-tool-section-heading span {
color: var(--ink-soft);
font-size: 11px;
}
.add-tool-template-section .add-tool-section-heading {
align-items: flex-start;
}
.add-tool-template-section .add-tool-section-heading h2 {
margin-bottom: 4px;
}
.add-tool-template-section .add-tool-section-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
}
.add-tool-template-badge {
flex: 0 0 auto;
padding: 5px 9px;
border: 1px solid rgba(120, 225, 190, 0.38);
border-radius: 999px;
background: rgba(70, 190, 145, 0.14);
color: #91efc5 !important;
font-weight: 700;
}
.add-tool-template-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 9px;
margin-top: 14px;
}
.add-tool-template {
display: flex;
align-items: center;
gap: 10px;
min-width: 0;
padding: 10px;
border: 1px solid var(--glass-border);
border-radius: 11px;
background: rgba(12, 28, 45, 0.48);
color: var(--ink);
text-align: left;
cursor: pointer;
transition: border-color 0.16s ease, background 0.16s ease, transform 0.16s ease;
}
.add-tool-template:hover {
border-color: rgba(140, 210, 255, 0.48);
background: rgba(28, 60, 86, 0.58);
transform: translateY(-1px);
}
.add-tool-template.is-active {
border-color: rgba(120, 210, 255, 0.68);
background: linear-gradient(180deg, rgba(65, 150, 210, 0.28), rgba(30, 80, 120, 0.32));
box-shadow: inset 0 1px 0 var(--shine);
}
.add-tool-template-icon {
width: 30px;
height: 30px;
flex: 0 0 30px;
display: grid;
place-items: center;
border-radius: 9px;
background: rgba(75, 180, 235, 0.14);
color: var(--accent);
font-size: 15px;
}
.add-tool-template strong,
.add-tool-template small {
display: block;
}
.add-tool-template strong {
margin-bottom: 2px;
font-size: 12px;
}
.add-tool-template small {
overflow: hidden;
color: var(--ink-soft);
font-size: 10px;
text-overflow: ellipsis;
white-space: nowrap;
}
.add-tool-template-controls {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.add-tool-template-controls[hidden] {
display: none;
}
.add-tool-template-controls .settings-input {
margin: 0;
}
.add-tool-script-section > summary {
display: flex;
align-items: center;
justify-content: space-between;
color: var(--ink);
font-size: 13px;
font-weight: 700;
cursor: pointer;
list-style: none;
}
.add-tool-script-section > summary::-webkit-details-marker {
display: none;
}
.add-tool-script-section > summary::before {
content: "";
margin-right: 8px;
color: var(--accent);
font-size: 18px;
transition: transform 0.16s ease;
}
.add-tool-script-section[open] > summary::before {
transform: rotate(90deg);
}
.add-tool-script-section > summary small {
margin-left: auto;
color: var(--ink-soft);
font-size: 10px;
font-weight: 500;
}
.add-tool-script-section[open] > .add-tool-section-heading {
margin-top: 16px;
}
.add-tool-script-meta {
display: flex;
align-items: center;
gap: 10px;
}
.lua-help-trigger {
border: 1px solid rgba(120, 210, 255, 0.42);
border-radius: 999px;
padding: 5px 10px;
background: rgba(50, 135, 200, 0.24);
color: #aee6ff;
font: inherit;
font-size: 11px;
font-weight: 700;
cursor: pointer;
transition: background 0.16s ease, transform 0.16s ease;
}
.lua-help-trigger:hover {
background: rgba(70, 165, 230, 0.38);
transform: translateY(-1px);
}
.add-tool-code-editor {
position: relative;
min-height: 150px;
border: 1px solid var(--glass-border);
border-radius: 10px;
background: rgba(12, 28, 45, 0.6);
box-shadow: inset 0 1px 0 var(--shine);
overflow: hidden;
}
.add-tool-code-editor pre,
.settings-page .add-tool-script {
width: 100%;
min-height: 150px;
margin: 0;
padding: 12px;
border: 0;
border-radius: 0;
box-sizing: border-box;
font-family: "Consolas", "Courier New", monospace;
font-size: 13px;
line-height: 1.5;
tab-size: 4;
white-space: pre;
}
.add-tool-code-editor pre {
position: absolute;
inset: 0;
z-index: 1;
overflow: hidden;
pointer-events: none;
color: var(--ink);
}
.settings-page .add-tool-script {
position: relative;
z-index: 2;
resize: vertical;
overflow: auto;
background: transparent;
color: transparent;
caret-color: var(--ink);
box-shadow: none;
}
.settings-page .add-tool-script::selection {
background: rgba(90, 175, 235, 0.35);
color: transparent;
}
.add-tool-code-editor .token.comment {
color: #7897a8;
font-style: italic;
}
.add-tool-code-editor .token.string {
color: #ffd27a;
}
.add-tool-code-editor .token.number,
.add-tool-code-editor .token.boolean {
color: #8fe7ff;
}
.add-tool-code-editor .token.keyword {
color: #88bfff;
font-weight: 600;
}
.add-tool-code-editor .token.function {
color: #8be0c5;
}
.add-tool-code-editor .token.operator,
.add-tool-code-editor .token.punctuation {
color: #b9d5e5;
}
.add-tool-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
min-height: 38px;
}
.add-tool-actions .settings-status {
margin: 0 auto 0 2px;
}
.add-tool-actions .settings-save {
margin: 0;
}
.add-tool-secondary {
border: 1px solid var(--glass-border);
border-radius: 999px;
padding: 8px 14px;
background: rgba(12, 28, 45, 0.6);
color: var(--ink-soft);
cursor: pointer;
}
.settings-status[data-kind="success"] {
color: #83e8b2;
}
.settings-status[data-kind="error"] {
color: #ff9a9a;
}
.settings-status[data-kind="pending"] {
color: var(--accent);
}
.lua-help-mark {
width: 26px;
height: 26px;
display: grid;
place-items: center;
border: 1px solid rgba(140, 210, 255, 0.38);
border-radius: 8px;
background: rgba(79, 195, 255, 0.14);
color: #8fe7ff;
font: 700 18px/1 Georgia, serif;
}
.lua-help-tabs {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 6px;
padding: 9px 16px 0;
border-bottom: 1px solid rgba(140, 210, 255, 0.18);
background: rgba(10, 26, 40, 0.68);
}
.lua-help-tab {
position: relative;
bottom: -1px;
border: 1px solid transparent;
border-bottom: 0;
border-radius: 10px 10px 0 0;
padding: 9px 14px 10px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-size: 12px;
font-weight: 650;
cursor: pointer;
}
.lua-help-tab.active {
border-color: rgba(140, 210, 255, 0.28);
background: rgba(19, 43, 63, 0.96);
color: var(--ink);
}
.lua-help-tab:hover {
color: #8fe7ff;
background: rgba(79, 195, 255, 0.1);
}
.lua-help-doc-status {
margin-left: auto;
padding-right: 4px;
color: #8fe7ff;
font-size: 11px;
}
.lua-help-panels {
display: flex;
flex: 1;
min-height: 0;
}
.lua-help-content {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 22px;
box-sizing: border-box;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.lua-help-hero {
display: flex;
align-items: center;
justify-content: space-between;
gap: 20px;
margin-bottom: 16px;
}
.lua-help-hero h1 {
margin: 4px 0 6px;
font-size: 27px;
color: var(--ink);
}
.lua-help-hero p {
margin: 0;
color: var(--ink-soft);
font-size: 13px;
}
.lua-help-hero p code {
color: #8fe7ff;
}
.lua-help-badge {
border: 1px solid rgba(140, 210, 255, 0.36);
border-radius: 12px;
padding: 10px 14px;
background: rgba(79, 195, 255, 0.1);
color: #9fe2ff;
font: 700 12px/1 "Consolas", monospace;
box-shadow: inset 0 1px 0 var(--shine);
}
.lua-help-notice {
display: grid;
grid-template-columns: auto 1fr;
gap: 12px;
align-items: baseline;
margin-bottom: 16px;
padding: 12px 14px;
border: 1px solid rgba(255, 210, 122, 0.24);
border-radius: 12px;
background: rgba(95, 72, 25, 0.16);
color: var(--ink-soft);
font-size: 12px;
}
.lua-help-notice strong {
color: #ffd27a;
}
.lua-help-notice code {
color: #8fe7ff;
}
.lua-help-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 13px;
}
.lua-help-card,
.lua-help-libraries {
min-width: 0;
border: 1px solid var(--glass-border);
border-radius: 14px;
background: var(--glass);
box-shadow: 0 10px 20px rgba(4, 10, 16, 0.32), inset 0 1px 0 var(--shine);
}
.lua-help-card {
padding: 14px;
}
.lua-help-card h2,
.lua-help-libraries h2 {
margin: 0 0 10px;
color: #dff6ff;
font-size: 14px;
}
.lua-help-card pre {
margin: 0;
overflow-x: auto;
white-space: pre;
border: 0;
background: transparent;
color: var(--ink);
font: 12px/1.55 "Consolas", "Courier New", monospace;
tab-size: 4;
}
.lua-help-card .token.comment {
color: #7897a8;
font-style: italic;
}
.lua-help-card .token.string {
color: #ffd27a;
}
.lua-help-card .token.number,
.lua-help-card .token.boolean {
color: #8fe7ff;
}
.lua-help-card .token.keyword {
color: #88bfff;
font-weight: 650;
}
.lua-help-card .token.function {
color: #8be0c5;
}
.lua-help-card .token.operator,
.lua-help-card .token.punctuation {
color: #b9d5e5;
}
.lua-help-libraries {
margin-top: 14px;
padding: 14px;
}
.lua-help-chips {
display: flex;
flex-wrap: wrap;
gap: 7px;
}
.lua-help-chips code {
border: 1px solid rgba(140, 210, 255, 0.24);
border-radius: 999px;
padding: 5px 9px;
background: rgba(79, 195, 255, 0.1);
color: #aee6ff;
font-size: 11px;
}
.lua-help-footer-note {
margin: 14px 0 0;
}
@media (max-width: 720px) {
.lua-help-grid {
grid-template-columns: 1fr;
}
.lua-help-doc-status,
.lua-help-tab.active {
display: none;
}
.add-tool-script-meta {
align-items: flex-end;
flex-direction: column;
gap: 6px;
}
}
#create-button:disabled {
cursor: wait;
opacity: 0.65;
}
.tool-settings-page {
flex: 1;
min-height: 0;
overflow-y: auto;
padding: 20px 22px;
position: relative;
z-index: 1;
scrollbar-width: thin;
scrollbar-color: rgba(120, 210, 255, 0.7) rgba(20, 45, 70, 0.5);
}
.tool-settings-heading {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 16px;
}
.tool-settings-heading h1 {
margin: 3px 0 4px;
overflow: hidden;
color: var(--ink);
font-size: 21px;
text-overflow: ellipsis;
white-space: nowrap;
}
.tool-settings-heading p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
}
.tool-settings-tabs {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 6px;
margin-bottom: 14px;
padding: 5px;
border: 1px solid rgba(140, 210, 255, 0.22);
border-radius: 12px;
background: rgba(8, 22, 34, 0.46);
}
.tool-settings-tab {
border: 1px solid transparent;
border-radius: 9px;
padding: 8px 7px;
background: transparent;
color: var(--ink-soft);
font: inherit;
font-size: 11px;
font-weight: 650;
cursor: pointer;
transition: background 0.16s ease, border-color 0.16s ease, color 0.16s ease;
}
.tool-settings-tab:hover {
background: rgba(79, 195, 255, 0.1);
color: var(--ink);
}
.tool-settings-tab.is-active {
border-color: rgba(140, 210, 255, 0.38);
background: linear-gradient(180deg, rgba(65, 150, 210, 0.28), rgba(30, 80, 120, 0.32));
color: var(--ink);
box-shadow: inset 0 1px 0 var(--shine);
}
.tool-settings-form,
.tool-settings-panel {
min-width: 0;
}
.tool-settings-panel:not(.is-active) {
display: none;
}
.tool-settings-panel.is-active {
display: grid;
gap: 12px;
}
.tool-settings-help {
display: block;
margin: 6px 0 10px;
color: var(--ink-soft);
font-size: 11px;
}
.tool-settings-link {
margin-top: 7px;
padding: 0;
border: 0;
background: transparent;
color: #8fd7ff;
font: inherit;
font-size: 11px;
cursor: pointer;
}
.tool-settings-link:hover {
color: #d2f5ff;
text-decoration: underline;
}
.tool-settings-target {
margin-top: 12px;
}
.tool-settings-target > label {
display: block;
margin-bottom: 6px;
color: var(--ink-soft);
font-size: 12px;
font-weight: 600;
}
.tool-settings-target .add-tool-secondary {
flex: 0 0 auto;
}
.tool-settings-editor {
min-height: 225px;
}
.tool-settings-editor pre,
.tool-settings-editor .add-tool-script {
min-height: 225px;
}
.tool-settings-generated {
display: flex;
align-items: flex-start;
gap: 12px;
}
.tool-settings-generated-icon {
width: 30px;
height: 30px;
flex: 0 0 30px;
display: grid;
place-items: center;
border: 1px solid rgba(120, 225, 190, 0.38);
border-radius: 50%;
background: rgba(70, 190, 145, 0.14);
color: #91efc5;
font-weight: 800;
}
.tool-settings-generated h2,
.tool-settings-action-card h2 {
margin-bottom: 5px;
}
.tool-settings-generated p,
.tool-settings-action-card p {
margin: 0;
color: var(--ink-soft);
font-size: 12px;
line-height: 1.5;
}
.tool-settings-generated code,
.tool-settings-action-card code {
color: var(--accent);
}
.tool-settings-action-card {
display: flex;
align-items: center;
justify-content: space-between;
gap: 18px;
}
.tool-settings-action-card .add-tool-secondary {
flex: 0 0 auto;
}
.tool-settings-footer {
position: relative;
z-index: 2;
display: flex;
align-items: center;
gap: 10px;
min-height: 38px;
padding: 10px 14px;
border-top: 1px solid rgba(140, 210, 255, 0.2);
background: linear-gradient(180deg, rgba(16, 36, 54, 0.92), rgba(8, 20, 31, 0.94));
box-shadow: 0 -8px 20px rgba(4, 10, 16, 0.28);
}
.tool-settings-footer .settings-status {
flex: 1;
margin: 0;
}
.tool-settings-footer .settings-save {
margin: 0;
}
#save-button:disabled {
cursor: wait;
opacity: 0.65;
}
/* Kontextmenü */
.context-menu {
position: fixed;
z-index: 9999;
min-width: 160px;
padding: 6px 0;
border-radius: 10px;
background: rgba(15, 30, 50, 0.98);
border: 1px solid rgba(140, 210, 255, 0.22);
box-shadow: 0 8px 28px rgba(0, 0, 0, 0.45);
backdrop-filter: blur(12px);
}
.context-menu button {
display: block;
width: 100%;
padding: 8px 16px;
border: none;
background: transparent;
color: #dff6ff;
font: 13px/1.3 "Segoe UI", sans-serif;
text-align: left;
cursor: pointer;
transition: background 0.12s ease;
}
.context-menu button:hover {
background: rgba(40, 80, 120, 0.6);
}
.context-menu .context-menu-danger {
color: #ff8a80;
}
.context-menu .context-menu-danger:hover {
background: rgba(180, 40, 40, 0.4);
}
/* Allgemeine Einstellungen Overlay */
.settings-overlay {
position: fixed;
inset: 0;
z-index: 9000;
background: rgba(4, 10, 16, 0.55);
backdrop-filter: blur(6px);
display: flex;
align-items: center;
justify-content: center;
}
.settings-overlay[hidden] {
display: none;
}
.settings-panel {
width: 340px;
max-height: 70vh;
overflow-y: auto;
background: linear-gradient(180deg, rgba(18, 40, 60, 0.97), rgba(12, 28, 45, 0.98));
border: 1px solid var(--glass-border);
border-radius: 16px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
}
.settings-panel-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
border-bottom: 1px solid rgba(140, 210, 255, 0.15);
}
.settings-panel-header h2 {
margin: 0;
font-size: 16px;
color: var(--ink);
}
.settings-panel-close {
background: none;
border: none;
color: var(--ink-soft);
font-size: 22px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
}
.settings-panel-close:hover {
color: var(--accent);
}
.settings-panel-body {
padding: 16px 20px;
}
.settings-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.settings-row-label {
flex: 1;
}
.settings-row-title {
display: block;
font-size: 13px;
font-weight: 600;
color: var(--ink);
margin-bottom: 2px;
}
.settings-row-desc {
display: block;
font-size: 11px;
color: var(--ink-soft);
opacity: 0.7;
line-height: 1.3;
}
.settings-row-control {
display: flex;
align-items: center;
gap: 8px;
}
.settings-row-value {
font-size: 12px;
font-weight: 600;
color: var(--accent);
min-width: 38px;
text-align: right;
}
.settings-row-control input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 100px;
height: 6px;
border-radius: 999px;
background: rgba(20, 45, 70, 0.6);
outline: none;
}
.settings-row-control input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: linear-gradient(180deg, var(--aqua-2), var(--aqua-3));
border: 2px solid rgba(20, 45, 70, 0.5);
cursor: pointer;
}
.settings-row-control input[type="range"]::-moz-range-thumb {
width: 14px;
height: 14px;
border-radius: 50%;
background: var(--aqua-3);
border: 2px solid rgba(20, 45, 70, 0.5);
cursor: pointer;
}
+33
View File
@@ -0,0 +1,33 @@
(function (root, factory) {
const api = factory();
if (typeof module === 'object' && module.exports) {
module.exports = api;
}
root.ToolRuntime = api;
}(typeof globalThis !== 'undefined' ? globalThis : this, function () {
'use strict';
function initialRunning(item) {
return item?.singleInstance === true && item?.running === true;
}
function isCardRunBlocked(card) {
return card?.dataset?.runDisabled === 'true';
}
function applyCardState(card, running) {
if (!card) {
return;
}
const active = running === true;
card.classList.toggle('tool-running', active);
card.dataset.runDisabled = active ? 'true' : 'false';
card.setAttribute('aria-busy', active ? 'true' : 'false');
const status = card.querySelector('.tool-running-status');
if (status) {
status.hidden = !active;
}
}
return { initialRunning, isCardRunBlocked, applyCardState };
}));