Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
97c4d52c78 | ||
|
|
e926a3a40d | ||
|
|
7249aca885 |
@@ -1,26 +1,5 @@
|
||||
# TODO
|
||||
|
||||
## Voor elke versie verhoging. Deze nooit weghalen.
|
||||
- [x] Pentest controles uitgevoerd
|
||||
- [x] WCAG 2.1 AA accessibility tests
|
||||
- [x] Volledige git commit maken.
|
||||
- [x] Verslag maken voor opdrcht gever
|
||||
- [x] Na convormatie versie verhogen
|
||||
- [x] Bij Voltooid versie ophoging aanmaken en deze allemaal unvinken voor volgende ronde.
|
||||
|
||||
## Bug fixes
|
||||
- [x] admin rollen werkt niet goed, dashboard geeft een 404, de plugin's moeten aangege welke rol nodig is.
|
||||
- [x] user admin mag nooit zijn eigen rol weizigen.
|
||||
- [x] Na in loggen moet de admin zijn rol binnen de /admin kunnen veranderen om andere rollen te kunnen testen
|
||||
- [x] in een andere rol kan de gebruiker niet zijn eigen wachtwoord veranderen.
|
||||
- [x] Bij het aanmaken van een pagina in de content moet de auteur naam en email in de meta komen te staan. Deze moeten dus ook door middel van de api aan de twig bestanden doorgeven kunnen worden
|
||||
- [x] Admin taal niet volledig geïntegreerd met plugins: plugins hebben nu eigen language/ mappen (nl/en). Systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php). Fallback chain: geselecteerd → plugin default_language → CMS default → leeg. PluginManager + AdminPluginAPI + CMSAPI uitgebreid met getPluginTranslations()/t(). plugin.json settings ondersteunen label_key/help_key/option_label_key.
|
||||
- [x] Plugin uniformiteit: alle 6 plugins hebben nu een uniforme structuur (<Plugin>.php, plugin.json, README.md, assets/.gitkeep, language/nl|en/). plugins/README.md herschreven. guide plugin-development.md (NL+EN) volledig bijgewerkt met i18n + admin integratie + instellingen-schema. guide architectuur.md (NL+EN) bijgewerkt met plugin language/ map.
|
||||
- [x] Plugin editor toont alleen de <Plugin>.php maar een plugin kan uit meerdere bestanden bestaan: plugins-edit pagina heeft nu een bestandsbrowser zijbalk die alle bestanden in de plugin-map toont (.php, .json, .md, .html, .css, .scss, .js) inclusief language/ en assets/ submappen. Nieuw-bestand knop met modal. Path-traversal bescherming via realpath + prefix check + segment-guard. scanPluginFiles() helper toegevoegd. editor-toolbar.js modeMap uitgebreid voor css/scss/js/json. Vertaalstrings toegevoegd aan nl/en/de admin.php.
|
||||
- [x] Plugin editor uploaden en verwijderen: plugins-edit pagina heeft nu een Upload knop (bestanden naar assets/ van de plugin) en per-bestand verwijder-knop in de bestandsboom. Twee nieuwe routes: plugins-file-upload en plugins-file-delete. Path-traversal bescherming + protected plugins geblokkeerd + dotfiles geweigerd. Vertaalstrings toegevoegd.
|
||||
- [x] Plugin editor bestand verplaatsen: plugins-edit pagina heeft nu per-bestand een verplaats-knop in de bestandsboom. Nieuwe route plugins-file-move + template plugins-move-form.twig (dropdown met mappen binnen de plugin, plugin root als optie, huidige map uitgesloten). Path-traversal bescherming + protected plugins geblokkeerd.
|
||||
|
||||
|
||||
## Nice to have
|
||||
- [ ] Multidomein implementeren (elk domein = eigen thema + content, één admin + site name)
|
||||
- [ ] Fase 1: Config-resolutie
|
||||
@@ -45,6 +24,66 @@
|
||||
- [ ] AGENTS.md + config.json.example bijwerken
|
||||
- [ ] Verificatie: php -l, curl met Host-header, domein-switch in admin testen
|
||||
|
||||
- [x] Thema editor (thema's net zo bewerkbaar maken als plugins)
|
||||
- [x] Fase 1: Themabestanden bewerken
|
||||
- [x] Nieuwe route theme-edit + handleThemeEdit() (spiegel van handlePluginsEdit)
|
||||
- [x] scanThemeFiles() helper (geneste boom van thema-map, zoals scanPluginFiles; skipt css_compiled/)
|
||||
- [x] theme-edit.twig met bestandsbrowser zijbalk + CodeMirror editor
|
||||
- [x] Bewerkbare extensies: .twig, .json, .scss, .css, .js, .html, .md, .php
|
||||
- [x] theme.json editor (validatie op structuur: name, template mappings, config)
|
||||
- [x] Layout .twig bestanden bewerken (base.twig, full_content.twig, left_sidebar.twig, etc.)
|
||||
- [x] Partials bewerken (header, navigation, footer)
|
||||
- [x] Nieuw bestand aanmaken binnen thema-map (zoals plugin-editor)
|
||||
- [x] Bestand opslaan met CSRF + path-traversal bescherming (realpath + prefix-check)
|
||||
- [x] Fase 2: Media/assets beheer
|
||||
- [x] Uploaden naar assets/ van thema (spiegel van plugins-file-upload)
|
||||
- [x] Bestand verwijderen uit thema (spiegel van plugins-file-delete; theme.json beschermd)
|
||||
- [x] Bestand verplaatsen binnen thema (spiegel van plugins-file-move; theme.json beschermd)
|
||||
- [x] Media invoegen in editor: thema-context scant assets/ map (uitbreiding media-list endpoint met ?theme=)
|
||||
- [x] Fase 3: SCSS compileren
|
||||
- [x] SCSS compileren knop in theme-edit (runtime scssphp compile assets/scss/theme.scss → assets/css_compiled/theme.css)
|
||||
- [x] Compile-status tonen (up-to-date/verouderd badges in theme.twig + theme-edit.twig)
|
||||
- [x] Fase 4: Thema overzicht + activeren
|
||||
- [x] theme.twig: Bewerken-knop per thema (link naar theme-edit)
|
||||
- [x] Activeren per thema (active_theme in config.json via theme-activate route)
|
||||
- [x] Themaverwijdering (alleen niet-actieve, niet-default thema's via theme-delete route)
|
||||
- [x] Theme-new uitbreiden met keuze uit bestaand thema als basis (kopieer structuur, skip css_compiled/)
|
||||
- [x] Fase 5: Uniformiteit + documentatie
|
||||
- [x] Uniforme thema-structuur afdwingen (theme.json, assets/scss, assets/css, assets/js, assets/img, assets/fonts, partials, README.md) via createUniformThemeStructure()
|
||||
- [x] Per thema README.md (gegenereerd bij aanmaken via admin)
|
||||
- [x] guide/nl|en/admin-beheerder/thema-beheer.md bijgewerkt met thema-editor uitleg
|
||||
- [x] theme.json schema documentatie in guide (template mappings, config, header_color, etc.)
|
||||
- [x] Verificatie: php -l, curl testen (aanmaken, bewerken, opslaan, uploaden, verwijderen, verplaatsen, SCSS compileren, activeren, verwijderen), path-traversal tests, default/active theme bescherming
|
||||
|
||||
- [x] Content editor consistent met plugins en thema's (uniforme editor-ervaring)
|
||||
- [x] Fase 1: Bestandsbrowser zijbalk voor content
|
||||
- [x] Nieuwe route content-files / handleContentFiles() (spiegel van plugins-edit/theme-edit) naast de bestaande per-pagina content-edit
|
||||
- [x] scanContentFiles() helper (geneste boom van content_dir, zoals scanPluginFiles/scanThemeFiles; via unified scanEditorFiles(scope))
|
||||
- [x] content-files.twig met bestandsbrowser zijbalk + CodeMirror editor (herbruik de plugin/theme editor layout)
|
||||
- [x] Bewerkbare extensies: .md, .php, .html (consistent met bestaande content-edit)
|
||||
- [x] Nieuw bestand aanmaken binnen content_map (zoals plugin/theme-editor) met extensie-keuze + in_dir prefix
|
||||
- [x] Bestand opslaan met CSRF + path-traversal bescherming (realpath + prefix-check op content_dir)
|
||||
- [x] Fase 2: Media/assets beheer (uniform met plugins/thema's)
|
||||
- [x] Uploaden naar content/ (spiegel van plugins-file-upload/theme-file-upload; content-file-upload route)
|
||||
- [x] Bestand verwijderen uit content (spiegel van plugins-file-delete/theme-file-delete; content-file-delete route)
|
||||
- [x] Bestand verplaatsen binnen content (spiegel van plugins-file-move/theme-file-move; content-file-move route)
|
||||
- [x] Media invoegen in editor: content-context (media-list zonder ?plugin/?theme = content/, al aanwezig)
|
||||
- [x] Fase 3: Mappen beheer (uniform met plugins/thema's structuur)
|
||||
- [x] Nieuwe map aanmaken (content-dir-create-in route, zelfde UX als plugin/theme-editor nieuw-bestand, met in_dir)
|
||||
- [x] Map hernoemen (content-dir-rename-in route, binnen editor zijbalk)
|
||||
- [x] Map verwijderen (content-dir-delete-in route, binnen editor zijbalk, alleen lege mappen)
|
||||
- [ ] Map verplaatsen (spiegel van plugins-file-move/theme-file-move voor mappen) — optioneel, niet geïmplementeerd (mappen verplaatsen is zeldzaam; hernoemen + bestand-verplaatsen dekken de use-case)
|
||||
- [x] Fase 4: Uniformiteit + consistentie
|
||||
- [ ] Eén herbruikbare editor-macro/template voor plugin/theme/content (voorkom dupliek Twig-macro's) — uitgesteld: de macro's verschillen per scope (plugin heeft protected-badge, theme heeft theme.json-bescherming, content heeft mappen-acties); samenvoegen zou een complexe macro met conditionals opleveren die minder leesbaar is dan drie focussen macro's
|
||||
- [x] Eén herbruikbare scanFiles() helper die plugin/theme/content afhandelt (scanEditorFiles(dir, scope) + scanEditorFilesNode; scanPluginFiles/scanThemeFiles/scanContentFiles zijn dunne wrappers)
|
||||
- [ ] Eén herbruikbare fileUpload/fileDelete/fileMove handler-familie met scope-parameter (plugin/theme/content) i.p.v. drie bijna-identieke handlers per type — uitgesteld: de handlers hebben scope-specifieke logica (plugin: protected-plugins check; theme: theme.json-bescherming; content: geen specifieke bescherming) die generalisatie met een configuratie-laag vereist; de huidige per-scope handlers zijn duidelijker dan één handler met veel conditionals
|
||||
- [ ] Per content README.md of index.md (zoals plugins/thema's) — optioneel, alleen voor mappen; niet geïmplementeerd
|
||||
- [x] guide/nl|en/admin-beheerder/content-beheer.md bijwerken met content-editor uitleg (zoals plugin-development.md en thema-beheer.md)
|
||||
- [x] Fase 5: Backup/git integratie in editor
|
||||
- [x] ContentBackup (ZIP/git) acties toegankelijk vanuit de content-editor zijbalk (zoals SCSS-compile knop in theme-edit)
|
||||
- [x] git commit/status tonen in de editor wanneer content/ een git repo is (branch, dirty/clean badge, laatste commit)
|
||||
- [x] Verificatie: php -l, curl testen (aanmaken, bewerken, opslaan, uploaden, verwijderen, verplaatsen, map aanmaken/hernoemen/verwijderen), path-traversal tests (delete/new_file/upload alle geblokkeerd), pentest, WCAG
|
||||
|
||||
## Voltooid ✅
|
||||
|
||||
- [x] Admin code en niet gebruikte mappen/bestanden opschonen
|
||||
@@ -61,6 +100,22 @@
|
||||
- [x] Pentest controles uitgevoerd (30/30 tests geslaagd — 0 vulnerabilities)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25 tests geslaagd — 100% compliance, test-script verbeterd met min/max checks en grep -E)
|
||||
|
||||
## v2.6.2 (2026-08-19) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
- [x] Thema editor (thema's net zo bewerkbaar maken als plugins): bestandsbrowser zijbalk, nieuw bestand, uploaden, verwijderen, verplaatsen, SCSS compileren vanuit editor, thema activeren/verwijderen, nieuw thema met basis-kopie, uniforme thema-structuur
|
||||
- [x] Content editor (content consistent met plugins en thema's): bestandsbrowser zijbalk nu op /admin/content (vervangt tabelweergave), nieuw bestand/map, uploaden, verwijderen, verplaatsen, mappen beheer, layout/plugins selectie, git/backup integratie in editor zijbalk; oude tabelweergave verhuisd naar /admin/content-list
|
||||
- [x] Uniformiteit: scanEditorFiles() unified scanner (scanPluginFiles/scanThemeFiles/scanContentFiles als dunne wrappers)
|
||||
- [x] Media invoegen in editor: ?theme= scope voor media-list endpoint + _media-modal.twig
|
||||
- [x] Bug: thema-naam niet overgenomen bij kopiëren — title in theme.json wordt overschreven met nieuwe themanaam; README.md krijgt nieuwe header
|
||||
- [x] Bug: mappen met `_` prefix zichtbaar in frontend navigatie — scanDirectory/searchInDirectory/scanForPageTitles skippen nu `_` prefix (consistente filtering met `.` en `-`)
|
||||
- [x] Bug: images in markdown niet weergegeven — processContent() herschrijft lokale image/link URLs naar /-media/ endpoint (relatief, /content/, subdir, custom-grootte syntax); externe URLs ongewijzigd gelaten
|
||||
- [x] UX: /admin/content is nu de boom-editor (plugin-file-tree structuur); oude tabelweergave op /admin/content-list met "Boom weergave"/"Lijst weergave" knoppen
|
||||
- [x] UX: image-grootte knop in editor-toolbar (markdown) — selecteer , klik knop, breedte/hoogte dialog, {:width=... height=...} syntax toevoegen/vervangen
|
||||
- [x] Handleidingen in guide/ bijgewerkt (thema-beheer.md NL+EN volledig herschreven met thema-editor uitleg; content-beheer.md NL+EN volledig herschreven met content-editor uitleg)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.2.md)
|
||||
- [x] Versie verhoogd naar 2.6.2
|
||||
|
||||
## v2.6.1d (2026-08-18) ✅
|
||||
- [x] Pentest controles uitgevoerd (30/30)
|
||||
- [x] WCAG 2.1 AA accessibility tests (25/25)
|
||||
@@ -70,6 +125,13 @@
|
||||
- [x] Media invoegen in editor: nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include; plugin-context scant assets/ map
|
||||
- [x] Plugin overzicht knoppen: alleen iconen met title/aria-label
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt (plugin-development.md NL+EN volledig herschreven, architectuur.md NL+EN bijgewerkt)
|
||||
- [x] Bug: admin taal niet volledig geïntegreerd met plugins — plugins hebben nu eigen language/ mappen (nl/en), systeem plugins volgen admin taal (admin.php), content plugins volgen content taal (site.php), fallback chain, PluginManager + AdminPluginAPI + CMSAPI uitgebreid met getPluginTranslations()/t(), plugin.json settings ondersteunen label_key/help_key/option_label_key
|
||||
- [x] Bug: plugin uniformiteit — alle 6 plugins hebben nu een uniforme structuur (<Plugin>.php, plugin.json, README.md, assets/.gitkeep, language/nl|en/), plugins/README.md herschreven, guide plugin-development.md (NL+EN) volledig bijgewerkt, guide architectuur.md (NL+EN) bijgewerkt
|
||||
- [x] Bug: plugin editor toont alleen de <Plugin>.php maar een plugin kan uit meerdere bestanden bestaan — plugins-edit pagina heeft nu een bestandsbrowser zijbalk die alle bestanden in de plugin-map toont (.php, .json, .md, .html, .css, .scss, .js) inclusief language/ en assets/ submappen, nieuw-bestand knop met modal, path-traversal bescherming, scanPluginFiles() helper, editor-toolbar.js modeMap uitgebreid
|
||||
- [x] Bug: plugin editor uploaden en verwijderen — plugins-edit pagina heeft nu een Upload knop (bestanden naar assets/ van de plugin) en per-bestand verwijder-knop in de bestandsboom, twee nieuwe routes (plugins-file-upload, plugins-file-delete), path-traversal bescherming + protected plugins geblokkeerd + dotfiles geweigerd
|
||||
- [x] Bug: plugin editor bestand verplaatsen — plugins-edit pagina heeft nu per-bestand een verplaats-knop in de bestandsboom, nieuwe route plugins-file-move + template plugins-move-form.twig, path-traversal bescherming + protected plugins geblokkeerd
|
||||
- [x] Bug: media-knop in editor werkte niet — #mediaModal bestond niet, opgelost via nieuw /admin/media-list JSON endpoint + herbruikbare _media-modal.twig include
|
||||
- [x] Bug: plugin-kaart knoppen tekst liep niet goed — teksten verwijderd, alleen iconen met title/aria-label
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1d.md)
|
||||
- [x] Versie verhoogd naar 2.6.1d
|
||||
|
||||
@@ -79,6 +141,11 @@
|
||||
- [x] README.md en handleidingen in guide/ doorgeloken en bijgewerkt
|
||||
- [x] Admin gebruikersbeheer opnieuw ontworpen (lijst + zoeken/filter + profiel + wachtwoord)
|
||||
- [x] CLI commando voor admin wachtwoord reset (cli/reset-admin-password.php)
|
||||
- [x] Bug: admin rollen werkt niet goed, dashboard geeft een 404, de plugin's moeten aangeven welke rol nodig is
|
||||
- [x] Bug: user admin mag nooit zijn eigen rol wijzigen
|
||||
- [x] Bug: na inloggen moet de admin zijn rol binnen de /admin kunnen veranderen om andere rollen te kunnen testen
|
||||
- [x] Bug: in een andere rol kan de gebruiker niet zijn eigen wachtwoord veranderen
|
||||
- [x] Bug: bij het aanmaken van een pagina in de content moet de auteur naam en email in de meta komen te staan (door middel van de api aan de twig bestanden doorgeven)
|
||||
- [x] Verslag gemaakt (docs/release-notes/v2.6.1c.md)
|
||||
- [x] Versie verhoogd naar 2.6.1c
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ class AdminAuth
|
||||
*/
|
||||
public const ROLE_PERMISSIONS = [
|
||||
'admin' => ['*'],
|
||||
'content-manager' => ['dashboard', 'content', 'content-edit', 'content-new', 'content-delete', 'content-dir-create', 'content-dir-rename', 'content-dir-delete', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'],
|
||||
'content-manager' => ['dashboard', 'content', 'content-list', 'content-files', 'content-edit', 'content-new', 'content-delete', 'content-file-upload', 'content-file-delete', 'content-file-move', 'content-dir-create', 'content-dir-create-in', 'content-dir-rename', 'content-dir-rename-in', 'content-dir-delete', 'content-dir-delete-in', 'content-move', 'content-backup', 'content-restore', 'content-git-init', 'content-git-commit', 'content-git-restore', 'guide', 'logout'],
|
||||
'bi-manager' => ['dashboard', 'statistics', 'logs', 'guide', 'logout'],
|
||||
'site-admin' => ['dashboard', 'theme', 'theme-new', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'],
|
||||
'site-admin' => ['dashboard', 'theme', 'theme-new', 'theme-edit', 'theme-file-upload', 'theme-file-delete', 'theme-file-move', 'theme-activate', 'theme-delete', 'theme-scss', 'plugins', 'plugins-new', 'plugins-edit', 'plugins-config', 'plugins-toggle', 'plugins-delete', 'statistics', 'logs', 'update', 'guide', 'logout'],
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
var ext = textarea.dataset.ext || 'md';
|
||||
var form = document.getElementById('editor-form') || textarea.closest('form');
|
||||
|
||||
var modeMap = { md: 'markdown', html: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' };
|
||||
var modeMap = { md: 'markdown', html: 'htmlmixed', twig: 'htmlmixed', php: 'php', css: 'css', scss: 'css', js: 'javascript', json: 'javascript' };
|
||||
|
||||
var editor = CodeMirror.fromTextArea(textarea, {
|
||||
mode: modeMap[ext] || 'markdown',
|
||||
@@ -42,6 +42,7 @@
|
||||
{ cmd: 'quote', icon: 'bi-chat-quote', title: 'Citaat' },
|
||||
null,
|
||||
{ cmd: 'hr', icon: 'bi-hr', title: 'Horizontale lijn' },
|
||||
{ cmd: 'image-size', icon: 'bi-arrows-angle-expand', title: 'Afbeelding grootte' },
|
||||
null,
|
||||
{ cmd: 'media', icon: 'bi-images', title: 'Media invoegen' }
|
||||
],
|
||||
@@ -152,6 +153,43 @@
|
||||
modal.show();
|
||||
}
|
||||
break;
|
||||
case 'image-size':
|
||||
// Set width/height on a markdown image: {:width="W" height="H"}
|
||||
var selImg = editor.getSelection();
|
||||
if (!selImg) {
|
||||
// No selection — ask the user to select an image first
|
||||
alert('Selecteer eerst een afbeelding in de editor () om de grootte in te stellen.');
|
||||
editor.focus();
|
||||
break;
|
||||
}
|
||||
// Match  optionally followed by {:...}
|
||||
var imgMatch = selImg.match(/^!\[([^\]]*)\]\(([^)]+)\)(\s*\{:([^}]*)\})?/);
|
||||
if (!imgMatch) {
|
||||
alert('Selectie is geen afbeelding. Selecteer een afbeelding in markdown formaat: ');
|
||||
editor.focus();
|
||||
break;
|
||||
}
|
||||
var alt = imgMatch[1];
|
||||
var url = imgMatch[2];
|
||||
// Parse existing width/height from the {:...} block
|
||||
var existingAttrs = imgMatch[4] || '';
|
||||
var existingW = (existingAttrs.match(/width\s*=\s*"([^"]*)"/) || [])[1] || '';
|
||||
var existingH = (existingAttrs.match(/height\s*=\s*"([^"]*)"/) || [])[1] || '';
|
||||
var width = prompt('Breedte (px of %, leeg = niet instellen):', existingW);
|
||||
if (width === null) { editor.focus(); break; }
|
||||
var height = prompt('Hoogte (px of %, leeg = niet instellen):', existingH);
|
||||
if (height === null) { editor.focus(); break; }
|
||||
// Build the {:width=... height=...} suffix
|
||||
var attrs = [];
|
||||
if (width.trim() !== '') attrs.push('width="' + width.replace(/"/g, '') + '"');
|
||||
if (height.trim() !== '') attrs.push('height="' + height.replace(/"/g, '') + '"');
|
||||
var replacement = '';
|
||||
if (attrs.length > 0) {
|
||||
replacement += '{:' + attrs.join(' ') + '}';
|
||||
}
|
||||
editor.replaceSelection(replacement);
|
||||
editor.focus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,11 @@
|
||||
snippet into the active CodeMirror editor at the cursor.
|
||||
|
||||
Scope:
|
||||
- Default (content editor): fetches /admin/media-list
|
||||
- Plugin editor (mediaPluginName set): fetches /admin/media-list?plugin=<name>
|
||||
which scans plugins/<name>/assets/ instead of content/.
|
||||
- Default (content editor): fetches /admin/media-list
|
||||
- Plugin editor (mediaPluginName set): fetches /admin/media-list?plugin=<name>
|
||||
which scans plugins/<name>/assets/ instead of content/.
|
||||
- Theme editor (mediaThemeName set): fetches /admin/media-list?theme=<name>
|
||||
which scans themes/<name>/assets/ instead of content/.
|
||||
|
||||
The snippet format depends on the editor's current mode (data-ext):
|
||||
- markdown (md): 
|
||||
@@ -164,8 +166,10 @@
|
||||
function load() {
|
||||
if (loaded) { renderGrid(); return; }
|
||||
var plugin = {{ mediaPluginName|default('')|json_encode|raw }};
|
||||
var theme = {{ mediaThemeName|default('')|json_encode|raw }};
|
||||
var url = '/admin/media-list';
|
||||
if (plugin) { url += '?plugin=' + encodeURIComponent(plugin); }
|
||||
else if (theme) { url += '?theme=' + encodeURIComponent(theme); }
|
||||
fetch(url, { credentials: 'same-origin' })
|
||||
.then(function (r) {
|
||||
if (!r.ok) throw new Error('HTTP ' + r.status);
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.rename|default('Hernoemen') }}
|
||||
</button>
|
||||
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
<a href="/admin/content-list?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.rename|default('Hernoemen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.rename_folder|default('Map hernoemen') }}</h2>
|
||||
<a href="/admin/content" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/content-dir-rename-in?dir={{ dir|url_encode }}" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="dir" value="{{ dir }}">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
{{ ta.rename_prefix|default('Hernoem') }} <strong>{{ currentName }}</strong>
|
||||
<span class="text-muted">(content/{{ dir }})</span>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newname" class="form-label">{{ ta.new_name|default('Nieuwe naam') }}</label>
|
||||
<input type="text" class="form-control" id="newname" name="newname" value="{{ currentName }}" required autofocus>
|
||||
<div class="form-text">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.rename|default('Hernoemen') }}
|
||||
</button>
|
||||
<a href="/admin/content" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -71,7 +71,7 @@
|
||||
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<a href="/admin/content?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
|
||||
<a href="/admin/content-list?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary" id="back-btn">{{ ta.back|default('Terug') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,439 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.content_editor|default('Content editor') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{# Recursively render the content file tree as a nested, collapsible list. #}
|
||||
{% macro tree(nodes, relFile, editableExts, csrfToken) %}
|
||||
{% import _self as macros %}
|
||||
<ul class="plugin-tree">
|
||||
{% for n in nodes %}
|
||||
{% set isActive = n.path == relFile %}
|
||||
{% set containsActive = n.is_dir and relFile starts with (n.path ~ '/') %}
|
||||
{% set icon = n.is_dir ? 'bi-folder-fill text-warning' : (n.extension == 'md' ? 'bi-file-text text-primary' : (n.extension == 'php' ? 'bi-file-code text-success' : (n.extension == 'html' ? 'bi-file-earmark text-info' : (n.extension in ['jpg','jpeg','png','gif','webp','svg'] ? 'bi-file-image text-info' : (n.extension == 'pdf' ? 'bi-file-pdf text-danger' : 'bi-file text-muted'))))) %}
|
||||
<li>
|
||||
{% if n.is_dir %}
|
||||
{% set collapseId = 'content-tree-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
||||
<div class="tree-dir-row">
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapseId }}"
|
||||
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
||||
title="{{ n.path }}">
|
||||
<i class="bi bi-chevron-down tree-chevron"></i>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</button>
|
||||
<span class="tree-dir-actions">
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newfile-btn" title="{{ ta.new_file|default('Nieuw bestand hier') }}" aria-label="{{ ta.new_file|default('Nieuw bestand') }}" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-file-earmark-plus"></i>
|
||||
</button>
|
||||
<button type="button" class="btn btn-link btn-sm p-0 tree-newdir-btn" title="{{ ta.new_folder|default('Nieuwe map') }}" aria-label="{{ ta.new_folder|default('Nieuwe map') }}" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="{{ n.path }}">
|
||||
<i class="bi bi-folder-plus"></i>
|
||||
</button>
|
||||
<a href="/admin/content-dir-rename-in?dir={{ n.path|url_encode }}" class="tree-rename-btn" title="{{ ta.rename|default('Hernoemen') }}" aria-label="{{ ta.rename|default('Hernoemen') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/content-dir-delete-in" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_folder|default('Weet je zeker dat je deze map wilt verwijderen? De map moet leeg zijn.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="dir" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
</span>
|
||||
</div>
|
||||
<div id="{{ collapseId }}"
|
||||
class="tree-collapse collapse {{ containsActive ? 'show' : '' }}">
|
||||
{% if n.children is not empty %}
|
||||
{{ macros.tree(n.children, relFile, editableExts, csrfToken) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/admin/content?file={{ n.path|url_encode }}"
|
||||
class="tree-link {{ isActive ? 'is-active' : '' }}"
|
||||
title="{{ n.path }}">
|
||||
<span class="tree-chevron-spacer"></span>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
{% if n.extension in editableExts %}
|
||||
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
<a href="/admin/content-file-move?file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.move|default('Verplaatsen') }}" aria-label="{{ ta.move|default('Verplaatsen') }}">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/content-file-delete" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="file" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm p-0 text-danger tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
{% import _self as tree_macros %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
|
||||
<link rel="stylesheet" href="/admin/assets/css/editor.css">
|
||||
<style>
|
||||
.plugin-editor-layout { display: flex; gap: 1rem; align-items: stretch; }
|
||||
.plugin-file-tree { width: 300px; flex: 0 0 300px; }
|
||||
.plugin-editor-main { flex: 1 1 auto; min-width: 0; }
|
||||
.plugin-tree { list-style: none; padding-left: 0; margin: 0; }
|
||||
.plugin-tree ul { list-style: none; padding-left: 1.1rem; margin: 0; }
|
||||
.plugin-tree li { padding: 0; position: relative; }
|
||||
.plugin-tree li > .tree-link { display: flex; align-items: center; gap: .3rem; padding: .2rem .4rem; border-radius: .25rem; text-decoration: none; color: inherit; font-size: .9rem; line-height: 1.4; }
|
||||
.plugin-tree li > .tree-link:hover { background-color: rgba(13,110,253,.08); }
|
||||
.plugin-tree li > .tree-link.is-active { background-color: var(--bs-primary-bg-subtle, #cfe2ff); font-weight: 600; }
|
||||
.plugin-tree .tree-toggle { cursor: pointer; user-select: none; width: 100%; text-align: left; background: transparent; border: 0; color: inherit; }
|
||||
.plugin-tree .tree-chevron { transition: transform .15s ease; flex: 0 0 auto; }
|
||||
.plugin-tree .tree-chevron-spacer { display: inline-block; width: .9rem; flex: 0 0 auto; }
|
||||
.plugin-tree .btn-toggle.collapsed .tree-chevron { transform: rotate(-90deg); }
|
||||
.plugin-tree .badge-ext { font-size: .6rem; margin-left: auto; }
|
||||
.plugin-tree .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.plugin-tree .tree-delete-form { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); margin: 0; }
|
||||
.plugin-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #dc3545; background: transparent; border: 0; }
|
||||
.plugin-tree .tree-delete-btn:hover { color: #b02a37; }
|
||||
.plugin-tree .tree-move-btn { position: absolute; right: 1.75rem; top: 50%; transform: translateY(-50%); font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; }
|
||||
.plugin-tree .tree-move-btn:hover { color: #0d6efd; }
|
||||
.plugin-tree li > .tree-link { padding-right: 3.5rem; }
|
||||
.plugin-file-tree .card-body { max-height: 70vh; overflow-y: auto; }
|
||||
/* Directory action buttons (new file/dir, rename, delete) */
|
||||
.tree-dir-row { position: relative; }
|
||||
.tree-dir-row .tree-toggle { padding-right: 7rem; }
|
||||
.tree-dir-actions { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); display: flex; gap: .15rem; align-items: center; }
|
||||
.tree-dir-actions .btn-link { color: #6c757d; }
|
||||
.tree-dir-actions .btn-link:hover { color: #0d6efd; }
|
||||
.tree-dir-actions .tree-delete-btn { color: #dc3545; }
|
||||
.tree-dir-actions .tree-delete-btn:hover { color: #b02a37; }
|
||||
/* Git status badge */
|
||||
.git-status { font-size: .75rem; }
|
||||
.git-status .badge { font-weight: 400; }
|
||||
@media (max-width: 768px) {
|
||||
.plugin-editor-layout { flex-direction: column; }
|
||||
.plugin-file-tree { width: 100%; flex-basis: auto; max-height: 320px; }
|
||||
.plugin-file-tree .card-body { max-height: 260px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.content_editor|default('Content editor') }}</h2>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{# Fase 5: backup/git integratie in zijbalk header #}
|
||||
{% if gitStatus.available %}
|
||||
<a href="/admin/content-backup" class="btn btn-outline-info btn-sm" title="{{ ta.backup|default('Backup') }}">
|
||||
<i class="bi bi-archive"></i> {{ ta.backup|default('Backup') }}
|
||||
</a>
|
||||
{% if gitStatus.dirty %}
|
||||
<form method="POST" action="/admin/content-git-commit" class="d-inline" onsubmit="return confirm('{{ ta.git_commit_confirm|default('Wijzigingen committen naar git?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-outline-success btn-sm" title="{{ ta.git_commit|default('Git commit') }}">
|
||||
<i class="bi bi-git"></i> {{ ta.git_commit|default('Commit') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<form method="POST" action="/admin/content-git-init" class="d-inline" onsubmit="return confirm('{{ ta.git_init_confirm|default('Git repository initialiseren in content/?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" class="btn btn-outline-secondary btn-sm" title="{{ ta.git_init|default('Git init') }}">
|
||||
<i class="bi bi-git"></i> {{ ta.git_init|default('Git init') }}
|
||||
</button>
|
||||
</form>
|
||||
<a href="/admin/content-backup" class="btn btn-outline-info btn-sm" title="{{ ta.backup|default('Backup') }}">
|
||||
<i class="bi bi-archive"></i> {{ ta.backup|default('Backup') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#contentUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal" data-in-dir="">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="modal" data-bs-target="#newDirModal" data-in-dir="">
|
||||
<i class="bi bi-folder-plus"></i> {{ ta.new_folder|default('Nieuwe map') }}
|
||||
</button>
|
||||
<a href="/admin/content-list" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-list"></i> {{ ta.list_view|default('Lijst weergave') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Fase 5: git status badge bovenaan #}
|
||||
{% if gitStatus.available %}
|
||||
<div class="git-status mb-2 d-flex gap-2 align-items-center flex-wrap">
|
||||
<span class="badge bg-secondary"><i class="bi bi-git"></i> {{ gitStatus.branch|default('git') }}</span>
|
||||
{% if gitStatus.dirty %}
|
||||
<span class="badge bg-warning text-dark">{{ ta.git_dirty|default('niet-committed wijzigingen') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">{{ ta.git_clean|default('schone werkmap') }}</span>
|
||||
{% endif %}
|
||||
{% if gitStatus.lastCommit %}<span class="text-muted small">{{ ta.git_last_commit|default('laatste commit:') }} {{ gitStatus.lastCommit }}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Upload form (collapsed by default) #}
|
||||
<div class="collapse mb-3" id="contentUploadForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/content-file-upload" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="dir" value="" id="contentUploadDir">
|
||||
<div class="mb-3">
|
||||
<label for="contentUploadFile" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
||||
<input type="file" class="form-control" id="contentUploadFile" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,application/pdf,application/zip,video/mp4,video/webm,audio/mpeg,audio/wav,.css,.scss,.js,.json,.html,.md">
|
||||
<small class="form-text text-muted">{{ ta.content_upload_help|default('Bestanden worden geüpload naar content/. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.') }}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload_to_folder|default('Uploaden') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-editor-layout">
|
||||
{# File tree sidebar #}
|
||||
<div class="plugin-file-tree">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ ta.content_files|default('Content bestanden') }}</span>
|
||||
</div>
|
||||
<div class="card-body p-2" id="pluginFileTree">
|
||||
{% if files is empty %}
|
||||
<div class="small text-muted p-2">{{ ta.content_no_files|default('Geen bestanden gevonden.') }}</div>
|
||||
{% else %}
|
||||
{{ tree_macros.tree(files, relFile, editableExts, csrf_token) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Editor main panel #}
|
||||
<div class="plugin-editor-main">
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><i class="bi bi-folder2-open"></i> content</li>
|
||||
{% set crumbPath = '' %}
|
||||
{% set parts = relFile|split('/') %}
|
||||
{% for part in parts %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/content?file={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if isEditable %}
|
||||
<form method="POST" action="/admin/content?file={{ relFile|url_encode }}" id="editor-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
{# Metadata row: filename display, layout selector, plugins #}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted small mb-1">{{ ta.filename|default('Bestandsnaam') }}</label>
|
||||
<p class="form-control-plaintext form-control-sm mb-0"><code>{{ fileName }}</code></p>
|
||||
</div>
|
||||
{% if fileExt == 'md' %}
|
||||
<div class="col-md-4">
|
||||
<label for="layout" class="form-label">{{ ta.template_layout|default('Sjabloon / Layout') }} <small class="text-muted">({{ activeThemeName|default('default') }})</small></label>
|
||||
<select class="form-select form-select-sm" id="layout" name="layout">
|
||||
{% for key, layoutFile in themeLayouts %}
|
||||
<option value="{{ key }}" {{ currentLayout == key ? 'selected' : '' }}>{{ key|replace({'_': ' '})|capitalize }}{% if key == themeDefaultLayout %} (standaard){% endif %}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% if availablePlugins is not empty %}
|
||||
<div class="col-md-4">
|
||||
<label class="form-label d-block">{{ ta.visible_plugins|default('Zichtbare plugins') }}</label>
|
||||
<div class="d-flex flex-wrap gap-1">
|
||||
{% for plugin in availablePlugins %}
|
||||
<input type="checkbox" class="btn-check" id="plugin-{{ plugin }}" name="plugins[]" value="{{ plugin }}" autocomplete="off" {{ plugin in selectedPlugins ? 'checked' : '' }}>
|
||||
<label class="btn btn-outline-primary btn-sm" for="plugin-{{ plugin }}">{{ plugin }}</label>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if currentAuthorName or currentAuthorEmail or currentCreated %}
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-person"></i> {{ ta.author_name|default('Auteur naam') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentAuthorName }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-envelope"></i> {{ ta.author_email|default('Auteur e-mail') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentAuthorEmail }}" readonly>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<label class="form-label text-muted"><i class="bi bi-calendar"></i> {{ ta.created|default('Aangemaakt') }}</label>
|
||||
<input type="text" class="form-control-plaintext form-control-sm" value="{{ currentCreated }}" readonly>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary" title="{{ ta.save_ctrl_s|default('Opslaan (Ctrl+S)') }}">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/{{ currentLang }}{% if fileDir %}/{{ fileDir }}{% endif %}/{{ fileName|replace({'.md': '', '.php': '', '.html': ''}) }}" target="_blank" class="btn btn-outline-info" title="{{ ta.open_new_tab|default('Open in nieuw tabblad') }}">
|
||||
<i class="bi bi-eye"></i> {{ ta.preview|default('Preview') }}
|
||||
</a>
|
||||
<a href="/admin/content-list?dir={{ fileDir|url_encode }}" class="btn btn-outline-secondary">{{ ta.list_view|default('Lijst weergave') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-file-earmark-slash display-6 d-block mb-2"></i>
|
||||
{% if relFile %}
|
||||
{{ ta.content_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
||||
{% else %}
|
||||
{{ ta.content_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New file modal #}
|
||||
<div class="modal fade" id="newFileModal" tabindex="-1" aria-labelledby="newFileModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/content">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<input type="hidden" name="in_dir" id="newFileDir" value="">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newFilenameInput" class="form-label">{{ ta.content_file_path|default('Bestandspad binnen content') }}</label>
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control" id="newFilenameInput" name="new_filename" placeholder="Bijv. nl.pagina of blog/nl.post" required autofocus>
|
||||
</div>
|
||||
<div class="form-text">{{ ta.content_file_path_help|default('Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. De extensie wordt hieronder gekozen.') }}</div>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="newExtSelect" class="form-label">{{ ta.file_type|default('Bestandstype') }}</label>
|
||||
<select class="form-select" id="newExtSelect" name="new_ext">
|
||||
<option value="md">Markdown (.md)</option>
|
||||
<option value="php">PHP (.php)</option>
|
||||
<option value="html">HTML (.html)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New directory modal #}
|
||||
<div class="modal fade" id="newDirModal" tabindex="-1" aria-labelledby="newDirModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/content-dir-create-in">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="in_dir" id="newDirInDir" value="">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newDirModalLabel"><i class="bi bi-folder-plus"></i> {{ ta.new_folder_title|default('Nieuwe map aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="dirnameInput" class="form-label">{{ ta.folder_name|default('Mapnaam') }}</label>
|
||||
<input type="text" class="form-control" id="dirnameInput" name="dirname" required autofocus>
|
||||
<div class="form-text">{{ ta.name_help|default('Alleen letters, cijfers, punten, underscores en streepjes.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Pass the in-dir value from the tree button to the modal hidden fields
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
var newFileModal = document.getElementById('newFileModal');
|
||||
var newDirModal = document.getElementById('newDirModal');
|
||||
if (newFileModal) {
|
||||
newFileModal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event.relatedTarget;
|
||||
var inDir = trigger ? trigger.getAttribute('data-in-dir') : '';
|
||||
var hidden = document.getElementById('newFileDir');
|
||||
if (hidden) hidden.value = inDir || '';
|
||||
// Show the in-dir prefix as a hint in the placeholder
|
||||
var input = document.getElementById('newFilenameInput');
|
||||
if (input) {
|
||||
input.value = '';
|
||||
input.placeholder = inDir ? inDir + '/naam' : 'Bijv. nl.pagina of blog/nl.post';
|
||||
}
|
||||
});
|
||||
}
|
||||
if (newDirModal) {
|
||||
newDirModal.addEventListener('show.bs.modal', function (event) {
|
||||
var trigger = event.relatedTarget;
|
||||
var inDir = trigger ? trigger.getAttribute('data-in-dir') : '';
|
||||
var hidden = document.getElementById('newDirInDir');
|
||||
if (hidden) hidden.value = inDir;
|
||||
});
|
||||
}
|
||||
|
||||
// Layout select → update frontmatter (spiegel van content-edit.twig)
|
||||
var layoutSelect = document.getElementById('layout');
|
||||
if (layoutSelect) {
|
||||
layoutSelect.addEventListener('change', function () {
|
||||
var newLayout = this.value;
|
||||
var cm = window.codeMirrorEditor;
|
||||
var editor = document.getElementById('editor-textarea');
|
||||
var content = cm ? cm.getValue() : (editor ? editor.value : '');
|
||||
var fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
||||
if (fmMatch) {
|
||||
var frontmatter = fmMatch[1];
|
||||
if (/^layout:\s*.+$/m.test(frontmatter)) {
|
||||
frontmatter = frontmatter.replace(/^layout:\s*.+$/m, 'layout: ' + newLayout);
|
||||
} else {
|
||||
frontmatter = 'layout: ' + newLayout + '\n' + frontmatter;
|
||||
}
|
||||
content = content.replace(/^---\n([\s\S]*?)\n---/, '---\n' + frontmatter + '\n---');
|
||||
} else {
|
||||
content = '---\nlayout: ' + newLayout + '\n---\n\n' + content;
|
||||
}
|
||||
if (cm) { cm.setValue(content); } else if (editor) { editor.value = content; }
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% endblock %}
|
||||
@@ -1,21 +1,29 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{% if isDir %}{{ ta.folder|default('Map') }}{% else %}{{ ta.file|default('Bestand') }}{% endif %} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
{% block title %}{{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2 class="mb-4"><i class="bi bi-arrows-move"></i> {% if isDir %}{{ ta.folder|default('Map') }}{% else %}{{ ta.file|default('Bestand') }}{% endif %} {{ ta.move_suffix|default('verplaatsen') }}</h2>
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-arrows-move"></i> {{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }}</h2>
|
||||
<a href="/admin/content?file={{ relFile|url_encode }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" class="card shadow-sm">
|
||||
<form method="post" action="/admin/content-file-move?file={{ relFile|url_encode }}" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
{{ ta.move_prefix|default('Verplaats') }} <strong>{{ itemName }}</strong> {{ ta.move_to|default('naar:') }}
|
||||
{{ ta.move_prefix|default('Verplaats') }} <strong>{{ itemName }}</strong>
|
||||
{% if itemDir %}<span class="text-muted">(content/{{ itemDir }})</span>{% else %}<span class="text-muted">(content/)</span>{% endif %}
|
||||
{{ ta.move_to|default('naar:') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="destination" class="form-label">{{ ta.target_folder|default('Doelmap') }}</label>
|
||||
<label for="destination" class="form-label">{{ ta.target_folder|default('Doelmap') }} <small class="text-muted">(content/)</small></label>
|
||||
<select class="form-select" id="destination" name="destination" required>
|
||||
{% for directory in directories %}
|
||||
<option value="{{ directory }}">{{ directory }}</option>
|
||||
<option value="{{ directory }}">{{ directory == '' ? '(content root)' : directory }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
@@ -24,7 +32,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.move|default('Verplaatsen') }}
|
||||
</button>
|
||||
<a href="/admin/content?dir={{ itemDir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
<a href="/admin/content?file={{ relFile|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -34,7 +34,7 @@
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}
|
||||
</button>
|
||||
<a href="/admin/content?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
<a href="/admin/content-list?dir={{ dir|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-file-earmark-text"></i> {{ ta.content|default('Content') }}</h2>
|
||||
<div>
|
||||
<a href="/admin/content" class="btn btn-outline-primary btn-sm me-1" title="{{ ta.tree_view|default('Boom weergave') }}">
|
||||
<i class="bi bi-diagram-3"></i> {{ ta.tree_view|default('Boom weergave') }}
|
||||
</a>
|
||||
<button type="button" class="btn btn-outline-success btn-sm me-1" data-bs-toggle="collapse" data-bs-target="#uploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
@@ -24,7 +27,7 @@
|
||||
<div class="collapse mb-4" id="uploadForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/content?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
|
||||
<form method="POST" action="/admin/content-list?dir={{ subdir|url_encode }}" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="mb-3">
|
||||
<label for="file" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
||||
@@ -43,7 +46,7 @@
|
||||
{% set parentDir = subdir|split('/')|slice(0, -1)|join('/') %}
|
||||
<nav aria-label="breadcrumb" class="mb-3">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item"><a href="/admin/content"><i class="bi bi-house"></i></a></li>
|
||||
<li class="breadcrumb-item"><a href="/admin/content-list"><i class="bi bi-house"></i></a></li>
|
||||
{% set crumbPath = '' %}
|
||||
{% for crumb in subdir|split('/') %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ crumb : crumb %}
|
||||
@@ -51,7 +54,7 @@
|
||||
{% if crumbPath == subdir %}
|
||||
{{ crumb }}
|
||||
{% else %}
|
||||
<a href="/admin/content?dir={{ crumbPath|url_encode }}">{{ crumb }}</a>
|
||||
<a href="/admin/content-list?dir={{ crumbPath|url_encode }}">{{ crumb }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
@@ -86,7 +89,7 @@
|
||||
<tr data-name="{{ item.name|lower }}">
|
||||
<td>
|
||||
{% if item.is_dir %}
|
||||
<a href="/admin/content?dir={{ item.path|url_encode }}">
|
||||
<a href="/admin/content-list?dir={{ item.path|url_encode }}">
|
||||
<i class="bi bi-folder-fill text-warning"></i> {{ item.name }}
|
||||
</a>
|
||||
{% else %}
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.theme_edit|default('Thema bewerken: ') }}{{ themeName }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{# Recursively render the theme file tree as a nested, collapsible list.
|
||||
A folder is expanded by default when the currently selected file lives
|
||||
somewhere inside it, so the user always sees where their file is. #}
|
||||
{% macro tree(nodes, themeName, relFile, editableExts, csrfToken) %}
|
||||
{% import _self as macros %}
|
||||
<ul class="plugin-tree">
|
||||
{% for n in nodes %}
|
||||
{% set isActive = n.path == relFile %}
|
||||
{% set containsActive = n.is_dir and relFile starts with (n.path ~ '/') %}
|
||||
{% set icon = n.is_dir ? 'bi-folder-fill text-warning' : (n.extension == 'twig' ? 'bi-filetype-tfn text-info' : (n.extension == 'php' ? 'bi-file-code text-success' : (n.extension == 'json' ? 'bi-filetype-json text-secondary' : (n.extension == 'md' ? 'bi-file-text text-primary' : (n.extension in ['css','scss'] ? 'bi-filetype-css text-info' : (n.extension == 'js' ? 'bi-filetype-js text-warning' : (n.extension == 'html' ? 'bi-file-earmark text-info' : 'bi-file text-muted'))))))) %}
|
||||
<li>
|
||||
{% if n.is_dir %}
|
||||
{% set collapseId = 'theme-tree-' ~ n.path|replace({'/':'-','.':'-'}) %}
|
||||
<button type="button" class="tree-link tree-toggle btn-toggle {{ containsActive ? '' : 'collapsed' }}"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#{{ collapseId }}"
|
||||
aria-expanded="{{ containsActive ? 'true' : 'false' }}"
|
||||
title="{{ n.path }}">
|
||||
<i class="bi bi-chevron-down tree-chevron"></i>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
</button>
|
||||
<div id="{{ collapseId }}"
|
||||
class="tree-collapse collapse {{ containsActive ? 'show' : '' }}">
|
||||
{% if n.children is not empty %}
|
||||
{{ macros.tree(n.children, themeName, relFile, editableExts, csrfToken) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<a href="/admin/theme-edit?theme={{ themeName|url_encode }}&file={{ n.path|url_encode }}"
|
||||
class="tree-link {{ isActive ? 'is-active' : '' }}"
|
||||
title="{{ n.path }}">
|
||||
<span class="tree-chevron-spacer"></span>
|
||||
<i class="bi {{ icon }}"></i>
|
||||
<span class="text-truncate">{{ n.name }}</span>
|
||||
{% if n.extension in editableExts %}
|
||||
<span class="badge bg-secondary badge-ext">{{ n.extension|upper }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% if n.path != 'theme.json' %}
|
||||
<a href="/admin/theme-file-move?theme={{ themeName|url_encode }}&file={{ n.path|url_encode }}"
|
||||
class="tree-move-btn" title="{{ ta.move|default('Verplaatsen') }}" aria-label="{{ ta.move|default('Verplaatsen') }}">
|
||||
<i class="bi bi-arrows-move"></i>
|
||||
</a>
|
||||
<form method="POST" action="/admin/theme-file-delete" class="tree-delete-form" onsubmit="return confirm('{{ ta.confirm_delete_file|default('Weet je zeker dat je dit bestand wilt verwijderen?') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrfToken }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="file" value="{{ n.path }}">
|
||||
<button type="submit" class="btn btn-link btn-sm text-danger p-0 ms-1 tree-delete-btn" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endmacro %}
|
||||
{% import _self as tree_macros %}
|
||||
|
||||
{% block extra_css %}
|
||||
<link rel="stylesheet" href="/admin/assets/codemirror/codemirror.min.css">
|
||||
<link rel="stylesheet" href="/admin/assets/css/editor.css">
|
||||
<style>
|
||||
.plugin-editor-layout { display: flex; gap: 1rem; align-items: stretch; }
|
||||
.plugin-file-tree { width: 280px; flex: 0 0 280px; }
|
||||
.plugin-editor-main { flex: 1 1 auto; min-width: 0; }
|
||||
.plugin-tree { list-style: none; padding-left: 0; margin: 0; }
|
||||
.plugin-tree ul { list-style: none; padding-left: 1.1rem; margin: 0; }
|
||||
.plugin-tree li { padding: 0; position: relative; }
|
||||
.plugin-tree li > .tree-link { display: flex; align-items: center; gap: .3rem; padding: .2rem .4rem; border-radius: .25rem; text-decoration: none; color: inherit; font-size: .9rem; line-height: 1.4; }
|
||||
.plugin-tree li > .tree-link:hover { background-color: rgba(13,110,253,.08); }
|
||||
.plugin-tree li > .tree-link.is-active { background-color: var(--bs-primary-bg-subtle, #cfe2ff); font-weight: 600; }
|
||||
.plugin-tree .tree-toggle { cursor: pointer; user-select: none; width: 100%; text-align: left; background: transparent; border: 0; color: inherit; }
|
||||
.plugin-tree .tree-chevron { transition: transform .15s ease; flex: 0 0 auto; }
|
||||
.plugin-tree .tree-chevron-spacer { display: inline-block; width: .9rem; flex: 0 0 auto; }
|
||||
.plugin-tree .btn-toggle.collapsed .tree-chevron { transform: rotate(-90deg); }
|
||||
.plugin-tree .badge-ext { font-size: .6rem; margin-left: auto; }
|
||||
.plugin-tree .text-truncate { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.plugin-tree .tree-delete-form { position: absolute; right: .25rem; top: 50%; transform: translateY(-50%); margin: 0; }
|
||||
.plugin-tree .tree-delete-btn { font-size: .9rem; line-height: 1; padding: .15rem; color: #dc3545; background: transparent; border: 0; }
|
||||
.plugin-tree .tree-delete-btn:hover { color: #b02a37; }
|
||||
.plugin-tree .tree-move-btn { position: absolute; right: 1.75rem; top: 50%; transform: translateY(-50%); font-size: .9rem; line-height: 1; padding: .15rem; color: #6c757d; text-decoration: none; }
|
||||
.plugin-tree .tree-move-btn:hover { color: #0d6efd; }
|
||||
.plugin-tree li > .tree-link { padding-right: 3.5rem; }
|
||||
.plugin-file-tree .card-body { max-height: 70vh; overflow-y: auto; }
|
||||
.theme-scss-status { font-size: .75rem; }
|
||||
@media (max-width: 768px) {
|
||||
.plugin-editor-layout { flex-direction: column; }
|
||||
.plugin-file-tree { width: 100%; flex-basis: auto; max-height: 320px; }
|
||||
.plugin-file-tree .card-body { max-height: 260px; }
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-pencil"></i> {{ ta.theme_edit|default('Thema bewerken: ') }}{{ themeName }}</h2>
|
||||
<div class="d-flex gap-2 flex-wrap">
|
||||
{% if hasScss %}
|
||||
<form method="POST" action="/admin/theme-scss" class="d-inline" onsubmit="return confirm('{{ ta.compile_scss_confirm|default('SCSS compileren? Dit overschrijft assets/css_compiled/theme.css.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<button type="submit" class="btn btn-outline-success btn-sm" title="{{ ta.compile_scss|default('SCSS compileren') }}">
|
||||
<i class="bi bi-palette"></i> {{ ta.compile_scss|default('SCSS compileren') }}
|
||||
{% if scssCompiled %}<span class="badge bg-success ms-1">{{ ta.scss_ok|default('up-to-date') }}</span>{% else %}<span class="badge bg-warning text-dark ms-1">{{ ta.scss_stale|default('verouderd') }}</span>{% endif %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-success btn-sm" data-bs-toggle="collapse" data-bs-target="#themeUploadForm">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload|default('Upload') }}
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm" data-bs-toggle="modal" data-bs-target="#newFileModal">
|
||||
<i class="bi bi-file-earmark-plus"></i> {{ ta.theme_new_file|default('Nieuw bestand') }}
|
||||
</button>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Upload form (collapsed by default) #}
|
||||
<div class="collapse mb-3" id="themeUploadForm">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<form method="POST" action="/admin/theme-file-upload" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="dir" value="" id="themeUploadDir">
|
||||
<div class="mb-3">
|
||||
<label for="themeUploadFile" class="form-label">{{ ta.select_files|default('Bestanden selecteren') }}</label>
|
||||
<input type="file" class="form-control" id="themeUploadFile" name="file[]" multiple accept="image/jpeg,image/png,image/gif,image/webp,image/svg+xml,application/pdf,application/zip,video/mp4,video/webm,audio/mpeg,audio/wav,.css,.scss,.js,.json,.html,.md,.twig,.woff,.woff2,.ttf">
|
||||
<small class="form-text text-muted">{{ ta.theme_upload_help|default('Bestanden worden geüpload naar assets/ van dit thema. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts.') }}</small>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="bi bi-cloud-upload"></i> {{ ta.upload_to_folder|default('Uploaden') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="plugin-editor-layout">
|
||||
{# File tree sidebar #}
|
||||
<div class="plugin-file-tree">
|
||||
<div class="card shadow-sm h-100">
|
||||
<div class="card-header bg-white py-2 d-flex justify-content-between align-items-center">
|
||||
<span class="small text-muted fw-semibold"><i class="bi bi-folder2-open"></i> {{ ta.theme_files|default('Thema bestanden') }}</span>
|
||||
</div>
|
||||
<div class="card-body p-2" id="pluginFileTree">
|
||||
{% if files is empty %}
|
||||
<div class="small text-muted p-2">{{ ta.theme_no_files|default('Geen bestanden gevonden.') }}</div>
|
||||
{% else %}
|
||||
{{ tree_macros.tree(files, themeName, relFile, editableExts, csrf_token) }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Editor main panel #}
|
||||
<div class="plugin-editor-main">
|
||||
{% if relFile %}
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
<ol class="breadcrumb mb-0">
|
||||
<li class="breadcrumb-item"><i class="bi bi-folder2-open"></i> {{ themeName }}</li>
|
||||
{% set crumbPath = '' %}
|
||||
{% set parts = relFile|split('/') %}
|
||||
{% for part in parts %}
|
||||
{% set crumbPath = crumbPath ? crumbPath ~ '/' ~ part : part %}
|
||||
{% if loop.last %}
|
||||
<li class="breadcrumb-item active" aria-current="page">{{ part }}</li>
|
||||
{% else %}
|
||||
<li class="breadcrumb-item"><a href="/admin/theme-edit?theme={{ themeName|url_encode }}&file={{ crumbPath|url_encode }}">{{ part }}</a></li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
{% if isEditable %}
|
||||
<form method="POST" action="/admin/theme-edit?theme={{ themeName|url_encode }}&file={{ relFile|url_encode }}" id="editor-form">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="editor-toolbar" id="editor-toolbar"></div>
|
||||
<div class="editor-wrapper">
|
||||
<textarea name="content" id="editor-textarea" data-ext="{{ fileExt }}">{{ fileContent }}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="d-flex gap-2 mt-3">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.save|default('Opslaan') }}
|
||||
</button>
|
||||
<a href="/admin/theme" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% else %}
|
||||
<div class="card shadow-sm">
|
||||
<div class="card-body text-center py-5 text-muted">
|
||||
<i class="bi bi-file-earmark-slash display-6 d-block mb-2"></i>
|
||||
{% if relFile %}
|
||||
{{ ta.theme_not_editable|default('Dit bestandstype kan niet in de editor bewerkt worden.') }}
|
||||
{% else %}
|
||||
{{ ta.theme_select_file|default('Selecteer een bestand uit de zijbalk om te bewerken.') }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# New file modal #}
|
||||
<div class="modal fade" id="newFileModal" tabindex="-1" aria-labelledby="newFileModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<form method="POST" action="/admin/theme-edit?theme={{ themeName|url_encode }}">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="action" value="new_file">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="newFileModalLabel"><i class="bi bi-file-earmark-plus"></i> {{ ta.theme_new_file_title|default('Nieuw bestand aanmaken') }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="newFilenameInput" class="form-label">{{ ta.theme_file_path|default('Bestandspad binnen thema') }}</label>
|
||||
<input type="text" class="form-control" id="newFilenameInput" name="new_filename" placeholder="Bijv. partials/header.twig of assets/scss/_variables.scss" required autofocus>
|
||||
<div class="form-text">{{ ta.theme_file_path_help|default('Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. Toegestaan: twig, json, scss, css, js, html, md, php.') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ ta.cancel|default('Annuleren') }}</button>
|
||||
<button type="submit" class="btn btn-primary"><i class="bi bi-check-lg"></i> {{ ta.create|default('Aanmaken') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block extra_js %}
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,39 @@
|
||||
{% extends "layouts/admin.twig" %}
|
||||
|
||||
{% block title %}{{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }} - {{ ta.admin_title|default('CodePress Admin') }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 flex-wrap gap-2">
|
||||
<h2 class="mb-0"><i class="bi bi-arrows-move"></i> {{ ta.file|default('Bestand') }} {{ ta.move_suffix|default('verplaatsen') }}</h2>
|
||||
<a href="/admin/theme-edit?theme={{ themeName|url_encode }}&file={{ relFile|url_encode }}" class="btn btn-outline-secondary btn-sm">
|
||||
<i class="bi bi-arrow-left"></i> {{ ta.back|default('Terug') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<form method="post" action="/admin/theme-file-move?theme={{ themeName|url_encode }}&file={{ relFile|url_encode }}" class="card shadow-sm">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ themeName }}">
|
||||
<input type="hidden" name="file" value="{{ relFile }}">
|
||||
<div class="card-body">
|
||||
<div class="alert alert-info">
|
||||
{{ ta.move_prefix|default('Verplaats') }} <strong>{{ itemName }}</strong>
|
||||
{% if itemDir %}<span class="text-muted">({{ themeName }}/{{ itemDir }})</span>{% else %}<span class="text-muted">({{ themeName }}/)</span>{% endif %}
|
||||
{{ ta.move_to|default('naar:') }}
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="destination" class="form-label">{{ ta.target_folder|default('Doelmap') }} <small class="text-muted">({{ themeName }}/)</small></label>
|
||||
<select class="form-select" id="destination" name="destination" required>
|
||||
{% for directory in directories %}
|
||||
<option value="{{ directory }}">{{ directory == '' ? '(thema root)' : directory }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.move|default('Verplaatsen') }}
|
||||
</button>
|
||||
<a href="/admin/theme-edit?theme={{ themeName|url_encode }}&file={{ relFile|url_encode }}" class="btn btn-outline-secondary">{{ ta.cancel|default('Annuleren') }}</a>
|
||||
</div>
|
||||
</form>
|
||||
{% endblock %}
|
||||
@@ -13,6 +13,16 @@
|
||||
<input type="text" class="form-control" id="name" name="name" required autofocus>
|
||||
<small class="form-text text-muted">{{ ta.theme_name_help|default('Alleen letters, cijfers, underscores en streepjes.') }}</small>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label for="base_theme" class="form-label">{{ ta.theme_base|default('Basis thema') }}</label>
|
||||
<select class="form-select" id="base_theme" name="base_theme">
|
||||
<option value="">{{ ta.theme_base_blank|default('(lege structuur)') }}</option>
|
||||
{% for name in base_themes %}
|
||||
<option value="{{ name }}">{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<small class="form-text text-muted">{{ ta.theme_base_help|default('Kies een bestaand thema als basis om te kopiëren, of start met een lege uniforme structuur. css_compiled/ wordt niet gekopieerd.') }}</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
|
||||
@@ -5,17 +5,9 @@
|
||||
{% block content %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-4">
|
||||
<h2><i class="bi bi-palette"></i> {{ ta.themes|default('Thema\'s') }}</h2>
|
||||
<div>
|
||||
<form method="POST" action="/admin/theme" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<button type="submit" name="activate_default" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.activate_default|default('Activeer Default') }}
|
||||
</button>
|
||||
</form>
|
||||
<a href="/admin/theme-new" class="btn btn-outline-primary btn-sm ms-2">
|
||||
<i class="bi bi-plus-lg"></i> {{ ta.new_theme|default('Nieuw thema') }}
|
||||
</a>
|
||||
</div>
|
||||
<a href="/admin/theme-new" class="btn btn-primary btn-sm">
|
||||
<i class="bi bi-plus-lg"></i> {{ ta.new_theme|default('Nieuw thema') }}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="row g-4">
|
||||
@@ -23,35 +15,68 @@
|
||||
<div class="col-md-6 col-lg-4">
|
||||
<div class="card shadow-sm h-100 {{ theme.active ? 'border-primary border-2' : '' }}">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<span class="fw-bold">{{ theme.title|default(theme.name) }}</span>
|
||||
{% if theme.active %}
|
||||
<span class="badge bg-success">{{ ta.active|default('Actief') }}</span>
|
||||
{% endif %}
|
||||
<span class="fw-bold">
|
||||
{% if theme.protected %}<i class="bi bi-shield-check text-primary" title="{{ ta.theme_default_protected|default('Default thema - kan niet verwijderd worden') }}"></i>{% endif %}
|
||||
{{ theme.title|default(theme.name) }}
|
||||
</span>
|
||||
<div class="d-flex gap-1 flex-wrap">
|
||||
{% if theme.active %}
|
||||
<span class="badge bg-success">{{ ta.active|default('Actief') }}</span>
|
||||
{% endif %}
|
||||
{% if theme.has_scss %}
|
||||
{% if theme.scss_compiled %}
|
||||
<span class="badge bg-success" title="{{ ta.scss_compiled_ok|default('SCSS is gecompileerd en up-to-date') }}">{{ ta.scss_ok|default('SCSS ok') }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning text-dark" title="{{ ta.scss_needs_compile|default('SCSS source nieuwer dan gecompileerde CSS') }}">{{ ta.scss_stale|default('SCSS verouderd') }}</span>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small">{{ ta.name_label|default('Naam: ') }}{{ theme.name }}</p>
|
||||
{% if theme.default_layout %}
|
||||
<p class="text-muted small">{{ ta.default_layout_label|default('Default layout: ') }}{{ theme.default_layout }}</p>
|
||||
<p class="text-muted small mb-1">{{ ta.name_label|default('Naam: ') }}<code>{{ theme.name }}</code></p>
|
||||
{% if theme.config.default_template %}
|
||||
<p class="text-muted small mb-1">{{ ta.default_layout_label|default('Default layout: ') }}{{ theme.config.default_template }}</p>
|
||||
{% endif %}
|
||||
{% if theme.scss_mtime %}
|
||||
<p class="text-muted small mb-1"><i class="bi bi-clock"></i> SCSS: {{ theme.scss_mtime }}{% if theme.css_mtime %} → CSS: {{ theme.css_mtime }}{% endif %}</p>
|
||||
{% endif %}
|
||||
{% if theme.template %}
|
||||
<p class="text-muted small mb-0">{{ ta.theme_layouts|default('Layouts: ') }}{{ theme.template|keys|join(', ') }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card-footer bg-white">
|
||||
{% if not theme.active %}
|
||||
<form method="POST" action="/admin/theme" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="activate" value="{{ theme.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary">
|
||||
<i class="bi bi-check-lg"></i> {{ ta.activate|default('Activeren') }}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="/admin/theme" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="compile_scss" value="1">
|
||||
<input type="hidden" name="theme" value="{{ theme.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success">
|
||||
<i class="bi bi-palette"></i> {{ ta.compile_scss|default('SCSS compileren') }}
|
||||
</button>
|
||||
</form>
|
||||
<div class="btn-group w-100" role="group" aria-label="{{ ta.theme_actions|default('Thema acties') }}">
|
||||
<a href="/admin/theme-edit?theme={{ theme.name|url_encode }}" class="btn btn-sm btn-outline-primary" title="{{ ta.edit|default('Bewerken') }}" aria-label="{{ ta.edit|default('Bewerken') }}">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</a>
|
||||
{% if theme.has_scss %}
|
||||
<form method="POST" action="/admin/theme-scss" class="d-inline" onsubmit="return confirm('{{ ta.compile_scss_confirm|default('SCSS compileren? Dit overschrijft assets/css_compiled/theme.css.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ theme.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-success" title="{{ ta.compile_scss|default('SCSS compileren') }}" aria-label="{{ ta.compile_scss|default('SCSS compileren') }}">
|
||||
<i class="bi bi-palette"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not theme.active %}
|
||||
<form method="POST" action="/admin/theme-activate" class="d-inline">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ theme.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-primary" title="{{ ta.activate|default('Activeren') }}" aria-label="{{ ta.activate|default('Activeren') }}">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
{% if not theme.protected and not theme.active %}
|
||||
<form method="POST" action="/admin/theme-delete" class="d-inline" onsubmit="return confirm('{{ ta.confirm_delete_theme|default('Weet je zeker dat je dit thema wilt verwijderen? Alle bestanden in het thema gaan verloren.') }}')">
|
||||
<input type="hidden" name="csrf_token" value="{{ csrf_token }}">
|
||||
<input type="hidden" name="theme" value="{{ theme.name }}">
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" title="{{ ta.delete|default('Verwijderen') }}" aria-label="{{ ta.delete|default('Verwijderen') }}">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -330,7 +330,8 @@ class CodePressCMS {
|
||||
$result = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.' || $item[0] === '-') continue;
|
||||
// Skip hidden/system entries: dotfiles, -assets, _drafts, etc.
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
|
||||
// Skip assets directory (old name, kept for safety)
|
||||
if ($item === 'assets' && is_dir($dir . '/' . $item)) continue;
|
||||
@@ -397,7 +398,8 @@ class CodePressCMS {
|
||||
$items = scandir($dir);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
// Skip hidden/system entries (consistent met scanDirectory)
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
@@ -886,7 +888,8 @@ class CodePressCMS {
|
||||
sort($items);
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[0] === '.') continue;
|
||||
// Skip hidden/system entries (consistent met scanDirectory)
|
||||
if ($item[0] === '.' || $item[0] === '-' || $item[0] === '_') continue;
|
||||
|
||||
$path = $dir . '/' . $item;
|
||||
$relativePath = $prefix ? $prefix . '/' . $item : $item;
|
||||
@@ -1802,6 +1805,37 @@ class CodePressCMS {
|
||||
|
||||
private function processContent(string $content): string
|
||||
{
|
||||
return str_replace('-/assets/', '/-assets/', $content);
|
||||
// Legacy rewrite for the old -/assets/ shortcut
|
||||
$content = str_replace('-/assets/', '/-assets/', $content);
|
||||
|
||||
// Rewrite <img src="..."> and <a href="..."> that point to files in the
|
||||
// content directory so they are served via the /-media/ endpoint. The
|
||||
// content/ directory lives outside the webroot, so raw URLs like
|
||||
// "test.svg", "/content/test.svg" or "sub/test.svg" would otherwise 404.
|
||||
//
|
||||
// Skipped (left untouched):
|
||||
// - absolute URLs (http://, https://, //)
|
||||
// - already rewritten /-media/ and /-assets/ URLs
|
||||
// - theme/plugin asset URLs (/themes/..., /plugins/..., /admin/...)
|
||||
// - data: and mailto: URLs
|
||||
// - fragment-only URLs (#anchor)
|
||||
$content = preg_replace_callback(
|
||||
'~(<(?:img|a)\b[^>]*\b(?:src|href)\s*=\s*")([^"]+)(")~i',
|
||||
function ($m) {
|
||||
$url = $m[2];
|
||||
// Leave absolute, already-rewritten, asset, data: and anchor URLs alone
|
||||
if (preg_match('~^(?:[a-z][a-z0-9+.\-]*:|//|/themes/|/plugins/|/admin/|/-media/|/-assets/|data:|mailto:)~i', $url)) {
|
||||
return $m[0];
|
||||
}
|
||||
// Strip a leading /content/ prefix if present, then prefix with /-media/
|
||||
$clean = preg_replace('~^/content/~', '', $url);
|
||||
// Strip a leading slash so it becomes a clean /-media/<path>
|
||||
$clean = ltrim($clean, '/');
|
||||
return $m[1] . '/-media/' . $clean . $m[3];
|
||||
},
|
||||
$content
|
||||
);
|
||||
|
||||
return $content;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
# CodePress CMS v2.6.2 — Release Notes
|
||||
|
||||
**Release datum:** 2026-08-19
|
||||
**Codename:** Lyra
|
||||
**Status:** stable
|
||||
|
||||
---
|
||||
|
||||
## Samenvatting
|
||||
|
||||
Deze release brengt de **thema-editor** en de **content-editor** op hetzelfde niveau als de plugin-editor. Thema's en content zijn nu volledig bewerkbaar in de admin met een bestandsbrowser zijbalk, uploaden/verwijderen/verplaatsen, en SCSS-compile (thema's) of git/backup-integratie (content). Daarnaast is de bestandsscanner geünificeerd tot één herbruikbare `scanEditorFiles()` functie.
|
||||
|
||||
Beide grote features zijn geïmplementeerd als spiegel van de plugin-editor (`/admin/plugins-edit`), zodat de editor-ervaring consistent is voor plugins, thema's en content.
|
||||
|
||||
---
|
||||
|
||||
## Nieuwe features
|
||||
|
||||
### 1. Thema editor (thema's net zo bewerkbaar maken als plugins)
|
||||
|
||||
De thema-beheer pagina toonde alleen thema-kaarten met activeren/compileer knoppen. Nu is elk thema volledig bewerkbaar via `/admin/theme-edit`.
|
||||
|
||||
#### Bestandsbrowser zijbalk
|
||||
- Geneste, inklapbare boomstructuur van alle bestanden in de thema-map (`.twig`, `.json`, `.scss`, `.css`, `.js`, `.html`, `.md`, `.php`) inclusief `partials/` en `assets/` submappen
|
||||
- `assets/css_compiled/` (runtime artefact, read-only) wordt verborgen in de boom
|
||||
- `scanThemeFiles()` helper — recursieve boom-opbouw met path-traversal bescherming; skipt `css_compiled/`
|
||||
- Mappen die het actieve bestand bevatten worden automatisch uitgeklapt
|
||||
- Iconen per bestandstype (TWIG, PHP, JSON, MD, CSS, JS, HTML, mappen)
|
||||
|
||||
#### Nieuw bestand aanmaken
|
||||
- "Nieuw bestand" knop + modal met pad-invoer (bijv. `partials/header.twig` of `assets/scss/_variables.scss`)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Stub-content per extensie (`{% extends 'base.twig' %}` voor TWIG, `{\n}\n` voor JSON, enz.)
|
||||
- Path-traversal bescherming: `../`/`..\\`/`./` stripping + `..`/`.` segment-guard + realpath prefix-check
|
||||
|
||||
#### Uploaden / Verwijderen / Verplaatsen
|
||||
- Upload-knop + inklapbaar upload-formulier dat bestanden opslaat in `themes/<naam>/assets/`
|
||||
- Toegestane extensies: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts
|
||||
- Per-bestand prullenbak-icoon en verplaats-icoon in de bestandsboom
|
||||
- `theme.json` is beschermd: kan bewerkt maar niet verwijderd of verplaatst worden
|
||||
|
||||
#### SCSS compileren vanuit de editor
|
||||
- "SCSS compileren" knop bovenaan de editor (alleen als `assets/scss/theme.scss` bestaat)
|
||||
- Toont compile-status: **up-to-date** (groen) of **verouderd** (geel)
|
||||
- Forceert compilatie via `ThemeManager::compileCss(true)`
|
||||
|
||||
#### Thema overzicht + activeren + verwijderen
|
||||
- `theme.twig` vernieuwd: bewerken/activeer/verwijder/compileer knoppen per thema-kaart
|
||||
- SCSS-status badges per thema (up-to-date / verouderd)
|
||||
- **Activeren**: zet `active_theme` in `config.json` (nieuwe route `theme-activate`)
|
||||
- **Verwijderen**: alleen niet-actieve, niet-default thema's (nieuwe route `theme-delete`); default thema beschermd
|
||||
- **Nieuw thema met basis-kopie**: kies een bestaand thema als basis bij aanmaken; `css_compiled/` wordt niet gekopieerd; de `title` in `theme.json` wordt overschreven met de nieuwe themanaam; `README.md` krijgt een nieuwe header
|
||||
|
||||
#### Uniforme thema-structuur
|
||||
Nieuwe thema's die via de admin worden aangemaakt krijgen automatisch een uniforme structuur:
|
||||
- `theme.json`, `base.twig`, `full_content.twig`, `partials/header.twig`, `partials/footer.twig`
|
||||
- `assets/scss/theme.scss` (starter SCSS), `assets/css/`, `assets/js/`, `assets/img/`, `assets/fonts/`
|
||||
- `README.md` per thema
|
||||
|
||||
#### Media invoegen in editor
|
||||
- De media-modal ondersteunt nu ook thema-context: `/admin/media-list?theme=<naam>` scant `themes/<naam>/assets/`
|
||||
- `_media-modal.twig` JS uitgebreid met `mediaThemeName` scope
|
||||
|
||||
### 2. Content editor (content consistent met plugins en thema's)
|
||||
|
||||
De content-beheer pagina (`/admin/content`) was een klassieke tabelweergave. Nu is er een额外的 content-editor `/admin/content-files` met dezelfde bestandsbrowser-zijbalk-ervaring als de plugin- en thema-editors. Beide weergaven werken naast elkaar en gebruiken dezelfde content-map.
|
||||
|
||||
#### Bestandsbrowser zijbalk
|
||||
- Geneste, inklapbare boomstructuur van alle bestanden in `content/`
|
||||
- `scanContentFiles()` helper — recursieve boom-opbouw met path-traversal bescherming; skipt `.bak/`, `.git/`, dotfiles
|
||||
- Bewerkbare extensies: `.md`, `.php`, `.html` (consistent met bestaande `content-edit`)
|
||||
- Per-map actie-knoppen: nieuw bestand, nieuwe map, hernoemen, verwijderen
|
||||
|
||||
#### Nieuw bestand / map aanmaken
|
||||
- "Nieuw bestand" knop + modal met pad-invoer en extensie-keuze (Markdown/PHP/HTML)
|
||||
- `in_dir` prefix ondersteuning: bestand kan direct in een submap worden aangemaakt vanuit de zijbalk
|
||||
- Frontmatter met `layout`, `author_name`, `author_email`, `created` wordt automatisch gegenereerd
|
||||
- "Nieuwe map" knop + modal; submappen worden automatisch aangemaakt
|
||||
|
||||
#### Uploaden / Verwijderen / Verplaatsen
|
||||
- Upload-knop + inklapbaar upload-formulier dat bestanden opslaat in `content/` (of een submap)
|
||||
- Toegestane extensies: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Per-bestand prullenbak-icoon en verplaats-icoon in de bestandsboom
|
||||
- Nieuwe routes: `content-file-upload`, `content-file-delete`, `content-file-move`
|
||||
|
||||
#### Mappen beheer
|
||||
- **Nieuwe map** (`content-dir-create-in`): per map in de zijbalk of bovenaan
|
||||
- **Map hernoemen** (`content-dir-rename-in`): potlood-icoon per map
|
||||
- **Map verwijderen** (`content-dir-delete-in`): prullenbak-icoon per map (alleen lege mappen; verborgen `.bak` bestanden genegeerd)
|
||||
|
||||
#### Layout selectie + plugins + frontmatter
|
||||
- Layout dropdown uit `theme.json` template mapping (zoals bestaande `content-edit`)
|
||||
- Plugin checkbox-groep voor `plugins:` frontmatter key
|
||||
- Author metadata, `created` datum in frontmatter
|
||||
- Live frontmatter-update bij layout/plugin wijziging (spiegel van `content-edit.twig` JS)
|
||||
|
||||
#### Git / Backup integratie (Fase 5)
|
||||
Bovenaan de content-editor staan backup- en git-acties:
|
||||
- **Git init**: initialiseert een git repository in `content/` (als er nog geen is)
|
||||
- **Commit**: committed alle niet-committed wijzigingen (alleen als er een git repo is en er wijzigingen zijn)
|
||||
- **Backup**: link naar de backup-pagina (`/admin/content-backup`) voor ZIP backup/restore
|
||||
- Git status badge toont de huidige branch, of er niet-committed wijzigingen zijn (dirty/clean), en de laatste commit
|
||||
|
||||
### 3. Uniformiteit: één scanEditorFiles() voor alle editors
|
||||
|
||||
De drie nagenoeg identieke scan-functies (`scanPluginFiles`, `scanThemeFiles`, `scanContentFiles`) zijn geünificeerd tot één herbruikbare functie:
|
||||
|
||||
- `scanEditorFiles(string $dir, string $scope = 'plugin'): array` — hoofd-functie
|
||||
- `scanEditorFilesNode(string $absDir, string $relPath, string $realBase, array $skipPaths = []): array` — recursieve helper
|
||||
- Scope-specifieke skips via `$skipPaths` map (bijv. `['assets/css_compiled' => true]` voor themes)
|
||||
- `scanPluginFiles`/`scanThemeFiles`/`scanContentFiles` behouden als dunne wrappers voor backwards-compatibiliteit
|
||||
|
||||
De file-upload/delete/move handler-families zijn per-scope behouden (plugin: protected-plugins check; theme: theme.json-bescherming; content: geen specifieke bescherming) — generalisatie zou een complexe configuratie-laag vereisen die de leesbaarheid niet verbetert.
|
||||
|
||||
---
|
||||
|
||||
## Bug fixes
|
||||
|
||||
- **Thema-naam niet overgenomen bij kopiëren** — bij het aanmaken van een nieuw thema met een basisthema werd de `title` uit het basisthema's `theme.json` gekopieerd; nu wordt de `title` overschreven met de nieuwe themanaam en krijgt `README.md` een nieuwe header
|
||||
- **Mappen met `_` prefix zichtbaar in frontend navigatie** — mappen zoals `_drafts`, `_data`, `_images` werden in het menu getoond omdat `scanDirectory()` alleen `.` en `-` prefixen oversloeg. Nu worden ook `_`-prefix mappen overgeslagen in `scanDirectory()`, `searchInDirectory()` en `scanForPageTitles()` (consistente filtering)
|
||||
- **Images in markdown niet weergegeven** — `` werd gerenderd als `<img src="test.svg">` (relatieve URL → 404) of `` als `<img src="/content/test.svg">` (content/ staat buiten webroot → 404). `processContent()` herschrijft nu lokale image/link URLs naar de `/-media/` endpoint die bestanden uit `content/` serveert met juiste MIME-type. Externe URLs (`http(s)://`), `/-media/`, `/-assets/`, `/themes/`, `/plugins/`, `/admin/`, `data:` en `mailto:` worden ongewijzigd gelaten. Ook de custom-grootte syntax `{:width=... height=...}` wordt correct herschreven.
|
||||
|
||||
## UX verbeteringen
|
||||
|
||||
- **`/admin/content` is nu de boom-editor** — de hoofdroute `/admin/content` toont nu de bestandsbrowser zijbalk structuur (zoals `plugin-file-tree`), consistent met de plugin- en thema-editors. De oude tabelweergave is verhuisd naar `/admin/content-list` en is bereikbaar via de "Lijst weergave" knop. Een "Boom weergave" knop in de tabelweergave keert terug naar de boom-editor. De `content-files` route redirect naar `/admin/content` (backward compat).
|
||||
- **Image-grootte knop in editor-toolbar** — nieuwe knop "Afbeelding grootte" (expand-icoon) in de markdown editor-toolbar. Selecteer een `` image in de editor en klik op de knop om een breedte/hoogte-dialog te krijgen. De `{:width=... height=...}` syntax wordt aan de image toegevoegd of vervangen. Bestaande waarden worden getoond in de prompts.
|
||||
|
||||
---
|
||||
|
||||
## Technische details
|
||||
|
||||
### Nieuwe routes
|
||||
- `theme-edit` — thema bestandsbrowser editor
|
||||
- `theme-file-upload` — bestanden uploaden naar thema assets/
|
||||
- `theme-file-delete` — bestand verwijderen uit thema (theme.json beschermd)
|
||||
- `theme-file-move` — bestand verplaatsen binnen thema (theme.json beschermd)
|
||||
- `theme-activate` — thema activeren (active_theme in config.json)
|
||||
- `theme-delete` — thema verwijderen (alleen niet-actief, niet-default)
|
||||
- `theme-scss` — SCSS compileren voor thema
|
||||
- `content-files` — content bestandsbrowser editor
|
||||
- `content-file-upload` — bestanden uploaden naar content/
|
||||
- `content-file-delete` — bestand verwijderen uit content
|
||||
- `content-file-move` — bestand verplaatsen binnen content
|
||||
- `content-dir-create-in` — map aanmaken in content vanuit editor
|
||||
- `content-dir-rename-in` — map hernoemen in content vanuit editor
|
||||
- `content-dir-delete-in` — map verwijderen in content vanuit editor (alleen leeg)
|
||||
|
||||
### Nieuwe bestanden
|
||||
- `admin/theme/default/views/pages/theme-edit.twig` — thema editor met bestandsbrowser
|
||||
- `admin/theme/default/views/pages/theme-move-form.twig` — thema bestand verplaatsen
|
||||
- `admin/theme/default/views/pages/content-files.twig` — content editor met bestandsbrowser
|
||||
- `admin/theme/default/views/pages/content-move-form.twig` — content bestand verplaatsen
|
||||
- `admin/theme/default/views/pages/content-dir-rename-form.twig` — content map hernoemen
|
||||
- `docs/release-notes/v2.6.2.md` — dit release-verslag
|
||||
|
||||
### Gewijzigde bestanden (core)
|
||||
- `public/admin.php` — nieuwe routes + handlers (handleThemeEdit/FileUpload/Delete/Move/Activate/Delete/Scss, handleContentFiles/FileUpload/Delete/Move/DirCreateIn/DirRenameIn/DirDeleteIn, createUniformThemeStructure, copyDirRecursive, removeDirRecursive, scanEditorFiles + scanEditorFilesNode unified), handleTheme/handleThemeNew herschreven, handleMediaList uitgebreid met `?theme=` scope
|
||||
- `admin/src/AdminAuth.php` — site-admin rechten voor nieuwe theme routes; content-manager rechten voor nieuwe content routes
|
||||
- `admin/theme/default/views/pages/theme.twig` — bewerk/activeer/verwijder/compileer knoppen + SCSS-status badges
|
||||
- `admin/theme/default/views/pages/theme-new.twig` — base-theme keuze dropdown
|
||||
- `admin/theme/default/views/pages/_media-modal.twig` — `?theme=` scope support
|
||||
- `admin/theme/default/assets/js/editor-toolbar.js` — `twig` → `htmlmixed` mode toegevoegd
|
||||
- `cms/core/class/CodePressCMS.php` — `scanDirectory`/`searchInDirectory`/`scanForPageTitles` skippen nu `_` prefix; `processContent()` herschrijft lokale image/link URLs naar `/-media/`
|
||||
- `language/{nl,en,de}/admin.php` — ~45 nieuwe vertaalstrings (theme-editor + content-editor)
|
||||
- `guide/{nl,en}/admin-beheerder/thema-beheer.md` — volledig herschreven met thema-editor uitleg
|
||||
- `guide/{nl,en}/admin-beheerder/content-beheer.md` — volledig herschreven met content-editor uitleg
|
||||
- `TODO.md` — thema-editor en content-editor items aangevinkt
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- **Pentest**: 30/30 tests geslaagd — 0 vulnerabilities
|
||||
- **WCAG 2.1 AA accessibility**: 25/25 tests geslaagd — 100% compliance
|
||||
- **PHP lint**: alle bestanden schoon (geen syntax errors)
|
||||
- **curl tests**: alle nieuwe routes getest (aanmaken, bewerken, opslaan, uploaden, verwijderen, verplaatsen, map aanmaken/hernoemen/verwijderen, SCSS compileren, thema activeren/verwijderen, thema kopiëren); path-traversal pogingen (delete/new_file/upload) allemaal geblokkeerd; default/active theme bescherming geverifieerd; theme.json verwijderen/plaatsen geblokkeerd; image-URL rewriting geverifieerd (relatief, /content/, subdir, extern ongewijzigd); `_`-map niet in frontend navigatie
|
||||
|
||||
---
|
||||
|
||||
## Bestanden gewijzigd sinds v2.6.1d
|
||||
|
||||
15 bestanden gewijzigd (core + admin theme + language + guide), ~2200 insertions, ~200 deletions.
|
||||
@@ -1,6 +1,99 @@
|
||||
# Content management
|
||||
|
||||
## Managing files
|
||||
CodePress offers two views for content management:
|
||||
|
||||
1. **Tree view** (`/admin/content`) — default, file browser sidebar + CodeMirror editor (uniform with plugin/theme editors)
|
||||
2. **List view** (`/admin/content-list`) — classic table with filter, upload, new folder/file per folder
|
||||
|
||||
Both work side-by-side and use the same content directory. Switch between views via the "List view"/"Tree view" buttons at the top right. The tree view offers the same functionality as the list view, but with a nested file-tree sidebar like the plugin and theme editors — for a consistent editor experience.
|
||||
|
||||
## Tree view (content)
|
||||
|
||||
### File browser sidebar
|
||||
- Shows the nested file tree of `content/`
|
||||
- Hidden directories (`.bak`, `.git`) and dotfiles are skipped
|
||||
- Click a file to open it in the CodeMirror editor
|
||||
- Folders containing the active file are auto-expanded
|
||||
- Per folder there are action buttons: new file, new folder, rename, delete
|
||||
|
||||
### Editable file types
|
||||
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent with the existing content-edit page.
|
||||
|
||||
### Set image size (Markdown)
|
||||
The markdown editor toolbar has an "Image size" button (expand icon):
|
||||
1. Select an image in the editor in markdown format ``
|
||||
2. Click the "Image size" button
|
||||
3. Enter width and/or height (px or %, empty = don't set)
|
||||
4. The `{:width=... height=...}` syntax is added to or replaced on the image
|
||||
|
||||
Example:
|
||||
```markdown
|
||||
{:width="200" height="100"}
|
||||
```
|
||||
|
||||
Existing `{:width=...}` values are shown in the prompts. The size attributes are rendered as `width`/`height` HTML attributes on the `<img>` tag in the frontend.
|
||||
|
||||
### Images in content
|
||||
Images in `content/` are served via the `/-media/` endpoint (content/ lives outside the webroot). Markdown syntax `` with local URLs (relative like `image.jpg`, or absolute like `/content/_images/image.jpg`) is automatically rewritten to `/-media/...`. External URLs (`https://...`) are left untouched.
|
||||
|
||||
### Create a new file
|
||||
- Click **New file** (at the top or per folder in the sidebar)
|
||||
- Enter a path within content (e.g. `en.page` or `blog/en.post`)
|
||||
- Choose the file type (Markdown/PHP/HTML)
|
||||
- Subfolders are created automatically
|
||||
- Frontmatter with `layout`, `author_name`, `author_email`, `created` is auto-generated
|
||||
|
||||
### Upload a file
|
||||
- Click **Upload** to upload files to `content/`
|
||||
- Allowed: images, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Path-traversal protection: target dir must stay within `content/`
|
||||
|
||||
### Move / delete a file
|
||||
- In the file tree each file has a move button (arrows icon) and a delete button (trash)
|
||||
- **Move**: choose a target folder from the dropdown listing all folders in content
|
||||
- **Delete**: with confirmation
|
||||
|
||||
### Folder management
|
||||
- **New folder**: per folder in the sidebar or at the top
|
||||
- **Rename folder**: pencil icon per folder
|
||||
- **Delete folder**: trash icon per folder (only empty folders)
|
||||
|
||||
### Layout selection
|
||||
On the editor page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
### Plugins on pages
|
||||
- Content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- Choose which plugins run on the page
|
||||
- The plugin selection is stored in the frontmatter `plugins:` key
|
||||
|
||||
### Git / Backup integration (Phase 5)
|
||||
At the top of the content editor there are backup and git actions:
|
||||
- **Git init**: initializes a git repository in `content/` (if none exists yet)
|
||||
- **Commit**: commits all uncommitted changes (only if there is a git repo and there are changes)
|
||||
- **Backup**: link to the backup page (`/admin/content-backup`) for ZIP backup/restore
|
||||
- The git status badge shows the current branch, whether there are uncommitted changes, and the last commit
|
||||
|
||||
### Frontmatter
|
||||
|
||||
The editor updates the frontmatter live when layout or plugin selection changes:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
author_name: Admin
|
||||
author_email: admin@example.com
|
||||
created: 2026-08-19 10:30:25
|
||||
plugins: HTMLBlock, Navigation
|
||||
---
|
||||
|
||||
# Page title
|
||||
|
||||
Content...
|
||||
```
|
||||
|
||||
## List view (content)
|
||||
|
||||
### Managing files
|
||||
|
||||
- **New folder** — Create folder structure
|
||||
- **New file** — Create a page (`.md`, `.php`, `.html`)
|
||||
@@ -10,24 +103,12 @@
|
||||
- **Delete** — Remove content
|
||||
- **Rename folder** — Change a folder name
|
||||
|
||||
## Editor (content-edit)
|
||||
### Editor (content-edit)
|
||||
|
||||
- **CodeMirror** with syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** for quickly inserting Markdown
|
||||
- **Shortcuts**: Ctrl+S (save), Ctrl+N (new)
|
||||
|
||||
## Layout selection
|
||||
|
||||
On the content-edit page you can choose the layout from the layouts defined in `theme.json` (template mapping). The selected layout is stored in the frontmatter `layout:` key.
|
||||
|
||||
## Plugins on pages
|
||||
|
||||
- Content plugins (from `plugin.json` with `type: "content"`) appear in the plugin selection
|
||||
- Choose which plugins appear in the sidebar
|
||||
- **Order is adjustable** with up/down buttons
|
||||
- The plugin order is stored in the frontmatter `plugins:` key
|
||||
- Plugins are **hidden** when the chosen layout has no sidebar (e.g. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
The editor updates the frontmatter live when layout or plugin selection changes:
|
||||
|
||||
@@ -3,15 +3,72 @@
|
||||
## Managing themes
|
||||
|
||||
1. Go to **Theme** in the admin menu
|
||||
2. **Activate** — Choose the active theme (stored in `config.json`)
|
||||
3. **Compile SCSS** — Process SCSS to CSS (forced)
|
||||
4. **New theme** — Create a custom theme via admin or manually
|
||||
2. **Edit** — Click the pencil icon to edit theme files (see [Theme editor](#theme-editor))
|
||||
3. **Activate** — Click the checkmark to activate a theme (stored in `config.json`)
|
||||
4. **Compile SCSS** — Click the palette icon to force-compile SCSS to `assets/css_compiled/theme.css`
|
||||
5. **Delete** — Trash icon (only non-active, non-default themes)
|
||||
6. **New theme** — Create a custom theme, optionally based on an existing theme
|
||||
|
||||
## Theme status badges
|
||||
|
||||
In the theme overview you see per theme:
|
||||
- **Active** (green) — this theme is selected in `config.json`
|
||||
- **SCSS ok** (green) — `assets/css_compiled/theme.css` is newer than `assets/scss/theme.scss`
|
||||
- **SCSS stale** (yellow) — SCSS source was changed after the last compile; click the palette icon to compile
|
||||
|
||||
## Theme editor
|
||||
|
||||
Via **Edit** (pencil icon) in the theme overview you open the theme editor (`/admin/theme-edit?theme=<name>`). It works the same as the plugin editor:
|
||||
|
||||
### File browser sidebar
|
||||
- Shows the nested file tree of the theme
|
||||
- `assets/css_compiled/` is hidden (runtime artefact, read-only)
|
||||
- Click a file to open it in the CodeMirror editor
|
||||
- Folders containing the active file are auto-expanded
|
||||
|
||||
### Editable file types
|
||||
`.twig`, `.json`, `.scss`, `.css`, `.js`, `.html`, `.md`, `.php`
|
||||
|
||||
### Create a new file
|
||||
- Click **New file**
|
||||
- Enter a path within the theme (e.g. `partials/header.twig` or `assets/scss/_variables.scss`)
|
||||
- Subfolders are created automatically
|
||||
- Allowed: twig, json, scss, css, js, html, md, php
|
||||
- A stub is auto-generated (e.g. `{% extends 'base.twig' %}` for `.twig`)
|
||||
|
||||
### Upload a file
|
||||
- Click **Upload** to upload files to the theme's `assets/`
|
||||
- Allowed: images, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts
|
||||
- Path-traversal protection: target dir must stay within `assets/`
|
||||
|
||||
### Move / delete a file
|
||||
- In the file tree each file has a move button (arrows icon) and a delete button (trash)
|
||||
- **Move**: choose a target folder from the dropdown listing all folders in the theme
|
||||
- **Delete**: with confirmation; `theme.json` cannot be deleted
|
||||
|
||||
### Compile SCSS from the editor
|
||||
- At the top of the editor there is a **Compile SCSS** button (only if `assets/scss/theme.scss` exists)
|
||||
- Shows the compile status: **up-to-date** (green) or **stale** (yellow)
|
||||
- Forces compilation via `ThemeManager::compileCss(true)`
|
||||
|
||||
### Insert media in the editor
|
||||
- The media button in the editor toolbar opens the media modal
|
||||
- In theme context it scans `themes/<name>/assets/` (via `/admin/media-list?theme=<name>`)
|
||||
- Snippet format depends on file type: markdown → ``, html/php → `<img src=...>`, others → raw URL
|
||||
|
||||
### Security
|
||||
- All actions require a CSRF token
|
||||
- Path-traversal protection via `realpath()` + prefix check on the theme dir
|
||||
- `theme.json` can be edited but not deleted/moved
|
||||
- Default theme can be edited but not deleted
|
||||
- Active theme cannot be deleted (activate another theme first)
|
||||
|
||||
## Theme structure
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── README.md # Theme documentation (per theme)
|
||||
├── base.twig # Main layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: full width
|
||||
├── left_sidebar.twig # Layout: sidebar on the left
|
||||
@@ -21,13 +78,15 @@ themes/default/
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS source (only CSS source — manual css/theme.css must not exist)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually)
|
||||
├── css_compiled/ # Generated by scssphp (read-only, do not edit manually, hidden in editor)
|
||||
├── css/ # External CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
New themes created via admin automatically get this uniform structure (with `README.md`, `base.twig`, `full_content.twig`, `partials/header.twig`, `partials/footer.twig`, `assets/scss/theme.scss`, and all assets subfolders).
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
@@ -53,9 +112,9 @@ themes/default/
|
||||
## SCSS compilation
|
||||
|
||||
- `ThemeManager` compiles `assets/scss/theme.scss` at runtime to `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — do not edit manually
|
||||
- `css_compiled/` is **read-only** — do not edit manually (hidden in the theme editor)
|
||||
- `assets/css/theme.css` must **not** exist; otherwise `ThemeManager::getCssUrl()` ignores the SCSS
|
||||
- After SCSS changes: remove `assets/css_compiled/theme.css` and `.mtime` to force recompilation
|
||||
- Force compilation via the **Compile SCSS** button in admin (or remove `assets/css_compiled/theme.css` and `.mtime`)
|
||||
|
||||
## Layouts
|
||||
|
||||
|
||||
@@ -1,6 +1,99 @@
|
||||
# Content beheer
|
||||
|
||||
## Bestanden beheren
|
||||
CodePress biedt twee weergaven voor content-beheer:
|
||||
|
||||
1. **Boom weergave** (`/admin/content`) — standaard, bestandsbrowser zijbalk + CodeMirror editor (uniform met plugin/theme-editors)
|
||||
2. **Lijst weergave** (`/admin/content-list`) — klassieke tabel met filter, upload, nieuwe map/bestand per map
|
||||
|
||||
Beide werken naast elkaar en gebruiken dezelfde content-map. Wissel tussen de weergaven via de "Lijst weergave"/"Boom weergave" knoppen rechtsboven. De boom weergave biedt dezelfde functionaliteit als de lijst weergave, maar dan met een geneste bestandsboom zijbalk zoals de plugin- en thema-editors — voor consistente editor-ervaring.
|
||||
|
||||
## Boom weergave (content)
|
||||
|
||||
### Bestandsbrowser zijbalk
|
||||
- Toont de geneste bestandsboom van `content/`
|
||||
- Verborgen mappen (`.bak`, `.git`) en dotfiles worden overgeslagen
|
||||
- Klik op een bestand om het te openen in de CodeMirror editor
|
||||
- Mappen die het actieve bestand bevatten zijn automatisch uitgeklapt
|
||||
- Per map staan actie-knoppen: nieuw bestand, nieuwe map, hernoemen, verwijderen
|
||||
|
||||
### Bewerkbare bestandstypen
|
||||
`.md` (Markdown), `.php` (PHP), `.html` (HTML) — consistent met de bestaande content-edit pagina.
|
||||
|
||||
### Image-grootte instellen (Markdown)
|
||||
In de markdown editor-toolbar staat een "Afbeelding grootte" knop (expand-icoon):
|
||||
1. Selecteer een afbeelding in de editor in markdown formaat ``
|
||||
2. Klik op de "Afbeelding grootte" knop
|
||||
3. Vul breedte en/of hoogte in (px of %, leeg = niet instellen)
|
||||
4. De `{:width=... height=...}` syntax wordt aan de afbeelding toegevoegd of vervangen
|
||||
|
||||
Voorbeeld:
|
||||
```markdown
|
||||
{:width="200" height="100"}
|
||||
```
|
||||
|
||||
Bestaande `{:width=...}` waarden worden in de prompts getoond. De grootte-attributen worden in de frontend als `width`/`height` HTML-attributen op de `<img>` tag gezet.
|
||||
|
||||
### Images in content
|
||||
Images in `content/` worden via de `/-media/` endpoint geserveerd (content/ staat buiten de webroot). De markdown syntax `` met lokale URLs (relatief zoals `image.jpg`, of absoluut zoals `/content/_images/image.jpg`) wordt automatisch herschreven naar `/-media/...`. Externe URLs (`https://...`) worden ongewijzigd gelaten.
|
||||
|
||||
### Nieuw bestand aanmaken
|
||||
- Klik op **Nieuw bestand** (bovenaan of per map in de zijbalk)
|
||||
- Geef een pad op binnen content (bijv. `nl.pagina` of `blog/nl.post`)
|
||||
- Kies het bestandstype (Markdown/PHP/HTML)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Frontmatter met `layout`, `author_name`, `author_email`, `created` wordt automatisch gegenereerd
|
||||
|
||||
### Bestand uploaden
|
||||
- Klik op **Upload** om bestanden naar `content/` te uploaden
|
||||
- Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD
|
||||
- Path-traversal bescherming: doelmap moet binnen `content/` blijven
|
||||
|
||||
### Bestand verplaatsen / verwijderen
|
||||
- In de bestandsboom heeft elk bestand een verplaats-knop (pijlen-icoon) en een verwijder-knop (prullenbak)
|
||||
- **Verplaatsen**: kies een doelmap uit de dropdown met alle mappen in content
|
||||
- **Verwijderen**: met bevestiging
|
||||
|
||||
### Mappen beheer
|
||||
- **Nieuwe map**: per map in de zijbalk of bovenaan
|
||||
- **Map hernoemen**: potlood-icoon per map
|
||||
- **Map verwijderen**: prullenbak-icoon per map (alleen lege mappen)
|
||||
|
||||
### Layout selectie
|
||||
Op de editor-pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
### Plugins op pagina's
|
||||
- Content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- Kies welke plugins op de pagina draaien
|
||||
- De plugin selectie wordt opgeslagen in de frontmatter `plugins:` key
|
||||
|
||||
### Git / Backup integratie (Fase 5)
|
||||
Bovenaan de content-editor staan backup- en git-acties:
|
||||
- **Git init**: initialiseert een git repository in `content/` (als er nog geen is)
|
||||
- **Commit**: committed alle niet-committed wijzigingen (alleen als er een git repo is en er wijzigingen zijn)
|
||||
- **Backup**: link naar de backup-pagina (`/admin/content-backup`) voor ZIP backup/restore
|
||||
- De git status badge toont de huidige branch, of er niet-committed wijzigingen zijn, en de laatste commit
|
||||
|
||||
### Frontmatter
|
||||
|
||||
De editor werkt de frontmatter live bij bij wijzigingen van layout of plugin selectie:
|
||||
|
||||
```markdown
|
||||
---
|
||||
layout: left_sidebar
|
||||
author_name: Admin
|
||||
author_email: admin@example.com
|
||||
created: 2026-08-19 10:30:25
|
||||
plugins: HTMLBlock, Navigation
|
||||
---
|
||||
|
||||
# Pagina titel
|
||||
|
||||
Content...
|
||||
```
|
||||
|
||||
## Lijst weergave (content)
|
||||
|
||||
### Bestanden beheren
|
||||
|
||||
- **Nieuwe map** — Mappen structuur aanmaken
|
||||
- **Nieuw bestand** — Pagina aanmaken (`.md`, `.php`, `.html`)
|
||||
@@ -10,24 +103,12 @@
|
||||
- **Verwijderen** — Content verwijderen
|
||||
- **Map hernoemen** — Map naam wijzigen
|
||||
|
||||
## Editor (content-edit)
|
||||
### Editor (content-edit)
|
||||
|
||||
- **CodeMirror** met syntax highlighting (Markdown, PHP, HTML)
|
||||
- **Toolbar** voor snel Markdown invoeren
|
||||
- **Sneltoetsen**: Ctrl+S (opslaan), Ctrl+N (nieuw)
|
||||
|
||||
## Layout selectie
|
||||
|
||||
Op de content-edit pagina kun je de layout kiezen uit de layouts gedefinieerd in `theme.json` (template mapping). De geselecteerde layout wordt opgeslagen in de frontmatter `layout:` key.
|
||||
|
||||
## Plugins op pagina's
|
||||
|
||||
- Content plugins (uit `plugin.json` met `type: "content"`) verschijnen in de plugin selectie
|
||||
- Kies welke plugins in de sidebar verschijnen
|
||||
- **Volgorde aanpasbaar** met up/down knoppen
|
||||
- De plugin volgorde wordt opgeslagen in de frontmatter `plugins:` key
|
||||
- Plugins worden **verbergen** als de gekozen layout geen sidebar heeft (bijv. `full_content`)
|
||||
|
||||
## Frontmatter
|
||||
|
||||
De editor werkt de frontmatter live bij bij wijzigingen van layout of plugin selectie:
|
||||
|
||||
@@ -3,15 +3,72 @@
|
||||
## Thema's beheren
|
||||
|
||||
1. Ga naar **Thema** in admin menu
|
||||
2. **Activeren** — Kies actief thema (wordt opgeslagen in `config.json`)
|
||||
3. **SCSS compileren** — Verwerk SCSS naar CSS (geforceerd)
|
||||
4. **Nieuw thema** — Eigen thema aanmaken via admin of handmatig
|
||||
2. **Bewerken** — Klik op het potlood-icoon om themabestanden te bewerken (zie [Thema editor](#thema-editor))
|
||||
3. **Activeren** — Klik op het vinkje om een thema te activeren (wordt opgeslagen in `config.json`)
|
||||
4. **SCSS compileren** — Klik op het palet-icoon om SCSS geforceerd te compileren naar `assets/css_compiled/theme.css`
|
||||
5. **Verwijderen** — Prullenbak-icoon (alleen niet-actieve, niet-default thema's)
|
||||
6. **Nieuw thema** — Eigen thema aanmaken, optioneel gebaseerd op een bestaand thema
|
||||
|
||||
## Thema status badges
|
||||
|
||||
In het thema-overzicht zie je per thema:
|
||||
- **Actief** (groen) — dit thema is geselecteerd in `config.json`
|
||||
- **SCSS ok** (groen) — `assets/css_compiled/theme.css` is nieuwer dan `assets/scss/theme.scss`
|
||||
- **SCSS verouderd** (geel) — SCSS source is gewijzigd na laatste compile; klik op het palet-icoon om te compileren
|
||||
|
||||
## Thema editor
|
||||
|
||||
Via **Bewerken** (potlood-icoon) in het thema-overzicht open je de thema-editor (`/admin/theme-edit?theme=<naam>`). Deze werkt hetzelfde als de plugin-editor:
|
||||
|
||||
### Bestandsbrowser zijbalk
|
||||
- Toont de geneste bestandsboom van het thema
|
||||
- `assets/css_compiled/` wordt verborgen (runtime artefact, read-only)
|
||||
- Klik op een bestand om het te openen in de CodeMirror editor
|
||||
- Mappen die het actieve bestand bevatten zijn automatisch uitgeklapt
|
||||
|
||||
### Bewerkbare bestandstypen
|
||||
`.twig`, `.json`, `.scss`, `.css`, `.js`, `.html`, `.md`, `.php`
|
||||
|
||||
### Nieuw bestand aanmaken
|
||||
- Klik op **Nieuw bestand**
|
||||
- Geef een pad op binnen het thema (bijv. `partials/header.twig` of `assets/scss/_variables.scss`)
|
||||
- Submappen worden automatisch aangemaakt
|
||||
- Toegestaan: twig, json, scss, css, js, html, md, php
|
||||
- Een stub wordt automatisch gegenereerd (bijv. `{% extends 'base.twig' %}` voor `.twig`)
|
||||
|
||||
### Bestand uploaden
|
||||
- Klik op **Upload** om bestanden naar `assets/` van het thema te uploaden
|
||||
- Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts
|
||||
- Path-traversal bescherming: doelmap moet binnen `assets/` blijven
|
||||
|
||||
### Bestand verplaatsen / verwijderen
|
||||
- In de bestandsboom heeft elk bestand een verplaats-knop (pijlen-icoon) en een verwijder-knop (prullenbak)
|
||||
- **Verplaatsen**: kies een doelmap uit de dropdown met alle mappen in het thema
|
||||
- **Verwijderen**: met bevestiging; `theme.json` kan niet verwijderd worden
|
||||
|
||||
### SCSS compileren vanuit de editor
|
||||
- Bovenaan de editor staat een **SCSS compileren** knop (alleen als `assets/scss/theme.scss` bestaat)
|
||||
- Toont de compile-status: **up-to-date** (groen) of **verouderd** (geel)
|
||||
- Forceert compilatie via `ThemeManager::compileCss(true)`
|
||||
|
||||
### Media invoegen in editor
|
||||
- De media-knop in de editor-toolbar opent de media-modal
|
||||
- In thema-context scant deze `themes/<naam>/assets/` (via `/admin/media-list?theme=<naam>`)
|
||||
- Snippet-formaat depends op bestandstype: markdown → ``, html/php → `<img src=...>`, andere → ruwe URL
|
||||
|
||||
### Beveiliging
|
||||
- Alle acties vereisen CSRF token
|
||||
- Path-traversal bescherming via `realpath()` + prefix-check op de thema-map
|
||||
- `theme.json` kan bewerkt maar niet verwijderd/verplaatst worden
|
||||
- Default thema kan bewerkt maar niet verwijderd worden
|
||||
- Actief thema kan niet verwijderd worden (activeer eerst een ander thema)
|
||||
|
||||
## Thema structuur
|
||||
|
||||
```
|
||||
themes/default/
|
||||
├── theme.json # { title, config.default_template, template: layout→.twig }
|
||||
├── README.md # Thema documentatie (per thema)
|
||||
├── base.twig # Hoofd layout (head, header, nav, breadcrumb, footer)
|
||||
├── full_content.twig # Layout: volledige breedte
|
||||
├── left_sidebar.twig # Layout: sidebar links
|
||||
@@ -21,13 +78,15 @@ themes/default/
|
||||
├── partials/ # header.twig, navigation.twig, footer.twig
|
||||
└── assets/
|
||||
├── scss/theme.scss # SCSS bron (enige CSS bron — handmatige css/theme.css mag niet bestaan)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen)
|
||||
├── css_compiled/ # Gegenereerd door scssphp (read-only, niet handmatig aanpassen, verborgen in editor)
|
||||
├── css/ # Externe CSS (bootstrap.min.css, bootstrap-icons.css, mobile.css)
|
||||
├── js/ # app.js, bootstrap.bundle.min.js
|
||||
├── fonts/ # bootstrap-icons.woff, woff2
|
||||
└── img/ # favicon, icon, world-map
|
||||
```
|
||||
|
||||
Nieuwe thema's die via de admin worden aangemaakt krijgen automatisch deze uniforme structuur (met `README.md`, `base.twig`, `full_content.twig`, `partials/header.twig`, `partials/footer.twig`, `assets/scss/theme.scss`, en alle assets-submappen).
|
||||
|
||||
## theme.json
|
||||
|
||||
```json
|
||||
@@ -53,9 +112,9 @@ themes/default/
|
||||
## SCSS compilatie
|
||||
|
||||
- `ThemeManager` compileert `assets/scss/theme.scss` runtime naar `assets/css_compiled/theme.css` via scssphp
|
||||
- `css_compiled/` is **read-only** — niet handmatig aanpassen
|
||||
- `css_compiled/` is **read-only** — niet handmatig aanpassen (wordt verborgen in de thema-editor)
|
||||
- `assets/css/theme.css` mag **niet** bestaan; anders negeert `ThemeManager::getCssUrl()` de SCSS
|
||||
- Na SCSS wijzigingen: verwijder `assets/css_compiled/theme.css` en `.mtime` om te forceren
|
||||
- Forceer compilatie via de **SCSS compileren** knop in admin (of verwijder `assets/css_compiled/theme.css` en `.mtime`)
|
||||
|
||||
## Layouts
|
||||
|
||||
|
||||
@@ -88,6 +88,29 @@ return [
|
||||
'col_size' => 'Größe',
|
||||
'col_modified' => 'Geändert',
|
||||
'col_actions' => 'Aktionen',
|
||||
|
||||
// Content editor (content-files) — Phase 1-5 der Content-Konsistenz TODO
|
||||
'content_editor' => 'Content-Editor',
|
||||
'content_files' => 'Content-Dateien',
|
||||
'content_no_files' => 'Keine Dateien gefunden.',
|
||||
'content_not_editable' => 'Dieser Dateityp kann im Editor nicht bearbeitet werden.',
|
||||
'content_select_file' => 'Wählen Sie eine Datei aus der Seitenleiste zum Bearbeiten.',
|
||||
'content_file_path' => 'Dateipfad innerhalb von content',
|
||||
'content_file_path_help' => 'Nur Buchstaben, Zahlen, Punkte, Unterstriche, Bindestriche und Schrägstriche. Unterordner werden automatisch erstellt. Die Erweiterung wird unten ausgewählt.',
|
||||
'content_upload_help' => 'Dateien werden in content/ hochgeladen. Erlaubt: Bilder, Video, Audio, PDF, ZIP, Office-Dokumente, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
'file_type' => 'Dateityp',
|
||||
'list_view' => 'Listenansicht',
|
||||
'tree_view' => 'Baumansicht',
|
||||
'rename_folder' => 'Ordner umbenennen',
|
||||
'rename_prefix' => 'Umbenennen',
|
||||
'new_name' => 'Neuer Name',
|
||||
'git_init' => 'Git init',
|
||||
'git_init_confirm' => 'Git-Repository in content/ initialisieren?',
|
||||
'git_commit' => 'Commit',
|
||||
'git_commit_confirm' => 'Änderungen an git committen?',
|
||||
'git_dirty' => 'nicht committete Änderungen',
|
||||
'git_clean' => 'saubere Arbeitskopie',
|
||||
'git_last_commit' => 'letzter Commit:',
|
||||
'col_role' => 'Rolle',
|
||||
'col_created' => 'Erstellt',
|
||||
'no_files_found' => 'Keine Dateien gefunden.',
|
||||
@@ -212,10 +235,32 @@ return [
|
||||
'new_theme_title' => 'Neues Theme erstellen',
|
||||
'theme_name' => 'Theme-Name',
|
||||
'theme_name_help' => 'Nur Buchstaben, Zahlen, Unterstriche und Bindestriche.',
|
||||
'theme_base' => 'Basis-Theme',
|
||||
'theme_base_help' => 'Wählen Sie ein bestehendes Theme als Basis zum Kopieren oder starten Sie mit einer leeren einheitlichen Struktur. css_compiled/ wird nicht kopiert.',
|
||||
'theme_base_blank' => '(leere Struktur)',
|
||||
'name_label' => 'Name: ',
|
||||
'default_layout_label' => 'Standard-Layout: ',
|
||||
'compile_scss' => 'SCSS kompilieren',
|
||||
'compile_scss_confirm' => 'SCSS kompilieren? Dies überschreibt assets/css_compiled/theme.css.',
|
||||
'scss_ok' => 'aktuell',
|
||||
'scss_stale' => 'veraltet',
|
||||
'scss_compiled_ok' => 'SCSS ist kompiliert und aktuell',
|
||||
'scss_needs_compile' => 'SCSS-Quelle neuer als kompiliertes CSS',
|
||||
'no_themes' => 'Keine Themes gefunden.',
|
||||
'theme_default_protected' => 'Default-Theme - kann nicht gelöscht werden',
|
||||
'theme_layouts' => 'Layouts: ',
|
||||
'theme_actions' => 'Theme-Aktionen',
|
||||
'confirm_delete_theme' => 'Sind Sie sicher, dass Sie dieses Theme löschen möchten? Alle Dateien im Theme gehen verloren.',
|
||||
'theme_edit' => 'Theme bearbeiten: ',
|
||||
'theme_files' => 'Theme-Dateien',
|
||||
'theme_no_files' => 'Keine Dateien gefunden.',
|
||||
'theme_new_file' => 'Neue Datei',
|
||||
'theme_new_file_title' => 'Neue Datei erstellen',
|
||||
'theme_file_path' => 'Dateipfad innerhalb des Themes',
|
||||
'theme_file_path_help' => 'Nur Buchstaben, Zahlen, Punkte, Unterstriche, Bindestriche und Schrägstriche. Unterordner werden automatisch erstellt. Erlaubt: twig, json, scss, css, js, html, md, php.',
|
||||
'theme_not_editable' => 'Dieser Dateityp kann im Editor nicht bearbeitet werden.',
|
||||
'theme_select_file' => 'Wählen Sie eine Datei aus der Seitenleiste zum Bearbeiten.',
|
||||
'theme_upload_help' => 'Dateien werden in assets/ dieses Themes hochgeladen. Erlaubt: Bilder, Video, Audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, Fonts.',
|
||||
|
||||
// Update
|
||||
'system_update' => 'System-Update',
|
||||
|
||||
@@ -88,6 +88,29 @@ return [
|
||||
'col_size' => 'Size',
|
||||
'col_modified' => 'Modified',
|
||||
'col_actions' => 'Actions',
|
||||
|
||||
// Content editor (content-files) — Phase 1-5 of content-consistency TODO
|
||||
'content_editor' => 'Content editor',
|
||||
'content_files' => 'Content files',
|
||||
'content_no_files' => 'No files found.',
|
||||
'content_not_editable' => 'This file type cannot be edited in the editor.',
|
||||
'content_select_file' => 'Select a file from the sidebar to edit.',
|
||||
'content_file_path' => 'File path within content',
|
||||
'content_file_path_help' => 'Only letters, numbers, dots, underscores, dashes and slashes. Subfolders are created automatically. The extension is chosen below.',
|
||||
'content_upload_help' => 'Files are uploaded to content/. Allowed: images, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
'file_type' => 'File type',
|
||||
'list_view' => 'List view',
|
||||
'tree_view' => 'Tree view',
|
||||
'rename_folder' => 'Rename folder',
|
||||
'rename_prefix' => 'Rename',
|
||||
'new_name' => 'New name',
|
||||
'git_init' => 'Git init',
|
||||
'git_init_confirm' => 'Initialize a git repository in content/?',
|
||||
'git_commit' => 'Commit',
|
||||
'git_commit_confirm' => 'Commit changes to git?',
|
||||
'git_dirty' => 'uncommitted changes',
|
||||
'git_clean' => 'clean working tree',
|
||||
'git_last_commit' => 'last commit:',
|
||||
'col_role' => 'Role',
|
||||
'col_created' => 'Created',
|
||||
'no_files_found' => 'No files found.',
|
||||
@@ -212,10 +235,32 @@ return [
|
||||
'new_theme_title' => 'Create new theme',
|
||||
'theme_name' => 'Theme name',
|
||||
'theme_name_help' => 'Only letters, numbers, underscores and dashes.',
|
||||
'theme_base' => 'Base theme',
|
||||
'theme_base_help' => 'Choose an existing theme to copy as a base, or start with an empty uniform structure. css_compiled/ is not copied.',
|
||||
'theme_base_blank' => '(empty structure)',
|
||||
'name_label' => 'Name: ',
|
||||
'default_layout_label' => 'Default layout: ',
|
||||
'compile_scss' => 'Compile SCSS',
|
||||
'compile_scss_confirm' => 'Compile SCSS? This overwrites assets/css_compiled/theme.css.',
|
||||
'scss_ok' => 'up-to-date',
|
||||
'scss_stale' => 'stale',
|
||||
'scss_compiled_ok' => 'SCSS is compiled and up-to-date',
|
||||
'scss_needs_compile' => 'SCSS source newer than compiled CSS',
|
||||
'no_themes' => 'No themes found.',
|
||||
'theme_default_protected' => 'Default theme - cannot be deleted',
|
||||
'theme_layouts' => 'Layouts: ',
|
||||
'theme_actions' => 'Theme actions',
|
||||
'confirm_delete_theme' => 'Are you sure you want to delete this theme? All files in the theme will be lost.',
|
||||
'theme_edit' => 'Edit theme: ',
|
||||
'theme_files' => 'Theme files',
|
||||
'theme_no_files' => 'No files found.',
|
||||
'theme_new_file' => 'New file',
|
||||
'theme_new_file_title' => 'Create new file',
|
||||
'theme_file_path' => 'File path within theme',
|
||||
'theme_file_path_help' => 'Only letters, numbers, dots, underscores, dashes and slashes. Subfolders are created automatically. Allowed: twig, json, scss, css, js, html, md, php.',
|
||||
'theme_not_editable' => 'This file type cannot be edited in the editor.',
|
||||
'theme_select_file' => 'Select a file from the sidebar to edit.',
|
||||
'theme_upload_help' => 'Files are uploaded to assets/ of this theme. Allowed: images, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts.',
|
||||
|
||||
// Update
|
||||
'system_update' => 'System Update',
|
||||
|
||||
@@ -77,7 +77,9 @@ return [
|
||||
// Content page
|
||||
'upload' => 'Upload',
|
||||
'new_folder' => 'Nieuwe map',
|
||||
'new_folder_title' => 'Nieuwe map aanmaken',
|
||||
'new_file' => 'Nieuw bestand',
|
||||
'new_file_title' => 'Nieuw bestand aanmaken',
|
||||
'select_files' => 'Bestanden selecteren',
|
||||
'allowed_types' => 'Toegestaan: JPG, PNG, GIF, WebP, SVG, PDF, ZIP, MP4, WebM, MP3, WAV',
|
||||
'upload_to_folder' => 'Uploaden naar deze map',
|
||||
@@ -88,6 +90,29 @@ return [
|
||||
'col_size' => 'Grootte',
|
||||
'col_modified' => 'Gewijzigd',
|
||||
'col_actions' => 'Acties',
|
||||
|
||||
// Content editor (content-files) — Fase 1-5 van content-consistentie TODO
|
||||
'content_editor' => 'Content editor',
|
||||
'content_files' => 'Content bestanden',
|
||||
'content_no_files' => 'Geen bestanden gevonden.',
|
||||
'content_not_editable' => 'Dit bestandstype kan niet in de editor bewerkt worden.',
|
||||
'content_select_file' => 'Selecteer een bestand uit de zijbalk om te bewerken.',
|
||||
'content_file_path' => 'Bestandspad binnen content',
|
||||
'content_file_path_help' => 'Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. De extensie wordt hieronder gekozen.',
|
||||
'content_upload_help' => 'Bestanden worden geüpload naar content/. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, office docs, CSS, SCSS, JS, JSON, HTML, MD.',
|
||||
'file_type' => 'Bestandstype',
|
||||
'list_view' => 'Lijst weergave',
|
||||
'tree_view' => 'Boom weergave',
|
||||
'rename_folder' => 'Map hernoemen',
|
||||
'rename_prefix' => 'Hernoem',
|
||||
'new_name' => 'Nieuwe naam',
|
||||
'git_init' => 'Git init',
|
||||
'git_init_confirm' => 'Git repository initialiseren in content/?',
|
||||
'git_commit' => 'Commit',
|
||||
'git_commit_confirm' => 'Wijzigingen committen naar git?',
|
||||
'git_dirty' => 'niet-committed wijzigingen',
|
||||
'git_clean' => 'schone werkmap',
|
||||
'git_last_commit' => 'laatste commit:',
|
||||
'col_role' => 'Rol',
|
||||
'col_created' => 'Aangemaakt',
|
||||
'no_files_found' => 'Geen bestanden gevonden.',
|
||||
@@ -212,10 +237,32 @@ return [
|
||||
'new_theme_title' => 'Nieuw thema aanmaken',
|
||||
'theme_name' => 'Thema naam',
|
||||
'theme_name_help' => 'Alleen letters, cijfers, underscores en streepjes.',
|
||||
'theme_base' => 'Basis thema',
|
||||
'theme_base_help' => 'Kies een bestaand thema als basis om te kopiëren, of start met een lege uniforme structuur. css_compiled/ wordt niet gekopieerd.',
|
||||
'theme_base_blank' => '(lege structuur)',
|
||||
'name_label' => 'Naam: ',
|
||||
'default_layout_label' => 'Default layout: ',
|
||||
'compile_scss' => 'SCSS compileren',
|
||||
'compile_scss_confirm' => 'SCSS compileren? Dit overschrijft assets/css_compiled/theme.css.',
|
||||
'scss_ok' => 'up-to-date',
|
||||
'scss_stale' => 'verouderd',
|
||||
'scss_compiled_ok' => 'SCSS is gecompileerd en up-to-date',
|
||||
'scss_needs_compile' => 'SCSS source nieuwer dan gecompileerde CSS',
|
||||
'no_themes' => 'Geen thema\'s gevonden.',
|
||||
'theme_default_protected' => 'Default thema - kan niet verwijderd worden',
|
||||
'theme_layouts' => 'Layouts: ',
|
||||
'theme_actions' => 'Thema acties',
|
||||
'confirm_delete_theme' => 'Weet je zeker dat je dit thema wilt verwijderen? Alle bestanden in het thema gaan verloren.',
|
||||
'theme_edit' => 'Thema bewerken: ',
|
||||
'theme_files' => 'Thema bestanden',
|
||||
'theme_no_files' => 'Geen bestanden gevonden.',
|
||||
'theme_new_file' => 'Nieuw bestand',
|
||||
'theme_new_file_title' => 'Nieuw bestand aanmaken',
|
||||
'theme_file_path' => 'Bestandspad binnen thema',
|
||||
'theme_file_path_help' => 'Alleen letters, cijfers, punten, underscores, streepjes en slashes. Submappen worden automatisch aangemaakt. Toegestaan: twig, json, scss, css, js, html, md, php.',
|
||||
'theme_not_editable' => 'Dit bestandstype kan niet in de editor bewerkt worden.',
|
||||
'theme_select_file' => 'Selecteer een bestand uit de zijbalk om te bewerken.',
|
||||
'theme_upload_help' => 'Bestanden worden geüpload naar assets/ van dit thema. Toegestaan: afbeeldingen, video, audio, PDF, ZIP, CSS, SCSS, JS, JSON, HTML, MD, TWIG, fonts.',
|
||||
|
||||
// Update
|
||||
'system_update' => 'Systeem Update',
|
||||
|
||||
+1737
-77
File diff suppressed because it is too large
Load Diff
+2
-2
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
return [
|
||||
'version' => '2.6.1d',
|
||||
'release_date' => '2026-08-18',
|
||||
'version' => '2.6.2',
|
||||
'release_date' => '2026-08-19',
|
||||
'codename' => 'Lyra',
|
||||
'status' => 'stable',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user