0 Votes

Wiki source code of Uncategorized Videos

Last modified by Ryan C on 2025/09/10 07:29

Hide last authors
Ryan C 493.1 1 {{velocity}}
Ryan C 507.1 2 #pagePicker_import
Ryan C 499.2 3
Ryan C 507.1 4 ## 1) Collect video attachments on this page
Ryan C 503.1 5 #set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v'])
6 #set($videos = [])
Ryan C 493.1 7 #foreach($att in $doc.getAttachmentList())
Ryan C 507.1 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
Ryan C 493.1 16 #end
17
Ryan C 507.1 18 ## 2) Cache HTML (auto-bust on version)
Ryan C 500.1 19 {{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}}
Ryan C 493.1 20 {{html wiki="false" clean="false"}}
21 <div id="xwiki-video-manager" style="margin:20px 0;">
Ryan C 494.1 22 <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2>
Ryan C 493.1 23
Ryan C 494.1 24 #if($videos.size() == 0)
Ryan C 507.1 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>
Ryan C 494.1 29 #else
Ryan C 507.1 30 <script>
31 window.VID_CHUNK_SIZE = 48;
32 window.VID_LAZY_MARGIN = '600px';
33 window.XWIKI_WIKI = ${jsontool.serialize($xcontext.database)};
34 window.SOURCE_SPACE = ${jsontool.serialize($doc.space)};
35 window.SOURCE_PAGE = ${jsontool.serialize($doc.name)};
36 </script>
Ryan C 493.1 37
Ryan C 507.1 38 <div id="video-chunks">
39 #set($i = 0)
40 #set($chunkIndex = 0)
41 #foreach($att in $videos)
42 #set($i = $i + 1)
43 #set($filename = $att.getFilename())
44 #set($lname = $filename.toLowerCase())
45 #set($url = $doc.getAttachmentURL($filename))
Ryan C 493.1 46
Ryan C 507.1 47 ## MIME guess
48 #set($videoType = "video/mp4")
49 #if($lname.endsWith(".webm"))
50 #set($videoType = "video/webm")
51 #elseif($lname.endsWith(".ogg"))
52 #set($videoType = "video/ogg")
53 #elseif($lname.endsWith(".avi"))
54 #set($videoType = "video/x-msvideo")
55 #elseif($lname.endsWith(".mov"))
56 #set($videoType = "video/quicktime")
57 #elseif($lname.endsWith(".wmv"))
58 #set($videoType = "video/x-ms-wmv")
59 #elseif($lname.endsWith(".flv"))
60 #set($videoType = "video/x-flv")
61 #elseif($lname.endsWith(".m4v"))
62 #set($videoType = "video/mp4")
63 #end
Ryan C 493.1 64
Ryan C 507.1 65 #if($i == 1 || ($i - 1) % 48 == 0)
66 #set($chunkIndex = $chunkIndex + 1)
67 <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;">
68 <div class="video-display-grid">
Ryan C 494.1 69 #end
Ryan C 493.1 70
Ryan C 507.1 71 <div class="video-container">
72 <div class="video-header">
73 <h4 class="video-title">${escapetool.xml($filename)}</h4>
Ryan C 494.1 74 </div>
Ryan C 493.1 75
Ryan C 507.1 76 <div class="video-frame"
77 data-src="${url}"
78 data-type="${videoType}"
79 data-name="${escapetool.xml($filename)}">
80 <canvas class="vid-canvas" width="320" height="180"></canvas>
81 <button class="btn btn-sm btn-primary" type="button">Load &amp; Play</button>
Ryan C 494.1 82 </div>
83
Ryan C 507.1 84 <div class="video-controls">
Ryan C 494.1 85 <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
Ryan C 507.1 86 <span class="vid-duration">Duration: —</span>
Ryan C 494.1 87 </div>
Ryan C 499.1 88
Ryan C 507.1 89 <!-- Move-to-page -->
90 <div class="move-box">
91 <label>Move to page:</label>
92 <div class="move-row">
93 <input type="text"
94 class="move-input suggest-pages"
95 placeholder="Find a page…"
96 data-search-scope="wiki:${escapetool.xml($xcontext.database)}"
97 data-filename="${escapetool.xml($filename)}">
98 <button type="button"
99 class="btn btn-sm btn-warning move-go"
100 data-filename="${escapetool.xml($filename)}">Move</button>
101 </div>
102 <small class="hint">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small>
103 </div>
104 </div> <!-- .video-container -->
Ryan C 493.1 105
Ryan C 494.1 106 #if(($i % 48 == 0) || $foreach.last)
Ryan C 507.1 107 </div> <!-- .video-display-grid -->
108 #if(!$foreach.last)
109 <div class="loadmore-wrap">
110 <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button>
111 </div>
112 #end
113 </div> <!-- .vid-chunk -->
114 #end
Ryan C 494.1 115 #end
116 </div>
117 #end
Ryan C 493.1 118 </div>
119
120 <script>
Ryan C 504.1 121 (function(){
Ryan C 507.1 122 /* ===== constants & helpers ===== */
Ryan C 505.2 123 var WIKI = window.XWIKI_WIKI;
Ryan C 507.1 124 var CURRENT_SPACE = document.documentElement.getAttribute('data-xwiki-space') || (window.SOURCE_SPACE || 'Main');
125 var ROOT_SPACE = CURRENT_SPACE.split('.')[0];
Ryan C 505.2 126
Ryan C 504.1 127 function spacesPath(dotPath){
128 if(!dotPath) return '';
Ryan C 507.1 129 return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/');
Ryan C 504.1 130 }
131 function parseFullName(full){
Ryan C 507.1 132 full = String(full||'').replace(/^[^:]+:/,'');
Ryan C 504.1 133 var parts = full.split('.');
134 var page = parts.pop();
Ryan C 507.1 135 return {spacePath: parts.join('.'), page: page};
Ryan C 504.1 136 }
Ryan C 507.1 137 function getFormToken(){
138 return document.documentElement.getAttribute('data-xwiki-form-token') || '';
139 }
Ryan C 504.1 140
Ryan C 507.1 141 /* ===== existence check (prevents DocumentDoesNotExist) ===== */
142 async function docExists(fullRef){
143 var p = parseFullName(fullRef);
144 var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' +
145 spacesPath(p.spacePath) + '/pages/' + encodeURIComponent(p.page) + '?media=json';
146 var r = await fetch(url, {credentials:'same-origin'});
147 return r.status === 200;
148 }
149
150 /* ===== robust resolver for Page Picker / typed titles ===== */
Ryan C 505.2 151 async function resolveReference(inp){
Ryan C 507.1 152 var ref = inp.getAttribute('data-reference') || (inp.dataset && inp.dataset.reference) || '';
153 var raw = (inp.value || '').trim();
Ryan C 505.2 154
Ryan C 507.1 155 // If the picker stored a reference, try it (page then WebHome)
156 if (ref){
157 if (/\.WebHome$/i.test(ref)) {
158 if (await docExists(ref)) return ref;
159 } else {
160 if (await docExists(ref)) return ref;
161 var rh = ref + '.WebHome'; if (await docExists(rh)) return rh;
162 }
Ryan C 506.1 163 }
Ryan C 505.2 164
Ryan C 507.1 165 // If only a title was typed, qualify under root space
166 var base = raw.indexOf('.') === -1 ? (ROOT_SPACE + '.' + raw) : raw;
167
168 if (await docExists(base)) return base;
169 var home2 = /\.WebHome$/i.test(base) ? base : (base + '.WebHome');
170 if (await docExists(home2)) return home2;
171
172 throw new Error('Target page not found: "' + raw + '". Choose a suggestion or type a full reference like "Main Categories.SomePage".');
Ryan C 505.2 173 }
174
Ryan C 507.1 175 /* ===== posters & video mount ===== */
Ryan C 504.1 176 async function makePoster(frame){
177 if(frame.getAttribute('data-poster-ready')==='1') return;
Ryan C 503.1 178 var src = frame.getAttribute('data-src');
Ryan C 504.1 179 try{
Ryan C 503.1 180 var v = document.createElement('video');
Ryan C 505.2 181 v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src;
Ryan C 505.1 182 function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});}
Ryan C 507.1 183 await once(v,'loadedmetadata');
184 if (typeof v.requestVideoFrameCallback === 'function'){
Ryan C 505.2 185 await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); });
Ryan C 503.1 186 } else {
Ryan C 505.2 187 try { v.currentTime = 0.25; } catch(e){}
Ryan C 505.1 188 await once(v,'seeked').catch(function(){});
189 await once(v,'loadeddata').catch(function(){});
Ryan C 503.1 190 }
191 var canvas = frame.querySelector('.vid-canvas');
Ryan C 504.1 192 if(canvas){
193 var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
194 canvas.width = 320; canvas.height = h>0?h:180;
Ryan C 505.2 195 var ctx = canvas.getContext('2d', {willReadFrequently:true});
Ryan C 503.1 196 ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
Ryan C 505.2 197 try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){}
Ryan C 503.1 198 }
Ryan C 504.1 199 frame.setAttribute('data-poster-ready','1');
Ryan C 505.1 200 }catch(e){}
Ryan C 494.1 201 }
202
Ryan C 504.1 203 function mountVideo(frame){
204 if(frame.getAttribute('data-mounted')==='1') return;
Ryan C 503.1 205 var src = frame.getAttribute('data-src');
206 var type = frame.getAttribute('data-type') || 'video/mp4';
Ryan C 504.1 207 var poster = frame.getAttribute('data-poster');
Ryan C 507.1 208
Ryan C 503.1 209 var v = document.createElement('video');
Ryan C 505.1 210 v.setAttribute('controls',''); v.setAttribute('preload','none');
Ryan C 504.1 211 if(poster) v.setAttribute('poster', poster);
212 v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px';
Ryan C 507.1 213
214 var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s);
Ryan C 504.1 215 v.addEventListener('loadedmetadata', function(){
216 var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
Ryan C 507.1 217 var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent = 'Duration: '+mm+':'+ss;
Ryan C 503.1 218 });
Ryan C 507.1 219
Ryan C 503.1 220 frame.replaceChildren(v);
Ryan C 504.1 221 frame.setAttribute('data-mounted','1');
Ryan C 503.1 222 }
Ryan C 502.1 223
Ryan C 507.1 224 /* ===== observers & UI wiring ===== */
Ryan C 504.1 225 if('IntersectionObserver' in window){
226 var io = new IntersectionObserver(function(entries){
227 entries.forEach(function(e){
228 if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
229 });
230 }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
231 document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
232 }
Ryan C 494.1 233
Ryan C 504.1 234 document.addEventListener('click', function(ev){
235 var frame = ev.target.closest('.video-frame');
236 if(frame){
237 mountVideo(frame);
238 var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
239 }
240 });
Ryan C 499.1 241
Ryan C 504.1 242 document.addEventListener('click', function(ev){
243 var b = ev.target.closest('.load-more'); if(!b) return;
244 var next = b.getAttribute('data-next');
245 var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
Ryan C 505.1 246 if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; }
Ryan C 504.1 247 });
Ryan C 494.1 248
Ryan C 507.1 249 /* ===== move: download -> PUT -> DELETE ===== */
Ryan C 505.1 250 async function moveAttachment(opts){
Ryan C 507.1 251 var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
Ryan C 505.1 252 var pf = parseFullName(dstFull);
253 var srcSpacesPath = spacesPath(srcSpace);
254 var dstSpacesPath = spacesPath(pf.spacePath);
Ryan C 499.1 255
Ryan C 505.2 256 var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
257 var inputEl = document.querySelector(sel);
Ryan C 505.1 258 var card = inputEl ? inputEl.closest('.video-container') : null;
259 var frame = card ? card.querySelector('.video-frame') : null;
260 var srcURL = frame ? frame.getAttribute('data-src') : null;
261 if(!srcURL) throw new Error('Missing source URL');
Ryan C 499.2 262
Ryan C 505.1 263 var downloading = await fetch(srcURL, {credentials:'same-origin'});
Ryan C 505.2 264 if(!downloading.ok) throw new Error('Download failed: ' + downloading.status);
Ryan C 505.1 265 var blob = await downloading.blob();
Ryan C 499.2 266
Ryan C 507.1 267 var token = getFormToken();
Ryan C 505.1 268
Ryan C 505.2 269 var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath +
270 '/pages/' + encodeURIComponent(pf.page) +
271 '/attachments/' + encodeURIComponent(filename) + '?media=json';
Ryan C 505.1 272 var uploading = await fetch(putURL, {
273 method: 'PUT',
274 body: blob,
Ryan C 505.2 275 headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token},
Ryan C 505.1 276 credentials:'same-origin'
277 });
278 if(!(uploading.status===201 || uploading.status===202)){
279 var txt = await uploading.text().catch(function(){ return String(uploading.status); });
280 throw new Error('Upload failed: ' + txt);
281 }
282
Ryan C 505.2 283 var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
284 '/pages/' + encodeURIComponent(srcPage) +
285 '/attachments/' + encodeURIComponent(filename);
Ryan C 507.1 286 var deleting = await fetch(delURL, {method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'});
Ryan C 505.2 287 if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
Ryan C 505.1 288 return true;
Ryan C 504.1 289 }
Ryan C 499.1 290
Ryan C 507.1 291 /* ===== Move button ===== */
Ryan C 505.1 292 document.addEventListener('click', function(e){
293 var btn = e.target.closest('.move-go'); if(!btn) return;
294 var box = btn.closest('.move-box');
295 var inp = box.querySelector('.move-input');
Ryan C 505.2 296 (async function(){
Ryan C 507.1 297 var notice = box.querySelector('.move-notice');
298 if(!notice){ notice = document.createElement('div'); notice.className = 'move-notice'; box.appendChild(notice); }
Ryan C 505.2 299 try{
300 var ref = await resolveReference(inp);
301 var filename = btn.getAttribute('data-filename');
Ryan C 507.1 302 notice.style.color = '#666';
Ryan C 505.2 303 notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …';
Ryan C 499.2 304
Ryan C 505.2 305 await moveAttachment({
306 srcSpace: window.SOURCE_SPACE,
307 srcPage: window.SOURCE_PAGE,
308 filename: filename,
309 dstFull: ref
310 });
311
312 notice.textContent = 'Moved ✔ — reloading…';
313 setTimeout(function(){ location.reload(); }, 600);
314 }catch(err){
315 notice.style.color = '#b00020';
316 notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
317 }
318 })();
Ryan C 504.1 319 });
Ryan C 505.1 320
Ryan C 494.1 321 })();
Ryan C 493.1 322 </script>
323
324 <style>
Ryan C 507.1 325 .video-display-grid{
326 display:grid;
327 grid-template-columns:repeat(auto-fit,minmax(320px,1fr));
328 gap:20px; align-items:start;
Ryan C 503.1 329 }
Ryan C 507.1 330 .video-container{
331 border:1px solid #ddd; border-radius:8px; padding:12px; background:#fff;
Ryan C 503.1 332 }
Ryan C 507.1 333 .video-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;}
334 .video-title{margin:0;flex:1;min-width:0;word-break:break-word;}
335 .video-frame{
336 position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px;
337 overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer;
Ryan C 503.1 338 }
Ryan C 507.1 339 .vid-canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;}
340 .video-controls{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;}
341 .move-box{margin-top:10px;}
342 .move-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;}
343 .move-input{flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;}
344 .hint{color:#888;}
345 .loadmore-wrap{text-align:center;margin:12px 0 28px;}
346 .btn{padding:4px 8px;border:1px solid #ddd;background:#f8f9fa;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;}
347 .btn:hover{background:#e9ecef;}
348 .btn-primary{background:#007bff;color:#fff;border-color:#007bff;}
349 .btn-secondary{background:#6c757d;color:#fff;border-color:#6c757d;}
350 .btn-success{background:#28a745;color:#fff;border-color:#28a745;}
351 .btn-sm{font-size:12px;padding:2px 6px;}
352 .move-notice{font-size:12px;color:#666;margin-top:6px;max-height:6.5em;overflow:auto;white-space:normal;overflow-wrap:anywhere;}
353 @media (max-width:768px){.video-display-grid{grid-template-columns:1fr}}
Ryan C 493.1 354 </style>
355 {{/html}}
Ryan C 494.1 356 {{/cache}}
Ryan C 501.1 357 {{/velocity}}
Ryan C 507.1 358