initial files

This commit is contained in:
2026-04-22 11:55:23 +02:00
commit 92444ff38c
85 changed files with 16324 additions and 0 deletions

View File

@@ -0,0 +1,72 @@
const Handlebars = require('handlebars');
module.exports = {
toJSON: function(object) {
if(typeof object === 'object') {
return JSON.stringify(object);
} else {
throw 'no object type';
}
},
isObject: function(value, options) {
return typeof value === 'object' && value !== null && !Array.isArray(value) ? options.fn(this) : options.inverse(this);
},
isArray: function(value, options) {
return Array.isArray(value) ? options.fn(this) : options.inverse(this);
},
isRGB: function(value) {
return Array.isArray(value) && (value.length === 3 || value.length === 4);
},
rgbString: function(value) {
if (Array.isArray(value)) {
return value.length === 3
? `rgb(${value.join(',')})`
: value.length === 4
? `rgba(${value.join(',')})`
: '';
}
return '';
},
toArray: function(value) {
if (Array.isArray(value)) return value;
if (typeof value === "string") return [value];
return [];
},
replaceAll: function(string, pattern, replacement) {
return new Handlebars.SafeString(string.replaceAll(pattern, replacement) || '');
},
equaler: function(v1, operator, v2, options) {
switch (operator) {
case '==':
return (v1 == v2) ? options.fn(this) : options.inverse(this);
case '===':
return (v1 === v2) ? options.fn(this) : options.inverse(this);
case '!=':
return (v1 != v2) ? options.fn(this) : options.inverse(this);
case '!==':
return (v1 !== v2) ? options.fn(this) : options.inverse(this);
case '<':
return (v1 < v2) ? options.fn(this) : options.inverse(this);
case '<=':
return (v1 <= v2) ? options.fn(this) : options.inverse(this);
case '>':
return (v1 > v2) ? options.fn(this) : options.inverse(this);
case '>=':
return (v1 >= v2) ? options.fn(this) : options.inverse(this);
case '&&':
return (v1 && v2) ? options.fn(this) : options.inverse(this);
case '||':
return (v1 || v2) ? options.fn(this) : options.inverse(this);
case 'typeof':
return (typeof v1 === v2) ? options.fn(this) : options.inverse(this);
case 'like':
return (v1.indexOf(v2)) > -1 ? options.fn(this) : options.inverse(this);
case 'includes':
return (v1.includes(v2)) <= 0 ? options.inverse(this) : options.fn(this);
case '%':
return (v1 % v2) == 0 ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
};

View File

@@ -0,0 +1,6 @@
// const moment = require('moment'); // npm install moment
// moment.locale('de');
module.exports = {
dateFormat: (date, format) => dateFormat(date, format)
};

View File

@@ -0,0 +1,24 @@
module.exports = {
// objectTree: function(context, options) {
// let ret = '';
// if (context === null || context === undefined) return ret;
// if (Array.isArray(context)) {
// context.forEach((item, index) => {
// ret += options.fn({ key: index, value: item });
// });
// } else if (typeof context === 'object') {
// for (const key in context) {
// if (context.hasOwnProperty(key)) {
// ret += options.fn({ key, value: context[key] });
// }
// }
// } else {
// // primitive Werte
// ret += options.fn({ key: null, value: context });
// }
// return ret;
// }
}

View File

@@ -0,0 +1,145 @@
module.exports = {
objectTree: function(obj) {
function traverse(o) {
if (o === null || typeof o !== 'object') return o;
if (Array.isArray(o)) {
// RGB oder RGBA-Erkennung: Array aus 3 oder 4 Zahlen
if ((o.length === 3 || o.length === 4) && o.every(v => typeof v === 'number')) {
// const rgbString = o.length === 3
// ? `rgb(${o.join(',')})`
// : `rgba(${o.join(',')})`;
const rgbString = o.join(',');
return { key: null, value: rgbString, children: null, isRGB: true };
}
// normales Array
return o.map((v, i) => ({
key: i,
value: typeof v !== 'object' ? v : null,
children: typeof v === 'object' ? traverse(v) : null
}));
}
const result = [];
for (const key in o) {
if (o.hasOwnProperty(key)) {
const val = o[key];
const children = (val !== null && typeof val === 'object') ? traverse(val) : null;
result.push({
key,
value: children ? null : val,
children
});
}
}
return result;
}
return traverse(obj);
},
jsonEntriesRecursive: function(obj, options) {
const entries = [];
function traverse(prefix, data) {
if (Array.isArray(data)) {
data.forEach((item, index) => {
const arrayKey = `${prefix}[${index}]`;
if (typeof item === "object" && item !== null) {
traverse(arrayKey, item); // Rekursion für Array-Objekte
} else {
entries.push({ key: arrayKey, value: item });
}
});
} else if (typeof data === "object" && data !== null) {
for (const key in data) {
if (!Object.prototype.hasOwnProperty.call(data, key)) continue;
const value = data[key];
const fullKey = prefix ? `${prefix}.${key}` : key;
if (typeof value === "object" && value !== null) {
traverse(fullKey, value); // Rekursion für verschachtelte Objekte
} else {
entries.push({ key: fullKey, value });
}
}
} else {
// Primitive Werte am obersten Level
entries.push({ key: prefix, value: data });
}
console.log({ key: prefix, value: data })
}
traverse("", obj);
// Handlebars-Block Helper
if (options.fn) {
return options.fn(entries);
}
return entries;
},
groupBy: function(items, keys, options) {
// keys kann String oder Array sein
if (!Array.isArray(keys)) keys = [keys];
if (!keys.length) return '';
const key = keys[0]; // aktueller Gruppierungsschlüssel
const remainingKeys = keys.slice(1); // restliche Schlüssel
// Gruppieren nach aktuellem Schlüssel
const groups = {};
items.forEach(item => {
let groupKeys = item[key];
if (!Array.isArray(groupKeys)) groupKeys = [groupKeys];
groupKeys.forEach(groupKey => {
if (!groups[groupKey]) groups[groupKey] = [];
groups[groupKey].push(item);
});
});
// Ergebnis zusammensetzen
let result = '';
for (const group in groups) {
const groupItems = groups[group];
if (remainingKeys.length > 0) {
// Rekursive Verschachtelung
// Wir rufen die gleiche Helper-Funktion intern auf
const nestedResult = Handlebars.helpers.groupBy(groupItems, remainingKeys, options);
result += options.fn({ key: group, items: nestedResult });
} else {
// Basisfall: keine weiteren Keys, items direkt weitergeben
result += options.fn({ key: group, items: groupItems });
}
}
return result;
},
parseArray: function(str){
try {
return JSON.parse(str);
} catch (e) {
return [];
}
},
ifSingle: function(array, options) {
if (Array.isArray(array) && array.length === 1) {
return options.fn(array[0]); // Item direkt übergeben
}
return options.inverse(this); // else-Block
},
findValue: function(array, searchKey, searchValue, returnKey) {
if (!Array.isArray(array)) return "";
for (let i = 0; i < array.length; i++) {
const item = array[i];
if (item && item[searchKey] === searchValue) {
return item[returnKey] ?? "";
}
}
return "";
}
};

BIN
public/images/app.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 729 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

BIN
public/images/brush.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

BIN
public/images/eventlog.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
public/images/folder.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

BIN
public/images/help.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

BIN
public/images/plugins.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 914 B

BIN
public/images/tutorial.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

361
public/javascript/JSON.js Normal file
View File

@@ -0,0 +1,361 @@
/*
createJsonTree({
container: domElement,
data: jsonData,
onChange: (data) => { }, // optional: callback on change
onSave: (data) => { } // optional: if set, a save button will be added
});
*/function createJsonTree({
container,
data,
onChange = () => {},
onSave = null
}) {
container.innerHTML = "";
container.classList.add("json-tree-root");
const history = [];
const redoStack = [];
let lastSnapshot = clone(data);
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function diff(oldObj, newObj) {
const changes = {};
function walk(o, n, path = "") {
const keys = new Set([
...Object.keys(o || {}),
...Object.keys(n || {})
]);
for (const key of keys) {
const oldVal = o?.[key];
const newVal = n?.[key];
const currentPath = path ? `${path}.${key}` : key;
if (
typeof oldVal === "object" &&
oldVal !== null &&
typeof newVal === "object" &&
newVal !== null &&
!Array.isArray(oldVal)
) {
walk(oldVal, newVal, currentPath);
continue;
}
if (JSON.stringify(oldVal) !== JSON.stringify(newVal)) {
changes[currentPath] = newVal;
}
}
}
walk(oldObj, newObj);
return changes;
}
function pushHistory() {
history.push(clone(data));
redoStack.length = 0;
}
function undo() {
if (!history.length) return;
redoStack.push(clone(data));
data = history.pop();
render();
}
function redo() {
if (!redoStack.length) return;
history.push(clone(data));
data = redoStack.pop();
render();
}
function save() {
if (onSave) onSave(data);
}
function autoResize(input) {
input.style.width = "10px";
input.style.width = input.scrollWidth + "px";
}
function remove(key, parent) {
if (key === null || parent == null) return;
if (!confirm("Wirklich löschen?")) return;
pushHistory();
if (Array.isArray(parent)) {
parent.splice(key, 1);
} else {
delete parent[key];
}
onChange(data);
render();
}
function render() {
container.innerHTML = "";
const controls = document.createElement("div");
controls.className = "json-controls";
const undoBtn = document.createElement("button");
undoBtn.className = "monolyth";
undoBtn.textContent = "Undo";
undoBtn.onclick = undo;
const redoBtn = document.createElement("button");
redoBtn.className = "monolyth";
redoBtn.textContent = "Redo";
redoBtn.onclick = redo;
controls.appendChild(undoBtn);
controls.appendChild(redoBtn);
if (onSave) {
const saveBtn = document.createElement("button");
saveBtn.className = "monolyth";
saveBtn.textContent = "Save";
saveBtn.onclick = save;
controls.appendChild(saveBtn);
}
container.appendChild(controls);
container.appendChild(renderNode(data, null, null, "root", 0));
}
function renderNode(value, key, parent, path, level) {
const wrapper = document.createElement("div");
wrapper.className = "json-line";
wrapper.style.marginLeft = `${level * 18}px`;
const keySpan = document.createElement("span");
keySpan.className = "json-key";
if (key !== null) {
keySpan.textContent = `"${key}": `;
}
const removeBtn = document.createElement("span");
removeBtn.className = "json-remove";
removeBtn.textContent = " [x]";
removeBtn.onclick = (evt) => {
evt.stopPropagation();
remove(key, parent);
};
// OBJECT / ARRAY
if (typeof value === "object" && value !== null) {
const isArray = Array.isArray(value);
const header = document.createElement("div");
header.className = "json-header";
const toggle = document.createElement("span");
toggle.className = "json-toggle";
toggle.textContent = isArray ? "[ ]" : "{ }";
const keyLabel = document.createElement("span");
keyLabel.className = "json-key";
if (key !== null) keyLabel.textContent = `"${key}": `;
const addBtn = document.createElement("span");
addBtn.className = "json-add";
addBtn.textContent = " [+]";
const children = document.createElement("div");
children.className = "json-children";
keyLabel.onclick = toggle.onclick = () => {
children.classList.toggle("collapsed");
};
addBtn.onclick = () => {
let newKey = "";
let newValue;
feedbox({
title: `<span style="color:#f44336">Key</span> hinzufügen`,
message: `
<div style="display:flex;flex-direction:column;gap:8px;">
${!isArray ? `
<input id="newJsonKeyName" placeholder="Key Name" style="padding:6px;" />
` : ``}
<select id="newJsonValueType">
<option value="text">Text</option>
<option value="number">Zahl</option>
<option value="boolean">Boolean</option>
<option value="object">Objekt</option>
<option value="array">Array</option>
</select>
</div>
`,
buttons: {
yes: {
text: '<b>Anlegen</b>',
onClick: () => {
pushHistory();
if (!isArray) {
const input = document.querySelector('#newJsonKeyName');
newKey = input?.value?.trim();
if (!newKey) return;
}
const selectedType = document.querySelector('#newJsonValueType').value;
switch (selectedType) {
case "number": newValue = 0; break;
case "boolean": newValue = false; break;
case "object": newValue = {}; break;
case "array": newValue = []; break;
default: newValue = "";
}
if (isArray) {
value.push(newValue);
} else {
value[newKey] = newValue;
}
onChange(data);
render();
}
},
no: { text: 'Abbrechen' }
}
});
};
const inner = document.createElement("div");
const entries = isArray
? value.map((v, i) => [i, v])
: Object.entries(value);
entries.forEach(([k, v]) => {
inner.appendChild(
renderNode(v, k, value, `${path}.${k}`, level + 1)
);
});
children.appendChild(inner);
header.appendChild(keyLabel);
header.appendChild(toggle);
header.appendChild(addBtn);
if (key !== null) header.appendChild(removeBtn);
wrapper.appendChild(header);
wrapper.appendChild(children);
return wrapper;
}
// STRING
if (typeof value === "string") {
const input = document.createElement("input");
input.className = "json-input string";
input.value = value;
input.oninput = () => {
pushHistory();
parent[key] = input.value;
autoResize(input);
onChange(data);
};
setTimeout(() => autoResize(input), 0);
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
// NUMBER
if (typeof value === "number") {
const input = document.createElement("input");
input.type = "number";
input.className = "json-input number";
input.value = value;
input.oninput = () => {
pushHistory();
parent[key] = Number(input.value);
autoResize(input);
onChange(data);
};
setTimeout(() => autoResize(input), 0);
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
// BOOLEAN
if (typeof value === "boolean") {
const input = document.createElement("input");
input.type = "checkbox";
input.checked = value;
input.onchange = () => {
pushHistory();
parent[key] = input.checked;
onChange(data);
};
wrapper.appendChild(keySpan);
wrapper.appendChild(input);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
const span = document.createElement("span");
span.textContent = String(value);
wrapper.appendChild(keySpan);
wrapper.appendChild(span);
if (key !== null) wrapper.appendChild(removeBtn);
return wrapper;
}
pushHistory();
render();
return {
undo,
redo,
save,
refresh: render,
getData: () => data,
getChanges() {
return diff(lastSnapshot, data);
},
commit() {
lastSnapshot = clone(data);
},
update(newData) {
data = newData;
lastSnapshot = clone(newData);
pushHistory();
render();
}
};
}

View File

@@ -0,0 +1,244 @@
/*
const ctx = new ContextMenu();
ctx.setItems([
{ label: "Öffnen", disable: true, onClick: () => alert("Öffnen!") },
{ label: "Öffnen", onClick: () => alert("Öffnen!") },
{ label: "Bearbeiten", onClick: () => alert("Bearbeiten…") },
{ type: "divider" },
{
label: "Mehr Optionen",
color: 'rgb()|color-name|color-value',
children: [
{ label: "Duplizieren", onClick: () => alert("Dupliziert!") },
{ label: "Umbenennen", onClick: () => alert("Rename…") },
{
label: "Exportieren",
children: [
{ label: "PDF", onClick: () => alert("PDF export") },
{ label: "CSV", onClick: () => alert("CSV export") },
]
}
]
},
{ label: "Löschen", onClick: () => alert("Gelöscht!") },
]);
ctx.show(element, or location, { position: "right" });
*/
class ContextMenu {
constructor() {
this.menu = document.createElement("div");
this.menu.className = "ctx-menu hidden";
document.body.appendChild(this.menu);
// close on click outside → sofort
document.addEventListener("click", (e) => {
if (!this.menu.contains(e.target)) {
this.closeAllSubmenus();
this.hide();
}
});
window.addEventListener("resize", () => this.hide());
window.addEventListener("scroll", () => this.hide());
}
setItems(items) {
this.menu.innerHTML = "";
this.menu.appendChild(this.buildMenu(items));
}
buildMenu(items) {
const ul = document.createElement("ul");
ul.className = "ctx-list";
items.forEach(item => {
if (item.type === "divider") {
const divider = document.createElement("li");
divider.className = "ctx-divider";
ul.appendChild(divider);
return;
}
const li = document.createElement("li");
li.className = "ctx-item";
if (item.color) li.style.boxShadow = `inset 5px 0px 0px 0px ${item.color}`;
if (item.disabled) li.classList.add("ctx-disabled");
li.innerHTML = `
<span class="ctx-label">${item.label}</span>
${item.children ? "<span class='ctx-arrow'>▶</span>" : ""}
`;
if (item.onClick && !item.children && !item.disabled) {
li.addEventListener("click", e => {
e.stopPropagation();
item.onClick(e);
this.hide(); // Hauptmenü sofort schließen
});
}
if (item.children) {
const submenu = this.buildMenu(item.children);
submenu.classList.add("ctx-submenu");
li.appendChild(submenu);
let hideTimeout;
const openSubmenu = () => {
clearTimeout(hideTimeout);
submenu.classList.add("open");
submenu.style.left = "";
submenu.style.top = "";
const liRect = li.getBoundingClientRect();
const submenuRect = submenu.getBoundingClientRect();
let left = li.offsetWidth;
if (liRect.right + submenuRect.width > window.innerWidth) left = -submenuRect.width;
submenu.style.left = left + "px";
let top = 0;
if (liRect.top + submenuRect.height > window.innerHeight) {
top = window.innerHeight - (liRect.top + submenuRect.height) - 4;
}
submenu.style.top = top + "px";
};
const closeSubmenu = (delay = 500) => {
clearTimeout(hideTimeout);
hideTimeout = setTimeout(() => {
submenu.classList.remove("open");
submenu.style.left = "";
submenu.style.top = "";
}, delay);
};
li.addEventListener("mouseenter", openSubmenu);
li.addEventListener("mouseleave", () => closeSubmenu(500));
submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
submenu.addEventListener("mouseleave", () => closeSubmenu(500));
}
ul.appendChild(li);
});
return ul;
}
closeAllSubmenus() {
this.menu.querySelectorAll(".ctx-submenu.open").forEach(sm => {
sm.classList.remove("open");
sm.style.left = "";
sm.style.top = "";
});
}
show(target, options = {}) {
this.closeAllSubmenus();
let x, y;
const position = options.position || "right"; // default
const offset = options.offset || 4;
// 🖱️ Mausposition
if (typeof target === "number") {
x = target;
y = options.y;
}
// 📦 Element als Anchor
else if (target instanceof HTMLElement) {
const rect = target.getBoundingClientRect();
switch (position) {
case "right":
x = rect.right + offset;
y = rect.top;
break;
case "left":
x = rect.left - this.menu.offsetWidth - offset;
y = rect.top;
break;
case "top":
x = rect.left;
y = rect.top - this.menu.offsetHeight - offset;
break;
case "bottom":
default:
x = rect.left;
y = rect.bottom + offset;
break;
}
}
// ❌ invalid fallback
else {
return;
}
// 👉 erstmal anzeigen (wichtig für width/height!)
this.menu.style.left = "0px";
this.menu.style.top = "0px";
this.menu.classList.add("show");
this.menu.classList.remove("hidden");
const rect = this.menu.getBoundingClientRect();
// 🧠 Reposition nach echten Maßen
if (target instanceof HTMLElement) {
const anchor = target.getBoundingClientRect();
switch (position) {
case "right":
x = anchor.right + offset;
y = anchor.top;
break;
case "left":
x = anchor.left - rect.width - offset;
y = anchor.top;
break;
case "top":
x = anchor.left;
y = anchor.top - rect.height - offset;
break;
case "bottom":
x = anchor.left;
y = anchor.bottom + offset;
break;
}
}
// 🧱 Screen Bounds
if (x + rect.width > window.innerWidth) {
x = window.innerWidth - rect.width - 4;
}
if (y + rect.height > window.innerHeight) {
y = window.innerHeight - rect.height - 4;
}
if (x < 0) x = 4;
if (y < 0) y = 4;
this.menu.style.left = x + "px";
this.menu.style.top = y + "px";
}
hide() {
this.menu.classList.remove("show");
setTimeout(() => this.menu.classList.add("hidden"), 200);
}
}

View File

@@ -0,0 +1,259 @@
let activeFeedbox = null;
//#region Messagebox
function getMessageColorByLevelId(levelId) {
return levelId == -1 ? 'test' :
levelId == 0 ? 'success' :
levelId == 1 ? 'info' :
levelId == 2 ? 'warn' :
levelId == 4 ? 'error' : 'throw_exception';
}
/**
* Shows messages in a containerbox on the right
* @param {string} title - Title
* @param {string} text - Message content
* @param {string} levelId - 1=info | 2=warn | 4=error | 8=throw_exception | 16=success
* @param {string} [targetId] - Which element is getting focused
* @param {number} duration - Time in ms until auto-close
*/
// function showMessage(title, text, levelId = 1, targetId = null, duration = 4000) {
function showMessage(title, text, levelId = 1, onclick = null, duration = 4000) {
// Falls kein Container existiert → automatisch anlegen
let container = document.getElementById('message-container');
if (!container) {
container = document.createElement('div');
container.id = 'message-container';
container.style.position = 'fixed';
container.style.top = '20px';
container.style.right = '20px';
container.style.display = 'flex';
container.style.flexDirection = 'column';
container.style.alignItems = 'flex-end';
container.style.zIndex = '9999';
document.body.appendChild(container);
}
const message = document.createElement('div');
message.classList.add('message', getMessageColorByLevelId(levelId));
// --- HEADER ---
const header = document.createElement('div');
header.className = 'message-header';
const titleContainer = document.createElement('div');
titleContainer.className = 'message-title';
titleContainer.innerHTML = title;
const pinDiv = document.createElement('div');
pinDiv.className = 'pin-div';
pinDiv.innerHTML = '📌';
pinDiv.title = 'Anpinnen';
const countdown = document.createElement('span');
countdown.className = 'countdown';
countdown.textContent = `${(duration / 1000).toFixed(1)}s`;
// Titel + Countdown + Pin in eine Zeile
header.appendChild(titleContainer);
header.appendChild(countdown);
header.appendChild(pinDiv);
// --- BODY ---
const body = document.createElement('div');
body.className = 'message-text';
body.innerHTML = text;
message.appendChild(header);
message.appendChild(body);
container.appendChild(message);
// --- LOGIK ---
let pinned = false;
let remainingTime = duration;
let interval = null;
let lastTick = Date.now();
function startCountdown() {
lastTick = Date.now();
clearInterval(interval);
interval = setInterval(() => {
const now = Date.now();
const delta = now - lastTick;
lastTick = now;
remainingTime -= delta;
if (remainingTime <= 0 && !pinned) {
clearInterval(interval);
slideOutMessage(message);
} else if (!pinned) {
countdown.textContent = `${(remainingTime / 1000).toFixed(1)}s`;
}
}, 100);
}
function stopCountdown() {
clearInterval(interval);
}
// Start Countdown
startCountdown();
// Hover pausiert Countdown
message.addEventListener('mouseenter', stopCountdown);
message.addEventListener('mouseleave', () => {
if (!pinned) startCountdown();
});
// Klick auf Nachricht → schließen (nur wenn nicht gepinnt)
message.addEventListener('click', (e) => {
if(onclick) {
onclick();
}
// if (e.target === pinDiv) return;
// if (targetId) {
// const target = document.getElementById(targetId);
// if (target) target.scrollIntoView({ behavior: 'smooth' });
// }
if (!pinned) slideOutMessage(message);
});
// Klick auf Pin → anheften / lösen
pinDiv.addEventListener('click', (e) => {
e.stopPropagation();
pinned = !pinned;
if (pinned) {
pinDiv.classList.add('pinned');
pinDiv.title = 'Loslösen';
stopCountdown();
countdown.textContent = '📍 Angeheftet';
} else {
pinDiv.classList.remove('pinned');
pinDiv.title = 'Anpinnen';
startCountdown();
}
});
}
function slideOutMessage(message) {
if (!message) return;
message.style.animation = 'slideOut 0.5s forwards';
setTimeout(() => {
if (message.parentNode) message.parentNode.removeChild(message);
}, 300);
}
//#region Feedbox
/**
* feedbox({
* title: `<span style="color:#f44336">⚠ Upload abbrechen?</span>`,
* message: `
* <p>Es laufen noch <b>aktive Uploads</b>.</p>
* <p>Möchtest du wirklich <u>alle abbrechen</u>?</p>
* `,
* buttons: {
* yes: {
* text: '<b>Ja</b>, abbrechen',
* onClick: () => stopUploadQueue()
* },
* no: {
* text: 'Weiter hochladen'
* },
* cancel: {
* text: 'Zurück'
* }
* }
*});
*/
function feedbox({
title = '',
message = '',
buttons = {}, // buttons: { yes: { text: 'Yes', onClick: () => { } } }
primary = null, // name of the primary button, to accept with enter-key
lock = false, // locks desktop
replace = false // replaces an actually shown feedbox
}) {
// 🚫 Nested verhindern
if (activeFeedbox) {
if (!replace) {
return Promise.resolve('blocked');
}
activeFeedbox.close('replaced');
}
return new Promise(resolve => {
const overlay = document.createElement('div');
overlay.className = 'feedbox-overlay';
const box = document.createElement('div');
box.className = 'feedbox';
const h = document.createElement('h3');
h.innerHTML = title;
const msg = document.createElement('div');
msg.className = 'feedbox-message';
msg.innerHTML = message;
const actions = document.createElement('div');
actions.className = 'feedbox-actions';
const btnMap = {};
Object.entries(buttons).forEach(([key, cfg]) => {
if (!cfg) return;
const btn = document.createElement('button');
btn.className = `feedbox-btn ${key}`;
btn.innerHTML = cfg.text ?? key;
btn.onclick = () => {
cfg.onClick?.();
close(key);
};
btnMap[key] = btn;
actions.appendChild(btn);
});
function close(result) {
document.removeEventListener('keydown', keyHandler);
overlay.remove();
activeFeedbox = null;
resolve(result);
}
function keyHandler(e) {
if (e.key === 'Escape') close('cancel');
if (e.key === 'Enter' && primary && btnMap[primary]) {
btnMap[primary].click();
}
}
if (!lock) {
overlay.onclick = e => {
if (e.target === overlay) close('cancel');
};
}
document.addEventListener('keydown', keyHandler);
box.append(h, msg, actions);
overlay.appendChild(box);
document.body.appendChild(overlay);
// Fokus
if (primary && btnMap[primary]) {
setTimeout(() => btnMap[primary].focus(), 0);
}
// 🔐 Singleton setzen
activeFeedbox = { close };
});
}
//#endregion

View File

@@ -0,0 +1,97 @@
const auth = { objectGuid: getCookie('ObjectGUID'), sAMAccountName: getCookie('sAMAccountName') };
const mainSocket = io.connect('/', { reconnect: true, auth: auth });
const adminSocket = io.connect('/admin', { reconnect: true, auth: auth });
const container = document.getElementById('message-container');
adminSocket.on('eventlog', (data) => {
showMessage('EventLog',
`${data.datetime}<br>${[-1,0].includes(data.levelId) ? '' : data.trace}<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join('&ensp;') : data.message.split('\r\n').join('<br>').split('\t').join('&ensp;'))}`,
data.levelId,
() => {
} ,
10000);
});
mainSocket.on('event', (data) => {
showMessage(data.pluginName,
`[${data.datetime}]<br><br>${(Array.isArray(data.message) ? data.message.split('\r\n').join('<br>').split('\t').join('&ensp;') : data.message.split('\r\n').join('<br>').split('\t').join('&ensp;'))}`,
data.levelId,
() => {
} ,
10000);
});
//-1=test, 0=success, 1=log, 2=warn, 4=error, 8=throw_exception
function writeEventLog(levelId, pluginName, message) {
adminSocket.emit('eventlog', {
objectGuid: getCookie('ObjectGUID'),
levelId: levelId,
pluginName: pluginName,
message: message.stack === undefined ? message : { message: message.message, stack: message.stack }
});
}
// levelId: if -1, then write no log entry
// sendToParams: where clause to find objectGUIDs to send
function sendUserEvent(pluginName, message, sendToParams, levelId = -1) {
mainSocket.emit('event', {
objectGuid: getCookie('ObjectGUID'),
levelId: levelId,
pluginName: pluginName,
message: message.stack === undefined ? message : { message: message.message },
sendToParams: sendToParams
});
}
/*
{
"status":"unload",
"pluginName":"user_management",
"metadata":{
"name":"user_management",
"menuName":"User Management",
"description":"Beschreibung hier einfügen",
"version":"0.9.25.11.14",
"icon":"",
"permissions":[],
"config":{
},
"active":false
},
"levelId":0,
"message":"Plugin user_management entladen",
"authorized": true
}
*/
mainSocket.on('plugin_status', payload => {
const startMenuItem = document.querySelector(`[data-appname=${payload.metadata.name}]`);
if(['load', 'unload', 'update'].includes(payload.status)) {
if(payload.status == 'load') {
if(payload.authorized) {
startMenuItem.classList.remove('unload');
startMenuItem.setAttribute('data-active', true);
} else {
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
}
} else if(payload.status == 'unload') {
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
} else if(payload.status == 'update') {
if(payload.authorized && payload.metadata.active) {
startMenuItem.classList.remove('unload');
startMenuItem.setAttribute('data-active', true);
} else if(!payload.authorized){
startMenuItem.classList.add('unload');
startMenuItem.setAttribute('data-active', false);
}
}
}
});

1494
public/javascript/main.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,157 @@
class NotifyBubble {
constructor(trayButton, bubbleSelector) {
this.button = trayButton;
this.bubble = document.querySelector(bubbleSelector);
this.hideTimeout = null;
this.initEvents();
this.initExistingItems();
this.updateCounter(); // 🔥 initialer Counter
}
/* =========================
SHOW / HIDE
========================== */
show() {
clearTimeout(this.hideTimeout);
this.button.classList.add("active");
// Tooltip entfernen beim Öffnen
this.button.removeAttribute('data-tooltip');
}
hide(delay = 1000) {
clearTimeout(this.hideTimeout);
this.hideTimeout = setTimeout(() => {
this.button.classList.remove("active");
// 🔥 Counter nach Schließen anzeigen
this.updateCounter();
}, delay);
}
/* =========================
COUNTER / TOOLTIP
========================== */
updateCounter() {
const items = this.bubble.querySelectorAll(".bubble-item");
const count = items.length;
if (!this.button.classList.contains("active") && count > 0) {
this.button.setAttribute('data-tooltip', `${count} neue Benachrichtigung${count > 1 ? 'en' : ''}`);
} else {
this.button.setAttribute('data-tooltip', "Keine Benachrichtigungen");
}
}
/* =========================
EVENTS
========================== */
initEvents() {
// Toggle bei Klick auf Button
this.button.addEventListener("click", (e) => {
e.stopPropagation();
if (this.button.classList.contains("active")) {
this.hide(0);
} else {
this.show();
}
});
// Klick innerhalb der Bubble soll NICHT schließen
this.bubble.addEventListener("click", (e) => {
e.stopPropagation();
});
// Klick irgendwo anders → schließen
document.addEventListener("click", () => {
if (this.button.classList.contains("active")) {
this.hide(0);
}
});
}
/* =========================
INIT EXISTING ITEMS
========================== */
initExistingItems() {
this.bubble.querySelectorAll(".bubble-item").forEach(item => {
this.attachItemEvents(item);
});
}
/* =========================
ITEM EVENTS
========================== */
attachItemEvents(item) {
item.addEventListener("click", () => {
const checkbox = item.querySelector("input[type='checkbox']");
if (checkbox) {
checkbox.checked = !checkbox.checked;
fetch('/api/NotifyTray/markAsSeen', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: item.dataset.id,
value: checkbox.checked
})
});
// 🔥 Counter ggf. aktualisieren
this.updateCounter();
}
});
}
/* =========================
ADD SINGLE ITEM
========================== */
addItem(notification) {
const item = document.createElement("label");
item.className = "bubble-item";
item.dataset.id = notification.ID;
item.innerHTML = `
<div>${notification.Message}</div>
<div style="display:flex;flex-direction:row;flex-wrap:no-wrap;justify-content:flex-end;gap:10px;align-items:center;">
<div>
<div>${dateFormat(notification.CreatedAt, 'dd.mm.yyyy HH:MM:SS')}</div>
<div>${notification.PluginName}</div>
</div>
<div>
<label class="cb cb-modern">
<input id="notification_${notification.ID}" type="checkbox">
<span class="cb-box"></span>
</label>
</div>
</div>
`;
this.attachItemEvents(item);
this.bubble.appendChild(item);
// 🔥 Counter aktualisieren
this.updateCounter();
}
/* =========================
ADD MULTIPLE ITEMS
========================== */
addItems(notifications) {
notifications.forEach(n => this.addItem(n));
}
/* =========================
CLEAR
========================== */
clear() {
this.bubble.innerHTML = "";
this.updateCounter();
}
}

1081
public/javascript/os.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,92 @@
const pluginAPI = {
async update(name, updates) {
if(Object.keys(updates)[0] && Object.values(updates)[0] == "") {
updates[Object.keys(updates)[0]] = [ ];
}
return await this._request(
`/api/plugins/${name}/update`,
'POST',
{ updates }
);
},
async activation(name, state) {
return await this._request(
`/api/plugins/activation`,
'POST',
{ name, state }
);
},
async create(name) {
return await this._request(
`/api/plugins/${name}/create`,
'POST'
);
},
async rename(name, newName) {
return await this._request(
`/api/plugins/${name}/rename`,
'POST',
{ newName }
);
},
async delete(name) {
return await this._request(
`/api/plugins/${name}/delete`,
'POST'
);
},
// --- zentrale Request-Funktion ---
async _request(url, method, body) {
try {
const options = { method };
if (body) {
options.headers = { 'Content-Type': 'application/json' };
options.body = JSON.stringify(body);
}
const res = await fetch(url, options);
if (!res.ok) {
const text = await res.text();
writeEventLog(4, 'CLIENT', `Request fehlgeschlagen: ${text}`);
throw new Error(`HTTP ${res.status}: ${text}`);
}
return res.json();
} catch (err) {
writeEventLog(4, 'CLIENT', err);
throw err;
}
}
};
class AttachOnBlurChange {
constructor(el, callback) {
this.el = el;
this.callback = callback;
this.initial = this.el.value;
this.el.addEventListener('blur', () => {
this.handleBlur();
});
}
handleBlur() {
if (this.el.value === this.initial) return;
const oldValue = this.initial;
const newValue = this.el.value;
this.initial = newValue;
this.callback(newValue, oldValue, this.el);
}
}

View File

@@ -0,0 +1,89 @@
function createRequiredProgress({ container, progress, onChange }) {
let total = 0;
let filled = 0;
const body = typeof container === 'string'
? document.querySelector(container)
: container;
const progressEl = typeof progress === 'string'
? document.querySelector(progress)
: progress;
if (!body || !progressEl) return;
function isElementVisible(el) {
const style = window.getComputedStyle(el);
return el.offsetParent !== null && style.opacity !== '0';
}
function updateRequiredProgress() {
const requiredElements = Array.from(
body.querySelectorAll('input[required], select[required], textarea[required]')
).filter(isElementVisible);
total = requiredElements.length;
filled = requiredElements.filter(el =>
el.classList.contains('is-required-filled')
).length;
progressEl.textContent = `${filled} / ${total} Pflichtfelder ausgefüllt`;
progressEl.classList.toggle('complete', filled === total);
progressEl.classList.toggle('incomplete', filled !== total);
}
function updateRequiredState(el) {
const isEmpty =
(el instanceof HTMLSelectElement && !el.value) ||
(el instanceof HTMLInputElement && el.type !== 'checkbox' && !el.value.trim()) ||
(el instanceof HTMLTextAreaElement && !el.value.trim());
el.classList.toggle('is-required-empty', isEmpty);
el.classList.toggle('is-required-filled', !isEmpty);
updateRequiredProgress();
if (typeof onChange === 'function') {
onChange({
element: el,
isEmpty,
isFilled: !isEmpty,
isFinished: total === filled
});
}
}
function initialize() {
const elements = Array.from(
body.querySelectorAll('input[required], select[required], textarea[required]')
).filter(isElementVisible);
elements.forEach(el => {
updateRequiredState(el);
el.addEventListener('input', () => updateRequiredState(el));
el.addEventListener('change', () => updateRequiredState(el));
});
updateRequiredProgress();
}
// 🔥 Reagiere auf Sichtbarkeitsänderungen
const observer = new MutationObserver(updateRequiredProgress);
observer.observe(body, {
attributes: true,
subtree: true,
attributeFilter: ['style', 'class', 'hidden']
});
initialize();
// Optional: API zurückgeben
return {
initialize: initialize,
refresh: updateRequiredProgress,
destroy() {
observer.disconnect();
}
};
}

View File

@@ -0,0 +1,450 @@
/*
//Generic table filter module
// Usage:
TableFilter({
table: document.querySelector('#myTable'),
exceptedColumns: [ 'Status_ID' ],
filterConfig: {
columnModes: {
ID: 'text', // Textsuche in dieser Spalte
Status: 'dropdown', // Dropdown-Werte aus Tabelle sammeln
Objekt: 'text', // Textsuche
Priorität: 'dropdown',
Gewerk: 'dropdown',
Typ: 'dropdown',
Bedarfsmelder: 'text'
Bearbeiter: 'text'
Genehmiger: 'text'
},
checkboxFilter: {
column: 'Status_ID',
rules: [
{ label: 'Bearbeitung', test: v => [1,4,6,7,12,13,14].includes(parseInt(v)) },
{ label: 'Genehmigt', test: v => [2,5,9,10,11].includes(parseInt(v)) },
{ label: 'Abgelehnt', test: v => [3].includes(parseInt(v)) },
{ label: 'Abgeschlossen', test: v => [8].includes(parseInt(v)) }
]
}
}
});
// Generic table filter module (one textbox OR column-based dropdown)
// New features:
// - Selecting a column in the column-selector decides: textbox OR dropdown (configured in options)
// - If column-selector = empty value ⇒ full-row search
// - No extra checkbox needed
// - UI stays compact (only ONE input element + ONE dropdown)
*/
function TableFilter(options) {
try {
const table = options.table;
const cfg = options.filterConfig;
const exceptedColumns = options.exceptedColumns || [];
// Gruppenerkennung + Verwaltung
const groupMap = new Map(); // groupRow → childRows[]
let groupsInitialized = false;
const state = {
selectedColumn: '',
searchText: '',
dropdownValue: '',
dropdownCache: {},
checkbox: new Set(),
};
//------------------------------------
// Sticky Container für Filter + Header
//------------------------------------
if(document.querySelectorAll('.table-filter-container').length > 0) {
// Array.from(document.querySelectorAll('.table-filter-container')).forEach(filterContainer => {
// filterContainer.remove();
// })
const existing = table.previousElementSibling;
if (existing && existing.classList.contains('table-filter-container')) {
existing.remove();
}
}
const container = document.createElement('div');
container.className = 'table-filter-container';
container.style.position = 'sticky';
container.style.top = '0px';
container.style.left = '0px';
container.style.zIndex = 20;
// Filter UI wird hier reingesetzt
table.before(container);
// Tabelle selbst: header sticky
const thead = table.querySelector('thead');
const headerCells = Array.from(table.querySelectorAll('thead th'));
const getColumnIndex = name => headerCells.findIndex(c => c.textContent.trim() === name);
//------------------------------------
// Dynamisches Filter-UI
//------------------------------------
function createDynamicFilterUI() {
const wrap = document.createElement('div');
wrap.style="display:flex;flex-direction:row;align-items:center;gap:10px"
const colSelectLabel = document.createElement('label');
const colSelect = document.createElement('select');
const emptyOpt = document.createElement('option');
emptyOpt.value = '';
emptyOpt.textContent = '-- Alles --';
colSelect.appendChild(emptyOpt);
colSelectLabel.innerText = 'Spalte';
headerCells.forEach(cell => {
if(!exceptedColumns.includes(cell.textContent.trim())) {
const name = cell.textContent.trim();
const opt = document.createElement('option');
opt.value = name;
opt.textContent = name;
colSelect.appendChild(opt);
}
});
const input = document.createElement('input');
input.type = 'search';
input.style.display = 'none';
const select = document.createElement('select');
select.style.display = 'none';
select.innerHTML = '<option value="">-- Alle --</option>';
function populateDropdownForColumn(colName) {
const colIndex = getColumnIndex(colName);
const rows = Array.from(table.querySelectorAll('tbody tr')).filter(r => !r.classList.contains('grouprow'));
const values = new Set();
rows.forEach(r => {
const cell = r.children[colIndex];
if (cell) values.add(cell.textContent.trim());
});
const sorted = new Set([...values].sort((a, b) => a.localeCompare(b)));
select.innerHTML = '<option value="">-- Alle --</option>' + [...sorted].map(v => `<option value="${v}">${v}</option>`).join('');
}
colSelect.addEventListener('change', () => {
state.selectedColumn = colSelect.value;
state.searchText = '';
state.dropdownValue = '';
input.value = '';
select.value = '';
if (!state.selectedColumn) {
input.style.display = '';
select.style.display = 'none';
return applyFilters();
}
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text') {
input.style.display = '';
select.style.display = 'none';
} else if (mode === 'dropdown') {
input.style.display = 'none';
select.style.display = '';
populateDropdownForColumn(state.selectedColumn);
} else {
input.style.display = '';
select.style.display = 'none';
}
applyFilters();
});
input.style.display = '';
input.addEventListener('input', () => { state.searchText = input.value.toLowerCase(); applyFilters(); });
select.addEventListener('change', () => { state.dropdownValue = select.value; applyFilters(); });
wrap.append(colSelectLabel, colSelect, document.createElement('br'));
wrap.append(input, select);
container.appendChild(wrap);
}
//--------------------------------------------
// Filterbreite an Parent minus Scrollbar
//--------------------------------------------
function updateFilterWidth() {
if (!table || !container) return;
const wrapper = table.parentElement; // z.B. .table-wrapper
if (!wrapper) return;
const style = getComputedStyle(table.querySelector('thead th'));
const rect = wrapper.getBoundingClientRect();
const scrollbarWidth = wrapper.offsetWidth - wrapper.clientWidth;
container.style.width = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
container.style.maxWidth = (rect.width - scrollbarWidth - parseFloat(style.paddingRight)) + "px";
}
// Initial setzen
updateFilterWidth();
// Fenstergröße beobachten
window.addEventListener('resize', updateFilterWidth);
// Dynamische Anpassung bei Wrapper Resize
const wrapper = table.parentElement;
if (wrapper) {
new ResizeObserver(updateFilterWidth).observe(wrapper);
}
//------------------------------------
// Checkbox Filter
//------------------------------------
function createCheckboxFilter() {
const cfgCheck = cfg.checkboxFilter;
const idx = getColumnIndex(cfgCheck.column);
if (idx < 0) return;
const wrapper = document.createElement('div');
cfgCheck.rules.forEach(rule => {
const checkWrapper = document.createElement('label');
const checkInput = document.createElement('input');
const checkTrack = document.createElement('span');
const checkThumb = document.createElement('span');
checkWrapper.classList.add("cb", "cb-switch");
checkWrapper.style.marginRight = '15px'
checkInput.type = 'checkbox';
checkInput.addEventListener('change', () => {
if (checkInput.checked) state.checkbox.add(rule);
else state.checkbox.delete(rule);
applyFilters();
});
checkTrack.classList.add('switch-track');
checkTrack.setAttribute("aria-hidden", 'true');
checkThumb.classList.add('switch-thumb');
checkThumb.setAttribute("aria-hidden", 'true');
checkTrack.append(checkThumb)
checkWrapper.append(rule.label, checkInput, checkTrack);
wrapper.appendChild(checkWrapper);
});
container.appendChild(wrapper);
}
//------------------------------------
// Live Counter
//------------------------------------
const counter = document.createElement('div');
counter.className = 'live-counter';
container.appendChild(counter);
//------------------------------------
// Filter Logik
//------------------------------------
function detectGroups() {
if (groupsInitialized) return;
groupsInitialized = true;
const rows = Array.from(table.querySelectorAll('tbody tr'));
let currentGroupRow = null;
let childBuffer = [];
rows.forEach(row => {
if (row.classList.contains('grouprow')) {
// vorige Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// neue Gruppe starten
currentGroupRow = row;
childBuffer = [];
}
else if (currentGroupRow) {
childBuffer.push(row);
}
});
// letzte Gruppe speichern
if (currentGroupRow && childBuffer.length > 0) {
groupMap.set(currentGroupRow, childBuffer);
}
// jedem groupRow ein Toggle verpassen (wenn nicht vorhanden)
groupMap.forEach((childRows, groupRow) => {
const toggle = groupRow.querySelector("span");
if (!toggle || toggle._groupToggleBound) return;
toggle._groupToggleBound = true;
toggle.addEventListener("click", () => toggleGroup(groupRow));
groupRow.addEventListener("dblclick", () => toggleGroup(groupRow));
});
}
function toggleGroup(groupRow) {
const childRows = groupMap.get(groupRow);
if (!childRows) return;
const toggle = groupRow.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// ausklappen → nur gefilterte Rows zeigen
childRows.forEach(r => {
if (!r._filteredOut) r.style.display = '';
});
toggle.textContent = '-';
} else {
// einklappen → alle verstecken
childRows.forEach(r => r.style.display = 'none');
toggle.textContent = '+';
}
}
function applyFilters() {
detectGroups();
const rows = Array.from(table.querySelectorAll('tbody tr'));
let visible = 0;
rows.forEach(row => {
if (row.classList.contains('grouprow')) { row.style.display = ''; return; }
let show = true;
if (state.selectedColumn === '') {
if (state.searchText) show = [...row.cells].some(c => c.textContent.toLowerCase().includes(state.searchText));
} else {
const idx = getColumnIndex(state.selectedColumn);
const mode = cfg.columnModes[state.selectedColumn];
if (mode === 'text' && state.searchText) {
show = row.cells[idx]?.textContent.toLowerCase().includes(state.searchText);
}
if (mode === 'dropdown' && state.dropdownValue) {
show = row.cells[idx]?.textContent === state.dropdownValue;
}
}
if (state.checkbox.size > 0) {
const idx = getColumnIndex(cfg.checkboxFilter.column);
const cellVal = row.cells[idx]?.textContent ?? '';
const pass = [...state.checkbox].some(rule => rule.test(cellVal));
if (!pass) show = false;
}
row._filteredOut = !show; // fürs Gruppensystem speichern
// Wenn Gruppen existieren:
if (groupMap.size > 0) {
// Prüfen ob row zu einer Gruppe gehört
let parentGroup = null;
for (const [g, list] of groupMap.entries()) {
if (list.includes(row)) {
parentGroup = g;
break;
}
}
if (parentGroup) {
const toggle = parentGroup.querySelector("span");
const collapsed = toggle.textContent === '+';
if (collapsed) {
// eingeklappt → immer ausblenden
row.style.display = 'none';
if (show) visible++;
} else {
// ausgeklappt → nur gefilterte anzeigen
row.style.display = show ? '' : 'none';
if (show) visible++;
}
return;
}
} else {
if (show) visible++;
}
// normale Zeile ohne Gruppe:
row.style.display = show ? '' : 'none';
});
counter.textContent = `${visible} Treffer`;
}
//------------------------------------
// Init
//------------------------------------
createDynamicFilterUI();
if(options.filterConfig.checkboxFilter) createCheckboxFilter();
applyFilters();
if (thead) {
thead.style.position = 'sticky';
thead.style.top = (container.offsetHeight - 1) + 'px';
thead.style.zIndex = 10;
}
//------------------------------------
// Public API
//------------------------------------
return {
refreshDropdown() { if (state.selectedColumn && cfg.columnModes[state.selectedColumn] === 'dropdown') {} },
reapply() { applyFilters(); }
};
} catch(err) {
alert(err.stack)
}
}
//#region Sort table
// let sortDirection = {};
function sortTable(tableId,colIndex) {
const sortDirection = {};
const table = document.getElementById(tableId);
const tbody = table.tBodies[0];
const rows = Array.from(tbody.querySelectorAll("tr"));
// Toggle sort direction
sortDirection[colIndex] = !sortDirection[colIndex];
rows.sort((a, b) => {
const cellA = a.cells[colIndex].textContent.trim();
const cellB = b.cells[colIndex].textContent.trim();
// Numeric sort if possible
const numA = parseFloat(cellA);
const numB = parseFloat(cellB);
if (!isNaN(numA) && !isNaN(numB)) {
return sortDirection[colIndex] ? numA - numB : numB - numA;
} else {
return sortDirection[colIndex]
? cellA.localeCompare(cellB)
: cellB.localeCompare(cellA);
}
});
// Remove old rows
tbody.innerHTML = "";
rows.forEach(row => tbody.appendChild(row));
// Update sort arrows
const th = table.querySelectorAll("th");
th.forEach((header, i) => {
header.classList.remove("sort-asc", "sort-desc");
if (i === colIndex) {
header.classList.add(sortDirection[colIndex] ? "sort-asc" : "sort-desc");
}
});
}
//#endregion

View File

@@ -0,0 +1,354 @@
class Tutorial {
constructor(steps, options = {}) {
this.steps = steps;
this.currentStep = 0;
this.isRunning = false;
this.isWaitingForEvent = false;
this.options = {
overlayOpacity: 0.7,
...options
};
this.zIndexBase = 1000;
this.zIndexStep = 0;
this.modifiedElements = new Map();
this.baseElement = null;
this.currentElement = null;
this.tooltipObserver = null;
this.tooltipRAF = null;
this.currentStepElement = null;
}
// ---------------- UI ----------------
createUI() {
document.addEventListener("keydown", e => {
if (this.isWaitingForEvent) return;
if (e.key === "ArrowRight") this.next();
if (e.key === "ArrowLeft") this.prev();
});
this.tooltip = document.createElement("div");
this.tooltip.id = "tutorial-tooltip";
Object.assign(this.tooltip.style, {
position: "absolute",
display: "flex",
padding: "12px",
borderRadius: "8px",
zIndex: 9999,
maxWidth: "350px",
boxShadow: "0 10px 30px rgba(0,0,0,0.2)",
backdropFilter: "blur(5px)",
justifyContent: "center"
});
this.text = document.createElement("p");
this.nextBtn = document.createElement("button");
this.nextBtn.innerHTML = ">";
this.nextBtn.classList.add("bluebutton");
this.prevBtn = document.createElement("button");
this.prevBtn.innerHTML = "<";
this.prevBtn.classList.add("yellowbutton");
this.nextBtn.onclick = () => this.next();
this.prevBtn.onclick = () => this.prev();
this.tooltip.appendChild(this.text);
this.tooltip.appendChild(this.prevBtn);
this.tooltip.appendChild(this.nextBtn);
document.body.appendChild(this.tooltip);
}
// ---------------- Element wait ----------------
waitForElement(selector, timeout = 5000) {
return new Promise(resolve => {
const start = Date.now();
const check = () => document.querySelector(selector);
let el = check();
if (el) return resolve(el);
const observer = new MutationObserver(() => {
el = check();
if (el) {
observer.disconnect();
resolve(el);
}
});
observer.observe(document.body, {
childList: true,
subtree: true
});
const timer = setInterval(() => {
if (Date.now() - start > timeout) {
clearInterval(timer);
observer.disconnect();
resolve(null);
}
el = check();
if (el) {
clearInterval(timer);
observer.disconnect();
resolve(el);
}
}, 100);
});
}
// ---------------- Highlight ----------------
highlightElement(el) {
this.removeHighlight();
if (!this.modifiedElements.has(el)) {
this.modifiedElements.set(el, {
zIndex: el.style.zIndex,
position: el.style.position,
boxShadow: el.style.boxShadow
});
}
if (this.currentStep === 0 && !this.baseElement) {
this.baseElement = el;
}
let zIndex;
if (el === this.baseElement) {
zIndex = this.zIndexBase;
} else {
this.zIndexStep++;
zIndex = this.zIndexBase + this.zIndexStep;
}
el.style.position = "relative";
el.style.zIndex = zIndex;
el.style.boxShadow =
"0 0 0 4px rgba(255, 0, 0, 1), 0 0 20px rgba(255,0,0,0.7)";
this.currentElement = el;
this.startTooltipTracking(el);
}
removeHighlight() {
if (!this.currentElement) return;
this.currentElement.style.boxShadow = "";
this.stopTooltipTracking();
}
// ---------------- Event system (NEW) ----------------
getEventConfig(type) {
const map = {
click: () => new MouseEvent("click", { bubbles: true, cancelable: true, view: window }),
contextmenu: () => new MouseEvent("contextmenu", { bubbles: true, cancelable: true, view: window, button: 2 }),
dblclick: () => new MouseEvent("dblclick", { bubbles: true, cancelable: true, view: window }),
mouseenter: () => new MouseEvent("mouseenter", { bubbles: true, cancelable: true, view: window }),
mouseover: () => new MouseEvent("mouseover", { bubbles: true, cancelable: true, view: window })
};
return map[type] ? map[type]() : null;
}
async executeStepOnly(step, el) {
if (!step.action) return;
const event = this.getEventConfig(step.action);
if (event) {
el.dispatchEvent(event);
}
}
waitForUserEvent(el, type) {
return new Promise(resolve => {
const handler = (e) => {
el.removeEventListener(type, handler);
resolve(e);
};
el.addEventListener(type, handler);
});
}
// ---------------- Tooltip tracking ----------------
startTooltipTracking(el) {
this.stopTooltipTracking();
this.currentStepElement = el;
const update = () => {
if (!this.currentStepElement || !this.tooltip) return;
const rect = this.currentStepElement.getBoundingClientRect();
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
this.tooltipRAF = requestAnimationFrame(update);
};
this.tooltipObserver = new ResizeObserver(() => {
if (!this.currentStepElement) return;
const rect = this.currentStepElement.getBoundingClientRect();
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
});
this.tooltipObserver.observe(el);
this.tooltipRAF = requestAnimationFrame(update);
}
stopTooltipTracking() {
if (this.tooltipObserver) {
this.tooltipObserver.disconnect();
this.tooltipObserver = null;
}
if (this.tooltipRAF) {
cancelAnimationFrame(this.tooltipRAF);
this.tooltipRAF = null;
}
this.currentStepElement = null;
}
// ---------------- Steps ----------------
async showStep() {
let step = this.steps[this.currentStep];
this.updateButtons(step);
this.isWaitingForEvent = false;
while (step && (!step.text || step.text.trim() === "")) {
const elSilent = await this.waitForElement(step.element);
if (elSilent) {
await this.executeStepOnly(step, elSilent);
}
this.currentStep++;
if (this.currentStep >= this.steps.length) {
this.destroy();
return;
}
step = this.steps[this.currentStep];
}
const el = await this.waitForElement(step.element);
if (!el) {
this.next();
return;
}
this.highlightElement(el);
el.scrollIntoView({ behavior: "smooth", block: "center" });
const rect = el.getBoundingClientRect();
this.tooltip.style.display = "block";
this.text.innerHTML = step.text;
this.tooltip.style.top = rect.bottom + 10 + "px";
this.tooltip.style.left = rect.left + "px";
this.isWaitingForEvent = !!step.waitFor;
this.nextBtn.style.display = step.waitFor ? "none" : "inline-block";
this.prevBtn.style.display =
this.currentStep === 0 || step.waitFor ? "none" : "inline-block";
if (step.waitFor) {
await this.waitForUserEvent(el, step.waitFor);
this.isWaitingForEvent = false;
this.next();
}
}
updateButtons(step) {
const isLastStep = this.currentStep === this.steps.length - 1;
this.nextBtn.innerHTML = isLastStep ? "Fertig" : ">";
}
// ---------------- Navigation ----------------
start() {
if (this.isRunning) return;
this.isRunning = true;
this.createUI();
this.currentStep = 0;
this.showStep();
}
next() {
if (this.isWaitingForEvent) return;
this.removeHighlight();
if (this.currentStep < this.steps.length - 1) {
this.currentStep++;
this.showStep();
} else {
this.destroy();
}
}
prev() {
if (this.isWaitingForEvent) return;
this.removeHighlight();
if (this.currentStep > 0) {
this.currentStep--;
this.showStep();
}
}
// ---------------- Cleanup ----------------
destroy() {
this.isRunning = false;
this.stopTooltipTracking();
this.modifiedElements.forEach((styles, el) => {
el.style.zIndex = styles.zIndex || "";
el.style.position = styles.position || "";
el.style.boxShadow = styles.boxShadow || "";
});
this.modifiedElements.clear();
this.baseElement = null;
this.zIndexStep = 0;
this.tooltip?.remove();
}
}

131
public/styles/colors.css Normal file
View File

@@ -0,0 +1,131 @@
/* #region OS */
#desktop { background:var(--theme-desktop-backcolor); background-image:var(--theme-desktop-image); }
#taskbar { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); }
#tutorial-tooltip {background:var(--theme-window-backcolor); color:var(--theme-window-color);}
#tutorial-tooltip img {filter: invert(1);}
#start-btn { background:var(--theme-accent-default-backcolor); color:var(--theme-accent-default-color); }
#start-btn:hover { background:var(--theme-accent-hover-backcolor); color:var(--theme-accent-hover-color); }
#start-btn:active { background:var(--theme-accent-active-backcolor); color:var(--theme-accent-active-color); }
/* #start-menu, .submenu { background:var(--theme-taskbar-backcolor); color:var(--theme-taskbar-color); } */
#start-menu { padding-bottom:10px; background:var(--theme-startmenu-backcolor); color:var(--theme-startmenu-color); border:3px solid rgb(128,128,128); }
.start-submenu-head { background:var(--theme-startmenu-submenu-header-backcolor); color:var(--theme-startmenu-submenu-header-backcolor) }
.start-icon { background:var(--theme-accent-default-backcolor); }
/* .start-item:hover, .start-sys-item:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); } */
.start-item.has-submenu.open > .menu-label , .start-item:not(.has-submenu.open), .start-sys-item { background-color:var(--theme-startmenu-item-default-backcolor); color:var(--theme-startmenu-item-default-color); }
.start-item:not(.has-submenu.open):not(.unload):hover, .start-sys-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
.start-item:not(.has-submenu):not(.has-submenu.open):not(.unload):active, .start-sys-item:active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
.start-item-sys-container { background:var(--theme-startmenu-syscontainer-backcolor);}
.start-item .unload { background:var(--theme-startmenu-item-disabled-backcolor); color:var(--theme-startmenu-item-disabled-color); }
/* #taskbar .taskbar-item { background:var(--theme-taskbar-item-backcolor); } */
.taskbar-item { position: relative;}
.taskbar-item::before { background: var(--theme-accent-active-color); }
.taskbar-item.focus::before { background: var(--theme-accent-active-backcolor); }
/* .taskbar-item.minimized { background:var(--theme-taskbar-item-minimized-backcolor); color:var(--theme-taskbar-item-minimized-color); border-color:var(--theme-taskbar-item-minimized-border-color);} */
.taskbar-item.default { background:var(--theme-taskbar-item-default-backcolor); color:var(--theme-taskbar-item-default-color); border-color:var(--theme-taskbar-item-default-border-color);}
.taskbar-item:hover { background-color:var(--theme-startmenu-item-hover-backcolor); color:var(--theme-startmenu-item-hover-color); }
.window { border:2px solid var(--theme-window-titlebar-backcolor); background:var(--theme-window-backcolor); }
.window-content { color:var(--theme-window-color); background:var(--theme-window-backcolor); }
.window-titlebar { color:var(--theme-window-titlebar-color); background:var(--theme-window-titlebar-backcolor); }
.window .controls button { color:var(--theme-window-titlebar-color); background:transparent; }
.window .controls button:hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
/* #endregion */
/* #region Table */
table, tr, td, th { border-color:var(--theme-table-border-color); }
table thead th { background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); border-color:var(--theme-table-border-color); }
table tbody tr:nth-child(even) { background-color:var(--theme-table-rows-even-backcolor); color:var(--theme-table-rows-even-color); }
table tbody tr:nth-child(odd) { background-color:var(--theme-table-rows-odd-backcolor); color:var(--theme-table-rows-odd-color); }
table tbody tr:not(.grouprow):not(.no-hover):hover { color:var(--theme-accent-hover-color); background:var(--theme-accent-hover-backcolor); }
table tbody tr.grouprow { background-color:var(--theme-table-rows-grouprow-backcolor); color:var(--theme-table-rows-grouprow-color); }
table tbody tr.active, table tbody td.active { color:var(--theme-accent-active-color); background:var(--theme-accent-active-backcolor); }
.table-filter-container { border-color:var(--theme-table-border-color); background-color:var(--theme-table-header-backcolor); color:var(--theme-table-header-color); }
/* #endregion */
/* #region Scrollbar */
::-webkit-scrollbar-track, ::-webkit-scrollbar-corner { background:var(--theme-scrollbar-backcolor); }
::-webkit-scrollbar-thumb { background-color:var(--theme-scrollbar-thumb-default-backcolor); border-color:var(--theme-scrollbar-thumb-default-border-color); }
::-webkit-scrollbar-thumb:hover { background-color:var(--theme-scrollbar-thumb-hover-backcolor); border-color:var(--theme-scrollbar-thumb-hover-border-color); }
/* #endregion */
/* #region Tooltip */
.global-tooltip { background:var(--theme-tooltip-backcolor); color:var(--theme-tooltip-color); border-color:var(--theme-tooltip-border-color) }
/* #endregion */
/* #region feedbox */
.feedbox { background:var(--theme-feedbox-backcolor); color:var(--theme-feedbox-color);box-shadow:0 5px 25px rgba(0,0,0,0.3); }
.feedbox-overlay { background:rgba(0,0,0,0.5); }
/* #endregion */
/* #region Messagbox */
.test, .test .countdown { color:var(--theme-message-test-color); background-color:var(--theme-message-test-backcolor); border-color:var(--theme-message-test-border-color); }
.success, .success .countdown { color:var(--theme-message-success-color); background-color:var(--theme-message-success-backcolor); border-color:var(--theme-message-success-border-color); }
.error, .error .countdown { color:var(--theme-message-error-color); background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); }
.info, .info .countdown { color:var(--theme-message-info-color); background-color:var(--theme-message-info-backcolor); border-color:var(--theme-message-info-border-color); }
.warn, .warn .countdown { color:var(--theme-message-warn-color); background-color:var(--theme-message-warn-backcolor); border-color:var(--theme-message-warn-border-color); }
.throw_exception, .throw_exception .countdown { color:var(--theme-message-throw-color); background-color:var(--theme-message-throw-backcolor); border-color:var(--theme-message-throw-border-color); }
.message { box-shadow:0 4px 10px rgba(0,0,0,0.3); } /* SHADOW? */
/* #endregion */
.error-field { background:var(--theme-message-error-backcolor); border-color:var(--theme-message-error-border-color); color:var(--theme-message-error-color); }
.success-field { background:var(--theme-message-success-backcolor); border-color: var(--theme-message-success-border-color); color:var(--theme-message-success-color); }
/* region Container*/
.card { background:var(--theme-container-card-backcolor); border-color:var(--theme-container-card-border); }
/* #endregion */
/* #region Checkbox */
.cb-box { border-color:var(--theme-checkbox-default-border-color); box-shadow:0 1px 2px var(--theme-checkbox-shadow-color) inset; }
.cb input:checked + .cb-box { background:var(--theme-checkbox-default-thumb); border-color:var(--theme-checkbox-default-thumb); }
.cb input:disabled + .cb-box { background:var(--theme-checkbox-disabled-backcolor); border-color:var(--theme-checkbox-disabled-border-color); }
.cb-label { color:var(--theme-checkbox-color); }
/* #endregion */
/* #region Switch */
.cb-switch .cb-label { color:var(--theme-switch-color); }
.switch-track { background:var(--theme-switch-backcolor); border-color:var(--theme-switch-border-color); }
/* #endregion */
/* #region Required */
.is-required-empty { border-width:2px; border-color:var(--theme-required-empty); border-style:solid; }
.is-required-filled { border-color:var(--theme-required-accept); }
/* #endregion */
/* #region Tabs */
.tabs { border-bottom-color: #ccc; }
.tab-content { border-color: #ccc; }
.tab:hover { background-color: var(--theme-accent-hover-backcolor); color: var(--theme-accent-hover-color); }
.tab.active { background-color: var(--theme-accent-active-backcolor); color: var(--theme-accent-active-color); border-color: var(--theme-accent-active-border-color); border-bottom-color: var(--theme-accent-active-backcolor); }
/* #endregion */
/* #region DOM */
button:not(:disabled).monolyth { background-color:var(--theme-button-monolyth-default-backcolor); color:var(--theme-button-monolyth-default-color); }
button:not(:disabled).monolyth:hover { background-color:var(--theme-button-monolyth-hover-backcolor); color:var(--theme-button-monolyth-hover-color); }
button:not(:disabled).monolyth:hover, button:not(:disabled).bluebutton:hover, button:not(:disabled).redbutton:hover, button:not(:disabled).yellowbutton:hover, button:not(:disabled).greenbutton:hover { box-shadow:0 6px 8px rgba(0,0,0,0.15); }
select { background:var(--theme-select-default-backcolor); color:var(--theme-select-default-color); box-shadow:0 0 5px var(--theme-select-default-border-color); }
input:hover, input[type="text"]:hover, input[type="email"]:hover, input[type="password"]:hover, input[type="number"]:hover, input[type="date"]:hover,textarea:hover, select:hover { background:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); box-shadow:0 0 5px var(--theme-input-hover-border-color); }
input:focus, input[type="text"]:focus, input[type="email"]:focus, input[type="password"]:focus, input[type="number"]:focus, input[type="date"]:focus, textarea:focus, select:focus { background:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); box-shadow:0 0 5px var(--theme-input-focus-border-color); }
input.valid { border: 2px solid var(--theme-input-accept-border-color); }
input.invalid { border: 2px solid var(--theme-input-decline-border-color); }
*::placeholder, input[id="sAMAccountName"] { color:var(--theme-input-placeholder-color); }
textarea, input { border-color:var(--theme-input-default-border-color); background:var(--theme-input-default-backcolor); color:var(--theme-input-default-color); }
textarea:not([required]):hover, input:not([required]):hover { border-color:var(--theme-input-hover-border-color); background-color:var(--theme-input-hover-backcolor); color:var(--theme-input-hover-color); }
textarea:focus, input:focus { border-color:var(--theme-input-focus-border-color); background-color:var(--theme-input-focus-backcolor); color:var(--theme-input-focus-color); }
select, select option { border-color:var(--theme-select-border-color); background-color:var(--theme-select-backcolor); color:var(--theme-select-color); }
select:focus, select:not([required]):hover, select:focus option,select:not([required]):hover option { border-color:var(--theme-select-hover-border-color); background-color:var(--theme-select-hover-backcolor); color:var(--theme-select-hover-color); }
/* #endregion */

View File

@@ -0,0 +1,79 @@
.ctx-menu {
position: fixed;
min-width: 180px;
background: var(--theme-contextmenu-backcolor);
border: 1px solid var(--theme-contextmenu-border-color);
color: var(--theme-contextmenu-color);
border-radius: 6px;
padding: 4px 0;
box-shadow: 0 8px 20px rgba(0,0,0,0.15);
z-index: 2;
}
.ctx-menu.hidden {
opacity: 0;
display: none;
}
.ctx-list {
list-style: none;
margin: 0;
padding: 0;
}
.ctx-item {
padding: 5px 7px 5px 20px;
margin-left: 1px;
white-space: nowrap;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.ctx-item:hover {
background: var(--theme-contextmenu-hover-backcolor);
color: var(--theme-contextmenu-hover-color);
}
.ctx-item .icon {
display: inline-flex;
position: relative;
justify-content: flex-start;
width: 20px;
}
.ctx-disabled {
opacity: 0.5;
/* cursor: default; */
}
.ctx-divider {
border-top: 1px solid var(--theme-contextmenu-divider);
margin: 4px 0;
}
.ctx-submenu {
position: absolute;
left: 100%;
top: 0;
min-width: 160px;
background: var(--theme-contextmenu-backcolor);
border: 1px solid var(--theme-contextmenu-border-color);
color: var(--theme-contextmenu-color);
padding: 4px 0;
border-radius: 6px;
display: none;
z-index: 99999;
}
.ctx-submenu.open {
display: block;
}
.ctx-arrow {
font-size: 11px;
opacity: 0.6;
}

434
public/styles/default.css Normal file
View File

@@ -0,0 +1,434 @@
/* #region DOM */
*::placeholder { text-align:center; }
* {
user-select:none;
cursor: var(--theme-cursor-default) 1 1, auto;
}
.selectable { user-select: text !important; cursor: var(--theme-cursor-pointer) 0 16, pointer; }
input, input[type="text"], input[type="email"], input[type="password"], input[type="search"], input[type="date"], textarea, select { border-width:2px; border-style:solid; font-size: var(--fontSize); font-family: var(--fontFamily); border-radius:10px; padding:10px 12px; outline:none; transition:all .5s ease; width:auto; }
input.width\:100px { width:100px; }
input.width\:90px { width:90px; }
input.width\:75px { width:75px; }
input.width\:50px { width:50px; }
input.width\:25px { width:25px; }
*::placeholder, input[id="sAMAccountName"] { font-style:italic; font-weight:100; letter-spacing:3px; }
html, button { font-size: var(--fontSize); font-family: var(--fontFamily); }
button.monolyth, button.bluebutton, button.greenbutton, button.yellowbutton, button.redbutton { display:inline-block; padding:8px 10px; margin:0.2rem 1.6rem; font-weight:600; text-align:center; text-decoration:none; color:rgb(255, 255, 255); border:none; border-radius:8px; box-shadow:0 4px 6px rgba(0,0,0,0.1); transition:all 0.2s ease; }
button.monolyth { background-color:transparent; }
button:not(:disabled).monolyth:hover { opacity:0.9; }
button.bluebutton { color:var(--theme-button-blue-default-color); background:var(--theme-button-blue-default-backcolor); }
button.greenbutton { color:var(--theme-button-green-default-color); background:var(--theme-button-green-default-backcolor); }
button.yellowbutton { color:var(--theme-button-yellow-default-color); background:var(--theme-button-yellow-default-backcolor); }
button.redbutton { color:var(--theme-button-red-default-color); background:var(--theme-button-red-default-backcolor); }
button:disabled { color:var(--theme-button-disabled-color); background:var(--theme-button-disabled-backcolor); }
button:not(:disabled).redbutton:hover { background:var(--theme-button-red-hover-color); background:var(--theme-button-red-hover-backcolor); }
button:not(:disabled).greenbutton:hover { background:var(--theme-button-green-hover-color); background:var(--theme-button-green-hover-backcolor); }
button:not(:disabled).bluebutton:hover { background:var(--theme-button-blue-hover-color); background:var(--theme-button-blue-hover-backcolor); }
button:not(:disabled).yellowbutton:hover { background:var(--theme-button-yellow-hover-color); background:var(--theme-button-yellow-hover-backcolor); }
/* #endregion */
/* #region Container */
.container.static { width:calc(100% - 20px); margin:10px auto; display:flex; gap:12px; min-height:0; overflow:auto; max-height:100%; flex-direction: column;}
/* .card.static { display:flex; flex-direction:column;flex: 0 0 auto; } */
.card.static.row { overflow:hidden; display:flex; flex-direction:row; flex-wrap: wrap;}
.card.static { overflow:hidden; display:flex; flex-direction:column; }
.container { width:calc(100% - 20px); margin:10px auto; display:grid; grid-template-columns:100%; gap:12px; min-height:0; overflow:auto; max-height:100%; }
.container:not(.static) * { box-sizing:border-box; }
.card { border-width:1px; border-style:solid; border-radius:8px; padding:20px; }
.grid { display:grid; gap:16px; grid-template-columns:repeat(auto-fit, minmax(200px, 1fr)); }
/* #endregion */
/* #region Copy icon */
.copy-icon { transition:opacity .25s; cursor:var(--theme-cursor-pointer) -16 16, pointer; opacity:0.7; }
.copy-icon:hover { opacity:1; }
/* #endregion */
/* #region Tooltip */
.global-tooltip { position:fixed; padding:6px 8px; border-width:1px; border-style:solid; border-radius:10px; max-width:50vw; white-space:normal; z-index:999; pointer-events:none; opacity:0; transform:translateY(-30px); transition:opacity .12s ease; }
.global-tooltip.visible { opacity:1; }
/* #endregion*/
/* #region Scrollbar */
::-webkit-scrollbar { width:12px; height:12px; }
::-webkit-scrollbar-track { border-radius:0px; }
::-webkit-scrollbar-thumb { border-radius:6px; border-width:2px; border-style:solid; }
/* #endregion */
/* #region Messagebox */
#message-container { position: fixed; top: 1rem; right: 1px; display: flex; flex-direction: column; gap: 0.5rem; z-index: 1000; max-height: 80vh; padding-left: 15px; overflow-y: auto; overflow-x: visible; scrollbar-width: none; -ms-overflow-style: none; }
#message-container::-webkit-scrollbar { display: none; }
.message { border-radius: 8px; margin: 8px 8px 8px 0; padding: 10px 14px; width: auto; max-width: 45vw; animation: slideIn 0.4s ease-out; transition: transform 0.2s ease; word-break: break-word; overflow-wrap: anywhere; }
.message:hover { transform:scale(1.02); }
.message-header { display:flex; justify-content:space-between; align-items:center; margin-bottom:6px; }
.message-title { flex:1; font-weight:600; }
.countdown { margin-right:8px; white-space:nowrap; }
.pin-div { margin-left:8px; cursor:var(--theme-cursor-pointer) -16 16, pointer; user-select:none; transition:transform 0.2s ease, color 0.2s ease; }
.pin-div:hover { transform:scale(1.2); }
.pin-div.pinned { transform:scale(1.1); }
@keyframes slideIn { 0% { opacity:0; transform:translateX(100%); } 60% { opacity:1; transform:translateX(-10px); } 80% { opacity:1; transform:translateX(5px); } 100% { opacity:1; transform:translateX(-10px); } }
@keyframes slideOut { 0% { opacity:1; transform:translateX(0); } 100% { opacity:1; transform:translateX(100%); } }
/* #endregion */
/* #region Toggle switch */
/*
<label class="cb cb-switch">
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} >
<span class="switch-track" aria-hidden="true">
<span class="switch-thumb" aria-hidden="true"></span>
</span>
<span class="cb-label">Ein / Aus</span>
</label>
*/
.cb-switch { --w:45px; --h:27px; display:inline-flex; align-items:center; }
.cb-switch input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
.switch-track { width:var(--w); height:var(--h); border-radius:999px; padding:3px; border-width:1px; border-style:solid; box-sizing:border-box; display:inline-flex; align-items:center; transition:background .18s ease, transform .12s ease, border-color .25s ease; }
.switch-thumb { min-width:calc(var(--h) - 2 * 3px); width:calc(var(--h) - 2 * 3px); height:calc(var(--h) - 2 * 3px); background:var(--theme-switch-thumb); border-radius:50%; box-shadow:0 2px 6px rgba(0,0,0,.15); transform:translateX(-1px); transition:transform .25s cubic-bezier(.2,.9,.2,1), background .18s ease }
.cb-switch input:disabled + .switch-track { background-color: dimgray;}
.cb-switch input:disabled + .switch-track .switch-thumb { background-color: rgb(54, 50, 50); }
.cb-switch input:not(:disabled):checked + .switch-track { background:var(--theme-switch-active); }
.cb-switch input:not(:disabled):hover + .switch-track { border-color:var(--theme-switch-hover); }
.cb-switch input:focus-visible + .switch-track { box-shadow:0 0 0 6px rgba(6,193,103,0.12); }
.cb-switch input:checked + .switch-track .switch-thumb { transform:translateX(calc(var(--w) - var(--h))); }
.cb-switch label { width: calc(100% - var(--w)); }
/* #endregion */
/* #region CheckBox */
/*
<label class="cb cb-modern">
<input data-status="{{name}}" type="checkbox" {{#if active}}checked{{/if}} disabled>
<span class="cb-box" aria-hidden="true"></span>
</label>
*/
.cb { display:inline-flex; align-items:center; gap:10px; user-select:none; transform:translateY(2px); }
.cb input { position:absolute; opacity:0; width:0; height:0; pointer-events:none; }
.cb-box { width:20px; height:20px; border-radius:6px; background:var(--theme-checkbox-backcolor); border-width:2px; border-style:solid; display:inline-grid; place-items:center; transition:transform .12s ease, border-color .12s ease, background .12s ease; }
.cb-box::after { content:""; width:12px; height:8px; transform:scale(0) translateY(-2px); background-repeat:no-repeat; background-position:center; background-size:contain; background-image:url("data:image/svg+xml,%3Csvg width='12' height='8' viewBox='0 0 12 8' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M1 4L4 7l7-7' stroke='%23fff' stroke-width='2' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); transition:transform .18s cubic-bezier(.2,.9,.2,1); }
.cb input:checked + .cb-box { transform:translateY(-1px); }
.cb input:checked + .cb-box::after { transform:scale(1) translate(-1px, 1px); }
table .cb input:checked + .cb-box::after { transform:scale(1) translate(1px, 1px); }
.cb input:focus-visible + .cb-box { outline:none; }
/* #endregion */
/* #region Required */
.is-required-empty { border-width:2px; border-style:solid; }
/* #endregion */
/* #region Tabs */
.tabs { display: flex; margin-bottom: 10px; border-bottom-width: 2px; border-bottom-style: solid; }
.tab { padding: 10px 20px; border: 1px solid transparent; border-top-left-radius: 5px; border-top-right-radius: 5px; margin-right: 5px; transition: background .25s, color .25s, border-color .25s; }
.tab-content { border-width: 1px; border-style: solid; border-radius: 5px; padding: 15px; }
.item { margin-bottom: 5px; }
/* #endregion */
/* #region Feebox */
.feedbox-overlay { position:fixed; top:0; left:0; width:100vw; height:100vh; display:flex; align-items:center; justify-content:center; z-index:999; }
.feedbox { border-radius:8px; max-width:50vw; width:100%; padding:20px; animation:feedboxFadeIn 0.2s ease-out; max-height:80vh; display:flex; flex-direction:column; overflow:hidden; }
.feedbox h3 { margin:0 0 10px 0; }
.feedbox-message { margin-bottom:20px; line-height:1.4; flex:1; overflow-y: auto; /* font-size:1rem; */ }
.feedbox-actions { display:flex; justify-content:flex-end; gap:10px; flex-wrap:wrap; flex-shrink:0; }
@keyframes feedboxFadeIn { from { transform:translateY(-20px); opacity:0; } to { transform:translateY(0); opacity:1; } }
.feedbox-btn {
padding:6px 14px;
/* font-size:0.95rem; */
border-radius:4px;
border:1px solid #ccc;
background:#f5f5f5;
transition:all 0.15s;
}
.feedbox-btn:hover {
background:#e5e5e5;
}
.feedbox-btn.primary {
background:#4a90e2;
color:#fff;
border-color:#4a90e2;
}
.feedbox-btn.primary:hover {
background:#357ABD;
}
.feedbox-btn.danger {
background:#e94e3c;
color:#fff;
border-color:#e94e3c;
}
.feedbox-btn.danger:hover {
background:#c0392b;
}
.feedbox-input {
width:100%;
padding:6px 8px;
/* font-size:0.95rem; */
margin-top:8px;
margin-bottom:12px;
border:1px solid #ccc;
border-radius:4px;
box-sizing:border-box;
}
.feedbox-btn:focus, .feedbox-input:focus {
outline:2px solid #4a90e2;
outline-offset:2px;
}
/* Responsive Small Screens */
@media (max-width:400px) {
.feedbox {
padding:15px;
}
.feedbox-actions {
flex-direction:column-reverse;
gap:8px;
}
.feedbox-btn {
width:100%;
}
}
/* #endregion */
.select-wrapper {
position: relative;
width: 250px;
}
.select-wrapper select {
width: 100%;
padding: 12px 40px 12px 14px;
font-size: 16px;
border: 2px solid #ddd;
border-radius: 10px;
background-color: #fff;
color: #333;
appearance: none; /* entfernt Standard-Styling */
-webkit-appearance: none;
-moz-appearance: none;
cursor: pointer;
transition: all 0.2s ease;
}
/* Hover / Focus */
.select-wrapper select:hover {
border-color: #999;
}
.select-wrapper select:focus {
outline: none;
border-color: #4a90e2;
box-shadow: 0 0 0 3px rgba(74,144,226,0.2);
}
/* Custom Pfeil */
.select-wrapper::after {
content: "▾";
position: absolute;
right: 14px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
font-size: 14px;
color: #666;
}
.error-field { padding:12px 16px; border-radius:10px; border-width:1px; border-style:solid; }
.success-field { border-width: 1px; border-style: solid; padding:12px 16px; border-radius:10px; }
label { color:var(--muted); display:block; margin-bottom:6px; }
/* #region Dropzone */
.dropzone {
max-width:360px;
padding:12px;
box-shadow:0 6px 18px rgba(0,0,0,.08);
background:var(--theme-dropzone-default-backcolor);
border:1px dashed #aaa;
border-radius:8px;
padding:10px;
/* font-size:13px; */
text-align:center;
margin-bottom:8px;
color:var(--theme-dropzone-default-color);
}
.dropzone.active {
border-color:var(--theme-dropzone-active-border-color);
color:var(--theme-dropzone-active-color);
background:var(--theme-dropzone-active-backcolor);
}
.dropzone-area ul {
list-style:none;
padding:0;
margin:0;
max-height:120px;
overflow-y:auto;
}
.dropzone-area li {
display:flex;
justify-content:space-between;
align-items:center;
/* font-size:13px; */
padding:4px 0;
border-bottom:1px solid #eee;
}
button.removeButton {
border:none;
background:none;
color:#d11a2a;
cursor:var(--theme-cursor-pointer) -16 16, pointer;
/* font-size:14px; */
}
input[type="file"] {
display:none;
}
tr.drop-hover {
outline:3px dashed var(--theme-dropzone-active-border-color);
/* outline:2px dashed var(--primary-color, #4a90e2);
background:rgba(74, 144, 226, 0.08); */
}
tr.no-drop-hover {
outline:3px dashed red;
cursor:no-drop !important;
/* outline:2px dashed var(--primary-color, #4a90e2);
background:rgba(74, 144, 226, 0.08); */
}
/* #endregion */
/* #region Multiselect textbox */
.mst-wrapper {
position:relative;
display:block;
border-radius:4px;
padding:4px 8px;
background:var(--theme-window-backcolor);
cursor:text;
width:300px; /* optional, je nach gewünschter Breite */
}
/* Eingabefeld oben */
.mst-input {
width:100%;
border:none;
outline:none;
/* font-size:14px; */
padding:4px 0;
}
/* Container für Chips unterhalb des Inputs */
.mst-chips-container {
display:flex;
flex-wrap:wrap; /* Chips umbrechen */
gap:4px;
margin-top:4px;
}
/* Einzelner Chip */
.mst-chip {
display:inline-flex;
align-items:center;
background-color:#007bff;
color:#fff;
border-radius:8px;
padding:8px 8px;
/* font-size:13px; */
}
/* Entfernen-Button im Chip */
.mst-chip-remove {
margin-left:4px;
cursor:var(--theme-cursor-pointer) -16 16, pointer;
font-weight:bold;
}
/* Dropdown-Liste */
.mst-dropdown {
position:absolute;
top:100%;
left:0;
right:0;
max-height:200px;
overflow-y:auto;
border:1px solid #ccc;
background:inherit;
z-index:1000;
}
.mst-item {
padding:6px 8px;
cursor:var(--theme-cursor-pointer) -16 16, pointer;
}
.mst-item:hover {
background-color:var(--theme-accent-hover-backcolor);
}
.mst-item.new {
font-style:italic;
color:#555;
}
/* #endregion */
.tutorial-highlight {
transition: all 0.3s ease;
}

View File

@@ -0,0 +1,79 @@
.json-controls {
position: sticky;
top: -8px;
backdrop-filter: blur(5px);
padding: 8px;
border-bottom: 2px solid var(--theme-window-titlebar-backcolor);
z-index: 1;
}
.json-controls button {
background: var(--theme-window-backcolor) !important;
color: var(--theme-window-color) !important;
}
.json-line {
padding: 3px 0;
white-space: pre;
}
.json-key {
color: #d73a49;
}
.json-input {
padding: 3px 8px;
border: none;
background: transparent;
outline: none;
}
.json-input.string {
color: #032f62;
}
.json-input.number {
color: #005cc5;
}
.json-toggle {
cursor:var(--theme-cursor-pointer) -16 16, pointer;
color: #6f42c1;
}
.json-children {
padding: 3px 0;
}
.json-children.collapsed {
display: none;
}
.json-header {
display: flex;
align-items: center;
gap: 6px;
}
.json-add {
cursor:var(--theme-cursor-pointer) -16 16, pointer;
color: #28a745;
opacity: 0.7;
font-size: 13px;
}
.json-add:hover {
opacity: 1;
}
.json-remove {
cursor:var(--theme-cursor-pointer) -16 16, pointer;
color: #dc3545;
opacity: 0.6;
margin-left: 6px;
font-size: 12px;
}
.json-remove:hover {
opacity: 1;
}

217
public/styles/os.css Normal file
View File

@@ -0,0 +1,217 @@
/* public/css/desktop.css */
body, html { margin:0; padding:0; height:100%; overflow: hidden; font-family: var(--fontFamily); font-size: var(--fontSize); }
#desktop { position:relative; height:100vh; overflow:hidden; background-size:var(--theme-desktop-background-size); background-repeat: no-repeat; background-position: center; touch-action: none; }
#windows { z-index: 1; position:absolute; inset:0; padding:8px; box-sizing:border-box; }
.window { position:absolute; width: 800px; height: 600px; border-radius:6px; box-shadow: 0 6px 20px rgba(0,0,0,0.4); overflow:hidden; top:50px; left:50px; display:flex; flex-direction:column; resize:both; }
.window-titlebar { padding: 0 0 1px 0; height: 28px; display:flex; justify-content:space-between; align-items:center; }
.window-icon { height: 20px; background-size:contain; transform: translate(1px, -1px); background-repeat: no-repeat; background-position: left; background-color: rgb(144, 179, 144); padding:4px;border-radius: 8px;}
.window-content { display: flex; flex-direction: column; flex:1; padding:8px; overflow: auto; }
.window .controls button { transition: background-color .3s, color .3s; padding: 6px 10px; border:none; }
.window[class="max"] .window-resize-handle { display: none; }
.window-resize-handle { position:absolute; right:0; bottom:0; width:12px; height:12px; cursor:se-resize; z-index: 10; }
.window-resize-n { top: -4px; left: 0; right: 0; height: 8px; cursor: n-resize; }
.window-resize-s { bottom: -4px; left: 0; right: 0; height: 8px; cursor: s-resize; }
.window-resize-e { right: -4px; top: 0; bottom: 0; width: 8px; cursor: e-resize; }
.window-resize-w { left: -4px; top: 0; bottom: 0; width: 8px; cursor: w-resize; }
.window-resize-ne { top: -4px; right: -4px; width: 12px; height: 12px; cursor: ne-resize; }
.window-resize-nw { top: -4px; left: -4px; width: 12px; height: 12px; cursor: nw-resize; }
.window-resize-se { bottom: -4px; right: -4px; width: 12px; height: 12px; cursor: se-resize; }
.window-resize-sw { bottom: -4px; left: -4px; width: 12px; height: 12px; cursor: sw-resize; }
#taskbar { z-index: 2; position: absolute; width:100%; bottom:0; left:0; height:42px; overflow:visible; display:flex; flex: 0 0 auto; min-width:0; align-items:center; padding:0 8px; box-sizing:border-box; }
#start-btn { transition: background-color 0.3s ease; padding: 8px 15px; border-radius: 5px; border: none; margin-right:8px; }
#taskbar-windows { display:flex; gap:6px; align-items:center; flex:1; overflow-y:hidden;overflow-x: auto; min-width: 0; }
.taskbar-item { display: flex; position: relative; padding:7px 10px; border-radius:4px; }
.taskbar-item::before { content: ''; position: absolute; bottom:1px; left:50%; width:40%; height: 4px; border-radius:4px; transform:translateX(-50%) scaleX(0); transform-origin:center; transition:transform 0.3s ease; }
.taskbar-item.focus::before { transform: translateX(-50%) scaleX(1); }
.notify-button { margin-left:auto; flex: 0 0 auto; display: flex; align-items: center; justify-content: center; }
.notify-button.resume, .notify-button.pulse { animation: pulse 1.5s infinite; animation-play-state: running; }
.notify-button.pause, .notify-button.pause *, .notify-button.active, .notify-button.active * { animation-play-state: paused; }
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.hidden { opacity: 0; pointer-events: none; }
/* Open submenu vertical */
#start-menu { z-index: 2; transition: opacity 0.3s; display:flex; flex-direction:column; position: absolute; bottom: 50px; left: 8px; width: auto; min-width: 300px; border-radius: 8px; overflow: hidden; }
.start-header { display: flex; align-items: center; padding: 8px; font-weight: 600; }
.start-header > img { height: 24px; width: 24px; margin-right: 5px; }
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 60vh; overflow-y: auto; }
.start-item { padding: 8px 12px 16px 20px; display: flex; gap: 8px; transition: background 0.2s; border-radius: 4px; }
.start-item:not(:has(.submenu)) { padding: 12px;align-items:center; }
.start-icon, #start-menu-icon { height: 20px; width: 20px; border-radius: 8px; padding:4px; }
.menu-label { display: block; margin-top: 4px; padding-bottom: 0; margin-left: 26px }
.start-item.has-submenu { position: relative; display: flex; flex-direction: column; padding-bottom: 8px; }
.start-item.has-submenu > .submenu { width: 100%; list-style: none; padding-left: 2px; max-height: 0; overflow: hidden; transition: max-height 0.3s ease; margin: 2px 0 0 8px; }
.start-item-sys-container { position: relative; left:0; bottom: -8px; padding: 5px 0px; width:100%; display:flex; height:30px; flex-direction:row; justify-content:flex-end; }
.start-sys-item { margin: 0 10px 0 8px !important; }
.start-submenu-head { position: relative; margin-left: 16px; height:32px; display:flex; flex-direction: row; align-items: center; gap:8px; }
.start-item.has-submenu > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(0deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
.start-item.has-submenu.open > .menu-label::after { content: ""; position: absolute; right: 14px; transform: translateY(50%) rotate(180deg); border: 5px solid transparent; border-top-color: #aaa; transition: transform 0.3s; }
.start-item.has-submenu.open > .submenu { max-height: 500px; }
.start-item.has-submenu > .submenu li { opacity: 0; transform: translateY(-5px); transition: opacity 0.2s, transform 0.2s; }
.start-item.has-submenu.open > .submenu li { opacity: 1; transform: translateY(0); }
img.icon { width: auto; height:20px; object-fit: contain; filter: var(--theme-notifybubble-filter); transform: translate(2px, 2px) }
@media (max-width: 768px) {
body, html {
overflow: auto;
}
#desktop {
height: 100dvh;
}
/* Fenster = Fullscreen */
.window {
width: 100% !important;
height: calc(100% - 54px) !important;
top: 0 !important;
left: 0 !important;
border-radius: 0;
resize: none;
}
.window-resize-handle {
display: none !important;
}
.window-titlebar {
height: 29px;
padding: 0 0px;
}
.window-content {
padding: 12px;
}
/* Taskbar größer für Touch */
#taskbar {
height: 50px;
padding: 0 6px;
}
#start-btn {
padding: 10px 14px;
font-size: 16px;
}
.taskbar-item {
padding: 10px;
}
.start-list { list-style: none; margin: 0; padding: 8px 0; height: 100%; overflow-y: auto; }
#start-menu {
bottom: 50px !important;
height: calc(100dvh - 65px) !important;
width: calc(100dvw - 5px);
left: 0 !important;
border-radius: 0 !important;
} */
.start-item {
padding: 14px;
font-size: 16px;
}
.start-icon {
height: 24px;
width: 24px;
}
.start-item.has-submenu > .menu-label { margin: 8px 0 0 40px; }
.start-item-sys-container { margin:auto; bottom: -10px; }
/* Buttons besser klickbar */
.window .controls button {
padding: 10px 12px;
}
/* Notify Button */
.notify-button {
padding: 8px;
}
}
@media (max-width: 1000px) AND (orientation: landscape) {
body, html {
overflow: auto;
}
#desktop {
height: 100dvh;
}
/* Fenster IMMER fullscreen */
.window {
width: 100% !important;
height: calc(100dvh - 46px) !important;
top: 0 !important;
left: 0 !important;
border-radius: 0 !important;
}
/* Resize komplett aus */
.window-resize-handle {
display: none !important;
}
.window-titlebar {
height: 30px;
padding: 0 0px;
}
/* Startmenü bis ganz oben */
#start-menu {
bottom: 44px !important;
height: calc(100dvh - 60px) !important;
width: 300px;
max-width: 35dvw;
left: 0 !important;
border-radius: 0 !important;
}
/* Taskbar bleibt sichtbar, aber oben drüber sauber */
#taskbar {
z-index: 9999;
}
/* Startliste nutzt volle Höhe */
.start-list {
height: 100%;
}
.start-item-sys-container { bottom: -10px;}
.start-item.has-submenu > .menu-label { margin: 8px 0 0 28px; }
}

86
public/styles/table.css Normal file
View File

@@ -0,0 +1,86 @@
/* .table-wrapper { display:grid; grid-column:auto; overflow:auto; min-height:0; min-width:0;} */
.table-wrapper { overflow:auto; min-width:0; }
.table-wrapper.fit-table { grid-template-columns:calc(1fr - 150px); }
/* .table-wrapper { flex:1 1 auto; overflow-y:auto; } */
/* .table-wrapper.fit-table { width:100%; max-width:100%; } */
/* .table-wrapper.fit-table th.word-wrap, .table-wrapper.fit-table td.word-wrap { white-space:normal; word-wrap:break-word; word-break:break-word; overflow-wrap:anywhere; } */
/* #region Dont's */
th.no-wrap, td.no-wrap { white-space:nowrap !important; }
th.wrap, td.wrap { white-space:wrap; word-wrap: break-word; word-break: keep-all; }
table.no-background tr, table.no-background td { background:none !important; }
table.border * { border:1px solid white; }
/* #endregion */
/* #region performance optimizing */
table thead { position:sticky; top:0; z-index:20; will-change:transform; transform:translateZ(0);}
/* #endregion */
/* echte Tabelle */
table { width:calc(100%); border-spacing:0 5px; }
table th, table td { min-width:100px; max-width:250px; overflow:hidden; white-space:nowrap; }
table tr.grouprow:hover { background: rgba(0,0,0,0.05);}
/* Header sticky — aber innerhalb echter Tabelle */
thead th { position:sticky; top:var(--filter-height); }
/* KEIN display:block mehr! */
thead, tbody { display:table-row-group; }
table thead th { padding:5px; }
/* table tbody td { padding:5px 0px 5px 20px; } */
table tbody td:not(:first-child):not(:last-child), table thead th:not(:first-child):not(:last-child) { border-width:0; border-style:solid; }
table tbody tr.grouprow { font-weight:700; }
table .text-align\:center { text-align:center; }
table .text-align\:right { text-align:right; }
table .text-align\:left { text-align:left; }
td { overflow:hidden; text-overflow:ellipsis; /* verhindert, dass Inhalt die Zelle sprengt */ }
.table-filter-container {
border-bottom-width:8px;
border-bottom-style:solid;
display:flex;
justify-content:flex-start;
flex-direction:column;
flex-wrap:wrap;
gap:10px;
position:sticky;
left:0px;
top:0px;
/* z-index:20; */
padding:5px 10px;
border-radius:var(--border-raduis) var(--border-raduis) 0 0;
}
.table-filter-container .live-counter { position:absolute; right:18px; margin-left:auto; font-weight:bold; }
.table-filter-container input, .table-filter-container select { padding:5px !important; }
th.sort-asc::after {
content:" ▲";
}
th.sort-desc::after {
content:" ▼";
}
td.vote { width:28px; height:28px; position:relative; padding:0; }
td.vote:hover { background:rgba(0,0,0,0.05); /* cursor:pointer;*/ }
td.vote::before,
td.vote::after { position:absolute; top:50%; left:50%; transform:translate(-50%, -50%); }
td.yes::before { content:""; width:8px; height:16px; border:solid #2ecc71; border-width:0 3px 3px 0; transform:translate(-50%, -60%) rotate(45deg); }
td.no::before, td.no::after { content:""; width:3px; height:18px; background:#e74c3c; }
td.no::before { transform:translate(-50%, -50%) rotate(45deg); }
td.no::after { transform:translate(-50%, -50%) rotate(-45deg); }
td.maybe::before { content:""; width:0px; height:0px; border-radius:50%; border:10px solid #f1c40f; transform:translate(-50%, -50%); }

View File

@@ -0,0 +1,67 @@
/* Bubble-Container */
#notify-bubble {
position: absolute;
bottom: 125%;
right: 3px;
min-width: 200px;
max-height: 50vh;
width: max-content; /* 🔑 wächst mit Inhalt */
max-width: 600px; /* aber capped */
background: var(--theme-taskbar-tray-backcolor);
color: var(--theme-taskbar-tray-color);
border-color: var(--theme-taskbar-tray-border-color);
border-radius: 10px;
overflow-y: auto; /* vertikal scrollen */
overflow-x: hidden; /* horizontal verhindern */
box-shadow: 0 8px 20px rgba(0,0,0,0.4);
padding: 10px 10px 6px 10px;
opacity: 0;
pointer-events: none;
transform: translateY(10px);
transition: all 0.25s ease;
z-index: 99999999;
text-align: end;
font-weight: normal;
}
/* Bubble-Items als Grid */
.bubble-item {
display: grid;
grid-template-columns: minmax(0, auto) 250px;
gap: 10px;
padding: 6px 8px;
border-radius: 6px;
/* cursor: pointer; */
align-items: center;
transition: transform 0.25s;
}
/* linke Spalte */
.bubble-item > :nth-child(1) {
min-width: 0; /* wichtig für Grid Shrink */
white-space: normal; /* Text darf umbrechen */
overflow-wrap: normal; /* Wörter nur bei Bedarf umbrechen */
word-break: normal; /* nicht mitten in Wörtern */
text-align: left;
}
/* rechte Spalte fix */
.bubble-item > :nth-child(2) {
width: 250px;
height: 50px;
overflow: hidden;
}
/* hover Effekt */
.bubble-item:hover {
background: rgba(64,64,64,0.4);
transform: scale(1.01);
}
/* Bubble sichtbar machen */
.notify-button.active #notify-bubble {
pointer-events: auto;
opacity:1;
transform: translateY(0);
}

153
public/views/desktop.hbs Normal file
View File

@@ -0,0 +1,153 @@
<head>
<script src="/socket.io/socket.io.js"></script>
<script src="javascript/main.js"></script>
<script src="javascript/customModal.js"></script>
<script src="javascript/pluginAPI.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles/default.css">
<link rel="stylesheet" href="styles/contextMenu.css">
<link rel="stylesheet" href="styles/userNotification.css">
<link rel="stylesheet" href="styles/table.css">
<link rel="stylesheet" href="styles/jsonTree.css">
<link rel="stylesheet" href="styles/colors.css" />
<link rel="stylesheet" href="styles/os.css" />
<div id="message-container"></div>
<div id="copy-toast"></div>
<title>Radix OS</title>
</head>
<div id="desktop">
<div id="windows"></div>
<!-- Startmenu -->
<div id="start-menu" class="hidden">
<div class="start-header"><img id="start-menu-icon"><span>Radix OS</span></div>
<ul class="start-list">
{{#groupBy startMenuItems "section"}}
<li class="start-submenu-head">
<span><i>{{this.key}}</i></span>
</li>
{{#each items}}
{{#ifSingle this.menu.items}}
{{#if this.authorized}}
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
{{#if this.icon}}
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
{{else}}
{{/if}}
<span>{{this.label}}</span>
</li>
{{/if}}
{{else}}
<li class="start-item has-submenu">
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/folder.png" class="start-icon" style="position:absolute;left:12px;"/>
<span class="menu-label">{{this.menu.label}}</span>
{{!-- {{#if this.version}}<small>v{{this.version}}</small>{{/if}} --}}
<ul class="submenu">
{{#each this.menu.items}}
{{#equaler this.label "==" "hr"}}
<li><hr></li>
{{else}}
{{#if this.authorized}}
<li class="start-item {{#unless ../this.active}}unload{{/unless}}" data-active="{{#equaler ../this.active "&&" this.authorized}}true{{else}}false{{/equaler}}" data-appname="{{../this.name}}" data-appview="{{this.view}}" data-viewlabel="{{this.label}}">
{{#if this.icon}}
<img src="{{#if ../this.pluginPath}}/{{../this.name}}{{/if}}/images/{{this.icon}}" class="start-icon" />
{{else}}
{{/if}}
<span>{{this.label}}</span>
{{#if this.version}}<small>v{{this.version}}</small>{{/if}}
</li>
{{/if}}
{{/equaler}}
{{/each}}
</ul>
</li>
{{/ifSingle}}
{{/each}}
{{/groupBy}}
{{!-- [Function restart] in javascript/main.js --}}
</ul>
<div class="start-item-sys-container">
<button class="monolyth start-sys-item" data-tooltip="Neustart" onclick="restart();" data-tooltip-mode="always">⟳</button>
<button class="monolyth start-sys-item" data-tooltip="Abmelden" onclick="logout()" data-tooltip-mode="always">🔓</button>
</div>
</div>
<!-- Taskbar -->
<div id="taskbar">
<button id="start-btn">☰</button>
<div id="taskbar-windows"></div>
<button style="margin-right:0;" class="monolyth notify-button pulse">
<img class="icon" src="/images/notifybubble.png">
<div id="notify-bubble"></div>
</button>
</div>
</div>
<script src="javascript/tutorial.js"></script>
<script src="javascript/notifyBubble.js"></script>
<script src="javascript/contextMenu.js"></script>
<script src="javascript/tableFilter.js"></script>
<script src="javascript/requiredFields.js"></script>
<script src="javascript/loadOnce.js"></script>
<script src="javascript/JSON.js"></script>
<script src="javascript/os.js"></script>
<script>
addRestartHook(() => localStorage.setItem('openWindows', JSON.stringify({ })));
fetch('/api/Plugins/loadScripts', { method: 'POST', headers: {'Content-Type': 'application/json'} })
.then(res => res.json())
.then(scripts => {
(scripts).forEach(script => {
reloadPluginScript(script);
});
});
loadServerStyles(getCookie('theme'));
const notifyButtons = document.querySelectorAll('.notify-button');
const trayNotifyButton = document.querySelector('#taskbar > .notify-button')
const notify = new NotifyBubble(trayNotifyButton, "#notify-bubble");
document.addEventListener("contextmenu", evt => evt.preventDefault());
document.querySelectorAll('#start-menu .start-item.has-submenu').forEach(item => {
item.addEventListener('click', evt => {
// Toggle nur dieses Submenu
item.classList.toggle('open');
});
});
async function logout() {
const response = await fetch('/logout', { method: 'POST', headers: {'Content-Type': 'application/json'} });
if (response.ok) {
window.history.pushState({ }, '', '/login');
location.reload();
}
}
fetch('/api/NotifyTray/getTrays')
.then(res => res.json())
.then(trays => {
notify.clear();
if (trays.length > 0) {
trays.forEach(tray => {
notify.addItem(tray, () => {
if(tray.JSON) {
//execution of tray action, e.g. open window
}
});
})
}
});
</script>

127
public/views/eventlog.hbs Normal file
View File

@@ -0,0 +1,127 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Event Log</title>
</head>
<body>
<div class="container">
<div class="card static">
<div class="table-wrapper">
<table id="eventTable" >
<thead class="no-wrap">
<tr>
<th class="text-align:left" onclick="">ID</th>
<th class="text-align:left" onclick="">Datum</th>
<th class="text-align:left" onclick="">Level</th>
<th class="text-align:left" onclick="">Plugin</th>
<th class="text-align:left" onclick="">Message</th>
<th class="text-align:left" onclick="">Trace</th>
<th class="text-align:left" onclick="">User</th>
</tr>
</thead>
<tbody>
{{#each logs}}
<tr>
<td>{{this.ID}}</td>
<td class="no-wrap">{{dateFormat this.Date "yyyy-mm-dd HH:MM:SS"}}</td>
<td class="{{this.Level_ID}}">
{{LevelDisplayName}}
</td>
<td>{{this.PluginName}}</td>
<td><span>{{replaceAll this.Message "||" "<br>"}}</span> <span class="copy-icon" onclick="copyToClipboard(`{{replaceAll this.Message "||" "<br>"}}`)">⧉</span></td>
<td>{{this.Trace}}</td>
<td class="no-wrap">{{this.ClearTextUser}}</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</div>
</div>
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
<button class="redbutton" id="confirmClearLog" data-tooltip="Löscht alle Einträge aus der Datenbank">Log leeren</button>
</div>
<script type="text/javascript">
const eventTable = document.getElementById("eventTable");
let mappedLogs = [];
confirmClearLog.onclick = evt => {
feedbox(
{ title: `Hast Du Sie nicht mehr Alle?`,
message: `<p>Damit werden alle Einträge aus dem EventLog entfernt!</p>`,
buttons: {
yes: {
text: 'Ich weiß, <b>Geht auf meinen Nacken!</b>',
onClick: async () => await clearLog()
},
no: {
text: 'Dazu habe ich nicht die Eier'
}
}
}
);
}
async function clearLog() {
const res = await fetch(`/api/eventlog/clearlog`, { method: 'POST' });
const data = await res.json();
vt.source([]);
}
const vt = virtualTable({
tableEl: eventTable,
rowHeight: 40,
buffer: 5,
groupKey: null, // optional zum Gruppieren
filterConfig:{
exceptedColumns: [''],
columnModes:{
Plugin: 'dropdown',
Level: 'dropdown',
User: 'dropdown'
},
checkboxFilter:{
column:'Level_ID',
rules:[
{ label:'Erfolgreich', test:v => parseInt(v) === 0 },
{ label:'Information', test:v => parseInt(v) === 1 },
{ label:'Warnung', test:v => parseInt(v) === 2 },
{ label:'Fehler', test:v => parseInt(v) === 4 },
{ label:'Absturz', test:v => parseInt(v) === 8 },
]
}
},
customRender: (row, tr) => {
tr.style.height = '40px';
createTd(tr, row['ID'], { classes: [ 'text-align:left'] });
createTd(tr, row['Datum']);
createTd(tr, row['Level']);
createTd(tr, row['Plugin']);
createTd(tr, row['Message'], { classes: [ 'no-wrap' ], attributes: { 'data-tooltip': formatHtml(row['Message']) } });
createTd(tr, row['Trace'], { attributes: { 'data-tooltip': formatHtml(row['Trace']) } });
createTd(tr, row['User']);
}
});
fetch('/api/eventlog/getLogs', { method: 'POST' })
.then(logs => logs.json())
.then(logs => {
mappedLogs = logs.map(row => ({ ...row, Plugin: row['PluginName'], Level: row['LevelDisplayName'], Datum: dateFormat(row['Date'], 'yyyy-mm-dd HH:MM:SS'), User: row['ClearTextUser'] }))
vt.addData(mappedLogs.length != 0 ? mappedLogs : {});
});
adminSocket.on('eventlog_table', (data) => {
data = {...data, Plugin: data['PluginName'], Level: data['LevelDisplayName'], Datum: dateFormat(data['Date'], 'yyyy-mm-dd HH:MM:SS'), User: data['ClearTextUser'] };
mappedLogs.unshift(data)
vt.source(mappedLogs);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<p>
Bilder, Symbole und Zeichnungen wurden durch KI generiert und teilweise aus bereits existierenden, lizenzierten Quellen im Auftrag der Stadt Frankfurt am Main verwendet.
</p>
<p>
Der Programmcode wurde ebenfalls durch KI unterstützend erstellt. Dies betrifft ausschließlich die Darstellungs- und Designlogik. Alle Verarbeitungen personenbezogener Daten liegen in der Obhut des nutzenden Betriebs.
</p>
<p>
Alle Inhalte der in RadixOS enthaltenen Plugins dienen ausschließlich dem dienstlichen Betrieb sowie der Weiterentwicklung interner Machine-Learning-Prozesse
<i>(automatisiertes Lernen computergestützter Verfahren)</i>.
</p>
<p>
Datenbanken, Inhalte sowie personenbezogene Daten werden nicht an Dritte weitergegeben oder öffentlich zugänglich gemacht. RadixOS ist als geschlossenes System konzipiert.
</p>
<p>
<b style="color:var(--theme-accent-default-backcolor)">
Das Urheberrecht an RADIX OS liegt beim Entwickler
</b>
</p>
</body>
<script>
</script>
</html>

View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
test
</body>
</html>

View File

@@ -0,0 +1,106 @@
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hilfe</title>
<style>
ul#help li a,
ul#help li a:visited {
text-decoration: none;
color: var(--theme-container-card-color);
}
ul#help {
display:flex;
width: 100%;
padding: 0;
margin: 0;
left: 0;
position: relative;
overflow: auto;
flex-wrap: nowrap;
scrollbar-width: thin;
}
ul#help li {
display: inline-block;
width: auto;
padding: 10px;
box-sizing: border-box;
text-align: center;
}
</style>
</head>
<body>
<div class="container static" style="height: 100vh;">
<div id="helpTabs" class="tabs" style="overflow-y: auto;scrollbar-width: thin; padding:0;flex: 0 0 auto;"></div>
<div class="card static" style="overflow-y:auto;flex: 1 1 auto;" >
<div id="tabWrapper" class="tab-contents" ></div>
</div>
<div id="helpFooter" class="card static" style="height:auto;bottom:0;text-align:left;flex:0 0 auto;flex-direction:row;justify-content:space-between;gap:4px">
<div class="selectable">
<div style="color:var(--theme-accent-default-backcolor);font-weight:bold;">
<span>&copy; Radix OS</span> <span id="year"></span>
<span>Manuel Sowada</span>
</div>
</div>
<div id="plugin"></div>
</div>
</div>
</div>
<script type="text/javascript">
function showTabs() {
fetch(`/api/help/getTabs`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
})
.then(data => data.json())
.then(async data => {
data.forEach(async t => {
const tabElement = document.createElement('div');
tabElement.className = 'tab';
tabElement.dataset.tab = t.name;
tabElement.textContent = t.name;
tabElement.addEventListener('click', async () => {
Array.from(document.querySelectorAll('.tab')).forEach(t => t.classList.remove('active'));
tabElement.classList.add('active');
tabWrapper.innerHTML = '';
const response = await fetch('/api/help/getHelp', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ name: t.name })
});
const item = await response.json();
const html = `
<div>
<p>${response.status === 500 ? 'Hilfe nicht verfügbar' : item.html}</p>
${item.description ? `<hr /><p>${item.description}</p>` : ''}
</div>
`;
const container = document.querySelector('#tabWrapper');
container.innerHTML = html;
});
document.getElementById('helpTabs').appendChild(tabElement);
})
})
}
showTabs();
year.textContent = new Date().getFullYear();
</script>
</body>
</html>

View File

@@ -0,0 +1,35 @@
<header>
<style>
</style>
</header>
<body>
<div class="container static">
<div class="card" style="overflow: auto;height:100vh">
<div id="jsonConfigTree" class="json-tree"></div>
</div>
</div>
</div>
</body>
<script>
fetch('/api/getConfig', { method: 'POST' })
.then(res => res.json())
.then(json => {
const tree = createJsonTree({
container: document.getElementById("jsonConfigTree"),
data: json,
expandInitially: true,
onSave: json => {
fetch('/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(json, null, 2)
}).then(() => writeEventLog(0, 'Serverconfig', tree.getChanges()) );
}
});
});
</script>

View File

@@ -0,0 +1,101 @@
<div class="container">
<div class="card">
<span style="font-weight: bold;">Dienst</span>
<button class="redbutton" id="shutdownButton">Abschalten</button>
<button class="yellowbutton" id="restartButton">Neustart</button>
<div>
<span>PID:</span> <span class="selectable" id="pid"></span>
<span class="copy-icon" onclick="copyToClipboard(`${document.getElementById('pid').textContent}`)">⧉</span>
</div>
</div>
<div class="card">
<div class="table-wrapper" style="max-height: 350px;">
<table id="releaseNotes">
<thead>
<tr>
<th class="text-align:left">Erledigt</th>
<th class="text-align:left">Timestamp</th>
<th class="text-align:left">User</th>
<th class="text-align:left">Note</th>
</tr>
</thead>
<tbody>
</tbody>
</table>
</div>
</div>
<div class="card">
<div id="package" style="overflow:auto"></div>
</div>
</div>
<script>
shutdownButton.onclick = evt => {
fetch('/api/shutdown', { method: 'POST' })
.then(res => res.json())
.then(json => {
console.log(json);
});
};
restartButton.onclick = evt => {
fetch('/api/restart', { method: 'POST' })
.then(res => res.json())
.then(json => {
console.log(json);
});
};
fetch('/api/getServerInfo', { method: 'POST' })
.then(res => res.json())
.then(json => {
document.querySelector('#pid').innerHTML = json.pid;
const vt = virtualTable({
tableEl: document.getElementById('releaseNotes'),
data: [],
buffer: 5,
rowHeight: 30,
filterConfig:{
exceptedColumns: [ 'Erledigt' ],
columnModes: {
'datetime': 'text',
'sAMAccountName': 'text',
'Note': 'text'
},
checkboxFilter: {
column: 'finish',
rules: [
{ label: 'Nur erledigte', test: v => v === true },
{ label: 'Nur unerledigte', test: v => v === false },
]
}
},
customRender: (row, tr) => {
createTd(tr,
`
<label class="cb cb-modern">
<input id="id1" data-status="{{name}}" type="checkbox" ${row.finish ? 'checked' : ''} >
<span class="cb-box" aria-hidden="true"></span>
</label>
`, { });
createTd(tr, row.datetime, { });
createTd(tr, row.sAMAccountName, { });
createTd(tr, row.value, { attributes: { "data-tooltip": row.value } });
}
});
vt.addData(json.releaseNotes)
document.querySelector('#package').innerHTML = JSON.stringify(json.package).split(',').join('<br>');
/*
createJsonTree({
container: document.getElementById("package-json"),
data: json,
expandInitially: true,
onSave: () => { }
});
*/
});
</script>

View File

@@ -0,0 +1,30 @@
<header>
<style>
</style>
</header>
<div style="width:100%;height:calc(100% - 64px);">
<div id="jsonStyleTree" class="json-tree"></div>
</div>
<script>
fetch('/api/getStyles', { method: 'POST' })
.then(res => res.json())
.then(json => {
const tree = createJsonTree({
container: document.getElementById("jsonStyleTree"),
data: json,
expandInitially: true,
onSave: json => {
fetch('/style', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(json, null, 2)
}).then(() => writeEventLog(0, 'Stylesheet', JSON.stringify(tree.getChanges())) );
}
});
});
</script>

View File

@@ -0,0 +1,113 @@
<body>
<div class="container">
<div class="card">
<label><b>Farbgebung</b></label>
<select id="themeSwitch" style="width: 100%;">
<option value="dark">Dunkel</option>
<option value="light">Hell</option>
<option value="modern">Modern</option>
</select>
</div>
<div class="card">
<label><b>Schrift</b></label>
<select id="fontSelector" style="width: 100%;">
<optgroup label="Sans-Serif (Windows Standard)">
<option value="Arial">Arial</option>
<option value="Calibri">Calibri</option>
<option value="Candara">Candara</option>
<option value="Segoe UI">Segoe UI</option>
<option value="Tahoma">Tahoma</option>
<option value="Trebuchet MS">Trebuchet MS</option>
<option value="Verdana">Verdana</option>
</optgroup>
<optgroup label="Serif (Windows Standard)">
<option value="Cambria">Cambria</option>
<option value="Constantia">Constantia</option>
<option value="Georgia">Georgia</option>
<option value="Times New Roman">Times New Roman</option>
<option value="Palatino Linotype">Palatino Linotype</option>
</optgroup>
<optgroup label="Monospace / Fixed-Width">
<option value="Consolas">Consolas</option>
<option value="Courier New">Courier New</option>
<option value="Lucida Console">Lucida Console</option>
</optgroup>
<optgroup label="Fun / Decorative (Windows enthält einige)">
<option value="Comic Sans MS">Comic Sans MS</option>
<option value="Impact">Impact</option>
<option value="Segoe Script">Segoe Script</option>
<option value="Segoe Print">Segoe Print</option>
</optgroup>
</select>
<div style="display:flex;flex-direction:row;align-items:center;">
<input type="range" id="sizeSlider" min="10" max="32" value="18" style="width:100%;">
<span id="sizeValue">18px</span>
</div>
</div>
<div class="card" style="display:flex;flex:1;flex-direction:column;">
<label><b>Vollbildmodus</b></label>
<button class="bluebutton" id="fullscreenBtn">Einschalten</button>
<label>
Der Vollbildmodus hat den Vorteil, dass der komplette Bildschirm mit dieser Webseite ausgefüllt wird.<br><i style="color:var(--theme-accent-default-backcolor)">Sehr nützlich für mehr als einen Bildschirme</i>
</label>
</div>
</body>
<script>
const themeSwitch = document.getElementById("themeSwitch");
const selector = document.getElementById("fontSelector");
const slider = document.getElementById("sizeSlider");
const label = document.getElementById("sizeValue");
const fullScreenBtn = document.getElementById("fullscreenBtn");
fullScreenBtn.addEventListener("click", goFullscreen);
function goFullscreen() {
if (document.fullscreenElement) {
document.exitFullscreen();
} else {
document.documentElement.requestFullscreen();
}
fullScreenBtn.textContent = !document.fullscreenElement ? "Ausschalten" : "Einschalten";
}
if(savedTheme) { themeSwitch.value = savedTheme; }
if(savedFontFamily) { selector.value = savedFontFamily; }
if(savedFontSize) { slider.value = savedFontSize; label.textContent = savedFontSize + 'px'; }
themeSwitch.addEventListener('change', (evt) => {
const theme = evt.target.value;
switchTheme(theme);
})
function applySettings() {
switchTheme(themeSwitch.value);
setFontFamily(selector.value);
setFontSize(slider.value)
}
applySettings();
selector.addEventListener("change", () => {
setFontFamily(selector.value);
});
slider.addEventListener("input", () => {
setFontSize(parseInt(slider.value))
label.textContent = slider.value + 'px';
});
// jede option bekommt ihre eigene Schrift
[...selector.options].forEach(option => {
option.style.fontFamily = option.value;
});
</script>

View File

@@ -0,0 +1,9 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
</head>
<body>
{{{body}}}
</body>
</html>

187
public/views/login.hbs Normal file
View File

@@ -0,0 +1,187 @@
<html>
<head>
<script src="/socket.io/socket.io.js"></script>
<script src="javascript/main.js"></script>
<script src="javascript/customModal.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="styles/default.css">
<link rel="stylesheet" href="styles/contextMenu.css">
<link rel="stylesheet" href="styles/userNotification.css">
<link rel="stylesheet" href="styles/table.css">
<link rel="stylesheet" href="styles/colors.css" />
<link rel="stylesheet" href="styles/os.css" />
<div id="message-container"></div>
<div id="copy-toast"></div>
<title>Radix OS</title>
<style>
</style>
</head>
<body id="desktop">
{{!-- <form id="login-form" action="/login" method="POST"> --}}
<form id="login-form" class="container" style="width:50vw;height:100vh;align-content:center;">
<div class="static card" style="flex-direction:column">
<label for="sAMAccountName">Benutzername:</label>
<input type="text" {{!-- autocomplete="username" --}} id="sAMAccountName" name="sAMAccountName" placeholder="< Vorname.Nachname >" data-tooltip="Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>" required>
<label for="password">Passwort:</label>
<input type="password" autocomplete="current-password" id="password" name="password" required>
<br>
<button class="bluebutton" type="submit">Einloggen</button>
</div>
</form>
<button class="yellowbutton" id="openSettings" type="button" style="position:absolute;top:10px;right:10px;">Einstellungen</button>
</body>
<script src="javascript/notifyBubble.js"></script>
<script src="javascript/tableFilter.js"></script>
<script src="javascript/requiredFields.js"></script>
<script src="javascript/loadOnce.js"></script>
<script src="javascript/JSON.js"></script>
<script type="text/javascript">
const sAMAccountName = document.querySelector('#sAMAccountName');
const form = document.getElementById('login-form');
loadServerStyles(getCookie('theme'));
setCSSVariable('fontFamily', getCookie('fontFamily') ?? 'Verdana');
setCSSVariable('fontSize', getCookie('fontSize') ?? '14');
setCSSVariable('theme', getCookie('theme') ?? 'light');
form.addEventListener('submit', async e => {
try {
e.preventDefault(); // verhindert Reload
let error = null;
const data = new FormData(form);
const obj = Object.fromEntries(data.entries());
// Sende Daten via Fetch an Backend
const resSendLoginData = await fetch('/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(obj)
});
if(resSendLoginData.ok) {
const loginData = await resSendLoginData.json();
if(resSendLoginData.status === 200) {
setTimeout(() => {
window.history.pushState({ }, '', '/');
location.reload();
}, 5000);
}
if(resSendLoginData.status === 401) {
}
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${loginData.message}`, data.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
} else {
const errorData = await resSendLoginData.json();
showMessage('Login', `${dateFormat(new Date(), 'yyyy-mm-dd HH:MM:SS')}<br><br>${errorData.message}`, errorData.levelId, () => { window.history.pushState({ }, '', '/'); location.reload(); } , 5000);
}
} catch(err) {
alert(err)
}
});
sAMAccountName.addEventListener('blur', async () => {
if(sAMAccountName.value.trim() === '') {
sAMAccountName.classList.remove('valid', 'invalid');
sAMAccountName.removeAttribute('data-tooltip');
sAMAccountName.setAttribute('data-tooltip', "Melde dich mit deinem Windows-Benutzernamen an.<br>Ist dies deine erste Anmeldung, dann vergib ein neues Kennwort!<br><i style='color:green;'>Hat keine Auswirkungen auf deine Windows-Anmeldung</i>")
return;
}
const responseVerifyAccount = await fetch('/checkLoginName', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ sAMAccountName: sAMAccountName.value })
});
if(responseVerifyAccount.ok || responseVerifyAccount.status === 200) {
sAMAccountName.classList.add('valid');
sAMAccountName.setAttribute('data-tooltip', `Du kannst dich mit dem Benutzername <b style="color:green;">${sAMAccountName.value}</b> anmelden`)
} else {
sAMAccountName.classList.add('invalid');
sAMAccountName.setAttribute('data-tooltip', `Der Benutzername <b style="color:red;">${sAMAccountName.value}</b> wurde nicht gefunden`)
}
});
document.querySelector('#openSettings').onclick = async evt => {
feedbox(
{ title: `Einstellungen`,
message: /*html*/`
<div class="container">
<div class="card static">
<label>Farbgebung</label>
<select id="themeSwitch" onchange="switchTheme(this.value)">
<option value="dark" ${getCookie('theme') == 'dark' ? 'selected' : ''}>Dunkel</option>
<option value="light" ${getCookie('theme') == 'light' ? 'selected' : ''}>Hell</option>
<option value="modern" ${getCookie('theme') == 'modern' ? 'selected' : ''}>Modern</option>
</select>
</div>
<div class="card static">
<label>Schrift</label>
<select id="fontSelector" size="8" onchange="setFontFamily(this.value)">
<optgroup label="Sans-Serif (Windows Standard)">
<option style="font-family:Arial" value="Arial">Arial</option>
<option style="font-family:Calibri" value="Calibri">Calibri</option>
<option style="font-family:Candara" value="Candara">Candara</option>
<option style="font-family:Segoe UI" value="Segoe UI">Segoe UI</option>
<option style="font-family:Tahoma" value="Tahoma">Tahoma</option>
<option style="font-family:Trebuchet MS" value="Trebuchet MS">Trebuchet MS</option>
<option style="font-family:Verdana" value="Verdana">Verdana</option>
</optgroup>
<optgroup label="Serif (Windows Standard)">
<option style="font-family:Cambria" value="Cambria">Cambria</option>
<option style="font-family:Constantia" value="Constantia">Constantia</option>
<option style="font-family:Georgia" value="Georgia">Georgia</option>
<option style="font-family:Times New Roman" value="Times New Roman">Times New Roman</option>
<option style="font-family:Palatino Linotype" value="Palatino Linotype">Palatino Linotype</option>
</optgroup>
<optgroup label="Monospace / Fixed-Width">
<option style="font-family:Consolas" value="Consolas">Consolas</option>
<option style="font-family:Courier New" value="Courier New">Courier New</option>
<option style="font-family:Lucida Console" value="Lucida Console">Lucida Console</option>
</optgroup>
<optgroup label="Fun / Decorative (Windows enthält einige)">
<option style="font-family:Comic Sans MS" value="Comic Sans MS">Comic Sans MS</option>
<option style="font-family:Impact" value="Impact">Impact</option>
<option style="font-family:Segoe Script" value="Segoe Script">Segoe Script</option>
<option style="font-family:Segoe Print" value="Segoe Print">Segoe Print</option>
</optgroup>
</select>
<div style="display:flex;flex-direction:row;align-items:center">
<input type="range" id="sizeSlider" min="10" max="32" value="${savedFontSize ?? '14'}" onchange="setFontSize(parseInt(this.value)); sizeValue.textContent = this.value + 'px';">
<span id="sizeValue">${savedFontSize ?? '14'}px</span>
</div>
</div>
</div>
`,
buttons: {
yes: {
text: '<b>Fertig</b>',
onClick: async () => {}
}
}
}
);
}
</script>
</html>

View File

@@ -0,0 +1,16 @@
<div class="window" data-winid="{{name}}-{{@index}}" data-plugin="{{name}}">
<div class="window-titlebar">
<div class="title">{{{viewLabel}}}</div>
<div class="controls">
{{#if this.printable}}
<button data-tooltip="Drucken" id="printbutton">🖨</button>
{{/if}}
<button class="close">✕</button>
</div>
</div>
<div class="window-content">
{{{contentHtml}}}
</div>
<div class="window-resize-handle"></div>
</div>

View File

@@ -0,0 +1,19 @@
<div class="window" data-winid="{{appname}}-{{@index}}" data-plugin="{{appname}}">
<div class="window-titlebar">
<img src="{{#equaler section "==" "Plugin"}}{{appname}}{{/equaler}}/images/{{icon}}" class="window-icon" />
<div class="title">{{appname}} [{{label}}]</div>
<div class="controls">
{{#if tutorial}}
<button class="tutorial" id="tutorial-{{appname}}-{{label}}" onclick="tutorial.start()" data-tooltip="Startet eine interaktive Einführung">&#128161;</button>
{{/if}}
<button class="minimize" style="transform:translateY(0px)">🗕</button>
<button class="maximize">▢</button>
<button class="close">✕</button>
</div>
</div>
<div class="window-content">
{{{contentHtml}}}
</div>
<div class="window-resize-handle"></div>
</div>

View File

@@ -0,0 +1,242 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Plugin Dashboard</title>
<style>
</style>
</head>
<body>
<div class="container">
<div id="pluginTabs" class="tabs"></div>
<div class="card static">
<div class="tab-contents container static" id="pluginsTabWrapper" style="height:100vh;grid-template-columns: repeat(3, 1fr);"></div>
</div>
<div class="grid" style="pointer-events:auto;position:absolute; bottom:18px;right:18px;grid-template-columns: 1fr;">
<button class="bluebutton" id="createNewPlugin">Neues Plugin</button>
</div>
</div>
</body>
<script type="text/javascript">
createNewPlugin.onclick = () => {
feedbox({
title: 'Neues Plugin erstellen',
message: `
<input id="newPluginName" placeholder="Name des Plugins" />
`,
buttons: {
yes: {
text: '<b>Erstellen</b>',
onClick: () => {
const name = document.getElementById('newPluginName').value;
pluginAPI.create(name);
}
},
no: {
text: 'Abbrechen',
onClick: () => { /* automatisch geschlossen */ }
}
}
});
}
/*
{"name":"Demo",
"description":"Beschreibung hier einfügen",
"version":"1.0.0.0",
"menu":{
"label":"Demo",
"items":[
{"label":"Demo",
"view":"index",
"defaultSize":{"width":"600px","height":"150px"},
"icon":"app.png",
"permissions":["*"]}
]},
"config":{},
"active":true
}
}*/
fetch('/api/plugins/getAll', { method: 'POST' })
.then(res => res.json())
.then(data => {
data.forEach(async metaData => {
createTab(pluginTabs, metaData.name, async (tabElement) => {
pluginsTabWrapper.innerHTML = '';
const objectsContainer = document.createElement('div');
objectsContainer.className = 'card grid';
objectsContainer.style = ``;
const card = document.createElement('div');
card.className = 'card static row';
card.style = `gap:0 10px;min-height:fit-content;justify-content: center;`;
pluginsTabWrapper.appendChild(card);
const sortedKeys = Object.keys(metaData).sort((a, b) => {
if (typeof metaData[a] === 'object' && typeof metaData[b] !== 'object') {
return 1;
} else if (typeof metaData[a] !== 'object' && typeof metaData[b] === 'object') {
return -1;
} else {
return a.localeCompare(b);
}
});
for (const key of sortedKeys) { // each plugin-metaData
if (metaData.hasOwnProperty(key) && metaData[key] !== null && !key.includes('Path')) { // only first level
const value = metaData[key];
const container = document.createElement('div');
if(typeof value === 'string') {
container.style = `flex:${key === 'description' ? '1 1' : '0 0'} auto;`;
const label = document.createElement('label');
label.style = `font-weight:bold`;
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
container.appendChild(label);
const input = document.createElement('input');
input.type = 'text';
input.style="width:100%"
input.value = value;
input.setAttribute('data-name', metaData.name);
input.setAttribute('data-field', key);
new AttachOnBlurChange(input, async (newValue, oldValue) => {
if(key === 'name') {
await pluginAPI.rename(oldValue, newValue);
return;
}
await pluginAPI.update(`${metaData.name}`, { [key]: newValue});
});
container.appendChild(input);
card.appendChild(container);
}
if(typeof value === 'boolean') {
container.style = `flex:0;flex-direction:column; display:flex;align-items:center;`;
const label = document.createElement('label');
label.style = `font-weight:bold`;
label.textContent = key.charAt(0).toUpperCase() + key.slice(1);
container.appendChild(label);
const checkbox = document.createElement('label');
checkbox.className = 'cb cb-modern';
checkbox.innerHTML = `
<input onchange="pluginAPI.activation('${metaData.name}', this.checked);" id="plugin-${metaData.name}" data-status="${metaData.name}" type="checkbox" ${metaData.active ? 'checked' : ''}>
<span class="cb-box" aria-hidden="true"></span>
`;
container.appendChild(checkbox);
card.appendChild(container);
}
if(typeof value === 'object') {
const objectCard = document.createElement('div');
objectCard.className = 'card';
objectCard.style = `height:100%`;
objectCard.innerHTML = `
<label style="font-weight:bold">${key}</label>
<div style="overflow:auto" id="${metaData.name}-${key}"></div>
`;
objectsContainer.appendChild(objectCard);
pluginsTabWrapper.appendChild(objectsContainer);
const menu = createJsonTree({
container: document.getElementById(`${metaData.name}-${key}`),
data: metaData[key],
schema: {
"active": { type: "boolean" }
},
onChange: (data) => { },
onSave: async (data) => {
await pluginAPI.update(metaData.name, { [key]: data });
}
});
}
}
}
pluginsTabWrapper.appendChild(objectsContainer);
/*pluginsTabWrapper.innerHTML = `
<div class="card static row" style="grid-column: span 3;height:50px;">
<label class="cb cb-modern">
<label for="plugin-${plugin.name}" style="font-weight:bold">Aktiv</label>
<input onchange="pluginAPI.activation('${plugin.name}', this.checked);" id="plugin-${plugin.name}" data-status="${plugin.name}" type="checkbox" ${plugin.active ? 'checked' : ''}>
<span class="cb-box" aria-hidden="true"></span>
</label>
</div>
<div class="card static row" style="gap:0 10px;min-height:fit-content;">
<div style="flex:0 0 auto">
<label style="font-weight:bold">Name</label>
<input type="text" data-name="${plugin.name}" data-field="name" value="${plugin.name}" />
</div>
<div style="flex:0 0 auto">
<label style="font-weight:bold">Version</label>
<input type="text" data-name="${plugin.name}" data-field="version" value="${plugin.version}" />
</div>
<div style="flex:1 1 auto">
<label style="font-weight:bold">Beschreibung</label>
<input type="text" data-name="${plugin.name}" data-field="description" style="width:100%" value="${plugin.description}" />
</div>
</div>
<div class="card static" style="grid-column: span 3;">
<label style="font-weight:bold">Startmenüeinträge</label>
<div style="overflow:auto" id="${plugin.name}-menu"></div>
</div>
`;
createJsonTree({
container: document.getElementById(`${plugin.name}-menu`),
data: plugin,
schema: {
"active": { type: "boolean" }
}
});
*/
});
/*
{
"name": "TEST",
"description": "Remote Desktop Verbindungen über MSRA realisieren",
"version": "0.2025.11.07",
"config": {},
"menu": {
"label": "TEST",
"items": [
{
"label": "Remoteunterstützung",
"view": "index",
"icon": "remoteServer.png",
"permissions": [
"Administration"
]
}
]
},
"active": true
}
*/
/*
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="name"]`), async (newValue, oldValue) => { await pluginAPI.rename(`${plugin.name}`, newValue); });
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="version"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { version: newValue }))
new AttachOnBlurChange(document.querySelector(`[data-name="${plugin.name}"][data-field="description"]`), async (newValue, oldValue) => await pluginAPI.update(`${plugin.name}`, { description: newValue }))
*/
});
})
</script>
</html>