0 Votes

Changes for page Conditioning

Last modified by Ryan C on 2025/09/15 10:19

From version 61.1
edited by Ryan C
on 2025/09/14 23:21
Change comment: Uploaded new attachment "MEGYNK~1.MP4", version 1.1
To version 62.1
edited by Ryan C
on 2025/09/15 10:19
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -1,10 +1,461 @@
1 -{{video attachment="Propaganda.mp4"/}}
1 +{{velocity}}
2 +#pagePicker_import
2 2  
3 -{{video attachment="2025-02-26_05-18-58_0.mp4"/}}
4 +## 1) Collect video attachments on this page
5 +#set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v'])
6 +#set($videos = [])
7 +#foreach($att in $doc.getAttachmentList())
8 + #set($n = $att.getFilename())
9 + #set($ln = $n.toLowerCase())
10 + #foreach($e in $videoExts)
11 + #if($ln.endsWith("." + $e))
12 + #set($discard = $videos.add($att))
13 + #break
14 + #end
15 + #end
16 +#end
4 4  
5 -{{videopicker video="1776General__20240817__1824613900312080533_1_18246138792021319680.mp4"/}}
18 +## 2) Cache HTML (auto-bust on version)
19 +{{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}}
20 +{{html wiki="false" clean="false"}}
21 +<div id="xwiki-video-manager" style="margin:20px 0;">
22 + <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2>
6 6  
7 -{{videopicker video="MEGYNK~~1.faMP4"/}}
24 + #if($videos.size() == 0)
25 + <div style="text-align:center;padding:40px;background:#f8f9fa;border-radius:8px;">
26 + <h3>No Videos Found</h3>
27 + <p>Attach video files to this page to see them here.</p>
28 + </div>
29 + #else
8 8  
9 -{{videopicker video="Propaganda.mp4"/}}
31 + <!-- Bulk toolbar -->
32 + <div id="bulk-bar" class="bulk-bar">
33 + <label style="margin-right:8px;">
34 + <input type="checkbox" id="pick-all"> Select visible
35 + </label>
36 + <span class="sep"></span>
37 + <input type="text" class="bulk-dest suggest-pages" placeholder="Find a page…"
38 + data-search-scope="wiki:${escapetool.xml($xcontext.database)}">
39 + <button type="button" class="btn btn-sm btn-warning" id="bulk-move">Move selected</button>
40 + <button type="button" class="btn btn-sm btn-danger" id="bulk-delete">Delete selected</button>
41 + <span id="bulk-status" class="bulk-status"></span>
42 + </div>
10 10  
44 + <script>
45 + window.VID_CHUNK_SIZE = 48;
46 + window.VID_LAZY_MARGIN = '600px';
47 + window.XWIKI_WIKI = ${jsontool.serialize($xcontext.database)};
48 + window.SOURCE_SPACE = ${jsontool.serialize($doc.space)};
49 + window.SOURCE_PAGE = ${jsontool.serialize($doc.name)};
50 + </script>
51 +
52 + <div id="video-chunks">
53 + #set($i = 0)
54 + #set($chunkIndex = 0)
55 + #foreach($att in $videos)
56 + #set($i = $i + 1)
57 + #set($filename = $att.getFilename())
58 + #set($lname = $filename.toLowerCase())
59 + #set($url = $doc.getAttachmentURL($filename))
60 +
61 + ## MIME guess
62 + #set($videoType = "video/mp4")
63 + #if($lname.endsWith(".webm"))
64 + #set($videoType = "video/webm")
65 + #elseif($lname.endsWith(".ogg"))
66 + #set($videoType = "video/ogg")
67 + #elseif($lname.endsWith(".avi"))
68 + #set($videoType = "video/x-msvideo")
69 + #elseif($lname.endsWith(".mov"))
70 + #set($videoType = "video/quicktime")
71 + #elseif($lname.endsWith(".wmv"))
72 + #set($videoType = "video/x-ms-wmv")
73 + #elseif($lname.endsWith(".flv"))
74 + #set($videoType = "video/x-flv")
75 + #elseif($lname.endsWith(".m4v"))
76 + #set($videoType = "video/mp4")
77 + #end
78 +
79 + #if($i == 1 || ($i - 1) % 48 == 0)
80 + #set($chunkIndex = $chunkIndex + 1)
81 + <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;">
82 + <div class="video-display-grid">
83 + #end
84 +
85 + <div class="video-container">
86 + <div class="video-header">
87 + <label class="pick"><input type="checkbox" class="vid-pick" data-filename="${escapetool.xml($filename)}"></label>
88 + <h4 class="video-title">${escapetool.xml($filename)}</h4>
89 + </div>
90 +
91 + <div class="video-frame"
92 + data-src="${url}"
93 + data-type="${videoType}"
94 + data-name="${escapetool.xml($filename)}">
95 + <canvas class="vid-canvas" width="320" height="180"></canvas>
96 + <button class="btn btn-sm btn-primary" type="button">Load &amp; Play</button>
97 + </div>
98 +
99 + <div class="video-controls">
100 + <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
101 + <button class="btn btn-sm btn-danger del-one" data-filename="${escapetool.xml($filename)}">Delete</button>
102 + <span class="vid-duration">Duration: —</span>
103 + </div>
104 +
105 + <!-- Move-to-page -->
106 + <div class="move-box">
107 + <label>Move to page:</label>
108 + <div class="move-row">
109 + <input type="text"
110 + class="move-input suggest-pages"
111 + placeholder="Find a page…"
112 + data-search-scope="wiki:${escapetool.xml($xcontext.database)}"
113 + data-filename="${escapetool.xml($filename)}">
114 + <button type="button"
115 + class="btn btn-sm btn-warning move-go"
116 + data-filename="${escapetool.xml($filename)}">Move</button>
117 + </div>
118 + <small class="hint">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small>
119 + </div>
120 + </div> <!-- .video-container -->
121 +
122 + #if(($i % 48 == 0) || $foreach.last)
123 + </div> <!-- .video-display-grid -->
124 + #if(!$foreach.last)
125 + <div class="loadmore-wrap">
126 + <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button>
127 + </div>
128 + #end
129 + </div> <!-- .vid-chunk -->
130 + #end
131 + #end
132 + </div>
133 + #end
134 +</div>
135 +
136 +<script>
137 +(function(){
138 + /* ===== constants & helpers ===== */
139 + var WIKI = window.XWIKI_WIKI;
140 + var CURRENT_SPACE = document.documentElement.getAttribute('data-xwiki-space') || (window.SOURCE_SPACE || 'Main');
141 + var ROOT_SPACE = CURRENT_SPACE.split('.')[0];
142 +
143 + function spacesPath(dotPath){
144 + if(!dotPath) return '';
145 + return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/');
146 + }
147 + function parseFullName(full){
148 + full = String(full||'').replace(/^[^:]+:/,'');
149 + var parts = full.split('.');
150 + var page = parts.pop();
151 + return {spacePath: parts.join('.'), page: page};
152 + }
153 + function getFormToken(){
154 + return document.documentElement.getAttribute('data-xwiki-form-token') || '';
155 + }
156 +
157 + /* ===== existence check (prevents DocumentDoesNotExist) ===== */
158 + async function docExists(fullRef){
159 + var p = parseFullName(fullRef);
160 + var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' +
161 + spacesPath(p.spacePath) + '/pages/' + encodeURIComponent(p.page) + '?media=json';
162 + var r = await fetch(url, {credentials:'same-origin'});
163 + return r.status === 200;
164 + }
165 +
166 + /* ===== robust resolver for Page Picker / typed titles ===== */
167 + async function resolveReference(inp){
168 + var ref = inp.getAttribute('data-reference') || (inp.dataset && inp.dataset.reference) || '';
169 + var raw = (inp.value || '').trim();
170 +
171 + if (ref){
172 + if (/\.WebHome$/i.test(ref)) { if (await docExists(ref)) return ref; }
173 + else {
174 + if (await docExists(ref)) return ref;
175 + var rh = ref + '.WebHome'; if (await docExists(rh)) return rh;
176 + }
177 + }
178 + var base = raw.indexOf('.') === -1 ? (ROOT_SPACE + '.' + raw) : raw;
179 + if (await docExists(base)) return base;
180 + var home2 = /\.WebHome$/i.test(base) ? base : (base + '.WebHome');
181 + if (await docExists(home2)) return home2;
182 +
183 + throw new Error('Target page not found: "' + raw + '". Choose a suggestion or type a full reference like "Main Categories.SomePage".');
184 + }
185 +
186 + /* ===== posters & video mount ===== */
187 + async function makePoster(frame){
188 + if(frame.getAttribute('data-poster-ready')==='1') return;
189 + var src = frame.getAttribute('data-src');
190 + try{
191 + var v = document.createElement('video');
192 + v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src;
193 + function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});}
194 + await once(v,'loadedmetadata');
195 + if (typeof v.requestVideoFrameCallback === 'function'){
196 + await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); });
197 + } else {
198 + try { v.currentTime = 0.25; } catch(e){}
199 + await once(v,'seeked').catch(function(){});
200 + await once(v,'loadeddata').catch(function(){});
201 + }
202 + var canvas = frame.querySelector('.vid-canvas');
203 + if(canvas){
204 + var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
205 + canvas.width = 320; canvas.height = h>0?h:180;
206 + var ctx = canvas.getContext('2d', {willReadFrequently:true});
207 + ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
208 + try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){}
209 + }
210 + frame.setAttribute('data-poster-ready','1');
211 + }catch(e){}
212 + }
213 +
214 + function mountVideo(frame){
215 + if(frame.getAttribute('data-mounted')==='1') return;
216 + var src = frame.getAttribute('data-src');
217 + var type = frame.getAttribute('data-type') || 'video/mp4';
218 + var poster = frame.getAttribute('data-poster');
219 +
220 + var v = document.createElement('video');
221 + v.setAttribute('controls',''); v.setAttribute('preload','none');
222 + if(poster) v.setAttribute('poster', poster);
223 + v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px';
224 +
225 + var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s);
226 + v.addEventListener('loadedmetadata', function(){
227 + var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
228 + var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent = 'Duration: '+mm+':'+ss;
229 + });
230 +
231 + frame.replaceChildren(v);
232 + frame.setAttribute('data-mounted','1');
233 + }
234 +
235 + /* ===== observers & UI wiring ===== */
236 + if('IntersectionObserver' in window){
237 + var io = new IntersectionObserver(function(entries){
238 + entries.forEach(function(e){
239 + if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
240 + });
241 + }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
242 + document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
243 + }
244 +
245 + document.addEventListener('click', function(ev){
246 + var frame = ev.target.closest('.video-frame');
247 + if(frame){
248 + mountVideo(frame);
249 + var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
250 + }
251 + });
252 +
253 + document.addEventListener('click', function(ev){
254 + var b = ev.target.closest('.load-more'); if(!b) return;
255 + var next = b.getAttribute('data-next');
256 + var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
257 + if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; }
258 + });
259 +
260 + /* ===== MOVE & DELETE core ===== */
261 + async function moveAttachment(opts){
262 + var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
263 + var pf = parseFullName(dstFull);
264 + var srcSpacesPath = spacesPath(srcSpace);
265 + var dstSpacesPath = spacesPath(pf.spacePath);
266 +
267 + var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
268 + var inp = document.querySelector(sel);
269 + var card = inp ? inp.closest('.video-container') : null;
270 + var frame = card ? card.querySelector('.video-frame') : null;
271 + var srcURL = frame ? frame.getAttribute('data-src') : null;
272 + if(!srcURL) throw new Error('Missing source URL');
273 +
274 + var downloading = await fetch(srcURL, {credentials:'same-origin'});
275 + if(!downloading.ok) throw new Error('Download failed: ' + downloading.status);
276 + var blob = await downloading.blob();
277 +
278 + var token = getFormToken();
279 +
280 + var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath +
281 + '/pages/' + encodeURIComponent(pf.page) +
282 + '/attachments/' + encodeURIComponent(filename) + '?media=json';
283 + var uploading = await fetch(putURL, {
284 + method: 'PUT',
285 + body: blob,
286 + headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token},
287 + credentials:'same-origin'
288 + });
289 + if(!(uploading.status===201 || uploading.status===202)){
290 + var txt = await uploading.text().catch(function(){ return String(uploading.status); });
291 + throw new Error('Upload failed: ' + txt);
292 + }
293 +
294 + var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
295 + '/pages/' + encodeURIComponent(srcPage) +
296 + '/attachments/' + encodeURIComponent(filename);
297 + var deleting = await fetch(delURL, {method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'});
298 + if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
299 + return true;
300 + }
301 +
302 + async function deleteAttachment(filename){
303 + var token = getFormToken();
304 + var srcSpacesPath = spacesPath(window.SOURCE_SPACE);
305 + var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
306 + '/pages/' + encodeURIComponent(window.SOURCE_PAGE) +
307 + '/attachments/' + encodeURIComponent(filename);
308 + var r = await fetch(url, {
309 + method: 'DELETE',
310 + headers: {'XWiki-Form-Token': token},
311 + credentials: 'same-origin'
312 + });
313 + if (r.status !== 204) {
314 + var t = await r.text().catch(()=>String(r.status));
315 + throw new Error('Delete failed: ' + t);
316 + }
317 + return true;
318 + }
319 +
320 + function removeCardByFilename(filename){
321 + var card = document.querySelector('.video-container input.vid-pick[data-filename="'+CSS.escape(filename)+'"]');
322 + card = card ? card.closest('.video-container') : null;
323 + if (card) card.remove();
324 + }
325 + function selectedFilenames(){
326 + return Array.from(document.querySelectorAll('.vid-pick:checked'))
327 + .map(ch => ch.getAttribute('data-filename'));
328 + }
329 +
330 + /* ===== Move (per-card) ===== */
331 + document.addEventListener('click', function(e){
332 + var btn = e.target.closest('.move-go'); if(!btn) return;
333 + var box = btn.closest('.move-box');
334 + var inp = box.querySelector('.move-input');
335 + (async function(){
336 + var notice = box.querySelector('.move-notice');
337 + if(!notice){ notice = document.createElement('div'); notice.className = 'move-notice'; box.appendChild(notice); }
338 + try{
339 + var ref = await resolveReference(inp);
340 + var filename = btn.getAttribute('data-filename');
341 + notice.style.color = '#666';
342 + notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …';
343 +
344 + await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: filename, dstFull: ref });
345 +
346 + notice.textContent = 'Moved ✔ — reloading…';
347 + setTimeout(function(){ location.reload(); }, 600);
348 + }catch(err){
349 + notice.style.color = '#b00020';
350 + notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
351 + }
352 + })();
353 + });
354 +
355 + /* ===== Delete (per-card) ===== */
356 + document.addEventListener('click', function(e){
357 + var btn = e.target.closest('.del-one'); if(!btn) return;
358 + var filename = btn.getAttribute('data-filename');
359 + if(!confirm('Delete this file permanently?\n\n' + filename)) return;
360 + var notice = document.createElement('div');
361 + notice.className = 'move-notice'; notice.textContent = 'Deleting…';
362 + btn.parentElement.appendChild(notice);
363 + deleteAttachment(filename)
364 + .then(()=>{ notice.textContent = 'Deleted ✔'; removeCardByFilename(filename); })
365 + .catch(err=>{ notice.style.color='#b00020'; notice.textContent = (err && err.message)||String(err); });
366 + });
367 +
368 + /* ===== Bulk: select visible ===== */
369 + document.addEventListener('change', function(e){
370 + if (e.target && e.target.id === 'pick-all'){
371 + var on = e.target.checked;
372 + document.querySelectorAll('.vid-pick').forEach(ch => { ch.checked = on; });
373 + }
374 + });
375 +
376 + /* ===== Bulk: Move ===== */
377 + document.getElementById('bulk-move')?.addEventListener('click', async function(){
378 + var files = selectedFilenames();
379 + if (!files.length) return alert('No videos selected.');
380 + var destInput = document.querySelector('.bulk-dest');
381 + var status = document.getElementById('bulk-status');
382 + try{
383 + var ref = await resolveReference(destInput);
384 + if(!confirm('Move '+files.length+' file(s) to:\n\n'+ref+' ?')) return;
385 + status.style.color=''; status.textContent = 'Moving 0/' + files.length + '…';
386 + let done = 0;
387 + for (const f of files){
388 + await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: f, dstFull: ref });
389 + done++; status.textContent = 'Moving ' + done + '/' + files.length + '…';
390 + removeCardByFilename(f);
391 + }
392 + status.textContent = 'Move complete ✔';
393 + }catch(err){
394 + status.style.color = '#b00020';
395 + status.textContent = 'Move failed: ' + ((err && err.message) || String(err));
396 + }
397 + });
398 +
399 + /* ===== Bulk: Delete ===== */
400 + document.getElementById('bulk-delete')?.addEventListener('click', async function(){
401 + var files = selectedFilenames();
402 + if (!files.length) return alert('No videos selected.');
403 + if(!confirm('DELETE '+files.length+' file(s)? This cannot be undone.')) return;
404 + var status = document.getElementById('bulk-status');
405 + try{
406 + status.style.color=''; status.textContent = 'Deleting 0/' + files.length + '…';
407 + let done = 0;
408 + for (const f of files){
409 + await deleteAttachment(f);
410 + done++; status.textContent = 'Deleting ' + done + '/' + files.length + '…';
411 + removeCardByFilename(f);
412 + }
413 + status.textContent = 'Delete complete ✔';
414 + }catch(err){
415 + status.style.color = '#b00020';
416 + status.textContent = 'Delete failed: ' + ((err && err.message) || String(err));
417 + }
418 + });
419 +
420 +})();
421 +</script>
422 +
423 +<style>
424 + .video-display-grid{
425 + display:grid;
426 + grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
427 + gap:20px; align-items:start;
428 + }
429 + .video-container{border:1px solid #ddd; border-radius:8px; padding:12px; background:#fff;}
430 + .video-header{display:flex;gap:8px;align-items:center;margin-bottom:8px;}
431 + .video-title{margin:0;flex:1;min-width:0;word-break:break-word;}
432 + .pick{margin-right:2px}
433 + .video-frame{
434 + position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px;
435 + overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer;
436 + }
437 + .vid-canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
438 + .video-controls{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
439 + .move-box{margin-top:10px;}
440 + .move-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
441 + .move-input{flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;}
442 + .hint{color:#888;}
443 + .loadmore-wrap{text-align:center;margin:12px 0 28px;}
444 + .btn{padding:4px 8px;border:1px solid #ddd;background:#f8f9fa;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;}
445 + .btn:hover{background:#e9ecef;}
446 + .btn-primary{background:#007bff;color:#fff;border-color:#007bff;}
447 + .btn-secondary{background:#6c757d;color:#fff;border-color:#6c757d;}
448 + .btn-success{background:#28a745;color:#fff;border-color:#28a745;}
449 + .btn-danger{background:#dc3545;color:#fff;border-color:#dc3545;}
450 + .btn-danger:hover{background:#c82333}
451 + .btn-sm{font-size:12px;padding:2px 6px;}
452 + .move-notice{font-size:12px;color:#666;margin-top:6px;max-height:6.5em;overflow:auto;white-space:normal;overflow-wrap:anywhere;}
453 + .bulk-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;}
454 + .bulk-bar .sep{width:1px;height:18px;background:#ddd;margin:0 4px;}
455 + .bulk-status{font-size:12px;color:#666;margin-left:6px;white-space:normal;overflow-wrap:anywhere;}
456 + @media (max-width:768px){.video-display-grid{grid-template-columns:1fr}}
457 +</style>
458 +{{/html}}
459 +{{/cache}}
460 +{{/velocity}}
461 +