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
res/html/add_tool.html Normal file
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>

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>

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>

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>

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>

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
res/html/tool-runtime.js Normal file
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 };
}));

21
res/vendor/prism/LICENSE.txt vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT LICENSE
Copyright (c) 2012 Lea Verou
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

1
res/vendor/prism/prism-core.min.js vendored Normal file

File diff suppressed because one or more lines are too long

1
res/vendor/prism/prism-lua.min.js vendored Normal file
View File

@@ -0,0 +1 @@
Prism.languages.lua={comment:/^#!.+|--(?:\[(=*)\[[\s\S]*?\]\1\]|.*)/m,string:{pattern:/(["'])(?:(?!\1)[^\\\r\n]|\\z(?:\r\n|\s)|\\(?:\r\n|[^z]))*\1|\[(=*)\[[\s\S]*?\]\2\]/,greedy:!0},number:/\b0x[a-f\d]+(?:\.[a-f\d]*)?(?:p[+-]?\d+)?\b|\b\d+(?:\.\B|(?:\.\d*)?(?:e[+-]?\d+)?\b)|\B\.\d+(?:e[+-]?\d+)?\b/i,keyword:/\b(?:and|break|do|else|elseif|end|false|for|function|goto|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,function:/(?!\d)\w+(?=\s*(?:[({]))/,operator:[/[-+*%^&|#]|\/\/?|<[<=]?|>[>=]?|[=~]=?/,{pattern:/(^|[^.])\.\.(?!\.)/,lookbehind:!0}],punctuation:/[\[\](){},;]|\.+|:+/};