... |
... |
@@ -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 |
|