... |
... |
@@ -1,461 +1,22 @@ |
1 |
|
-{{velocity}} |
2 |
|
-#pagePicker_import |
|
1 |
+{{video attachment="Propaganda.mp4"/}} |
3 |
3 |
|
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 |
|
3 |
+{{video attachment="2025-02-26_05-18-58_0.mp4"/}} |
17 |
17 |
|
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> |
23 |
23 |
|
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 |
|
6 |
+{{videopicker video="1776General__20240817__1824613900312080533_1_18246138792021319680.mp4"/}} |
30 |
30 |
|
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> |
43 |
43 |
|
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 |
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 |
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 |
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 |
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> |
|
13 |
+{{jwplayer attachment="MEGYNK~~1.MP4" /}} |
90 |
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> |
|
15 |
+2345 |
98 |
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 |
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 |
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> |
|
19 |
+" |
135 |
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 |
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 |
|
- |
|
22 |
+ |