... |
... |
@@ -1,35 +1,461 @@ |
1 |
|
-{{box cssClass="floatinginfobox" title="**Contents**"}} |
2 |
|
-{{toc/}} |
3 |
|
-{{/box}} |
|
1 |
+{{velocity}} |
|
2 |
+#pagePicker_import |
4 |
4 |
|
5 |
|
-= Paragraph 1 = |
|
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 |
6 |
6 |
|
7 |
|
-The conditioning of Western society has been imposed on it for decades but which have reached a level of absurdity that's unmatched in all of recorded history only recently. |
|
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> |
8 |
8 |
|
9 |
|
-== Sub-paragraph == |
|
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 |
10 |
10 |
|
11 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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> |
12 |
12 |
|
13 |
|
-== Sub-paragraph == |
|
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> |
14 |
14 |
|
15 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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)) |
16 |
16 |
|
17 |
|
-=== Sub-sub paragraph === |
|
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 |
18 |
18 |
|
19 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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 |
20 |
20 |
|
|
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> |
21 |
21 |
|
22 |
|
-= Paragraph 2 = |
|
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> |
23 |
23 |
|
24 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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> |
25 |
25 |
|
26 |
|
-== Sub-paragraph == |
|
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 --> |
27 |
27 |
|
28 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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> |
29 |
29 |
|
30 |
|
-== Sub-paragraph == |
|
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]; |
31 |
31 |
|
32 |
|
-Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. |
|
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 |
+ } |
33 |
33 |
|
|
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 |
+ } |
34 |
34 |
|
35 |
|
- |
|
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 |
+ |