Wiki source code of Conditioning
Show last authors
author | version | line-number | content |
---|---|---|---|
1 | {{velocity}} | ||
2 | #pagePicker_import | ||
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 | ||
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 | |||
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 | ||
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 | |||
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}} |