0 Votes

Changes for page Uncategorized Videos

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

From version 505.1
edited by Ryan C
on 2025/09/10 06:45
Change comment: There is no comment for this version
To version 505.2
edited by Ryan C
on 2025/09/10 07:08
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -104,8 +104,8 @@
104 104   </div>
105 105   <small style="color:#888;">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small>
106 106  </div>
107 +</div> <!-- closes .video-container -->
107 107  
108 -
109 109   #if(($i % 48 == 0) || $foreach.last)
110 110   </div>
111 111   #if(!$foreach.last)
... ... @@ -122,45 +122,79 @@
122 122  
123 123  <script>
124 124  (function(){
125 - /* -------- helpers -------- */
125 + /* ---------- constants ---------- */
126 + var ROOT_SPACE = (window.SOURCE_SPACE || 'Main').split('.')[0]; // e.g., "Main Categories"
127 + var WIKI = window.XWIKI_WIKI;
128 +
129 + /* ---------- helpers ---------- */
126 126   function spacesPath(dotPath){
127 127   if(!dotPath) return '';
128 128   return dotPath.split('.').map(function(s){ return 'spaces/'+encodeURIComponent(s); }).join('/');
129 129   }
130 130   function parseFullName(full){
131 - full = String(full||'').replace(/^[^:]+:/,''); // drop wiki: if present
135 + full = String(full||'').replace(/^[^:]+:/,''); // drop "wiki:" if present
132 132   var parts = full.split('.');
133 133   var page = parts.pop();
134 134   return { spacePath: parts.join('.'), page: page };
135 135   }
136 136  
137 - /* -------- thumbnail (poster) generation, unchanged idea -------- */
141 + /* ---------- resolve a picker input to a full page reference ---------- */
142 + async function resolveReference(inp){
143 + // Some picker builds expose a stored reference on the element:
144 + var refAttr = inp.getAttribute('data-reference') || (inp.dataset ? inp.dataset.reference : '');
145 + if (refAttr && refAttr.indexOf('.') !== -1) return refAttr;
146 +
147 + var v = (inp.value || '').trim();
148 + if (!v) throw new Error('No page selected');
149 +
150 + // If the value already looks like a reference, use it.
151 + if (v.indexOf('.') !== -1) return v;
152 +
153 + // Otherwise, try REST search (title/name) and prefer same root space.
154 + var url = '/rest/wikis/' + encodeURIComponent(WIKI) +
155 + '/search?q=' + encodeURIComponent(v) +
156 + '&scope=title,name&number=8&media=json';
157 + var r = await fetch(url, {credentials:'same-origin'});
158 + if (r.ok) {
159 + var j = await r.json();
160 + var items = (j.searchResults && j.searchResults.searchResult) || [];
161 + var refs = items.map(function(it){
162 + return (it.pageFullName || it.fullName || '').replace(/^.*:/,''); // strip "wiki:"
163 + }).filter(Boolean);
164 +
165 + // Prefer a result under the current root space
166 + var preferred = refs.find(function(f){ return f.startsWith(ROOT_SPACE + '.'); });
167 + if (preferred) return preferred;
168 + if (refs[0]) return refs[0];
169 + }
170 +
171 + // Fallback: assume the user meant a page under the current root space
172 + return ROOT_SPACE + '.' + v;
173 + }
174 +
175 + /* ---------- poster (unchanged core idea, trimmed) ---------- */
138 138   async function makePoster(frame){
139 139   if(frame.getAttribute('data-poster-ready')==='1') return;
140 140   var src = frame.getAttribute('data-src');
141 141   try{
142 142   var v = document.createElement('video');
143 - v.preload = 'metadata';
144 - v.muted = true; v.playsInline = true;
145 - v.src = src;
146 -
181 + v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src;
147 147   function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});}
148 - await once(v,'loadedmetadata');
183 + await once(v, 'loadedmetadata');
149 149   if (typeof v.requestVideoFrameCallback === 'function') {
150 - await new Promise(function(res){ v.requestVideoFrameCallback(function(){res();}); });
185 + await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); });
151 151   } else {
152 - try{ v.currentTime = 0.25; }catch(e){}
187 + try { v.currentTime = 0.25; } catch(e){}
153 153   await once(v,'seeked').catch(function(){});
154 154   await once(v,'loadeddata').catch(function(){});
155 155   }
156 -
157 157   var canvas = frame.querySelector('.vid-canvas');
158 158   if(canvas){
159 159   var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16));
160 160   canvas.width = 320; canvas.height = h>0?h:180;
161 - var ctx = canvas.getContext('2d', { willReadFrequently:true });
195 + var ctx = canvas.getContext('2d', {willReadFrequently:true});
162 162   ctx.drawImage(v, 0, 0, canvas.width, canvas.height);
163 - try{ frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); }catch(e){}
197 + try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){}
164 164   }
165 165   frame.setAttribute('data-poster-ready','1');
166 166   }catch(e){}
... ... @@ -184,7 +184,7 @@
184 184   frame.setAttribute('data-mounted','1');
185 185   }
186 186  
187 - /* -------- lazy posters + chunk reveal -------- */
221 + /* ---------- lazy posters + click handlers ---------- */
188 188   if('IntersectionObserver' in window){
189 189   var io = new IntersectionObserver(function(entries){
190 190   entries.forEach(function(e){
... ... @@ -209,27 +209,25 @@
209 209   if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; }
210 210   });
211 211  
212 - /* -------- CSRF token (form token) for REST writes -------- */
246 + /* ---------- CSRF token (recommended for REST writes) ---------- */
213 213   var FORM_TOKEN = null;
214 214   async function getFormToken(){
215 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'});
250 + var r = await fetch('/rest/wikis/'+encodeURIComponent(WIKI), {credentials:'same-origin'});
218 218   FORM_TOKEN = r.headers.get('XWiki-Form-Token') || '';
219 219   return FORM_TOKEN;
220 220   }
221 221  
222 - /* -------- move attachment: download -> PUT -> DELETE -------- */
255 + /* ---------- move: download -> PUT -> DELETE ---------- */
223 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;
257 + var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename;
258 + var dstFull = opts.dstFull;
226 226   var pf = parseFullName(dstFull);
227 227   var srcSpacesPath = spacesPath(srcSpace);
228 228   var dstSpacesPath = spacesPath(pf.spacePath);
229 229  
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);
263 + var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]';
264 + var inputEl = document.querySelector(sel);
233 233   var card = inputEl ? inputEl.closest('.video-container') : null;
234 234   var frame = card ? card.querySelector('.video-frame') : null;
235 235   var srcURL = frame ? frame.getAttribute('data-src') : null;
... ... @@ -236,22 +236,18 @@
236 236   if(!srcURL) throw new Error('Missing source URL');
237 237  
238 238   var downloading = await fetch(srcURL, {credentials:'same-origin'});
239 - if(!downloading.ok) throw new Error('Download failed: '+downloading.status);
271 + if(!downloading.ok) throw new Error('Download failed: ' + downloading.status);
240 240   var blob = await downloading.blob();
241 241  
242 242   var token = await getFormToken();
243 243  
244 - var putURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + dstSpacesPath +
245 - '/pages/' + encodeURIComponent(pf.page) +
246 - '/attachments/' + encodeURIComponent(filename) + '?media=json';
247 -
276 + var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath +
277 + '/pages/' + encodeURIComponent(pf.page) +
278 + '/attachments/' + encodeURIComponent(filename) + '?media=json';
248 248   var uploading = await fetch(putURL, {
249 249   method: 'PUT',
250 250   body: blob,
251 - headers: {
252 - 'Content-Type':'application/octet-stream',
253 - 'XWiki-Form-Token': token // harmless when not required
254 - },
282 + headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token},
255 255   credentials:'same-origin'
256 256   });
257 257   if(!(uploading.status===201 || uploading.status===202)){
... ... @@ -259,52 +259,50 @@
259 259   throw new Error('Upload failed: ' + txt);
260 260   }
261 261  
262 - var delURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + srcSpacesPath +
263 - '/pages/' + encodeURIComponent(srcPage) +
264 - '/attachments/' + encodeURIComponent(filename);
265 -
290 + var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath +
291 + '/pages/' + encodeURIComponent(srcPage) +
292 + '/attachments/' + encodeURIComponent(filename);
266 266   var deleting = await fetch(delURL, {
267 - method:'DELETE',
268 - headers: {'XWiki-Form-Token': token},
269 - credentials:'same-origin'
294 + method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'
270 270   });
271 - if(!(deleting.status===204)){
272 - console.warn('Delete original failed', deleting.status);
273 - }
296 + if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); }
274 274   return true;
275 275   }
276 276  
277 - /* -------- button handler (explicit Move) -------- */
300 + /* ---------- Move button: resolve -> confirm -> move ---------- */
278 278   document.addEventListener('click', function(e){
279 279   var btn = e.target.closest('.move-go'); if(!btn) return;
280 280   var box = btn.closest('.move-box');
281 281   var inp = box.querySelector('.move-input');
282 - var selected = (inp && inp.value ? inp.value.trim() : '');
283 - if(!selected){ inp && inp.focus(); return; }
305 + (async function(){
306 + try{
307 + var ref = await resolveReference(inp);
308 + var filename = btn.getAttribute('data-filename');
284 284  
285 - var notice = document.createElement('div');
286 - notice.style.fontSize='12px'; notice.style.color='#666'; notice.textContent='Moving…';
287 - box.appendChild(notice);
310 + var notice = document.createElement('div');
311 + notice.className = 'move-notice';
312 + notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …';
313 + box.appendChild(notice);
288 288  
289 - moveAttachment({
290 - srcSpace: window.SOURCE_SPACE,
291 - srcPage: window.SOURCE_PAGE,
292 - filename: btn.getAttribute('data-filename'),
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 - });
315 + await moveAttachment({
316 + srcSpace: window.SOURCE_SPACE,
317 + srcPage: window.SOURCE_PAGE,
318 + filename: filename,
319 + dstFull: ref
320 + });
302 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
322 + notice.textContent = 'Moved ✔ — reloading…';
323 + setTimeout(function(){ location.reload(); }, 600);
324 + }catch(err){
325 + var notice = box.querySelector('.move-notice') || document.createElement('div');
326 + notice.className = 'move-notice';
327 + notice.style.color = '#b00020';
328 + notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err);
329 + box.appendChild(notice);
330 + }
331 + })();
307 307   });
333 +
308 308  })();
309 309  </script>
310 310