-Feature: Ab sofort werden alle Configdateien über eine Example.config.php definiert. über das Dashboard kann nun über den neuen Punkt "Config" die eigentliche config.php bearbeitet werden. Änderungen durch Programmupdates werden jetzt automatisch in der example.config.php definiert und beim nächsten Speichern der config-datei über die Website angepasst.

-Feature: Scrollen der Listen können einzeln abeschalten werden
This commit is contained in:
2025-10-10 10:46:33 +02:00
parent 91c0a2d9d9
commit 36944c71bf
6 changed files with 1004 additions and 336 deletions

413
dist/editconfig.php vendored Normal file
View File

@@ -0,0 +1,413 @@
<?php
declare(strict_types=1);
/**
* Config-Editor für EPI Webview
* - Liest ../config.php (falls vorhanden) und ../example.config.php
* - Zeigt editierbare Felder für alle Keys aus example.config.php
* - Zeigt veraltete (nur in config.php existierende) Keys rot & disabled
* - Speichert Werte zurück nach ../config.php
*/
error_reporting(E_ALL);
// ---- Dateipfade anpassen falls nötig ----
$CONFIG_FILE = realpath(__DIR__ . '/../config.php') ?: (__DIR__ . '/../config.php');
$EXAMPLE_FILE = realpath(__DIR__ . '/../example.config.php') ?: (__DIR__ . '/../example.config.php');
// ---- Hilfsfunktionen: Parsing ----
function parse_define_file(string $file): array {
if (!is_file($file)) return ['defines' => [], 'order' => [], 'ui' => []];
$lines = file($file, FILE_IGNORE_NEW_LINES);
$defines = [];
$order = [];
$ui = [];
// BOM entfernen, falls vorhanden
if (isset($lines[0])) {
$lines[0] = preg_replace('/^\xEF\xBB\xBF/', '', $lines[0]);
}
// Regex
$reDefine = '/^\s*define\(\s*([\'"])([^\'"]+)\1\s*,\s*(.+?)\s*\)\s*;\s*(?:(?:\/\/|#)\s*(.*))?$/u';
$reSection = '/^\s*\/\/\s*@section\s*:\s*(.+)\s*$/iu';
$reHR = '/^\s*\/\/\s*@hr\s*$/iu';
$reNote = '/^\s*\/\/\s*@note\s*:\s*(.+)\s*$/iu';
foreach ($lines as $ln) {
// Meta-Kommentare zuerst prüfen (UI-Struktur)
if (preg_match($reSection, $ln, $m)) {
$ui[] = ['kind' => 'section', 'title' => trim($m[1])];
continue;
}
if (preg_match($reHR, $ln)) {
$ui[] = ['kind' => 'hr'];
continue;
}
if (preg_match($reNote, $ln, $m)) {
$ui[] = ['kind' => 'note', 'text' => trim($m[1])];
continue;
}
// define(…) Zeilen
if (!preg_match($reDefine, $ln, $m)) {
continue;
}
$name = $m[2];
$rawValueExpr = trim($m[3]);
$inlineC = isset($m[4]) ? trim($m[4]) : '';
// Typ bestimmen
$type = 'string';
$valForForm = '';
$raw = rtrim($rawValueExpr, ',');
if (preg_match('/^(true|false)$/i', $raw)) {
$type = 'bool';
$valForForm = (strtolower($raw) === 'true');
} elseif (preg_match('/^[+-]?\d+$/', $raw)) {
$type = 'int';
$valForForm = (int)$raw;
} elseif (preg_match('/^([\'"])(.*)\1$/s', $raw, $mm)) {
$type = 'string';
$valForForm = stripcslashes($mm[2]);
} else {
$type = 'string';
$valForForm = $raw;
}
$defines[$name] = [
'name' => $name,
'type' => $type,
'value' => $valForForm,
'raw' => $rawValueExpr,
'comment' => $inlineC,
'line' => $ln,
];
$order[] = $name;
// Wichtig: define auch in die UI-Reihenfolge aufnehmen
$ui[] = ['kind' => 'define', 'name' => $name];
}
return ['defines' => $defines, 'order' => $order, 'ui' => $ui];
}
function php_export_define_value(string $type, $value): string {
// Baut den rechten Teil von define('X', <HIER>);
if ($type === 'bool') {
return $value ? 'true' : 'false';
}
if ($type === 'int') {
return (string)intval($value);
}
// String:
// einfache Quotes nehmen und escapen
$escaped = str_replace("'", "\\'", (string)$value);
return "'" . $escaped . "'";
}
function h(?string $s): string { return htmlspecialchars((string)$s ?? '', ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); }
// ---- Dateien parsen ----
$example = parse_define_file($EXAMPLE_FILE);
$current = parse_define_file($CONFIG_FILE);
// Keys klassifizieren
$exampleKeys = array_keys($example['defines']);
$currentKeys = array_keys($current['defines']);
$deprecatedKeys = array_values(array_diff($currentKeys, $exampleKeys)); // nur in config.php
$newKeys = array_values(array_diff($exampleKeys, $currentKeys)); // nur in example (neu)
$sharedKeys = array_values(array_intersect($exampleKeys, $currentKeys));
// Beim Rendern: Reihenfolge aus example beibehalten
$renderKeys = $example['order'];
// ---- POST: Speichern ----
$message = '';
$err = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['__save_config']) && isset($_POST['cfg']) && is_array($_POST['cfg'])) {
$incoming = $_POST['cfg']; // ['NAME' => valueString/checkbox etc.]
// Build neuer config.php Inhalt:
$out = [];
$out[] = "<?php";
$out[] = "// Diese Datei wurde durch den Epi Webview Config-Editor erzeugt/aktualisiert.";
$out[] = "// Bearbeite Werte bevorzugt über die Weboberfläche.";
$out[] = "";
// --- Inhalt in der Reihenfolge & Struktur der example.config.php ---
foreach ($example['ui'] as $item) {
if ($item['kind'] === 'section') {
$out[] = "// @section: " . $item['title'];
continue;
}
if ($item['kind'] === 'hr') {
$out[] = "// @hr";
continue;
}
if ($item['kind'] === 'note') {
$out[] = "// @note: " . $item['text'];
continue;
}
if ($item['kind'] !== 'define') {
continue;
}
$label = $item['name'];
$ex = $example['defines'][$label] ?? null;
if (!$ex) continue;
$type = $ex['type']; // Typ an example orientieren
$tooltip = $ex['comment'] ?? '';
// Wert aus POST übernehmen (Checkbox-Fix: existiert das Feld?)
if ($type === 'bool') {
$bool = array_key_exists($label, $incoming);
$valueForDefine = php_export_define_value('bool', $bool);
} elseif ($type === 'int') {
$raw = $incoming[$label] ?? '';
$valueForDefine = php_export_define_value('int', $raw);
} else {
$raw = $incoming[$label] ?? '';
$valueForDefine = php_export_define_value('string', $raw);
}
$line = "define('{$label}', {$valueForDefine});";
if (!empty($tooltip)) {
$line .= " // " . $tooltip; // Kommentare als Tooltip auch in config.php mitführen
}
$out[] = $line;
}
// Deprecated Block (nur in aktueller config.php vorhanden, nicht in example)
if (!empty($deprecatedKeys)) {
$out[] = "";
$out[] = "// --- Veraltete/entfernte Parameter (nur Referenz, werden nicht mehr genutzt) ---";
foreach ($deprecatedKeys as $key) {
$c = $current['defines'][$key];
$valueForDefine = php_export_define_value($c['type'], $c['value']);
$line = "// define('{$c['name']}', {$valueForDefine});";
if (!empty($c['comment'])) {
$line .= " // " . $c['comment'];
}
$out[] = $line;
}
}
$out[] = "?>";
$content = implode("\n", $out) . "\n";
try {
if (false === file_put_contents($CONFIG_FILE, $content)) {
$err = "Konnte config.php nicht schreiben: " . h($CONFIG_FILE);
} else {
$message = "Konfiguration gespeichert.";
// Nach dem Speichern neu parsen, damit UI aktuell ist
$current = parse_define_file($CONFIG_FILE);
$currentKeys = array_keys($current['defines']);
$deprecatedKeys = array_values(array_diff($currentKeys, $exampleKeys));
$sharedKeys = array_values(array_intersect($exampleKeys, $currentKeys));
}
} catch (Throwable $e) {
$err = "Fehler beim Schreiben: " . h($e->getMessage());
}
}
// ---- Werte für Formular vorbereiten ----
// Für jeden example-Key: Formularwert = aus aktueller config (falls vorhanden), sonst example default
function valueForForm(array $example, array $current, string $key) {
$ex = $example['defines'][$key] ?? null;
$cu = $current['defines'][$key] ?? null;
$type = $ex ? $ex['type'] : ($cu ? $cu['type'] : 'string');
$comment = $ex && $ex['comment'] !== '' ? $ex['comment'] : ($cu['comment'] ?? '');
if ($cu) {
$val = $cu['value'];
} else {
$val = $ex['value'];
}
return [$type, $val, $comment];
}
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="description" content="Konfiguration bearbeiten" />
<meta name="author" content="" />
<title>Konfiguration Epi Webview</title>
<link href="css/styles.css" rel="stylesheet" />
<link href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css" rel="stylesheet" crossorigin="anonymous" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.1/js/all.min.js" crossorigin="anonymous"></script>
<script src="js/jquery-3.5.1.min.js"></script>
<style>
.deprecated { color: #c62828; }
.cfg-help { margin-left: .35rem; cursor: help; }
.form-group { margin-bottom: .75rem; }
.small-muted { font-size:.85rem; color:#6c757d; }
.checkbox-inline { display:flex; align-items:center; gap:.5rem; }
.cfg-label { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
</style>
<script>
$(function(){
// Tooltips (Bootstrap 4)
$('[data-toggle="tooltip"]').tooltip();
// Side UI nachladen wie in deinem Template
loadSidenav();
loadFooter();
});
function loadSidenav(){
$('#layoutSidenav_nav').load('../sources/getSidenav.php');
}
function loadFooter(){
$('#footerholder').load('../sources/getFooter.php');
}
</script>
</head>
<body class="sb-nav-fixed">
<nav class="sb-topnav navbar navbar-expand navbar-dark bg-dark">
<a class="navbar-brand" href="index.php">Epi Webview</a>
<button class="btn btn-link btn-sm order-1 order-lg-0" id="sidebarToggle" type="button"><i class="fas fa-bars"></i></button>
<ul class="navbar-nav ml-auto ml-md-0"></ul>
</nav>
<div id="layoutSidenav">
<div id="layoutSidenav_nav"></div>
<div id="layoutSidenav_content">
<main>
<div class="container-fluid">
<h1 class="mt-4">Konfiguration</h1>
<ol class="breadcrumb mb-4">
<li class="breadcrumb-item active">Dashboard / Konfiguration</li>
</ol>
<?php if ($message): ?>
<div class="alert alert-success"><?= h($message) ?></div>
<?php endif; ?>
<?php if ($err): ?>
<div class="alert alert-danger"><?= h($err) ?></div>
<?php endif; ?>
<div class="card mb-4">
<div class="card-header">
<i class="fas fa-sliders-h mr-1"></i>
Einstellungen
<span class="small-muted">( <code><?=h(basename($CONFIG_FILE))?></code> Source: <code><?=h(basename($EXAMPLE_FILE))?></code>)</span>
</div>
<div class="card-body">
<form method="post">
<input type="hidden" name="__save_config" value="1" />
<?php foreach ($example['ui'] as $item): ?>
<?php
if ($item['kind'] === 'section') {
echo '<h5 class="mt-3">'.h($item['title']).'</h5>';
continue;
}
if ($item['kind'] === 'hr') {
echo '<hr />';
continue;
}
if ($item['kind'] === 'note') {
echo '<p class="small-muted">'.h($item['text']).'</p>';
continue;
}
if ($item['kind'] !== 'define') {
continue;
}
$key = $item['name'];
// Nur Keys rendern, die in example vorkommen
if (!isset($example['defines'][$key])) continue;
[$type, $val, $tooltip] = valueForForm($example, $current, $key);
?>
<div class="form-group">
<label class="cfg-label" for="cfg_<?=h($key)?>">
<?= h($key) ?>
<?php if ($tooltip): ?>
<i class="far fa-question-circle cfg-help"
data-toggle="tooltip" data-placement="right"
title="<?=h($tooltip)?>"></i>
<?php endif; ?>
</label>
<?php if ($type === 'bool'): ?>
<div class="checkbox-inline">
<input type="checkbox"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="1" <?= $val ? 'checked' : '' ?> />
<span><?= $val ? 'Aktiviert' : 'Deaktiviert' ?></span>
</div>
<?php elseif ($type === 'int'): ?>
<input type="number" step="1" class="form-control"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="<?=h((string)$val)?>" />
<?php else: ?>
<input type="text" class="form-control"
id="cfg_<?=h($key)?>"
name="cfg[<?=h($key)?>]"
value="<?=h((string)$val)?>" />
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (!empty($deprecatedKeys)): ?>
<hr />
<h5 class="deprecated">Veraltete Parameter (nur in <code><?=h(basename($CONFIG_FILE))?></code>, nicht mehr in <code><?=h(basename($EXAMPLE_FILE))?></code>)</h5>
<?php foreach ($deprecatedKeys as $key): ?>
<?php $c = $current['defines'][$key]; ?>
<div class="form-group">
<label class="cfg-label deprecated" for="dep_<?=h($key)?>">
<?= h($key) ?>
<?php if (!empty($c['comment'])): ?>
<i class="far fa-question-circle cfg-help" data-toggle="tooltip" data-placement="right" title="<?=h($c['comment'])?>"></i>
<?php endif; ?>
</label>
<input type="text" class="form-control is-invalid" id="dep_<?=h($key)?>" value="<?=
h($c['type']==='bool' ? ($c['value']?'true':'false') : (string)$c['value'])
?>" disabled />
<div class="invalid-feedback">Dieser Parameter ist in der aktuellen Version nicht mehr vorgesehen.</div>
</div>
<?php endforeach; ?>
<?php endif; ?>
<div class="mt-4">
<button type="submit" class="btn btn-primary"><i class="fas fa-save mr-1"></i> Speichern</button>
<span class="small-muted ml-2">Speichert nach <code><?=h($CONFIG_FILE)?></code></span>
</div>
</form>
</div>
</div>
</div>
</main>
<div id="footerholder"></div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.5.3/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
<script src="js/scripts.js"></script>
<script>
// Sidebar Toggle (falls benötigt)
$('#sidebarToggle').on('click', function(){ $('body').toggleClass('sb-sidenav-toggled'); });
</script>
</body>
</html>