0 Votes

Wiki source code of Uncategorized Videos

Version 505.1 by Ryan C on 2025/09/10 06:45

Hide last authors
Ryan C 493.1 1 {{velocity}}
Ryan C 503.1 2 #pagePicker_import ## load the native Page Picker resources once
Ryan C 499.2 3
Ryan C 501.1 4 ## 1) Collect video attachments from the current 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 503.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
Ryan C 493.1 14 #end
Ryan C 503.1 15 #end
16 #end
Ryan C 493.1 17
Ryan C 501.1 18 ## 2) Cache the rendered HTML (auto-bust on each save via 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 503.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 503.1 30 <script>
31 window.VID_CHUNK_SIZE = 48;
32 window.VID_LAZY_MARGIN = '600px';
33 window.XWIKI_WIKI = ${ jsontool.serialize($xcontext.database) }; // e.g., "xwiki"
34 window.SOURCE_SPACE = ${ jsontool.serialize($doc.space) }; // e.g., "Main" or "Main.Sub"
35 window.SOURCE_PAGE = ${ jsontool.serialize($doc.name) }; // e.g., "WebHome"
36 </script>
Ryan C 493.1 37
Ryan C 503.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 503.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 503.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"
69 style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:20px;">
Ryan C 494.1 70 #end
Ryan C 493.1 71
Ryan C 494.1 72 <div class="video-container" style="border:1px solid #ddd;border-radius:8px;padding:12px;background:#fff;">
73 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
74 <h4 style="margin:0;flex:1;min-width:0;">${escapetool.xml($filename)}</h4>
75 </div>
Ryan C 493.1 76
Ryan C 501.1 77 <!-- Placeholder (poster drawn on-demand) -->
Ryan C 503.1 78 <div class="video-frame" data-src="${url}" data-type="${videoType}" data-name="${escapetool.xml($filename)}"
79 style="position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px;overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer;">
80 <canvas class="vid-canvas" width="320" height="180"
81 style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></canvas>
82 <button class="btn btn-sm btn-primary" type="button" style="position:relative;z-index:1;">Load &amp;
83 Play</button>
Ryan C 494.1 84 </div>
85
Ryan C 499.1 86 <div class="video-controls" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;">
Ryan C 494.1 87 <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
88 <span class="vid-duration" style="font-size:12px;color:#666;">Duration: —</span>
89 </div>
Ryan C 499.1 90
Ryan C 505.1 91 <!-- Move-to-page (native Page Picker) -->
92 <div class="move-box" style="margin-top:10px;">
93 <label style="font-size:12px;color:#555;">Move to page:</label>
94 <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;">
95 <input type="text"
96 class="move-input suggest-pages"
97 placeholder="Find a page…"
98 data-search-scope="wiki:${escapetool.xml($xcontext.database)}"
99 data-filename="${escapetool.xml($filename)}"
100 style="flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;">
101 <button type="button"
102 class="btn btn-sm btn-warning move-go"
103 data-filename="${escapetool.xml($filename)}">Move</button>
104 </div>
105 <small style="color:#888;">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small>
106 </div>
Ryan C 493.1 107
Ryan C 505.1 108
Ryan C 494.1 109 #if(($i % 48 == 0) || $foreach.last)
Ryan C 503.1 110 </div>
111 #if(!$foreach.last)
112 <div style="text-align:center;margin:12px 0 28px;">
113 <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button>
114 </div>
Ryan C 494.1 115 #end
116 </div>
Ryan C 503.1 117 #end
118 #end
119 </div>
Ryan C 494.1 120 #end
Ryan C 493.1 121 </div>
122
123 <script>
Ryan C 504.1 124 (function(){
Ryan C 505.1 125 /* -------- helpers -------- */
Ryan C 504.1 126 function spacesPath(dotPath){
127 if(!dotPath) return '';
Ryan C 505.1 128 return dotPath.split('.').map(function(s){ return 'spaces/'+encodeURIComponent(s); }).join('/');
Ryan C 504.1 129 }
130 function parseFullName(full){
Ryan C 505.1 131 full = String(full||'').replace(/^[^:]+:/,''); // drop wiki: if present
Ryan C 504.1 132 var parts = full.split('.');
133 var page = parts.pop();
Ryan C 505.1 134 return { spacePath: parts.join('.'), page: page };
Ryan C 504.1 135 }
136
Ryan C 505.1 137 /* -------- thumbnail (poster) generation, unchanged idea -------- */
Ryan C 504.1 138 async function makePoster(frame){
139 if(frame.getAttribute('data-poster-ready')==='1') return;
Ryan C 503.1 140 var src = frame.getAttribute('data-src');
Ryan C 504.1 141 try{
Ryan C 503.1 142 var v = document.createElement('video');
143 v.preload = 'metadata';
Ryan C 504.1 144 v.muted = true; v.playsInline = true;
Ryan C 503.1 145 v.src = src;
Ryan C 494.1 146
Ryan C 505.1 147 function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});}
148 await once(v,'loadedmetadata');
Ryan C 503.1 149 if (typeof v.requestVideoFrameCallback === 'function') {
Ryan C 505.1 150 await new Promise(function(res){ v.requestVideoFrameCallback(function(){res();}); });
Ryan C 503.1 151 } else {
Ryan C 505.1 152 try{ v.currentTime = 0.25; }catch(e){}
153 await once(v,'seeked').catch(function(){});
154 await once(v,'loadeddata').catch(function(){});
Ryan C 503.1 155 }
Ryan C 494.1 156
Ryan C 503.1 157 var canvas = frame.querySelector('.vid-canvas');
Ryan C 504.1 158 if(canvas){
159 var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
160 canvas.width = 320; canvas.height = h>0?h:180;
Ryan C 505.1 161 var ctx = canvas.getContext('2d', { willReadFrequently:true });
Ryan C 503.1 162 ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
Ryan C 505.1 163 try{ frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); }catch(e){}
Ryan C 503.1 164 }
Ryan C 504.1 165 frame.setAttribute('data-poster-ready','1');
Ryan C 505.1 166 }catch(e){}
Ryan C 494.1 167 }
168
Ryan C 504.1 169 function mountVideo(frame){
170 if(frame.getAttribute('data-mounted')==='1') return;
Ryan C 503.1 171 var src = frame.getAttribute('data-src');
172 var type = frame.getAttribute('data-type') || 'video/mp4';
Ryan C 504.1 173 var poster = frame.getAttribute('data-poster');
Ryan C 503.1 174 var v = document.createElement('video');
Ryan C 505.1 175 v.setAttribute('controls',''); v.setAttribute('preload','none');
Ryan C 504.1 176 if(poster) v.setAttribute('poster', poster);
177 v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px';
Ryan C 505.1 178 var s = document.createElement('source'); s.src=src; s.type=type; v.appendChild(s);
Ryan C 504.1 179 v.addEventListener('loadedmetadata', function(){
180 var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
Ryan C 505.1 181 var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent='Duration: '+mm+':'+ss;
Ryan C 503.1 182 });
183 frame.replaceChildren(v);
Ryan C 504.1 184 frame.setAttribute('data-mounted','1');
Ryan C 503.1 185 }
Ryan C 502.1 186
Ryan C 505.1 187 /* -------- lazy posters + chunk reveal -------- */
Ryan C 504.1 188 if('IntersectionObserver' in window){
189 var io = new IntersectionObserver(function(entries){
190 entries.forEach(function(e){
191 if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
192 });
193 }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
194 document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
195 }
Ryan C 494.1 196
Ryan C 504.1 197 document.addEventListener('click', function(ev){
198 var frame = ev.target.closest('.video-frame');
199 if(frame){
200 mountVideo(frame);
201 var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
202 }
203 });
Ryan C 499.1 204
Ryan C 504.1 205 document.addEventListener('click', function(ev){
206 var b = ev.target.closest('.load-more'); if(!b) return;
207 var next = b.getAttribute('data-next');
208 var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
Ryan C 505.1 209 if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; }
Ryan C 504.1 210 });
Ryan C 494.1 211
Ryan C 505.1 212 /* -------- CSRF token (form token) for REST writes -------- */
213 var FORM_TOKEN = null;
214 async function getFormToken(){
215 if (FORM_TOKEN) return FORM_TOKEN;
216 var wiki = window.XWIKI_WIKI;
217 var r = await fetch('/rest/wikis/'+encodeURIComponent(wiki), {credentials:'same-origin'});
218 FORM_TOKEN = r.headers.get('XWiki-Form-Token') || '';
219 return FORM_TOKEN;
220 }
Ryan C 499.2 221
Ryan C 505.1 222 /* -------- move attachment: download -> PUT -> DELETE -------- */
223 async function moveAttachment(opts){
224 var wiki = window.XWIKI_WIKI;
225 var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
226 var pf = parseFullName(dstFull);
227 var srcSpacesPath = spacesPath(srcSpace);
228 var dstSpacesPath = spacesPath(pf.spacePath);
Ryan C 499.1 229
Ryan C 505.1 230 // Find the card to read the current download URL
231 var cardInputSel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
232 var inputEl = document.querySelector(cardInputSel);
233 var card = inputEl ? inputEl.closest('.video-container') : null;
234 var frame = card ? card.querySelector('.video-frame') : null;
235 var srcURL = frame ? frame.getAttribute('data-src') : null;
236 if(!srcURL) throw new Error('Missing source URL');
Ryan C 499.2 237
Ryan C 505.1 238 var downloading = await fetch(srcURL, {credentials:'same-origin'});
239 if(!downloading.ok) throw new Error('Download failed: '+downloading.status);
240 var blob = await downloading.blob();
Ryan C 499.2 241
Ryan C 505.1 242 var token = await getFormToken();
243
244 var putURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + dstSpacesPath +
245 '/pages/' + encodeURIComponent(pf.page) +
246 '/attachments/' + encodeURIComponent(filename) + '?media=json';
247
248 var uploading = await fetch(putURL, {
249 method: 'PUT',
250 body: blob,
251 headers: {
252 'Content-Type':'application/octet-stream',
253 'XWiki-Form-Token': token // harmless when not required
254 },
255 credentials:'same-origin'
256 });
257 if(!(uploading.status===201 || uploading.status===202)){
258 var txt = await uploading.text().catch(function(){ return String(uploading.status); });
259 throw new Error('Upload failed: ' + txt);
260 }
261
262 var delURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + srcSpacesPath +
263 '/pages/' + encodeURIComponent(srcPage) +
264 '/attachments/' + encodeURIComponent(filename);
265
266 var deleting = await fetch(delURL, {
267 method:'DELETE',
268 headers: {'XWiki-Form-Token': token},
269 credentials:'same-origin'
270 });
271 if(!(deleting.status===204)){
272 console.warn('Delete original failed', deleting.status);
273 }
274 return true;
Ryan C 504.1 275 }
Ryan C 499.1 276
Ryan C 505.1 277 /* -------- button handler (explicit Move) -------- */
278 document.addEventListener('click', function(e){
279 var btn = e.target.closest('.move-go'); if(!btn) return;
280 var box = btn.closest('.move-box');
281 var inp = box.querySelector('.move-input');
282 var selected = (inp && inp.value ? inp.value.trim() : '');
283 if(!selected){ inp && inp.focus(); return; }
Ryan C 499.1 284
Ryan C 504.1 285 var notice = document.createElement('div');
286 notice.style.fontSize='12px'; notice.style.color='#666'; notice.textContent='Moving…';
Ryan C 505.1 287 box.appendChild(notice);
Ryan C 499.2 288
Ryan C 504.1 289 moveAttachment({
290 srcSpace: window.SOURCE_SPACE,
291 srcPage: window.SOURCE_PAGE,
Ryan C 505.1 292 filename: btn.getAttribute('data-filename'),
Ryan C 504.1 293 dstFull: selected
294 }).then(function(){
295 notice.textContent = 'Moved ✔ — reloading…';
296 setTimeout(function(){ location.reload(); }, 600);
297 }).catch(function(err){
298 notice.style.color = '#b00020';
299 notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
300 });
301 });
Ryan C 505.1 302
303 /* Optional: still move when the picker fires a native change */
304 document.addEventListener('change', function(e){
305 var inp = e.target.closest('.move-input.suggest-pages'); if(!inp) return;
306 // no auto-move here to avoid surprises; the Move button is the canonical trigger
307 });
Ryan C 494.1 308 })();
Ryan C 493.1 309 </script>
310
311 <style>
Ryan C 503.1 312 .video-container:hover {
313 box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);
314 transition: box-shadow .25s;
315 }
316
317 .btn {
318 padding: 4px 8px;
319 border: 1px solid #ddd;
320 background: #f8f9fa;
321 border-radius: 4px;
322 cursor: pointer;
323 text-decoration: none;
324 display: inline-block;
325 }
326
327 .btn:hover {
328 background: #e9ecef;
329 }
330
331 .btn-primary {
332 background: #007bff;
333 color: #fff;
334 border-color: #007bff;
335 }
336
337 .btn-secondary {
338 background: #6c757d;
339 color: #fff;
340 border-color: #6c757d;
341 }
342
343 .btn-success {
344 background: #28a745;
345 color: #fff;
346 border-color: #28a745;
347 }
348
349 .btn-sm {
350 font-size: 12px;
351 padding: 2px 6px;
352 }
353
354 @media (max-width:768px) {
355 .video-display-grid {
356 grid-template-columns: 1fr
357 }
358 }
Ryan C 493.1 359 </style>
360 {{/html}}
Ryan C 494.1 361 {{/cache}}
Ryan C 501.1 362 {{/velocity}}