const restartHooks = [];
let tableFetchController = null
let root = document.querySelector(":root");
let copyToastTimeout;
//#region Systemwide colors
const COLORS = {
BLUE: 'rgb(100, 149, 237)',
GRAY: 'rgb(128, 128, 128)',
GREEN: 'rgb(198, 244, 180)',
YELLOW: 'rgb(255, 230, 153)',
RED: 'rgb(248, 191, 173)',
}
//#endregion
//#region Restart
function addRestartHook(fn) {
if (typeof fn === "function") restartHooks.push(fn);
}
function restart() {
restartHooks.forEach(fn => fn());
location.reload();
}
//#endregion
//#region CSS
// sets the css root variable name by value //
function setCSSVariable(name, value) {
root.style.setProperty(`--${name}`, value);
}
// gets the value of asked css variable name //
function getCSSVariable(name) {
return getComputedStyle(root).getPropertyValue(`--${name}`).trim();
}
async function loadServerStyles(activeTheme = 'dark') {
const res = await fetch('models/stylesheet.json');
const json = await res.json();
function toCssVars(obj, prefix = '', group = '') {
let vars = [];
for (const key in obj) {
const value = obj[key];
const newPrefix = prefix ? `${prefix}-${key}` : key;
if (Array.isArray(value)) {
if (value.length === 3) {
vars.push(`--${group}-${newPrefix}: rgb(${value.join(', ')});`);
} else if (value.length === 4) {
vars.push(`--${group}-${newPrefix}: rgba(${value.join(', ')});`);
}
}
else if (typeof value === 'object' && value !== null) {
vars.push(toCssVars(value, newPrefix, group));
}
else if (value !== '') {
vars.push(`--${group}-${newPrefix}: ${value};`);
}
}
return vars.join('\n');
}
let css = '';
// =========================
// 1. GLOBAL ROOT (times + base tokens)
// =========================
if (json.times) {
css += `:root {\n${toCssVars(json.times, '', 'times')}\n}\n`;
}
// =========================
// 2. THEME (selected variant)
// =========================
const themeVars = json.theme?.[activeTheme] || {};
css += `
:root {
${toCssVars(themeVars, '', 'theme')}
}
`;
// =========================
// 3. RESPONSIVE (media queries)
// =========================
const responsive = json.responsive || {};
for (const breakpoint in responsive) {
const bpVars = responsive[breakpoint];
// flatten vars
const vars = toCssVars(bpVars, '', 'responsive');
let media = '';
if (breakpoint === 'mobile') {
media = '@media (max-width: 600px)';
}
else if (breakpoint === 'tablet') {
media = '@media (min-width: 601px) and (max-width: 1024px)';
}
else if (breakpoint === 'desktop') {
media = '@media (min-width: 1025px)';
}
else {
continue; // unknown breakpoint skip
}
css += `${media} {
:root {
${vars}
}
}
`;
}
// =========================
// 4. INJECT STYLE TAG
// =========================
let styleTag = document.getElementById('theme-styles');
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.id = 'theme-styles';
document.head.appendChild(styleTag);
}
styleTag.textContent = css;
document.getElementById('start-menu-icon').src =
`../images/radix_os_${activeTheme}_img.png`;
console.log(css)
return true;
}
function switchTheme(themeName) {
setCookie('theme', themeName); // Theme direkt im Cookie speichern
savedTheme = themeName;
loadServerStyles(themeName);
}
function setFontFamily(fontFamily) {
setCookie('fontfamily', fontFamily); // FontFamily direkt im Cookie speichern
setCSSVariable('fontFamily', fontFamily)
document.documentElement.style.fontFamily = fontFamily
}
function setFontSize(fontSize) {
setCookie('fontsize', fontSize); // FontSize direkt im Cookie speichern
setCSSVariable('fontSize', fontSize + 'px')
document.documentElement.style.fontSize = fontSize + 'px'
}
//#endregion
function copyToClipboard(text) {
navigator.clipboard.writeText(text)
.then(() => {
showMessage('Kopiert!', `Text in Zwischenablage kopiert: ${text}`, levelId = -1, onclick = null, duration = 4000)
})
.catch(err => {
console.error('Copy fehlgeschlagen', err);
});
}
//#region Cookies
try {
// create or replace cookie
function setCookie(name, value) {
document.cookie=name + "=" + value + "; path=/; expires=" + (new Date(Number(new Date()) + (365 * 24 * 60 * 60 * 1000 * 10))).toGMTString();
}
// read cookie value
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
}
// remove cookie
function deleteCookie(name) {
if(getCookie(name) ) {
document.cookie = `${name}= ;expires=Thu, 01 Jan 1970 00:00:01 GMT`;
}
}
} catch( err ) {
alert(err);
}
//#endregion
let savedFontFamily = getCookie("fontfamily") || 'Arial';
let savedFontSize = getCookie("fontsize") || 14;
let savedTheme = getCookie('theme') || 'dark';
//#region Format
function formatHtml(text) {
return text?.replace(/\r?\n/g, '
').replace(/\t/g, ' ')
}
function dateFormat(date, format = null) {
if(date == null) return "";
format = (format == null ? "dd.mm.yyyy HH:MM:SS" : format);
const finish_date = new Date(date);
return format
.replace('yyyy', finish_date.getFullYear())
.replace('yy', finish_date.getFullYear().toString().slice(-2))
.replace('mm', ("0" + (finish_date.getMonth() + 1)).slice(-2))
.replace('dd', ("0" + finish_date.getDate()).slice(-2))
.replace('HH', ("0" + finish_date.getHours()).slice(-2))
.replace('MM', ("0" + finish_date.getMinutes()).slice(-2))
.replace('SS', ("0" + finish_date.getSeconds()).slice(-2));
}
function isNumeric(value) {
return typeof value === "string"
&& value.trim() !== ""
&& !Number.isNaN(Number(value));
}
function numberToCurrency(value, locale = 'de-DE', currency = 'EUR') {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency
}).format(value);
}
//#endregion
//#region Custom style sheets
async function loadCSSVars(path) {
try {
const res = await fetch(path);
const vars = await res.json();
const root = document.documentElement;
function setVars(obj, prefix = '') {
for (const [key, value] of Object.entries(obj)) {
if (typeof value === 'object') {
setVars(value, `${prefix}${key}-`);
} else {
root.style.setProperty(`--${prefix}${key}`, value);
}
}
}
setVars(vars);
} catch(err) {
alert(err)
}
}
//#endregion
//#region Dropzones
/**
* HOWTO USE:
* const dz = makeElementDropzone(tr, {
* accept: ["image/png", "image/jpeg"],
* maxFiles: 5,
* maxFileSizeMB: 5,
* onDrop: files => console.log(files)
* });
* @param {*} container
* @param {*} options
* @returns
*/
function makeElementDropzone(el, {
accept = [],
maxFiles = Infinity,
maxFileSizeMB = Infinity,
onDrop
}, allow = true) {
el.addEventListener('dragover', e => {
e.preventDefault();
el.classList.add(allow ? 'drop-hover' : 'no-drop-hover');
});
el.addEventListener('dragleave', () => {
el.classList.remove(allow ? 'drop-hover' : 'no-drop-hover');
});
el.addEventListener('drop', e => {
e.preventDefault();
if(allow) {
el.classList.remove('drop-hover');
const files = Array.from(e.dataTransfer.files);
if (!files.length) return;
const validFiles = [];
for (const file of files) {
if (accept.length && !accept.includes(file.type)) {
alert(`${file.name}: Dateityp nicht erlaubt`);
continue;
}
if (file.size > maxFileSizeMB * 1024 * 1024) {
alert(`${file.name}: Datei zu groß`);
continue;
}
validFiles.push(file);
if (validFiles.length >= maxFiles) break;
}
if (validFiles.length && typeof onDrop === 'function') {
onDrop(validFiles);
}
}
});
}
/**
* HOWTO USE:
* const dz = createDropzone(document.getElementById("dropzone"), {
* accept: ["image/png", "image/jpeg"],
* maxFiles: 5,
* maxFileSizeMB: 5,
* upload: file => fakeUpload(file), // optional
* onChange: files => console.log(files)
* });
* @param {*} container
* @param {*} options
* @returns
*/
function createDropzone(container, options = {}) {
const {
accept = [],
maxFiles = Infinity,
maxFileSizeMB = Infinity,
upload = null,
onChange = () => {}
} = options;
let files = [];
// ---------- DOM ----------
const wrapper = document.createElement('div');
wrapper.className = 'dropzone-area';
const dropzone = document.createElement("div");
dropzone.className = "dropzone";
dropzone.textContent = "Dateien hier ablegen oder klicken";
const input = document.createElement("input");
input.type = "file";
input.hidden = true;
input.multiple = true;
input.accept = accept.join(",");
const list = document.createElement("ul");
list.className = "file-list";
wrapper.append(dropzone, input, list);
container.append(wrapper);
// ---------- Helpers ----------
const isDuplicate = file =>
files.some(f => f.name === file.name && f.size === file.size);
const isValid = file => {
if (maxFiles !== undefined && files.length >= maxFiles) return "Maximale Dateianzahl erreicht";
if (maxFileSizeMB !== undefined && file.size > maxFileSizeMB * 1024 * 1024)
return `Max. ${maxFileSizeMB}MB erlaubt`;
if (accept !== undefined && accept.length && !accept.includes(file.type))
return "Dateityp nicht erlaubt";
if (isDuplicate(file)) return "Datei bereits vorhanden";
return null;
};
const addFiles = selected => {
Array.from(selected).forEach(file => {
const error = isValid(file);
if (!error) {
files.push({ file, progress: 0 });
if (upload) startUpload(file);
} else {
alert(`${file.name}: ${error}`);
}
});
render();
onChange(getFiles());
};
const removeFile = index => {
files.splice(index, 1);
render();
onChange(getFiles());
};
// ---------- Upload ----------
function startUpload(file) {
const entry = files.find(f => f.file === file);
upload(file, p => {
entry.progress = p;
render();
});
}
// ---------- Drag Reorder ----------
let dragIndex = null;
function handleDragStart(i) {
dragIndex = i;
}
function handleDrop(i) {
const [moved] = files.splice(dragIndex, 1);
files.splice(i, 0, moved);
dragIndex = null;
render();
onChange(getFiles());
}
// ---------- Render ----------
function render() {
list.innerHTML = "";
files.forEach((item, index) => {
const li = document.createElement("li");
li.draggable = true;
li.addEventListener("dragstart", () => handleDragStart(index));
li.addEventListener("dragover", e => e.preventDefault());
li.addEventListener("drop", () => handleDrop(index));
li.innerHTML = `
${item.file.name}
${upload !== null ? `` : ''}
${(item.file.size / 1024 / 1000).toFixed(2)} MB
`;
li.querySelector("button").onclick = () => removeFile(index);
list.appendChild(li);
});
}
// ---------- Events ----------
dropzone.onclick = () => input.click();
input.onchange = e => {
addFiles(e.target.files);
input.value = "";
};
dropzone.ondragover = e => {
e.preventDefault();
dropzone.classList.add("active");
};
dropzone.ondragleave = () => dropzone.classList.remove("active");
dropzone.ondrop = e => {
e.preventDefault();
dropzone.classList.remove("active");
addFiles(e.dataTransfer.files);
};
// ---------- API ----------
const getFiles = () => files.map(f => f.file);
return {
getFiles,
clear() {
files = [];
render();
onChange([]);
}
};
}
//#endregion
/* #region Multiselect textbox
const multi = new MultiSelectTextbox({
container: document.getElementById('users'),
source: [
{ id: 1, name: "Max" },
{ id: 2, name: "Anna" }
],
keyFn: u => u.id,
valueFn: u => u.name
});
// API
multi.add({ id: 3, name: "Tom" });
multi.removeByKey(1);
multi.set([
{ id: 1, name: "Max" },
{ id: 2, name: "Anna" }
]);
console.log(multi.getValues());
// Event
document.getElementById('users')
.addEventListener('change', e => console.log(e.detail));
*/
class MultiSelectTextbox {
constructor(options) {
this.container = options.container;
this.source = options.source ?? [];
this.allowNew = options.allowNew ?? true;
this.keyFn = options.keyFn ?? (item => item.key);
this.valueFn = options.valueFn ?? (item => item.value);
this.selected = options.selected ? [...options.selected] : [];
this.init();
}
/* ================= INIT ================= */
init() {
this.inputWrapper = document.createElement('div');
this.inputWrapper.className = 'mst-wrAberapper';
this.container.appendChild(this.inputWrapper);
this.input = document.createElement('input');
this.input.type = 'text';
this.input.className = 'mst-input';
this.inputWrapper.appendChild(this.input);
this.chipsContainer = document.createElement('div');
this.chipsContainer.className = 'mst-chips-container';
this.inputWrapper.appendChild(this.chipsContainer);
this.dropdown = document.createElement('div');
this.dropdown.className = 'mst-dropdown';
this.dropdown.style.display = 'none';
this.inputWrapper.appendChild(this.dropdown);
this.input.addEventListener('input', () => this.updateDropdown());
this.input.addEventListener('keydown', e => this.handleKey(e));
this.input.addEventListener('dblclick', () => this.open());
document.addEventListener('click', e => {
if (!this.container.contains(e.target)) this.close();
});
this.renderSelected();
}
/* ================= RENDER ================= */
renderSelected() {
this.chipsContainer.innerHTML = '';
this.selected.forEach(item => {
const chip = document.createElement('span');
chip.className = 'mst-chip';
chip.textContent = this.valueFn(item);
const removeBtn = document.createElement('span');
removeBtn.className = 'mst-chip-remove';
removeBtn.textContent = '×';
removeBtn.onclick = () => {
this.removeByKey(this.keyFn(item));
};
chip.appendChild(removeBtn);
this.chipsContainer.appendChild(chip);
});
this.emitChange();
}
/* ================= DROPDOWN ================= */
updateDropdown(query = this.input.value.toLowerCase()) {
query = query.toLowerCase();
const filtered = this.source.filter(item => {
const val = this.valueFn(item).toLowerCase();
return val.includes(query) &&
!this.selected.some(s => this.keyFn(s) === this.keyFn(item));
});
if (filtered.length === 0 && !this.allowNew && !query) {
this.close();
return;
}
this.dropdown.innerHTML = '';
filtered.forEach(item => {
const div = document.createElement('div');
div.className = 'mst-item';
div.textContent = this.valueFn(item);
div.onclick = () => this.add(item);
this.dropdown.appendChild(div);
});
if (this.allowNew && query && !filtered.some(f => this.valueFn(f).toLowerCase() === query)) {
const div = document.createElement('div');
div.className = 'mst-item new';
div.textContent = `Neuer Wert: "${this.input.value}"`;
div.onclick = () => this.add({ key: query, value: this.input.value });
this.dropdown.appendChild(div);
}
this.dropdown.style.display = 'block';
}
handleKey(e) {
if (e.key === 'Enter') {
e.preventDefault();
const first = this.dropdown.querySelector('.mst-item');
if (first) first.click();
}
else if (e.key === 'Backspace' && !this.input.value) {
this.selected.pop();
this.renderSelected();
}
}
open() {
this.updateDropdown('');
this.input.focus();
}
close() {
this.dropdown.style.display = 'none';
}
/* ================= PUBLIC API ================= */
getValues() {
return this.selected.map(s => ({
key: this.keyFn(s),
value: this.valueFn(s)
}));
}
add(item) {
const key = this.keyFn(item);
if (this.selected.some(s => this.keyFn(s) === key))
return;
this.selected.push(item);
this.input.value = '';
this.renderSelected();
this.close();
}
set(items) {
this.selected = [...items];
this.renderSelected();
}
removeByKey(key) {
this.selected = this.selected.filter(s => this.keyFn(s) !== key);
this.renderSelected();
}
clear() {
this.selected = [];
this.renderSelected();
}
setSource(items) {
this.source = [...items];
this.updateDropdown();
}
addSource(items) {
if (!Array.isArray(items)) items = [items];
const existing = new Set(this.source.map(i => this.keyFn(i)));
items.forEach(i => {
if (!existing.has(this.keyFn(i)))
this.source.push(i);
});
this.updateDropdown();
}
/* ================= EVENTS ================= */
emitChange() {
this.container.dispatchEvent(new CustomEvent('change', {
detail: this.getValues()
}));
}
}
//#endregion
/**
* e.g. client-side:
*
* @param {*} runtimeId
* @param {*} fn
*/
function registerWindowCleanup(runtimeId, fn) {
if (!windowCleanup.has(runtimeId)) {
windowCleanup.set(runtimeId, []);
}
windowCleanup.get(runtimeId).push(fn);
}
//#region Reload Plugin Script
function reloadPluginScript(src) {
try {
const old = document.querySelector(`script[src="${src}"]`);
if (old) old.remove();
const script = document.createElement("script");
script.src = src + "?t=" + Date.now();
script.defer = true;
document.body.appendChild(script);
} catch(err) {
alert(err);
}
}
//#endregion
//#region ToolTip
//