... |
... |
@@ -1,10 +1,461 @@ |
1 |
|
-{{video attachment="Propaganda.mp4"/}} |
|
1 |
+{{velocity}} |
|
2 |
+#pagePicker_import |
2 |
2 |
|
3 |
|
-{{video attachment="2025-02-26_05-18-58_0.mp4"/}} |
|
4 |
+## 1) Collect video attachments on this page |
|
5 |
+#set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v']) |
|
6 |
+#set($videos = []) |
|
7 |
+#foreach($att in $doc.getAttachmentList()) |
|
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 |
|
16 |
+#end |
4 |
4 |
|
5 |
|
-{{videopicker video="1776General__20240817__1824613900312080533_1_18246138792021319680.mp4"/}} |
|
18 |
+## 2) Cache HTML (auto-bust on version) |
|
19 |
+{{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}} |
|
20 |
+{{html wiki="false" clean="false"}} |
|
21 |
+<div id="xwiki-video-manager" style="margin:20px 0;"> |
|
22 |
+ <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2> |
6 |
6 |
|
7 |
|
-{{videopicker video="MEGYNK~~1.faMP4"/}} |
|
24 |
+ #if($videos.size() == 0) |
|
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> |
|
29 |
+ #else |
8 |
8 |
|
9 |
|
-{{videopicker video="Propaganda.mp4"/}} |
|
31 |
+ <!-- Bulk toolbar --> |
|
32 |
+ <div id="bulk-bar" class="bulk-bar"> |
|
33 |
+ <label style="margin-right:8px;"> |
|
34 |
+ <input type="checkbox" id="pick-all"> Select visible |
|
35 |
+ </label> |
|
36 |
+ <span class="sep"></span> |
|
37 |
+ <input type="text" class="bulk-dest suggest-pages" placeholder="Find a page…" |
|
38 |
+ data-search-scope="wiki:${escapetool.xml($xcontext.database)}"> |
|
39 |
+ <button type="button" class="btn btn-sm btn-warning" id="bulk-move">Move selected</button> |
|
40 |
+ <button type="button" class="btn btn-sm btn-danger" id="bulk-delete">Delete selected</button> |
|
41 |
+ <span id="bulk-status" class="bulk-status"></span> |
|
42 |
+ </div> |
10 |
10 |
|
|
44 |
+ <script> |
|
45 |
+ window.VID_CHUNK_SIZE = 48; |
|
46 |
+ window.VID_LAZY_MARGIN = '600px'; |
|
47 |
+ window.XWIKI_WIKI = ${jsontool.serialize($xcontext.database)}; |
|
48 |
+ window.SOURCE_SPACE = ${jsontool.serialize($doc.space)}; |
|
49 |
+ window.SOURCE_PAGE = ${jsontool.serialize($doc.name)}; |
|
50 |
+ </script> |
|
51 |
+ |
|
52 |
+ <div id="video-chunks"> |
|
53 |
+ #set($i = 0) |
|
54 |
+ #set($chunkIndex = 0) |
|
55 |
+ #foreach($att in $videos) |
|
56 |
+ #set($i = $i + 1) |
|
57 |
+ #set($filename = $att.getFilename()) |
|
58 |
+ #set($lname = $filename.toLowerCase()) |
|
59 |
+ #set($url = $doc.getAttachmentURL($filename)) |
|
60 |
+ |
|
61 |
+ ## MIME guess |
|
62 |
+ #set($videoType = "video/mp4") |
|
63 |
+ #if($lname.endsWith(".webm")) |
|
64 |
+ #set($videoType = "video/webm") |
|
65 |
+ #elseif($lname.endsWith(".ogg")) |
|
66 |
+ #set($videoType = "video/ogg") |
|
67 |
+ #elseif($lname.endsWith(".avi")) |
|
68 |
+ #set($videoType = "video/x-msvideo") |
|
69 |
+ #elseif($lname.endsWith(".mov")) |
|
70 |
+ #set($videoType = "video/quicktime") |
|
71 |
+ #elseif($lname.endsWith(".wmv")) |
|
72 |
+ #set($videoType = "video/x-ms-wmv") |
|
73 |
+ #elseif($lname.endsWith(".flv")) |
|
74 |
+ #set($videoType = "video/x-flv") |
|
75 |
+ #elseif($lname.endsWith(".m4v")) |
|
76 |
+ #set($videoType = "video/mp4") |
|
77 |
+ #end |
|
78 |
+ |
|
79 |
+ #if($i == 1 || ($i - 1) % 48 == 0) |
|
80 |
+ #set($chunkIndex = $chunkIndex + 1) |
|
81 |
+ <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;"> |
|
82 |
+ <div class="video-display-grid"> |
|
83 |
+ #end |
|
84 |
+ |
|
85 |
+ <div class="video-container"> |
|
86 |
+ <div class="video-header"> |
|
87 |
+ <label class="pick"><input type="checkbox" class="vid-pick" data-filename="${escapetool.xml($filename)}"></label> |
|
88 |
+ <h4 class="video-title">${escapetool.xml($filename)}</h4> |
|
89 |
+ </div> |
|
90 |
+ |
|
91 |
+ <div class="video-frame" |
|
92 |
+ data-src="${url}" |
|
93 |
+ data-type="${videoType}" |
|
94 |
+ data-name="${escapetool.xml($filename)}"> |
|
95 |
+ <canvas class="vid-canvas" width="320" height="180"></canvas> |
|
96 |
+ <button class="btn btn-sm btn-primary" type="button">Load & Play</button> |
|
97 |
+ </div> |
|
98 |
+ |
|
99 |
+ <div class="video-controls"> |
|
100 |
+ <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a> |
|
101 |
+ <button class="btn btn-sm btn-danger del-one" data-filename="${escapetool.xml($filename)}">Delete</button> |
|
102 |
+ <span class="vid-duration">Duration: —</span> |
|
103 |
+ </div> |
|
104 |
+ |
|
105 |
+ <!-- Move-to-page --> |
|
106 |
+ <div class="move-box"> |
|
107 |
+ <label>Move to page:</label> |
|
108 |
+ <div class="move-row"> |
|
109 |
+ <input type="text" |
|
110 |
+ class="move-input suggest-pages" |
|
111 |
+ placeholder="Find a page…" |
|
112 |
+ data-search-scope="wiki:${escapetool.xml($xcontext.database)}" |
|
113 |
+ data-filename="${escapetool.xml($filename)}"> |
|
114 |
+ <button type="button" |
|
115 |
+ class="btn btn-sm btn-warning move-go" |
|
116 |
+ data-filename="${escapetool.xml($filename)}">Move</button> |
|
117 |
+ </div> |
|
118 |
+ <small class="hint">Pick a page (e.g., <code>Main.SomePage</code>). Click <b>Move</b> to relocate this file.</small> |
|
119 |
+ </div> |
|
120 |
+ </div> <!-- .video-container --> |
|
121 |
+ |
|
122 |
+ #if(($i % 48 == 0) || $foreach.last) |
|
123 |
+ </div> <!-- .video-display-grid --> |
|
124 |
+ #if(!$foreach.last) |
|
125 |
+ <div class="loadmore-wrap"> |
|
126 |
+ <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button> |
|
127 |
+ </div> |
|
128 |
+ #end |
|
129 |
+ </div> <!-- .vid-chunk --> |
|
130 |
+ #end |
|
131 |
+ #end |
|
132 |
+ </div> |
|
133 |
+ #end |
|
134 |
+</div> |
|
135 |
+ |
|
136 |
+<script> |
|
137 |
+(function(){ |
|
138 |
+ /* ===== constants & helpers ===== */ |
|
139 |
+ var WIKI = window.XWIKI_WIKI; |
|
140 |
+ var CURRENT_SPACE = document.documentElement.getAttribute('data-xwiki-space') || (window.SOURCE_SPACE || 'Main'); |
|
141 |
+ var ROOT_SPACE = CURRENT_SPACE.split('.')[0]; |
|
142 |
+ |
|
143 |
+ function spacesPath(dotPath){ |
|
144 |
+ if(!dotPath) return ''; |
|
145 |
+ return dotPath.split('.').map(function(s){ return 'spaces/' + encodeURIComponent(s); }).join('/'); |
|
146 |
+ } |
|
147 |
+ function parseFullName(full){ |
|
148 |
+ full = String(full||'').replace(/^[^:]+:/,''); |
|
149 |
+ var parts = full.split('.'); |
|
150 |
+ var page = parts.pop(); |
|
151 |
+ return {spacePath: parts.join('.'), page: page}; |
|
152 |
+ } |
|
153 |
+ function getFormToken(){ |
|
154 |
+ return document.documentElement.getAttribute('data-xwiki-form-token') || ''; |
|
155 |
+ } |
|
156 |
+ |
|
157 |
+ /* ===== existence check (prevents DocumentDoesNotExist) ===== */ |
|
158 |
+ async function docExists(fullRef){ |
|
159 |
+ var p = parseFullName(fullRef); |
|
160 |
+ var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + |
|
161 |
+ spacesPath(p.spacePath) + '/pages/' + encodeURIComponent(p.page) + '?media=json'; |
|
162 |
+ var r = await fetch(url, {credentials:'same-origin'}); |
|
163 |
+ return r.status === 200; |
|
164 |
+ } |
|
165 |
+ |
|
166 |
+ /* ===== robust resolver for Page Picker / typed titles ===== */ |
|
167 |
+ async function resolveReference(inp){ |
|
168 |
+ var ref = inp.getAttribute('data-reference') || (inp.dataset && inp.dataset.reference) || ''; |
|
169 |
+ var raw = (inp.value || '').trim(); |
|
170 |
+ |
|
171 |
+ if (ref){ |
|
172 |
+ if (/\.WebHome$/i.test(ref)) { if (await docExists(ref)) return ref; } |
|
173 |
+ else { |
|
174 |
+ if (await docExists(ref)) return ref; |
|
175 |
+ var rh = ref + '.WebHome'; if (await docExists(rh)) return rh; |
|
176 |
+ } |
|
177 |
+ } |
|
178 |
+ var base = raw.indexOf('.') === -1 ? (ROOT_SPACE + '.' + raw) : raw; |
|
179 |
+ if (await docExists(base)) return base; |
|
180 |
+ var home2 = /\.WebHome$/i.test(base) ? base : (base + '.WebHome'); |
|
181 |
+ if (await docExists(home2)) return home2; |
|
182 |
+ |
|
183 |
+ throw new Error('Target page not found: "' + raw + '". Choose a suggestion or type a full reference like "Main Categories.SomePage".'); |
|
184 |
+ } |
|
185 |
+ |
|
186 |
+ /* ===== posters & video mount ===== */ |
|
187 |
+ async function makePoster(frame){ |
|
188 |
+ if(frame.getAttribute('data-poster-ready')==='1') return; |
|
189 |
+ var src = frame.getAttribute('data-src'); |
|
190 |
+ try{ |
|
191 |
+ var v = document.createElement('video'); |
|
192 |
+ v.preload = 'metadata'; v.muted = true; v.playsInline = true; v.src = src; |
|
193 |
+ function once(t,e){return new Promise(function(res){t.addEventListener(e,res,{once:true});});} |
|
194 |
+ await once(v,'loadedmetadata'); |
|
195 |
+ if (typeof v.requestVideoFrameCallback === 'function'){ |
|
196 |
+ await new Promise(function(res){ v.requestVideoFrameCallback(function(){ res(); }); }); |
|
197 |
+ } else { |
|
198 |
+ try { v.currentTime = 0.25; } catch(e){} |
|
199 |
+ await once(v,'seeked').catch(function(){}); |
|
200 |
+ await once(v,'loadeddata').catch(function(){}); |
|
201 |
+ } |
|
202 |
+ var canvas = frame.querySelector('.vid-canvas'); |
|
203 |
+ if(canvas){ |
|
204 |
+ var w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16)); |
|
205 |
+ canvas.width = 320; canvas.height = h>0?h:180; |
|
206 |
+ var ctx = canvas.getContext('2d', {willReadFrequently:true}); |
|
207 |
+ ctx.drawImage(v, 0, 0, canvas.width, canvas.height); |
|
208 |
+ try { frame.setAttribute('data-poster', canvas.toDataURL('image/webp', 0.85)); } catch(e){} |
|
209 |
+ } |
|
210 |
+ frame.setAttribute('data-poster-ready','1'); |
|
211 |
+ }catch(e){} |
|
212 |
+ } |
|
213 |
+ |
|
214 |
+ function mountVideo(frame){ |
|
215 |
+ if(frame.getAttribute('data-mounted')==='1') return; |
|
216 |
+ var src = frame.getAttribute('data-src'); |
|
217 |
+ var type = frame.getAttribute('data-type') || 'video/mp4'; |
|
218 |
+ var poster = frame.getAttribute('data-poster'); |
|
219 |
+ |
|
220 |
+ var v = document.createElement('video'); |
|
221 |
+ v.setAttribute('controls',''); v.setAttribute('preload','none'); |
|
222 |
+ if(poster) v.setAttribute('poster', poster); |
|
223 |
+ v.style.width='100%'; v.style.maxWidth='100%'; v.style.borderRadius='4px'; |
|
224 |
+ |
|
225 |
+ var s = document.createElement('source'); s.src = src; s.type = type; v.appendChild(s); |
|
226 |
+ v.addEventListener('loadedmetadata', function(){ |
|
227 |
+ var d = Math.round(v.duration||0), mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0'); |
|
228 |
+ var dur = frame.parentElement.querySelector('.vid-duration'); if(dur) dur.textContent = 'Duration: '+mm+':'+ss; |
|
229 |
+ }); |
|
230 |
+ |
|
231 |
+ frame.replaceChildren(v); |
|
232 |
+ frame.setAttribute('data-mounted','1'); |
|
233 |
+ } |
|
234 |
+ |
|
235 |
+ /* ===== observers & UI wiring ===== */ |
|
236 |
+ if('IntersectionObserver' in window){ |
|
237 |
+ var io = new IntersectionObserver(function(entries){ |
|
238 |
+ entries.forEach(function(e){ |
|
239 |
+ if(e.isIntersecting){ makePoster(e.target); io.unobserve(e.target); } |
|
240 |
+ }); |
|
241 |
+ }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') }); |
|
242 |
+ document.querySelectorAll('.video-frame').forEach(function(el){ io.observe(el); }); |
|
243 |
+ } |
|
244 |
+ |
|
245 |
+ document.addEventListener('click', function(ev){ |
|
246 |
+ var frame = ev.target.closest('.video-frame'); |
|
247 |
+ if(frame){ |
|
248 |
+ mountVideo(frame); |
|
249 |
+ var v = frame.querySelector('video'); if(v) v.play().catch(function(){}); |
|
250 |
+ } |
|
251 |
+ }); |
|
252 |
+ |
|
253 |
+ document.addEventListener('click', function(ev){ |
|
254 |
+ var b = ev.target.closest('.load-more'); if(!b) return; |
|
255 |
+ var next = b.getAttribute('data-next'); |
|
256 |
+ var nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]'); |
|
257 |
+ if(nxt){ nxt.style.display='block'; b.parentElement.style.display='none'; } |
|
258 |
+ }); |
|
259 |
+ |
|
260 |
+ /* ===== MOVE & DELETE core ===== */ |
|
261 |
+ async function moveAttachment(opts){ |
|
262 |
+ var srcSpace = opts.srcSpace, srcPage = opts.srcPage, filename = opts.filename, dstFull = opts.dstFull; |
|
263 |
+ var pf = parseFullName(dstFull); |
|
264 |
+ var srcSpacesPath = spacesPath(srcSpace); |
|
265 |
+ var dstSpacesPath = spacesPath(pf.spacePath); |
|
266 |
+ |
|
267 |
+ var sel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]'; |
|
268 |
+ var inp = document.querySelector(sel); |
|
269 |
+ var card = inp ? inp.closest('.video-container') : null; |
|
270 |
+ var frame = card ? card.querySelector('.video-frame') : null; |
|
271 |
+ var srcURL = frame ? frame.getAttribute('data-src') : null; |
|
272 |
+ if(!srcURL) throw new Error('Missing source URL'); |
|
273 |
+ |
|
274 |
+ var downloading = await fetch(srcURL, {credentials:'same-origin'}); |
|
275 |
+ if(!downloading.ok) throw new Error('Download failed: ' + downloading.status); |
|
276 |
+ var blob = await downloading.blob(); |
|
277 |
+ |
|
278 |
+ var token = getFormToken(); |
|
279 |
+ |
|
280 |
+ var putURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + dstSpacesPath + |
|
281 |
+ '/pages/' + encodeURIComponent(pf.page) + |
|
282 |
+ '/attachments/' + encodeURIComponent(filename) + '?media=json'; |
|
283 |
+ var uploading = await fetch(putURL, { |
|
284 |
+ method: 'PUT', |
|
285 |
+ body: blob, |
|
286 |
+ headers: {'Content-Type':'application/octet-stream','XWiki-Form-Token': token}, |
|
287 |
+ credentials:'same-origin' |
|
288 |
+ }); |
|
289 |
+ if(!(uploading.status===201 || uploading.status===202)){ |
|
290 |
+ var txt = await uploading.text().catch(function(){ return String(uploading.status); }); |
|
291 |
+ throw new Error('Upload failed: ' + txt); |
|
292 |
+ } |
|
293 |
+ |
|
294 |
+ var delURL = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath + |
|
295 |
+ '/pages/' + encodeURIComponent(srcPage) + |
|
296 |
+ '/attachments/' + encodeURIComponent(filename); |
|
297 |
+ var deleting = await fetch(delURL, {method:'DELETE', headers:{'XWiki-Form-Token': token}, credentials:'same-origin'}); |
|
298 |
+ if(!(deleting.status===204)){ console.warn('Delete original failed', deleting.status); } |
|
299 |
+ return true; |
|
300 |
+ } |
|
301 |
+ |
|
302 |
+ async function deleteAttachment(filename){ |
|
303 |
+ var token = getFormToken(); |
|
304 |
+ var srcSpacesPath = spacesPath(window.SOURCE_SPACE); |
|
305 |
+ var url = '/rest/wikis/' + encodeURIComponent(WIKI) + '/' + srcSpacesPath + |
|
306 |
+ '/pages/' + encodeURIComponent(window.SOURCE_PAGE) + |
|
307 |
+ '/attachments/' + encodeURIComponent(filename); |
|
308 |
+ var r = await fetch(url, { |
|
309 |
+ method: 'DELETE', |
|
310 |
+ headers: {'XWiki-Form-Token': token}, |
|
311 |
+ credentials: 'same-origin' |
|
312 |
+ }); |
|
313 |
+ if (r.status !== 204) { |
|
314 |
+ var t = await r.text().catch(()=>String(r.status)); |
|
315 |
+ throw new Error('Delete failed: ' + t); |
|
316 |
+ } |
|
317 |
+ return true; |
|
318 |
+ } |
|
319 |
+ |
|
320 |
+ function removeCardByFilename(filename){ |
|
321 |
+ var card = document.querySelector('.video-container input.vid-pick[data-filename="'+CSS.escape(filename)+'"]'); |
|
322 |
+ card = card ? card.closest('.video-container') : null; |
|
323 |
+ if (card) card.remove(); |
|
324 |
+ } |
|
325 |
+ function selectedFilenames(){ |
|
326 |
+ return Array.from(document.querySelectorAll('.vid-pick:checked')) |
|
327 |
+ .map(ch => ch.getAttribute('data-filename')); |
|
328 |
+ } |
|
329 |
+ |
|
330 |
+ /* ===== Move (per-card) ===== */ |
|
331 |
+ document.addEventListener('click', function(e){ |
|
332 |
+ var btn = e.target.closest('.move-go'); if(!btn) return; |
|
333 |
+ var box = btn.closest('.move-box'); |
|
334 |
+ var inp = box.querySelector('.move-input'); |
|
335 |
+ (async function(){ |
|
336 |
+ var notice = box.querySelector('.move-notice'); |
|
337 |
+ if(!notice){ notice = document.createElement('div'); notice.className = 'move-notice'; box.appendChild(notice); } |
|
338 |
+ try{ |
|
339 |
+ var ref = await resolveReference(inp); |
|
340 |
+ var filename = btn.getAttribute('data-filename'); |
|
341 |
+ notice.style.color = '#666'; |
|
342 |
+ notice.textContent = 'Moving “' + filename + '” to ' + ref + ' …'; |
|
343 |
+ |
|
344 |
+ await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: filename, dstFull: ref }); |
|
345 |
+ |
|
346 |
+ notice.textContent = 'Moved ✔ — reloading…'; |
|
347 |
+ setTimeout(function(){ location.reload(); }, 600); |
|
348 |
+ }catch(err){ |
|
349 |
+ notice.style.color = '#b00020'; |
|
350 |
+ notice.textContent = 'Move failed: ' + (err && err.message ? err.message : err); |
|
351 |
+ } |
|
352 |
+ })(); |
|
353 |
+ }); |
|
354 |
+ |
|
355 |
+ /* ===== Delete (per-card) ===== */ |
|
356 |
+ document.addEventListener('click', function(e){ |
|
357 |
+ var btn = e.target.closest('.del-one'); if(!btn) return; |
|
358 |
+ var filename = btn.getAttribute('data-filename'); |
|
359 |
+ if(!confirm('Delete this file permanently?\n\n' + filename)) return; |
|
360 |
+ var notice = document.createElement('div'); |
|
361 |
+ notice.className = 'move-notice'; notice.textContent = 'Deleting…'; |
|
362 |
+ btn.parentElement.appendChild(notice); |
|
363 |
+ deleteAttachment(filename) |
|
364 |
+ .then(()=>{ notice.textContent = 'Deleted ✔'; removeCardByFilename(filename); }) |
|
365 |
+ .catch(err=>{ notice.style.color='#b00020'; notice.textContent = (err && err.message)||String(err); }); |
|
366 |
+ }); |
|
367 |
+ |
|
368 |
+ /* ===== Bulk: select visible ===== */ |
|
369 |
+ document.addEventListener('change', function(e){ |
|
370 |
+ if (e.target && e.target.id === 'pick-all'){ |
|
371 |
+ var on = e.target.checked; |
|
372 |
+ document.querySelectorAll('.vid-pick').forEach(ch => { ch.checked = on; }); |
|
373 |
+ } |
|
374 |
+ }); |
|
375 |
+ |
|
376 |
+ /* ===== Bulk: Move ===== */ |
|
377 |
+ document.getElementById('bulk-move')?.addEventListener('click', async function(){ |
|
378 |
+ var files = selectedFilenames(); |
|
379 |
+ if (!files.length) return alert('No videos selected.'); |
|
380 |
+ var destInput = document.querySelector('.bulk-dest'); |
|
381 |
+ var status = document.getElementById('bulk-status'); |
|
382 |
+ try{ |
|
383 |
+ var ref = await resolveReference(destInput); |
|
384 |
+ if(!confirm('Move '+files.length+' file(s) to:\n\n'+ref+' ?')) return; |
|
385 |
+ status.style.color=''; status.textContent = 'Moving 0/' + files.length + '…'; |
|
386 |
+ let done = 0; |
|
387 |
+ for (const f of files){ |
|
388 |
+ await moveAttachment({ srcSpace: window.SOURCE_SPACE, srcPage: window.SOURCE_PAGE, filename: f, dstFull: ref }); |
|
389 |
+ done++; status.textContent = 'Moving ' + done + '/' + files.length + '…'; |
|
390 |
+ removeCardByFilename(f); |
|
391 |
+ } |
|
392 |
+ status.textContent = 'Move complete ✔'; |
|
393 |
+ }catch(err){ |
|
394 |
+ status.style.color = '#b00020'; |
|
395 |
+ status.textContent = 'Move failed: ' + ((err && err.message) || String(err)); |
|
396 |
+ } |
|
397 |
+ }); |
|
398 |
+ |
|
399 |
+ /* ===== Bulk: Delete ===== */ |
|
400 |
+ document.getElementById('bulk-delete')?.addEventListener('click', async function(){ |
|
401 |
+ var files = selectedFilenames(); |
|
402 |
+ if (!files.length) return alert('No videos selected.'); |
|
403 |
+ if(!confirm('DELETE '+files.length+' file(s)? This cannot be undone.')) return; |
|
404 |
+ var status = document.getElementById('bulk-status'); |
|
405 |
+ try{ |
|
406 |
+ status.style.color=''; status.textContent = 'Deleting 0/' + files.length + '…'; |
|
407 |
+ let done = 0; |
|
408 |
+ for (const f of files){ |
|
409 |
+ await deleteAttachment(f); |
|
410 |
+ done++; status.textContent = 'Deleting ' + done + '/' + files.length + '…'; |
|
411 |
+ removeCardByFilename(f); |
|
412 |
+ } |
|
413 |
+ status.textContent = 'Delete complete ✔'; |
|
414 |
+ }catch(err){ |
|
415 |
+ status.style.color = '#b00020'; |
|
416 |
+ status.textContent = 'Delete failed: ' + ((err && err.message) || String(err)); |
|
417 |
+ } |
|
418 |
+ }); |
|
419 |
+ |
|
420 |
+})(); |
|
421 |
+</script> |
|
422 |
+ |
|
423 |
+<style> |
|
424 |
+ .video-display-grid{ |
|
425 |
+ display:grid; |
|
426 |
+ grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); |
|
427 |
+ gap:20px; align-items:start; |
|
428 |
+ } |
|
429 |
+ .video-container{border:1px solid #ddd; border-radius:8px; padding:12px; background:#fff;} |
|
430 |
+ .video-header{display:flex;gap:8px;align-items:center;margin-bottom:8px;} |
|
431 |
+ .video-title{margin:0;flex:1;min-width:0;word-break:break-word;} |
|
432 |
+ .pick{margin-right:2px} |
|
433 |
+ .video-frame{ |
|
434 |
+ position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px; |
|
435 |
+ overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer; |
|
436 |
+ } |
|
437 |
+ .vid-canvas{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;} |
|
438 |
+ .video-controls{margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;} |
|
439 |
+ .move-box{margin-top:10px;} |
|
440 |
+ .move-row{display:flex;gap:6px;align-items:center;flex-wrap:wrap;} |
|
441 |
+ .move-input{flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;} |
|
442 |
+ .hint{color:#888;} |
|
443 |
+ .loadmore-wrap{text-align:center;margin:12px 0 28px;} |
|
444 |
+ .btn{padding:4px 8px;border:1px solid #ddd;background:#f8f9fa;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;} |
|
445 |
+ .btn:hover{background:#e9ecef;} |
|
446 |
+ .btn-primary{background:#007bff;color:#fff;border-color:#007bff;} |
|
447 |
+ .btn-secondary{background:#6c757d;color:#fff;border-color:#6c757d;} |
|
448 |
+ .btn-success{background:#28a745;color:#fff;border-color:#28a745;} |
|
449 |
+ .btn-danger{background:#dc3545;color:#fff;border-color:#dc3545;} |
|
450 |
+ .btn-danger:hover{background:#c82333} |
|
451 |
+ .btn-sm{font-size:12px;padding:2px 6px;} |
|
452 |
+ .move-notice{font-size:12px;color:#666;margin-top:6px;max-height:6.5em;overflow:auto;white-space:normal;overflow-wrap:anywhere;} |
|
453 |
+ .bulk-bar{display:flex;gap:8px;align-items:center;flex-wrap:wrap;margin:8px 0 16px;} |
|
454 |
+ .bulk-bar .sep{width:1px;height:18px;background:#ddd;margin:0 4px;} |
|
455 |
+ .bulk-status{font-size:12px;color:#666;margin-left:6px;white-space:normal;overflow-wrap:anywhere;} |
|
456 |
+ @media (max-width:768px){.video-display-grid{grid-template-columns:1fr}} |
|
457 |
+</style> |
|
458 |
+{{/html}} |
|
459 |
+{{/cache}} |
|
460 |
+{{/velocity}} |
|
461 |
+ |