Changeset 48ebfa7 in Klonkt
- Timestamp:
- 06/21/2026 04:19:21 PM (3 months ago)
- Branches:
- main
- Children:
- 5a394e5
- Parents:
- 75c6919
- Location:
- src
- Files:
-
- 2 added
- 2 edited
-
assets/vendor/cropper.min.css (added)
-
assets/vendor/cropper.min.js (added)
-
services/i18n.js (modified) (3 diffs)
-
views/pages/post-edit.ejs (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/i18n.js
r75c6919 r48ebfa7 753 753 'pedit.tp_search_placeholder': 'Zoek op titel of artiest…', 754 754 'pedit.tp_list_aria': 'Tracks', 755 'imed.title': 'Afbeelding bewerken', 756 'imed.rotate_left': 'Linksom draaien', 'imed.rotate_right': 'Rechtsom draaien', 757 'imed.flip_h': 'Horizontaal spiegelen', 'imed.flip_v': 'Verticaal spiegelen', 758 'imed.zoom_in': 'Inzoomen', 'imed.zoom_out': 'Uitzoomen', 'imed.reset': 'Herstellen', 759 'imed.cancel': 'Annuleren', 'imed.apply': 'Toepassen', 755 760 'acct.back_home': 'Terug naar home', 756 761 'acct.title': 'Account', … … 1706 1711 'pedit.tp_search_placeholder': 'Search by title or artist…', 1707 1712 'pedit.tp_list_aria': 'Tracks', 1713 'imed.title': 'Edit image', 1714 'imed.rotate_left': 'Rotate left', 'imed.rotate_right': 'Rotate right', 1715 'imed.flip_h': 'Flip horizontally', 'imed.flip_v': 'Flip vertically', 1716 'imed.zoom_in': 'Zoom in', 'imed.zoom_out': 'Zoom out', 'imed.reset': 'Reset', 1717 'imed.cancel': 'Cancel', 'imed.apply': 'Apply', 1708 1718 'acct.back_home': 'Back to home', 1709 1719 'acct.title': 'Account', … … 2659 2669 'pedit.tp_search_placeholder': 'Nach Titel oder Künstler suchen…', 2660 2670 'pedit.tp_list_aria': 'Tracks', 2671 'imed.title': 'Bild bearbeiten', 2672 'imed.rotate_left': 'Nach links drehen', 'imed.rotate_right': 'Nach rechts drehen', 2673 'imed.flip_h': 'Horizontal spiegeln', 'imed.flip_v': 'Vertikal spiegeln', 2674 'imed.zoom_in': 'Vergrößern', 'imed.zoom_out': 'Verkleinern', 'imed.reset': 'Zurücksetzen', 2675 'imed.cancel': 'Abbrechen', 'imed.apply': 'Übernehmen', 2661 2676 'acct.back_home': 'Zurück zur Startseite', 2662 2677 'acct.title': 'Konto', -
src/views/pages/post-edit.ejs
r75c6919 r48ebfa7 947 947 tabbalk, anders zou die zweven boven de actiebalk. */ 948 948 body:has(.post-edit-page) .audio-player { bottom: 0; } 949 950 /* ── Afbeeldings-editor (draaien / bijsnijden / spiegelen) ── */ 951 .imed-backdrop { 952 position: fixed; inset: 0; z-index: 9999; 953 display: flex; align-items: center; justify-content: center; 954 background: rgba(0,0,0,.62); backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px); 955 padding: 14px; 956 } 957 .imed-modal { 958 display: flex; flex-direction: column; gap: 12px; 959 width: 100%; max-width: 640px; max-height: 92vh; 960 background: var(--paper, #fff); color: var(--ink, #111); 961 border: 1px solid var(--rule, rgba(128,128,128,.3)); border-radius: 14px; 962 padding: 14px; box-sizing: border-box; 963 } 964 .imed-stage { 965 flex: 1 1 auto; min-height: 0; 966 background: var(--paper-2, rgba(128,128,128,.08)); border-radius: 10px; overflow: hidden; 967 } 968 /* Cropper heeft een blok-img met begrensde hoogte nodig. */ 969 .imed-stage img { display: block; max-width: 100%; } 970 .imed-stage .cropper-container { max-height: 64vh; } 971 .imed-tools { 972 display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; 973 } 974 .imed-tools button { 975 width: 42px; height: 42px; font-size: 18px; line-height: 1; 976 border: 1px solid var(--rule, rgba(128,128,128,.4)); border-radius: 10px; 977 background: var(--paper-2, transparent); color: inherit; cursor: pointer; 978 } 979 .imed-tools button:hover { border-color: var(--accent); } 980 .imed-actions { display: flex; gap: 8px; justify-content: flex-end; } 949 981 </style> 950 982 … … 969 1001 } 970 1002 return await res.json(); // {url, size, mime} 1003 } 1004 1005 // ── Afbeeldings-editor (draaien / bijsnijden / spiegelen) ────────── 1006 // Lazy-load Cropper.js (lokaal gevendord) pas bij het eerste gebruik. 1007 let _cropperReady = null; 1008 function ensureCropper() { 1009 if (window.Cropper) return Promise.resolve(); 1010 if (_cropperReady) return _cropperReady; 1011 _cropperReady = new Promise((resolve, reject) => { 1012 if (!document.querySelector('link[data-cropper-css]')) { 1013 const l = document.createElement('link'); 1014 l.rel = 'stylesheet'; l.href = '/assets/vendor/cropper.min.css'; l.setAttribute('data-cropper-css', ''); 1015 document.head.appendChild(l); 1016 } 1017 const s = document.createElement('script'); 1018 s.src = '/assets/vendor/cropper.min.js'; 1019 s.onload = () => resolve(); 1020 s.onerror = () => reject(new Error('cropper load failed')); 1021 document.head.appendChild(s); 1022 }); 1023 return _cropperReady; 1024 } 1025 1026 // Opent de editor voor een gekozen bestand; resolved met een bewerkt File, 1027 // of null als de gebruiker annuleert. Animatie-GIF's worden niet door de 1028 // canvas-editor gehaald (zouden statisch worden) — die uploaden direct. 1029 async function openImageEditor(file) { 1030 if (!file || !file.type || !file.type.startsWith('image/')) return file; 1031 if (file.type === 'image/gif') return file; // behoud animatie 1032 try { await ensureCropper(); } catch (_) { return file; } // editor onbeschikbaar → direct uploaden 1033 1034 return new Promise((resolve) => { 1035 const back = document.createElement('div'); 1036 back.className = 'imed-backdrop'; 1037 back.innerHTML = 1038 '<div class="imed-modal" role="dialog" aria-modal="true" aria-label="<%= t('imed.title') %>">' + 1039 '<div class="imed-stage"><img alt=""></div>' + 1040 '<div class="imed-tools">' + 1041 '<button type="button" data-act="rl" title="<%= t('imed.rotate_left') %>">⟲</button>' + 1042 '<button type="button" data-act="rr" title="<%= t('imed.rotate_right') %>">⟳</button>' + 1043 '<button type="button" data-act="fh" title="<%= t('imed.flip_h') %>">⇆</button>' + 1044 '<button type="button" data-act="fv" title="<%= t('imed.flip_v') %>">⇅</button>' + 1045 '<button type="button" data-act="zi" title="<%= t('imed.zoom_in') %>">+</button>' + 1046 '<button type="button" data-act="zo" title="<%= t('imed.zoom_out') %>">-</button>' + 1047 '<button type="button" data-act="reset" title="<%= t('imed.reset') %>">↺</button>' + 1048 '</div>' + 1049 '<div class="imed-actions">' + 1050 '<button type="button" data-act="cancel" class="pe-btn pe-btn-secondary"><%= t('imed.cancel') %></button>' + 1051 '<button type="button" data-act="apply" class="pe-btn pe-btn-primary"><%= t('imed.apply') %></button>' + 1052 '</div>' + 1053 '</div>'; 1054 document.body.appendChild(back); 1055 const img = back.querySelector('img'); 1056 const url = URL.createObjectURL(file); 1057 let cropper = null, sx = 1, sy = 1; 1058 1059 function cleanup() { 1060 try { if (cropper) cropper.destroy(); } catch (_) {} 1061 URL.revokeObjectURL(url); 1062 back.remove(); 1063 document.removeEventListener('keydown', onKey); 1064 } 1065 function onKey(e) { if (e.key === 'Escape') { cleanup(); resolve(null); } } 1066 document.addEventListener('keydown', onKey); 1067 1068 img.onload = () => { 1069 cropper = new Cropper(img, { viewMode: 1, autoCropArea: 1, background: false, responsive: true }); 1070 }; 1071 img.src = url; 1072 1073 back.addEventListener('click', (e) => { 1074 const btn = e.target.closest('[data-act]'); 1075 if (e.target === back) { cleanup(); resolve(null); return; } 1076 if (!btn || !cropper) return; 1077 const a = btn.getAttribute('data-act'); 1078 if (a === 'rl') cropper.rotate(-90); 1079 else if (a === 'rr') cropper.rotate(90); 1080 else if (a === 'fh') { sx = -sx; cropper.scaleX(sx); } 1081 else if (a === 'fv') { sy = -sy; cropper.scaleY(sy); } 1082 else if (a === 'zi') cropper.zoom(0.1); 1083 else if (a === 'zo') cropper.zoom(-0.1); 1084 else if (a === 'reset') { sx = 1; sy = 1; cropper.reset(); } 1085 else if (a === 'cancel') { cleanup(); resolve(null); } 1086 else if (a === 'apply') { 1087 const canvas = cropper.getCroppedCanvas({ maxWidth: 3000, maxHeight: 3000, imageSmoothingEnabled: true, imageSmoothingQuality: 'high' }); 1088 const png = (file.type === 'image/png' || file.type === 'image/webp'); 1089 const mime = png ? 'image/png' : 'image/jpeg'; 1090 const ext = png ? '.png' : '.jpg'; 1091 canvas.toBlob((blob) => { 1092 cleanup(); 1093 if (!blob) { resolve(file); return; } 1094 const base = (file.name || 'afbeelding').replace(/\.[^.]+$/, ''); 1095 resolve(new File([blob], base + ext, { type: mime })); 1096 }, mime, 0.92); 1097 } 1098 }); 1099 }); 971 1100 } 972 1101 … … 998 1127 coverField.addEventListener('change', async () => { 999 1128 if (!coverField.files[0]) return; 1129 const edited = await openImageEditor(coverField.files[0]); 1130 coverField.value = ''; 1131 if (!edited) return; // geannuleerd 1000 1132 coverStatus.classList.remove('is-error'); 1001 1133 coverStatus.textContent = '<%= t('pedit.js_uploading') %>'; 1002 1134 try { 1003 const j = await uploadImage( coverField.files[0]);1135 const j = await uploadImage(edited); 1004 1136 coverUrl.value = j.url; 1005 1137 showCoverPreview(j.url); … … 1405 1537 // ── Image upload (button + drag-drop into the editor) 1406 1538 async function uploadAndInsertImage(file) { 1539 const edited = await openImageEditor(file); 1540 if (!edited) return; // geannuleerd 1407 1541 contentStatus.classList.remove('is-error'); 1408 1542 contentStatus.textContent = '<%= t('pedit.js_uploading') %>'; 1409 1543 try { 1410 const j = await uploadImage( file);1544 const j = await uploadImage(edited); 1411 1545 const img = '<img src="' + j.url + '" alt="">'; 1412 1546 editor.focus({ preventScroll: true });
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)