0 Votes

Changes for page Uncategorized Videos

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

From version 503.1
edited by Ryan C
on 2025/09/10 06:18
Change comment: There is no comment for this version
To version 504.1
edited by Ryan C
on 2025/09/10 06:24
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -117,176 +117,176 @@
117 117  </div>
118 118  
119 119  <script>
120 - /* replace your makePoster with this */
121 - async function makePoster(frame) {
122 - if (frame.getAttribute('data-poster-ready') === '1') return;
120 +(function(){
121 + // ---- Helpers
122 + function spacesPath(dotPath){
123 + if(!dotPath) return '';
124 + return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/');
125 + }
126 + function parseFullName(full){
127 + // Accept "xwiki:Main.Sub.Page" or "Main.Sub.Page"
128 + full = String(full || '').replace(/^[^:]+:/,'');
129 + var parts = full.split('.');
130 + var page = parts.pop();
131 + return {spacePath: parts.join('.'), page: page};
132 + }
133 +
134 + // ---- Posters: reliable first-frame capture
135 + async function makePoster(frame){
136 + if(frame.getAttribute('data-poster-ready')==='1') return;
123 123   var src = frame.getAttribute('data-src');
124 - try {
138 + try{
125 125   var v = document.createElement('video');
126 126   v.preload = 'metadata';
127 - v.muted = true; // allow autoplay on some browsers if needed
128 - v.playsInline = true;
129 - v.crossOrigin = 'anonymous'; // harmless on same-origin; avoids taint if proxied
130 -
131 - // Promise that resolves after we actually have a decodable frame
132 - function once(target, evt) { return new Promise(function (res) { target.addEventListener(evt, res, { once: true }); }); }
141 + v.muted = true; v.playsInline = true;
142 + v.crossOrigin = 'anonymous';
143 + function once(t,ev){ return new Promise(function(res){ t.addEventListener(ev,res,{once:true}); }); }
133 133   v.src = src;
134 134  
135 - // Wait for dimensions
136 136   await once(v, 'loadedmetadata');
137 -
138 - // Prefer rVFC when available (more reliable than seek for some codecs)
139 139   if (typeof v.requestVideoFrameCallback === 'function') {
140 - await new Promise(function (res) { v.requestVideoFrameCallback(function () { res(); }); });
148 + await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); });
141 141   } else {
142 - // Seek a bit into the file so we land on a keyframe
143 - try { v.currentTime = Math.min(0.25, (v.seekable && v.seekable.length ? v.seekable.start(0) + 0.25 : 0.25)); } catch (e) { }
150 + try { v.currentTime = Math.min(0.25, (v.seekable && v.seekable.length ? v.seekable.start(0) + 0.25 : 0.25)); } catch(e){}
144 144   await once(v, 'seeked');
145 - // Some browsers still need data decoded:
146 - try { await once(v, 'loadeddata'); } catch (e) { }
152 + try { await once(v, 'loadeddata'); } catch(e){}
147 147   }
148 148  
149 149   var canvas = frame.querySelector('.vid-canvas');
150 - if (canvas) {
151 - var w = 320, h = Math.round(320 * (v.videoHeight || 9) / (v.videoWidth || 16));
152 - canvas.width = 320; canvas.height = h > 0 ? h : 180;
153 - var ctx = canvas.getContext('2d', { willReadFrequently: true });
156 + if(canvas){
157 + var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
158 + canvas.width = 320; canvas.height = h>0?h:180;
159 + var ctx = canvas.getContext('2d', {willReadFrequently:true});
154 154   ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
155 -
156 - // cache the poster for when we mount the real <video>
157 157   try {
158 158   var dataURL = canvas.toDataURL('image/webp', 0.85);
159 159   frame.setAttribute('data-poster', dataURL);
160 - } catch (e) { /* ignore */ }
164 + } catch(e){}
161 161   }
162 - frame.setAttribute('data-poster-ready', '1');
163 - } catch (e) {
164 - // keep dark fallback
165 - }
166 + frame.setAttribute('data-poster-ready','1');
167 + }catch(e){/* keep dark box */}
166 166   }
167 167  
168 - /* replace your mountVideo with this */
169 - function mountVideo(frame) {
170 - if (frame.getAttribute('data-mounted') === '1') return;
170 + // ---- Mount real <video> (uses cached poster)
171 + function mountVideo(frame){
172 + if(frame.getAttribute('data-mounted')==='1') return;
171 171   var src = frame.getAttribute('data-src');
172 172   var type = frame.getAttribute('data-type') || 'video/mp4';
173 - var poster = frame.getAttribute('data-poster'); // from makePoster()
175 + var poster = frame.getAttribute('data-poster');
174 174  
175 175   var v = document.createElement('video');
176 - v.setAttribute('controls', '');
177 - v.setAttribute('preload', 'none'); // keep perf
178 - if (poster) v.setAttribute('poster', poster); // show the captured image
179 - v.style.width = '100%'; v.style.maxWidth = '100%'; v.style.borderRadius = '4px';
178 + v.setAttribute('controls','');
179 + v.setAttribute('preload','none');
180 + if(poster) v.setAttribute('poster', poster);
181 + v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px';
180 180  
181 181   var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s);
182 -
183 - v.addEventListener('loadedmetadata', function () {
184 - var d = Math.round(v.duration || 0), mm = Math.floor(d / 60), ss = String(d % 60).padStart(2, '0');
184 + v.addEventListener('loadedmetadata', function(){
185 + var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
185 185   var dur = frame.parentElement.querySelector('.vid-duration');
186 - if (dur) dur.textContent = 'Duration: ' + mm + ':' + ss;
187 + if(dur) dur.textContent = 'Duration: '+mm+':'+ss;
187 187   });
188 188  
189 189   frame.replaceChildren(v);
190 - frame.setAttribute('data-mounted', '1');
191 + frame.setAttribute('data-mounted','1');
191 191   }
192 -</script>
193 193  
194 -// ---- Lazy poster
195 -if('IntersectionObserver' in window){
196 -var io = new IntersectionObserver(function(entries){
197 -entries.forEach(function(e){
198 -if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
199 -});
200 -}, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
201 -document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
202 -}
194 + // ---- Lazy posters for near-viewport frames
195 + if('IntersectionObserver' in window){
196 + var io = new IntersectionObserver(function(entries){
197 + entries.forEach(function(e){
198 + if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); }
199 + });
200 + }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
201 + document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); });
202 + }
203 203  
204 -// ---- Click to load & play
205 -document.addEventListener('click', function(ev){
206 -var frame = ev.target.closest('.video-frame');
207 -if(frame){
208 -mountVideo(frame);
209 -var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
210 -}
211 -});
204 + // ---- Click to load & play
205 + document.addEventListener('click', function(ev){
206 + var frame = ev.target.closest('.video-frame');
207 + if(frame){
208 + mountVideo(frame);
209 + var v = frame.querySelector('video'); if(v) v.play().catch(function(){});
210 + }
211 + });
212 212  
213 -// ---- Chunk reveal
214 -document.addEventListener('click', function(ev){
215 -var b = ev.target.closest('.load-more'); if(!b) return;
216 -var next = b.getAttribute('data-next');
217 -var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
218 -if(nxt){ nxt.style.display = 'block'; b.parentElement.style.display = 'none'; }
219 -});
213 + // ---- Chunk reveal
214 + document.addEventListener('click', function(ev){
215 + var b = ev.target.closest('.load-more'); if(!b) return;
216 + var next = b.getAttribute('data-next');
217 + var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
218 + if(nxt){ nxt.style.display = 'block'; b.parentElement.style.display = 'none'; }
219 + });
220 220  
221 -// ---- Move: when a page is picked in the native Page Picker (fires change)
222 -var wiki = window.XWIKI_WIKI;
223 -function moveAttachment(opts){
224 -return (async function(){
225 -var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
226 -var pf = (function(full){ full = String(full||'').replace(/^[^:]+:/,''); var p=full.split('.'); var page=p.pop(); return
227 -{spacePath:p.join('.'), page:page}; })(dstFull);
228 -var srcSpacesPath = spacesPath(srcSpace), dstSpacesPath = spacesPath(pf.spacePath);
221 + // ---- Move: Page Picker -> PUT/DELETE via REST
222 + var wiki = window.XWIKI_WIKI;
223 + function moveAttachment(opts){
224 + return (async function(){
225 + var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull;
226 + var pf = parseFullName(dstFull);
227 + var srcSpacesPath = spacesPath(srcSpace), dstSpacesPath = spacesPath(pf.spacePath);
229 229  
230 -// Download -> Upload -> Delete original
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');
229 + var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
230 + var inputEl = document.querySelector(sel);
231 + var card = inputEl ? inputEl.closest('.video-container') : null;
232 + var frame = card ? card.querySelector('.video-frame') : null;
233 + var srcURL = frame ? frame.getAttribute('data-src') : null;
234 + if(!srcURL) throw new Error('Missing source URL');
237 237  
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();
236 + var downloading = await fetch(srcURL, {credentials:'same-origin'});
237 + if(!downloading.ok) throw new Error('Download failed: ' + downloading.status);
238 + var blob = await downloading.blob();
241 241  
242 -var putURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + dstSpacesPath +
243 -'/pages/' + encodeURIComponent(pf.page) +
244 -'/attachments/' + encodeURIComponent(filename) + '?media=json';
245 -var uploading = await fetch(putURL, {
246 -method: 'PUT',
247 -body: blob,
248 -headers: {'Content-Type':'application/octet-stream'},
249 -credentials:'same-origin'
250 -});
251 -if(!(uploading.status===201 || uploading.status===202)){
252 -var txt = await uploading.text().catch(function(){ return String(uploading.status); });
253 -throw new Error('Upload failed: ' + txt);
254 -}
240 + var putURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + dstSpacesPath +
241 + '/pages/' + encodeURIComponent(pf.page) +
242 + '/attachments/' + encodeURIComponent(filename) + '?media=json';
243 + var uploading = await fetch(putURL, {
244 + method: 'PUT',
245 + body: blob,
246 + headers: {'Content-Type':'application/octet-stream'},
247 + credentials:'same-origin'
248 + });
249 + if(!(uploading.status===201 || uploading.status===202)){
250 + var txt = await uploading.text().catch(function(){ return String(uploading.status); });
251 + throw new Error('Upload failed: ' + txt);
252 + }
255 255  
256 -var delURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + srcSpacesPath +
257 -'/pages/' + encodeURIComponent(srcPage) +
258 -'/attachments/' + encodeURIComponent(filename);
259 -var deleting = await fetch(delURL, {method:'DELETE', credentials:'same-origin'});
260 -if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
261 -return true;
262 -})();
263 -}
254 + var delURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + srcSpacesPath +
255 + '/pages/' + encodeURIComponent(srcPage) +
256 + '/attachments/' + encodeURIComponent(filename);
257 + var deleting = await fetch(delURL, {method:'DELETE', credentials:'same-origin'});
258 + if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
259 + return true;
260 + })();
261 + }
264 264  
265 -document.addEventListener('change', function(e){
266 -var inp = e.target.closest('.move-input.suggest-pages'); if(!inp) return;
267 -var selected = (inp.value || '').trim(); if(!selected) return;
263 + // Fire move when native Page Picker changes the input value
264 + document.addEventListener('change', function(e){
265 + var inp = e.target.closest('.move-input.suggest-pages'); if(!inp) return;
266 + var selected = (inp.value || '').trim(); if(!selected) return;
268 268  
269 -var filename = inp.getAttribute('data-filename');
270 -var notice = document.createElement('div');
271 -notice.style.fontSize='12px'; notice.style.color='#666'; notice.textContent='Moving…';
272 -inp.parentElement.appendChild(notice);
268 + var filename = inp.getAttribute('data-filename');
269 + var notice = document.createElement('div');
270 + notice.style.fontSize='12px'; notice.style.color='#666'; notice.textContent='Moving…';
271 + inp.parentElement.appendChild(notice);
273 273  
274 -moveAttachment({
275 -srcSpace: window.SOURCE_SPACE,
276 -srcPage: window.SOURCE_PAGE,
277 -filename: filename,
278 -dstFull: selected
279 -}).then(function(){
280 -notice.textContent = 'Moved ✔ — reloading…';
281 -setTimeout(function(){ location.reload(); }, 600);
282 -}).catch(function(err){
283 -notice.style.color = '#b00020';
284 -notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
285 -});
286 -});
273 + moveAttachment({
274 + srcSpace: window.SOURCE_SPACE,
275 + srcPage: window.SOURCE_PAGE,
276 + filename: filename,
277 + dstFull: selected
278 + }).then(function(){
279 + notice.textContent = 'Moved ✔ — reloading…';
280 + setTimeout(function(){ location.reload(); }, 600);
281 + }).catch(function(err){
282 + notice.style.color = '#b00020';
283 + notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
284 + });
285 + });
287 287  })();
288 288  </script>
289 289  
289 +
290 290  <style>
291 291   .video-container:hover {
292 292   box-shadow: 0 4px 8px rgba(0, 0, 0, 0.08);