Wiki source code of Uncategorized Videos
Hide last authors
author | version | line-number | content |
---|---|---|---|
![]() |
493.1 | 1 | {{velocity}} |
![]() |
507.1 | 2 | #pagePicker_import |
![]() |
499.2 | 3 | |
![]() |
507.1 | 4 | ## 1) Collect video attachments on this page |
![]() |
503.1 | 5 | #set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v']) |
6 | #set($videos = []) | ||
![]() |
493.1 | 7 | #foreach($att in $doc.getAttachmentList()) |
![]() |
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 | ||
![]() |
493.1 | 16 | #end |
17 | |||
![]() |
507.1 | 18 | ## 2) Cache HTML (auto-bust on version) |
![]() |
500.1 | 19 | {{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}} |
![]() |
493.1 | 20 | {{html wiki="false" clean="false"}} |
21 | <div id="xwiki-video-manager" style="margin:20px 0;"> | ||
![]() |
494.1 | 22 | <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2> |
![]() |
493.1 | 23 | |
![]() |
494.1 | 24 | #if($videos.size() == 0) |
![]() |
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> | ||
![]() |
494.1 | 29 | #else |
![]() |
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> | ||
![]() |
493.1 | 37 | |
![]() |
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)) | ||
![]() |
493.1 | 46 | |
![]() |
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 | ||
![]() |
493.1 | 64 | |
![]() |
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"> | ||
![]() |
494.1 | 69 | #end |
![]() |
493.1 | 70 | |
![]() |
507.1 | 71 | <div class="video-container"> |
72 | <div class="video-header"> | ||
73 | <h4 class="video-title">${escapetool.xml($filename)}</h4> | ||
![]() |
494.1 | 74 | </div> |
![]() |
493.1 | 75 | |
![]() |
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 & Play</button> | ||
![]() |
494.1 | 82 | </div> |
83 | |||
![]() |
507.1 | 84 | <div class="video-controls"> |
![]() |
494.1 | 85 | <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a> |
![]() |
507.1 | 86 | <span class="vid-duration">Duration: —</span> |
![]() |
494.1 | 87 | </div> |
![]() |
499.1 | 88 | |
![]() |
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 --> | ||
![]() |
493.1 | 105 | |
![]() |
494.1 | 106 | #if(($i % 48 == 0) || $foreach.last) |
![]() |
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 | ||
![]() |
494.1 | 115 | #end |
116 | </div> | ||
117 | #end | ||
![]() |
493.1 | 118 | </div> |
119 | |||
120 | <script> | ||
![]() |
504.1 | 121 | (function(){ |
![]() |
507.1 | 122 | /* ===== constants & helpers ===== */ |
![]() |
505.2 | 123 | var WIKI = window.XWIKI_WIKI; |
![]() |
507.1 | 124 | var CURRENT_SPACE = document.documentElement.getAttribute('data-xwiki-space') || (window.SOURCE_SPACE || 'Main'); |
125 | var ROOT_SPACE = CURRENT_SPACE.split('.')[0]; | ||
![]() |
505.2 | 126 | |
![]() |
504.1 | 127 | function spacesPath(dotPath){ |
128 | if(!dotPath) return ''; | ||
![]() |
507.1 | 129 | return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/'); |
![]() |
504.1 | 130 | } |
131 | function parseFullName(full){ | ||
![]() |
507.1 | 132 | full = String(full||'').replace(/^[^:]+:/,''); |
![]() |
504.1 | 133 | var parts = full.split('.'); |
134 | var page = parts.pop(); | ||
![]() |
507.1 | 135 | return {spacePath: parts.join('.'), page: page}; |
![]() |
504.1 | 136 | } |
![]() |
507.1 | 137 | function getFormToken(){ |
138 | return document.documentElement.getAttribute('data-xwiki-form-token') || ''; | ||
139 | } | ||
![]() |
504.1 | 140 | |
![]() |
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 ===== */ | ||
![]() |
505.2 | 151 | async function resolveReference(inp){ |
![]() |
507.1 | 152 | var ref = inp.getAttribute('data-reference') || (inp.dataset && inp.dataset.reference) || ''; |
153 | var raw = (inp.value || '').trim(); | ||
![]() |
505.2 | 154 | |
![]() |
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 | } | ||
![]() |
506.1 | 163 | } |
![]() |
505.2 | 164 | |
![]() |
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".'); | ||
![]() |
505.2 | 173 | } |
174 | |||
![]() |
507.1 | 175 | /* ===== posters & video mount ===== */ |
![]() |
504.1 | 176 | async function makePoster(frame){ |
177 | if(frame.getAttribute('data-poster-ready')==='1') return; | ||
![]() |
503.1 | 178 | var src = frame.getAttribute('data-src'); |
![]() |
504.1 | 179 | try{ |
![]() |
503.1 | 180 | var v = document.createElement('video'); |
![]() |
505.2 | 181 | v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src; |
![]() |
505.1 | 182 | function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});} |
![]() |
507.1 | 183 | await once(v,'loadedmetadata'); |
184 | if (typeof v.requestVideoFrameCallback === 'function'){ | ||
![]() |
505.2 | 185 | await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); }); |
![]() |
503.1 | 186 | } else { |
![]() |
505.2 | 187 | try { v.currentTime = 0.25; } catch(e){} |
![]() |
505.1 | 188 | await once(v,'seeked').catch(function(){}); |
189 | await once(v,'loadeddata').catch(function(){}); | ||
![]() |
503.1 | 190 | } |
191 | var canvas = frame.querySelector('.vid-canvas'); | ||
![]() |
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; | ||
![]() |
505.2 | 195 | var ctx = canvas.getContext('2d', {willReadFrequently:true}); |
![]() |
503.1 | 196 | ctx.drawImage(v, 0, 0, canvas.width, canvas.height); |
![]() |
505.2 | 197 | try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){} |
![]() |
503.1 | 198 | } |
![]() |
504.1 | 199 | frame.setAttribute('data-poster-ready','1'); |
![]() |
505.1 | 200 | }catch(e){} |
![]() |
494.1 | 201 | } |
202 | |||
![]() |
504.1 | 203 | function mountVideo(frame){ |
204 | if(frame.getAttribute('data-mounted')==='1') return; | ||
![]() |
503.1 | 205 | var src = frame.getAttribute('data-src'); |
206 | var type = frame.getAttribute('data-type') || 'video/mp4'; | ||
![]() |
504.1 | 207 | var poster = frame.getAttribute('data-poster'); |
![]() |
507.1 | 208 | |
![]() |
503.1 | 209 | var v = document.createElement('video'); |
![]() |
505.1 | 210 | v.setAttribute('controls',''); v.setAttribute('preload','none'); |
![]() |
504.1 | 211 | if(poster) v.setAttribute('poster', poster); |
212 | v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px'; | ||
![]() |
507.1 | 213 | |
214 | var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s); | ||
![]() |
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'); | ||
![]() |
507.1 | 217 | var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent = 'Duration: '+mm+':'+ss; |
![]() |
503.1 | 218 | }); |
![]() |
507.1 | 219 | |
![]() |
503.1 | 220 | frame.replaceChildren(v); |
![]() |
504.1 | 221 | frame.setAttribute('data-mounted','1'); |
![]() |
503.1 | 222 | } |
![]() |
502.1 | 223 | |
![]() |
507.1 | 224 | /* ===== observers & UI wiring ===== */ |
![]() |
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 | } | ||
![]() |
494.1 | 233 | |
![]() |
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 | }); | ||
![]() |
499.1 | 241 | |
![]() |
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 +'"]'); | ||
![]() |
505.1 | 246 | if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; } |
![]() |
504.1 | 247 | }); |
![]() |
494.1 | 248 | |
![]() |
507.1 | 249 | /* ===== move: download -> PUT -> DELETE ===== */ |
![]() |
505.1 | 250 | async function moveAttachment(opts){ |
![]() |
507.1 | 251 | var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull; |
![]() |
505.1 | 252 | var pf = parseFullName(dstFull); |
253 | var srcSpacesPath = spacesPath(srcSpace); | ||
254 | var dstSpacesPath = spacesPath(pf.spacePath); | ||
![]() |
499.1 | 255 | |
![]() |
505.2 | 256 | var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]'; |
257 | var inputEl = document.querySelector(sel); | ||
![]() |
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'); | ||
![]() |
499.2 | 262 | |
![]() |
505.1 | 263 | var downloading = await fetch(srcURL, {credentials:'same-origin'}); |
![]() |
505.2 | 264 | if(!downloading.ok) throw new Error('Download failed: ' + downloading.status); |
![]() |
505.1 | 265 | var blob = await downloading.blob(); |
![]() |
499.2 | 266 | |
![]() |
507.1 | 267 | var token = getFormToken(); |
![]() |
505.1 | 268 | |
![]() |
505.2 | 269 | var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath + |
270 | '/pages/' + encodeURIComponent(pf.page) + | ||
271 | '/attachments/' + encodeURIComponent(filename) + '?media=json'; | ||
![]() |
505.1 | 272 | var uploading = await fetch(putURL, { |
273 | method: 'PUT', | ||
274 | body: blob, | ||
![]() |
505.2 | 275 | headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token}, |
![]() |
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 | |||
![]() |
505.2 | 283 | var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath + |
284 | '/pages/' + encodeURIComponent(srcPage) + | ||
285 | '/attachments/' + encodeURIComponent(filename); | ||
![]() |
507.1 | 286 | var deleting = await fetch(delURL, {method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'}); |
![]() |
505.2 | 287 | if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); } |
![]() |
505.1 | 288 | return true; |
![]() |
504.1 | 289 | } |
![]() |
499.1 | 290 | |
![]() |
507.1 | 291 | /* ===== Move button ===== */ |
![]() |
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'); | ||
![]() |
505.2 | 296 | (async function(){ |
![]() |
507.1 | 297 | var notice = box.querySelector('.move-notice'); |
298 | if(!notice){ notice = document.createElement('div'); notice.className = 'move-notice'; box.appendChild(notice); } | ||
![]() |
505.2 | 299 | try{ |
300 | var ref = await resolveReference(inp); | ||
301 | var filename = btn.getAttribute('data-filename'); | ||
![]() |
507.1 | 302 | notice.style.color = '#666'; |
![]() |
505.2 | 303 | notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …'; |
![]() |
499.2 | 304 | |
![]() |
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 | })(); | ||
![]() |
504.1 | 319 | }); |
![]() |
505.1 | 320 | |
![]() |
494.1 | 321 | })(); |
![]() |
493.1 | 322 | </script> |
323 | |||
324 | <style> | ||
![]() |
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; | ||
![]() |
503.1 | 329 | } |
![]() |
507.1 | 330 | .video-container{ |
331 | border:1px solid #ddd; border-radius:8px; padding:12px; background:#fff; | ||
![]() |
503.1 | 332 | } |
![]() |
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; | ||
![]() |
503.1 | 338 | } |
![]() |
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}} | ||
![]() |
493.1 | 354 | </style> |
355 | {{/html}} | ||
![]() |
494.1 | 356 | {{/cache}} |
![]() |
501.1 | 357 | {{/velocity}} |
![]() |
507.1 | 358 |