0 Votes

Changes for page Evidence of Intent

Last modified by Ryan C on 2025/09/12 08:22

From version 49.1
edited by Ryan C
on 2025/09/12 08:19
Change comment: There is no comment for this version
To version 50.1
edited by Ryan C
on 2025/09/12 08:22
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -175,6 +175,470 @@
175 175  
176 176  However, these contributions have not been without controversy. Critics have argued that more liberal immigration policies contribute to social and economic challenges, while supporters maintain that they strengthen multicultural societies and uphold fundamental humanitarian values.
177 177  
178 +
179 +{{velocity}}
180 +#pagePicker_import
181 +
182 +## 1) Collect video attachments on this page
183 +#set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v'])
184 +#set($videos = [])
185 +#foreach($att in $doc.getAttachmentList())
186 + #set($n = $att.getFilename())
187 + #set($ln = $n.toLowerCase())
188 + #foreach($e in $videoExts)
189 + #if($ln.endsWith("." + $e))
190 + #set($discard = $videos.add($att))
191 + #break
192 + #end
193 + #end
194 +#end
195 +
196 +## 2) Cache HTML (auto-bust on version)
197 +{{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}}
198 +{{html wiki="false" clean="false"}}
199 +<div id="xwiki-video-manager" style="margin:20px 0;">
200 + <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2>
201 +
202 + #if($videos.size() == 0)
203 + <div style="text-align:center;padding:40px;background:#f8f9fa;border-radius:8px;">
204 + <h3>No Videos Found</h3>
205 + <p>Attach video files to this page to see them here.</p>
206 + </div>
207 + #else
208 +
209 + <!-- Bulk toolbar -->
210 + <div id="bulk-bar" class="bulk-bar">
211 + <label style="margin-right:8px;">
212 + <input type="checkbox" id="pick-all"> Select visible
213 + </label>
214 + <span class="sep"></span>
215 + <input type="text" class="bulk-dest suggest-pages" placeholder="Find a page…"
216 + data-search-scope="wiki:${escapetool.xml($xcontext.database)}">
217 + <button type="button" class="btn btn-sm btn-warning" id="bulk-move">Move selected</button>
218 + <button type="button" class="btn btn-sm btn-danger" id="bulk-delete">Delete selected</button>
219 + <span id="bulk-status" class="bulk-status"></span>
220 + </div>
221 +
222 + <script>
223 + window.VID_CHUNK_SIZE = 48;
224 + window.VID_LAZY_MARGIN = '600px';
225 + window.XWIKI_WIKI = ${jsontool.serialize($xcontext.database)};
226 + window.SOURCE_SPACE = ${jsontool.serialize($doc.space)};
227 + window.SOURCE_PAGE = ${jsontool.serialize($doc.name)};
228 + </script>
229 +
230 + <div id="video-chunks">
231 + #set($i = 0)
232 + #set($chunkIndex = 0)
233 + #foreach($att in $videos)
234 + #set($i = $i + 1)
235 + #set($filename = $att.getFilename())
236 + #set($lname = $filename.toLowerCase())
237 + #set($url = $doc.getAttachmentURL($filename))
238 +
239 + ## MIME guess
240 + #set($videoType = "video/mp4")
241 + #if($lname.endsWith(".webm"))
242 + #set($videoType = "video/webm")
243 + #elseif($lname.endsWith(".ogg"))
244 + #set($videoType = "video/ogg")
245 + #elseif($lname.endsWith(".avi"))
246 + #set($videoType = "video/x-msvideo")
247 + #elseif($lname.endsWith(".mov"))
248 + #set($videoType = "video/quicktime")
249 + #elseif($lname.endsWith(".wmv"))
250 + #set($videoType = "video/x-ms-wmv")
251 + #elseif($lname.endsWith(".flv"))
252 + #set($videoType = "video/x-flv")
253 + #elseif($lname.endsWith(".m4v"))
254 + #set($videoType = "video/mp4")
255 + #end
256 +
257 + #if($i == 1 || ($i - 1) % 48 == 0)
258 + #set($chunkIndex = $chunkIndex + 1)
259 + <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;">
260 + <div class="video-display-grid">
261 + #end
262 +
263 + <div class="video-container">
264 + <div class="video-header">
265 + <label class="pick"><input type="checkbox" class="vid-pick" data-filename="${escapetool.xml($filename)}"></label>
266 + <h4 class="video-title">${escapetool.xml($filename)}</h4>
267 + </div>
268 +
269 + <div class="video-frame"
270 + data-src="${url}"
271 + data-type="${videoType}"
272 + data-name="${escapetool.xml($filename)}">
273 + <canvas class="vid-canvas" width="320" height="180"></canvas>
274 + <button class="btn btn-sm btn-primary" type="button">Load &amp; Play</button>
275 + </div>
276 +
277 + <div class="video-controls">
278 + <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
279 + <button class="btn btn-sm btn-danger del-one" data-filename="${escapetool.xml($filename)}">Delete</button>
280 + <span class="vid-duration">Duration: —</span>
281 + </div>
282 +
283 + <!-- Move-to-page -->
284 + <div class="move-box">
285 + <label>Move to page:</label>
286 + <div class="move-row">
287 + <input type="text"
288 + class="move-input suggest-pages"
289 + placeholder="Find a page…"
290 + data-search-scope="wiki:${escapetool.xml($xcontext.database)}"
291 + data-filename="${escapetool.xml($filename)}">
292 + <button type="button"
293 + class="btn btn-sm btn-warning move-go"
294 + data-filename="${escapetool.xml($filename)}">Move</button>
295 + </div>
296 + <small class="hint">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small>
297 + </div>
298 + </div> <!-- .video-container -->
299 +
300 + #if(($i % 48 == 0) || $foreach.last)
301 + </div> <!-- .video-display-grid -->
302 + #if(!$foreach.last)
303 + <div class="loadmore-wrap">
304 + <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button>
305 + </div>
306 + #end
307 + </div> <!-- .vid-chunk -->
308 + #end
309 + #end
310 + </div>
311 + #end
312 +</div>
313 +
314 +<script>
315 +(function(){
316 + /* ===== constants & helpers ===== */
317 + var WIKI = window.XWIKI_WIKI;
318 + var CURRENT_SPACE = document.documentElement.getAttribute('data-xwiki-space') || (window.SOURCE_SPACE || 'Main');
319 + var ROOT_SPACE = CURRENT_SPACE.split('.')[0];
320 +
321 + function spacesPath(dotPath){
322 + if(!dotPath) return '';
323 + return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/');
324 + }
325 + function parseFullName(full){
326 + full = String(full||'').replace(/^[^:]+:/,'');
327 + var parts = full.split('.');
328 + var page = parts.pop();
329 + return {spacePath: parts.join('.'), page: page};
330 + }
331 + function getFormToken(){
332 + return document.documentElement.getAttribute('data-xwiki-form-token') || '';
333 + }
334 +
335 + /* ===== existence check (prevents DocumentDoesNotExist) ===== */
336 + async function docExists(fullRef){
337 + var p = parseFullName(fullRef);
338 + var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' +
339 + spacesPath(p.spacePath) + '/pages/' + encodeURIComponent(p.page) + '?media=json';
340 + var r = await fetch(url, {credentials:'same-origin'});
341 + return r.status === 200;
342 + }
343 +
344 + /* ===== robust resolver for Page Picker / typed titles ===== */
345 + async function resolveReference(inp){
346 + var ref = inp.getAttribute('data-reference') || (inp.dataset && inp.dataset.reference) || '';
347 + var raw = (inp.value || '').trim();
348 +
349 + if (ref){
350 + if (/\.WebHome$/i.test(ref)) { if (await docExists(ref)) return ref; }
351 + else {
352 + if (await docExists(ref)) return ref;
353 + var rh = ref + '.WebHome'; if (await docExists(rh)) return rh;
354 + }
355 + }
356 + var base = raw.indexOf('.') === -1 ? (ROOT_SPACE + '.' + raw) : raw;
357 + if (await docExists(base)) return base;
358 + var home2 = /\.WebHome$/i.test(base) ? base : (base + '.WebHome');
359 + if (await docExists(home2)) return home2;
360 +
361 + throw new Error('Target page not found: "' + raw + '". Choose a suggestion or type a full reference like "Main Categories.SomePage".');
362 + }
363 +
364 + /* ===== posters & video mount ===== */
365 + async function makePoster(frame){
366 + if(frame.getAttribute('data-poster-ready')==='1') return;
367 + var src = frame.getAttribute('data-src');
368 + try{
369 + var v = document.createElement('video');
370 + v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src;
371 + function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});}
372 + await once(v,'loadedmetadata');
373 + if (typeof v.requestVideoFrameCallback === 'function'){
374 + await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); });
375 + } else {
376 + try { v.currentTime = 0.25; } catch(e){}
377 + await once(v,'seeked').catch(function(){});
378 + await once(v,'loadeddata').catch(function(){});
379 + }
380 + var canvas = frame.querySelector('.vid-canvas');
381 + if(canvas){
382 + var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
383 + canvas.width = 320; canvas.height = h>0?h:180;
384 + var ctx = canvas.getContext('2d', {willReadFrequently:true});
385 + ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
386 + try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){}
387 + }
388 + frame.setAttribute('data-poster-ready','1');
389 + }catch(e){}
390 + }
391 +
392 + function mountVideo(frame){
393 + if(frame.getAttribute('data-mounted')==='1') return;
394 + var src = frame.getAttribute('data-src');
395 + var type = frame.getAttribute('data-type') || 'video/mp4';
396 + var poster = frame.getAttribute('data-poster');
397 +
398 + var v = document.createElement('video');
399 + v.setAttribute('controls',''); v.setAttribute('preload','none');
400 + if(poster) v.setAttribute('poster', poster);
401 + v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px';
402 +
403 + var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s);
404 + v.addEventListener('loadedmetadata', function(){
405 + var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
406 + var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent = 'Duration: '+mm+':'+ss;
407 + });
408 +
409 + frame.replaceChildren(v);
410 + frame.setAttribute('data-mounted','1');
411 + }
412 +
413 + /* ===== observers & UI wiring ===== */
414 + if('IntersectionObserver' in window){
415 + var io = new IntersectionObserver(function(entries){
416 + entries.forEach(function(e){
417 + if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
418 + });
419 + }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
420 + document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
421 + }
422 +
423 + document.addEventListener('click', function(ev){
424 + var frame = ev.target.closest('.video-frame');
425 + if(frame){
426 + mountVideo(frame);
427 + var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
428 + }
429 + });
430 +
431 + document.addEventListener('click', function(ev){
432 + var b = ev.target.closest('.load-more'); if(!b) return;
433 + var next = b.getAttribute('data-next');
434 + var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
435 + if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; }
436 + });
437 +
438 + /* ===== MOVE & DELETE core ===== */
439 + async function moveAttachment(opts){
440 + var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
441 + var pf = parseFullName(dstFull);
442 + var srcSpacesPath = spacesPath(srcSpace);
443 + var dstSpacesPath = spacesPath(pf.spacePath);
444 +
445 + var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
446 + var inp = document.querySelector(sel);
447 + var card = inp ? inp.closest('.video-container') : null;
448 + var frame = card ? card.querySelector('.video-frame') : null;
449 + var srcURL = frame ? frame.getAttribute('data-src') : null;
450 + if(!srcURL) throw new Error('Missing source URL');
451 +
452 + var downloading = await fetch(srcURL, {credentials:'same-origin'});
453 + if(!downloading.ok) throw new Error('Download failed: ' + downloading.status);
454 + var blob = await downloading.blob();
455 +
456 + var token = getFormToken();
457 +
458 + var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath +
459 + '/pages/' + encodeURIComponent(pf.page) +
460 + '/attachments/' + encodeURIComponent(filename) + '?media=json';
461 + var uploading = await fetch(putURL, {
462 + method: 'PUT',
463 + body: blob,
464 + headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token},
465 + credentials:'same-origin'
466 + });
467 + if(!(uploading.status===201 || uploading.status===202)){
468 + var txt = await uploading.text().catch(function(){ return String(uploading.status); });
469 + throw new Error('Upload failed: ' + txt);
470 + }
471 +
472 + var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
473 + '/pages/' + encodeURIComponent(srcPage) +
474 + '/attachments/' + encodeURIComponent(filename);
475 + var deleting = await fetch(delURL, {method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'});
476 + if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
477 + return true;
478 + }
479 +
480 + async function deleteAttachment(filename){
481 + var token = getFormToken();
482 + var srcSpacesPath = spacesPath(window.SOURCE_SPACE);
483 + var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
484 + '/pages/' + encodeURIComponent(window.SOURCE_PAGE) +
485 + '/attachments/' + encodeURIComponent(filename);
486 + var r = await fetch(url, {
487 + method: 'DELETE',
488 + headers: {'XWiki-Form-Token': token},
489 + credentials: 'same-origin'
490 + });
491 + if (r.status !== 204) {
492 + var t = await r.text().catch(()=>String(r.status));
493 + throw new Error('Delete failed: ' + t);
494 + }
495 + return true;
496 + }
497 +
498 + function removeCardByFilename(filename){
499 + var card = document.querySelector('.video-container input.vid-pick[data-filename="'+CSS.escape(filename)+'"]');
500 + card = card ? card.closest('.video-container') : null;
501 + if (card) card.remove();
502 + }
503 + function selectedFilenames(){
504 + return Array.from(document.querySelectorAll('.vid-pick:checked'))
505 + .map(ch => ch.getAttribute('data-filename'));
506 + }
507 +
508 + /* ===== Move (per-card) ===== */
509 + document.addEventListener('click', function(e){
510 + var btn = e.target.closest('.move-go'); if(!btn) return;
511 + var box = btn.closest('.move-box');
512 + var inp = box.querySelector('.move-input');
513 + (async function(){
514 + var notice = box.querySelector('.move-notice');
515 + if(!notice){ notice = document.createElement('div'); notice.className = 'move-notice'; box.appendChild(notice); }
516 + try{
517 + var ref = await resolveReference(inp);
518 + var filename = btn.getAttribute('data-filename');
519 + notice.style.color = '#666';
520 + notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …';
521 +
522 + await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: filename, dstFull: ref });
523 +
524 + notice.textContent = 'Moved ✔ — reloading…';
525 + setTimeout(function(){ location.reload(); }, 600);
526 + }catch(err){
527 + notice.style.color = '#b00020';
528 + notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
529 + }
530 + })();
531 + });
532 +
533 + /* ===== Delete (per-card) ===== */
534 + document.addEventListener('click', function(e){
535 + var btn = e.target.closest('.del-one'); if(!btn) return;
536 + var filename = btn.getAttribute('data-filename');
537 + if(!confirm('Delete this file permanently?\n\n' + filename)) return;
538 + var notice = document.createElement('div');
539 + notice.className = 'move-notice'; notice.textContent = 'Deleting…';
540 + btn.parentElement.appendChild(notice);
541 + deleteAttachment(filename)
542 + .then(()=>{ notice.textContent = 'Deleted ✔'; removeCardByFilename(filename); })
543 + .catch(err=>{ notice.style.color='#b00020'; notice.textContent = (err && err.message)||String(err); });
544 + });
545 +
546 + /* ===== Bulk: select visible ===== */
547 + document.addEventListener('change', function(e){
548 + if (e.target && e.target.id === 'pick-all'){
549 + var on = e.target.checked;
550 + document.querySelectorAll('.vid-pick').forEach(ch => { ch.checked = on; });
551 + }
552 + });
553 +
554 + /* ===== Bulk: Move ===== */
555 + document.getElementById('bulk-move')?.addEventListener('click', async function(){
556 + var files = selectedFilenames();
557 + if (!files.length) return alert('No videos selected.');
558 + var destInput = document.querySelector('.bulk-dest');
559 + var status = document.getElementById('bulk-status');
560 + try{
561 + var ref = await resolveReference(destInput);
562 + if(!confirm('Move '+files.length+' file(s) to:\n\n'+ref+' ?')) return;
563 + status.style.color=''; status.textContent = 'Moving 0/' + files.length + '…';
564 + let done = 0;
565 + for (const f of files){
566 + await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: f, dstFull: ref });
567 + done++; status.textContent = 'Moving ' + done + '/' + files.length + '…';
568 + removeCardByFilename(f);
569 + }
570 + status.textContent = 'Move complete ✔';
571 + }catch(err){
572 + status.style.color = '#b00020';
573 + status.textContent = 'Move failed: ' + ((err && err.message) || String(err));
574 + }
575 + });
576 +
577 + /* ===== Bulk: Delete ===== */
578 + document.getElementById('bulk-delete')?.addEventListener('click', async function(){
579 + var files = selectedFilenames();
580 + if (!files.length) return alert('No videos selected.');
581 + if(!confirm('DELETE '+files.length+' file(s)? This cannot be undone.')) return;
582 + var status = document.getElementById('bulk-status');
583 + try{
584 + status.style.color=''; status.textContent = 'Deleting 0/' + files.length + '…';
585 + let done = 0;
586 + for (const f of files){
587 + await deleteAttachment(f);
588 + done++; status.textContent = 'Deleting ' + done + '/' + files.length + '…';
589 + removeCardByFilename(f);
590 + }
591 + status.textContent = 'Delete complete ✔';
592 + }catch(err){
593 + status.style.color = '#b00020';
594 + status.textContent = 'Delete failed: ' + ((err && err.message) || String(err));
595 + }
596 + });
597 +
598 +})();
599 +</script>
600 +
601 +<style>
602 + .video-display-grid{
603 + display:grid;
604 + grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
605 + gap:20px; align-items:start;
606 + }
607 + .video-container{border:1px solid #ddd; border-radius:8px; padding:12px; background:#fff;}
608 + .video-header{display:flex;gap:8px;align-items:center;margin-bottom:8px;}
609 + .video-title{margin:0;flex:1;min-width:0;word-break:break-word;}
610 + .pick{margin-right:2px}
611 + .video-frame{
612 + position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px;
613 + overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer;
614 + }
615 + .vid-canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
616 + .video-controls{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
617 + .move-box{margin-top:10px;}
618 + .move-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
619 + .move-input{flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;}
620 + .hint{color:#888;}
621 + .loadmore-wrap{text-align:center;margin:12px 0 28px;}
622 + .btn{padding:4px 8px;border:1px solid #ddd;background:#f8f9fa;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;}
623 + .btn:hover{background:#e9ecef;}
624 + .btn-primary{background:#007bff;color:#fff;border-color:#007bff;}
625 + .btn-secondary{background:#6c757d;color:#fff;border-color:#6c757d;}
626 + .btn-success{background:#28a745;color:#fff;border-color:#28a745;}
627 + .btn-danger{background:#dc3545;color:#fff;border-color:#dc3545;}
628 + .btn-danger:hover{background:#c82333}
629 + .btn-sm{font-size:12px;padding:2px 6px;}
630 + .move-notice{font-size:12px;color:#666;margin-top:6px;max-height:6.5em;overflow:auto;white-space:normal;overflow-wrap:anywhere;}
631 + .bulk-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;}
632 + .bulk-bar .sep{width:1px;height:18px;background:#ddd;margin:0 4px;}
633 + .bulk-status{font-size:12px;color:#666;margin-left:6px;white-space:normal;overflow-wrap:anywhere;}
634 + @media (max-width:768px){.video-display-grid{grid-template-columns:1fr}}
635 +</style>
636 +{{/html}}
637 +{{/cache}}
638 +{{/velocity}}
639 +
640 +
641 +
178 178  == Sources ==
179 179  
180 180  * **HIAS Historical Archives**: Official organizational records documenting refugee assistance efforts