Wiki source code of Uncategorized Videos
Hide last authors
author | version | line-number | content |
---|---|---|---|
![]() |
493.1 | 1 | {{velocity}} |
![]() |
499.2 | 2 | |
3 | #set($videoExts = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v']) | ||
4 | #set($videos = []) | ||
5 | |||
![]() |
493.1 | 6 | #foreach($att in $doc.getAttachmentList()) |
![]() |
499.1 | 7 | #set($n = $att.getFilename()) |
8 | #set($ln = $n.toLowerCase()) | ||
9 | #foreach($e in $videoExts) | ||
10 | #if($ln.endsWith("." + $e)) | ||
![]() |
493.1 | 11 | #set($discard = $videos.add($att)) |
12 | #break | ||
13 | #end | ||
14 | #end | ||
15 | #end | ||
16 | |||
![]() |
500.1 | 17 | {{cache id="vid-list-$doc.fullName-$doc.version" timeToLive="21600"}} |
![]() |
499.2 | 18 | |
![]() |
493.1 | 19 | {{html wiki="false" clean="false"}} |
![]() |
499.2 | 20 | |
![]() |
493.1 | 21 | <div id="xwiki-video-manager" style="margin:20px 0;"> |
![]() |
494.1 | 22 | <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2> |
![]() |
493.1 | 23 | |
![]() |
494.1 | 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> | ||
![]() |
493.1 | 28 | </div> |
![]() |
494.1 | 29 | #else |
30 | <script> | ||
![]() |
499.1 | 31 | window.VID_CHUNK_SIZE = 48; |
32 | window.VID_LAZY_MARGIN = '600px'; | ||
![]() |
499.2 | 33 | window.XWIKI_WIKI = ${jsontool.serialize($xcontext.database)}; // current wiki id (e.g., "xwiki") |
34 | window.SOURCE_SPACE = ${jsontool.serialize($doc.space)}; // e.g., "Main" or "Main.Sub" | ||
35 | window.SOURCE_PAGE = ${jsontool.serialize($doc.name)}; // e.g., "WebHome" | ||
![]() |
494.1 | 36 | </script> |
![]() |
493.1 | 37 | |
![]() |
494.1 | 38 | <div id="video-chunks"> |
39 | #set($i = 0) | ||
40 | #set($chunkIndex = 0) | ||
![]() |
499.2 | 41 | |
![]() |
494.1 | 42 | #foreach($att in $videos) |
43 | #set($i = $i + 1) | ||
44 | #set($filename = $att.getFilename()) | ||
![]() |
499.2 | 45 | #set($lname = $filename.toLowerCase()) |
46 | #set($url = $doc.getAttachmentURL($filename)) | ||
![]() |
493.1 | 47 | |
![]() |
499.2 | 48 | ## MIME type detection |
![]() |
493.1 | 49 | #set($videoType = "video/mp4") |
![]() |
494.1 | 50 | #if($lname.endsWith(".webm")) |
51 | #set($videoType = "video/webm") | ||
52 | #elseif($lname.endsWith(".ogg")) | ||
53 | #set($videoType = "video/ogg") | ||
54 | #elseif($lname.endsWith(".avi")) | ||
55 | #set($videoType = "video/x-msvideo") | ||
56 | #elseif($lname.endsWith(".mov")) | ||
57 | #set($videoType = "video/quicktime") | ||
58 | #elseif($lname.endsWith(".wmv")) | ||
59 | #set($videoType = "video/x-ms-wmv") | ||
60 | #elseif($lname.endsWith(".flv")) | ||
61 | #set($videoType = "video/x-flv") | ||
62 | #elseif($lname.endsWith(".m4v")) | ||
63 | #set($videoType = "video/mp4") | ||
64 | #end | ||
![]() |
493.1 | 65 | |
![]() |
494.1 | 66 | #if($i == 1 || ($i - 1) % 48 == 0) |
67 | #set($chunkIndex = $chunkIndex + 1) | ||
68 | <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;"> | ||
69 | <div class="video-display-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:20px;"> | ||
70 | #end | ||
![]() |
493.1 | 71 | |
![]() |
494.1 | 72 | <div class="video-container" style="border:1px solid #ddd;border-radius:8px;padding:12px;background:#fff;"> |
73 | <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;"> | ||
74 | <h4 style="margin:0;flex:1;min-width:0;">${escapetool.xml($filename)}</h4> | ||
75 | #if($xcontext.action == 'edit') | ||
76 | <input type="checkbox" class="video-selector" data-video="${escapetool.xml($filename)}" title="Select for bulk actions"> | ||
77 | #end | ||
78 | </div> | ||
![]() |
493.1 | 79 | |
![]() |
499.2 | 80 | <!-- Video placeholder with auto-generated poster --> |
81 | <div class="video-frame" data-src="${url}" data-type="${videoType}" data-name="${escapetool.xml($filename)}" | ||
![]() |
499.1 | 82 | style="position:relative;width:100%;aspect-ratio:16/9;background:#111;border-radius:4px;overflow:hidden;display:flex;align-items:center;justify-content:center;cursor:pointer;"> |
83 | <canvas class="vid-canvas" width="320" height="180" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover;"></canvas> | ||
84 | <button class="btn btn-sm btn-primary" type="button" style="position:relative;z-index:1;">Load & Play</button> | ||
![]() |
494.1 | 85 | </div> |
86 | |||
![]() |
499.1 | 87 | <div class="video-controls" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;align-items:center;"> |
![]() |
494.1 | 88 | <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a> |
89 | <span class="vid-duration" style="font-size:12px;color:#666;">Duration: —</span> | ||
90 | </div> | ||
![]() |
499.1 | 91 | |
![]() |
499.2 | 92 | <!-- Move-to-page functionality --> |
![]() |
499.1 | 93 | <div class="move-box" style="margin-top:10px;"> |
94 | <label style="font-size:12px;color:#555;">Move to page:</label> | ||
95 | <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap;"> | ||
96 | <input type="text" class="move-input" placeholder="Type page name (e.g., Main.MyPage)" | ||
![]() |
499.2 | 97 | data-filename="${escapetool.xml($filename)}" style="flex:1;min-width:220px;padding:4px;border:1px solid #ccc;border-radius:4px;"> |
![]() |
499.1 | 98 | <div class="move-results" style="position:relative;min-width:220px;max-width:420px;"></div> |
99 | </div> | ||
100 | <small style="color:#888;">Search is wiki-wide; click a result to move this file.</small> | ||
101 | </div> | ||
![]() |
493.1 | 102 | </div> |
103 | |||
![]() |
494.1 | 104 | #if(($i % 48 == 0) || $foreach.last) |
105 | </div> | ||
![]() |
499.2 | 106 | |
107 | #if(!$foreach.last) | ||
108 | <div style="text-align:center;margin:12px 0 28px;"> | ||
109 | <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button> | ||
110 | </div> | ||
111 | #end | ||
112 | |||
![]() |
494.1 | 113 | </div> |
114 | #end | ||
115 | #end | ||
116 | </div> | ||
117 | #end | ||
![]() |
493.1 | 118 | </div> |
119 | |||
120 | <script> | ||
![]() |
494.1 | 121 | (function(){ |
![]() |
499.1 | 122 | // ---- Helper: build REST path for nested spaces |
123 | function spacesPath(dotPath){ | ||
124 | if(!dotPath) return ''; | ||
![]() |
499.2 | 125 | return dotPath.split('.').map(function(s){ |
126 | return 'spaces/' + encodeURIComponent(s); | ||
127 | }).join('/'); | ||
![]() |
499.1 | 128 | } |
129 | |||
130 | // ---- On-demand poster: draw first frame into the placeholder canvas | ||
131 | async function makePoster(frame){ | ||
132 | if(frame.getAttribute('data-poster-ready')==='1') return; | ||
![]() |
499.2 | 133 | |
134 | const src = frame.getAttribute('data-src'); | ||
![]() |
494.1 | 135 | const type = frame.getAttribute('data-type') || 'video/mp4'; |
![]() |
499.2 | 136 | |
![]() |
499.1 | 137 | try{ |
138 | const v = document.createElement('video'); | ||
139 | v.preload = 'metadata'; | ||
![]() |
499.2 | 140 | v.muted = true; |
141 | v.playsInline = true; | ||
![]() |
499.1 | 142 | v.src = src; |
![]() |
494.1 | 143 | |
![]() |
499.1 | 144 | await new Promise((res, rej)=>{ |
145 | let done=false; | ||
![]() |
499.2 | 146 | function finish(){ |
147 | if(done) return; | ||
148 | done=true; | ||
149 | res(); | ||
150 | } | ||
![]() |
499.1 | 151 | v.addEventListener('loadeddata', finish, {once:true}); |
152 | v.addEventListener('loadedmetadata', ()=>{ | ||
![]() |
499.2 | 153 | try { |
154 | v.currentTime = 0.04; | ||
155 | } catch(e){} | ||
![]() |
499.1 | 156 | }); |
157 | v.addEventListener('error', ()=>rej(new Error('metadata error'))); | ||
158 | // Safety timeout | ||
159 | setTimeout(finish, 2500); | ||
160 | }); | ||
![]() |
494.1 | 161 | |
![]() |
499.1 | 162 | const canvas = frame.querySelector('.vid-canvas'); |
163 | if(canvas){ | ||
164 | const w = 320, h = Math.round(320 * (v.videoHeight||9) / (v.videoWidth||16)); | ||
![]() |
499.2 | 165 | canvas.width = 320; |
166 | canvas.height = h>0?h:180; | ||
![]() |
499.1 | 167 | const ctx = canvas.getContext('2d'); |
168 | if(ctx){ | ||
169 | ctx.drawImage(v, 0, 0, canvas.width, canvas.height); | ||
170 | } | ||
171 | } | ||
172 | frame.setAttribute('data-poster-ready','1'); | ||
173 | }catch(e){ | ||
174 | // Ignore; fallback stays as dark box | ||
175 | } | ||
176 | } | ||
![]() |
494.1 | 177 | |
![]() |
499.1 | 178 | // ---- Mount real <video> on click / near viewport (preload="none") |
179 | function mountVideo(frame){ | ||
180 | if(frame.getAttribute('data-mounted')==='1') return; | ||
![]() |
499.2 | 181 | |
182 | const src = frame.getAttribute('data-src'); | ||
![]() |
499.1 | 183 | const type = frame.getAttribute('data-type') || 'video/mp4'; |
![]() |
499.2 | 184 | const v = document.createElement('video'); |
185 | v.setAttribute('controls',''); | ||
186 | v.setAttribute('preload','none'); | ||
187 | v.style.width='100%'; | ||
188 | v.style.maxWidth='100%'; | ||
189 | v.style.borderRadius='4px'; | ||
190 | |||
191 | const s = document.createElement('source'); | ||
192 | s.src=src; | ||
193 | s.type=type; | ||
194 | v.appendChild(s); | ||
195 | |||
![]() |
494.1 | 196 | v.addEventListener('loadedmetadata', function(){ |
![]() |
499.2 | 197 | const d = Math.round(v.duration||0), |
198 | mm = Math.floor(d/60), | ||
199 | ss = String(d%60).padStart(2,'0'); | ||
200 | const dur = frame.parentElement.querySelector('.vid-duration'); | ||
201 | if(dur) dur.textContent = 'Duration: '+mm+':'+ss; | ||
![]() |
493.1 | 202 | }); |
![]() |
499.2 | 203 | |
![]() |
494.1 | 204 | frame.replaceChildren(v); |
205 | frame.setAttribute('data-mounted','1'); | ||
206 | } | ||
207 | |||
![]() |
499.1 | 208 | // Observe frames for posters + optional pre-mount |
![]() |
494.1 | 209 | if('IntersectionObserver' in window){ |
210 | const io = new IntersectionObserver((entries)=>{ | ||
211 | entries.forEach(e=>{ | ||
212 | if(e.isIntersecting){ | ||
![]() |
499.1 | 213 | makePoster(e.target); // generate preview only |
![]() |
494.1 | 214 | io.unobserve(e.target); |
215 | } | ||
216 | }); | ||
217 | }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') }); | ||
![]() |
499.2 | 218 | |
![]() |
494.1 | 219 | document.querySelectorAll('.video-frame').forEach(el=>io.observe(el)); |
![]() |
493.1 | 220 | } |
![]() |
494.1 | 221 | |
![]() |
499.1 | 222 | // Click to load & play |
![]() |
494.1 | 223 | document.addEventListener('click', function(ev){ |
![]() |
499.1 | 224 | const frame = ev.target.closest('.video-frame'); |
225 | if(frame){ | ||
226 | mountVideo(frame); | ||
![]() |
499.2 | 227 | const v = frame.querySelector('video'); |
228 | if(v) v.play().catch(()=>{}); | ||
![]() |
499.1 | 229 | } |
230 | }); | ||
231 | |||
232 | // Chunk reveal | ||
233 | document.addEventListener('click', function(ev){ | ||
![]() |
499.2 | 234 | const b = ev.target.closest('.load-more'); |
235 | if(!b) return; | ||
![]() |
494.1 | 236 | const next = b.getAttribute('data-next'); |
![]() |
499.1 | 237 | const nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]'); |
![]() |
499.2 | 238 | if(nxt){ |
239 | nxt.style.display = 'block'; | ||
240 | b.parentElement.style.display = 'none'; | ||
241 | } | ||
![]() |
494.1 | 242 | }); |
243 | |||
![]() |
499.1 | 244 | // ---- Move to page: search & move |
245 | const wiki = window.XWIKI_WIKI; | ||
![]() |
499.2 | 246 | |
![]() |
499.1 | 247 | async function searchPages(q){ |
![]() |
499.2 | 248 | const url = '/rest/wikis/' + encodeURIComponent(wiki) + '/search?q=' + encodeURIComponent(q) + '&scope=title,name&number=8&media=json'; |
![]() |
499.1 | 249 | // Title/name search is backed by Solr in recent XWiki; last token supports wildcard. |
250 | const r = await fetch(url, {credentials:'same-origin'}); | ||
251 | if(!r.ok) return []; | ||
252 | const json = await r.json(); | ||
253 | const items = (json.searchResults && json.searchResults.searchResult) || []; | ||
254 | // Return list of {fullName, title} | ||
255 | return items.map(it => ({ | ||
256 | fullName: (it.pageFullName || it.fullName || '').replace(/^.*:/,''), | ||
257 | title: it.title || it.pageTitle || it.highlight || it.fullName | ||
258 | })).filter(it=>it.fullName); | ||
259 | } | ||
260 | |||
261 | function renderResults(box, results, onPick){ | ||
![]() |
499.2 | 262 | function esc(s){ |
263 | return String(s).replace(/&/g,'&').replace(/</g,'<'); | ||
264 | } | ||
265 | |||
![]() |
499.1 | 266 | var wrap = document.createElement('div'); |
267 | wrap.className = 'move-suggest'; | ||
268 | wrap.style.position='absolute'; | ||
269 | wrap.style.zIndex='1000'; | ||
![]() |
499.2 | 270 | wrap.style.top='0'; |
271 | wrap.style.left='0'; | ||
272 | wrap.style.right='0'; | ||
![]() |
499.1 | 273 | wrap.style.background='#fff'; |
274 | wrap.style.border='1px solid #ddd'; | ||
275 | wrap.style.borderRadius='4px'; | ||
276 | wrap.style.boxShadow='0 6px 20px rgba(0,0,0,.08)'; | ||
277 | wrap.style.maxHeight='240px'; | ||
278 | wrap.style.overflow='auto'; | ||
279 | |||
280 | var html = ''; | ||
281 | if (results && results.length){ | ||
282 | for (var i=0;i<results.length;i++){ | ||
283 | var r = results[i]; | ||
284 | var title = r.title ? esc(r.title) : '(untitled)'; | ||
![]() |
499.2 | 285 | var full = esc(r.fullName || ''); |
![]() |
499.1 | 286 | html += '<div class="move-item" data-full="' + full + '" ' + |
287 | 'style="padding:6px 10px;cursor:pointer;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + | ||
288 | '<strong>' + title + '</strong>' + | ||
289 | '<span style="color:#777;margin-left:6px;">' + full + '</span>' + | ||
290 | '</div>'; | ||
291 | } | ||
292 | } else { | ||
293 | html = '<div style="padding:8px 10px;color:#777;">No matches</div>'; | ||
294 | } | ||
295 | wrap.innerHTML = html; | ||
296 | box.textContent = ''; | ||
297 | box.appendChild(wrap); | ||
![]() |
499.2 | 298 | |
![]() |
499.1 | 299 | box.onpointerdown = function(e){ |
![]() |
499.2 | 300 | var it = e.target.closest('.move-item'); |
301 | if(!it) return; | ||
![]() |
499.1 | 302 | var full = it.getAttribute('data-full'); |
303 | onPick(full); | ||
304 | box.textContent = ''; | ||
305 | }; | ||
306 | } | ||
307 | |||
308 | function parseFullName(full){ | ||
309 | // "Main.MyPage" or "Main.Sub.MyPage" | ||
310 | const parts = full.split('.'); | ||
311 | const page = parts.pop(); | ||
312 | const spacePath = parts.join('.'); | ||
313 | return {spacePath, page}; | ||
314 | } | ||
315 | |||
316 | async function moveAttachment(opts){ | ||
317 | var srcSpace = opts.srcSpace; | ||
318 | var srcPage = opts.srcPage; | ||
319 | var filename = opts.filename; | ||
320 | var dstFull = opts.dstFull; | ||
![]() |
499.2 | 321 | |
![]() |
499.1 | 322 | const pf = parseFullName(dstFull); |
323 | const dstSpace = pf.spacePath; | ||
324 | const dstPage = pf.page; | ||
![]() |
499.2 | 325 | |
![]() |
499.1 | 326 | const srcSpacesPath = spacesPath(srcSpace); |
327 | const dstSpacesPath = spacesPath(dstSpace); | ||
328 | |||
329 | // 1) GET the file as blob from the current attachment URL we already have in the card | ||
330 | // 2) PUT to target page's attachments | ||
331 | // 3) DELETE original | ||
![]() |
499.2 | 332 | |
![]() |
499.1 | 333 | var cardInputSel = '.video-container input.move-input[data-filename="' + CSS.escape(filename) + '"]'; |
334 | var inputEl = document.querySelector(cardInputSel); | ||
335 | var card = inputEl ? inputEl.closest('.video-container') : null; | ||
336 | var frame = card ? card.querySelector('.video-frame') : null; | ||
337 | const srcURL = frame ? frame.getAttribute('data-src') : null; | ||
338 | if(!srcURL) throw new Error('Missing source URL'); | ||
339 | |||
340 | const downloading = await fetch(srcURL, {credentials:'same-origin'}); | ||
341 | if(!downloading.ok) throw new Error('Download failed: '+downloading.status); | ||
342 | const blob = await downloading.blob(); | ||
343 | |||
![]() |
499.2 | 344 | const putURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + dstSpacesPath + '/pages/' + encodeURIComponent(dstPage) + '/attachments/' + encodeURIComponent(filename) + '?media=json'; |
![]() |
499.1 | 345 | const uploading = await fetch(putURL, { |
346 | method: 'PUT', | ||
347 | body: blob, | ||
348 | headers: {'Content-Type':'application/octet-stream'}, | ||
349 | credentials:'same-origin' | ||
350 | }); | ||
![]() |
499.2 | 351 | |
![]() |
499.1 | 352 | if(!(uploading.status===201 || uploading.status===202)){ |
353 | const txt = await uploading.text().catch(()=>String(uploading.status)); | ||
354 | throw new Error('Upload failed: '+txt); | ||
355 | } | ||
356 | |||
![]() |
499.2 | 357 | const delURL = '/rest/wikis/' + encodeURIComponent(wiki) + '/' + srcSpacesPath + '/pages/' + encodeURIComponent(srcPage) + '/attachments/' + encodeURIComponent(filename); |
![]() |
499.1 | 358 | const deleting = await fetch(delURL, {method:'DELETE', credentials:'same-origin'}); |
![]() |
499.2 | 359 | |
![]() |
499.1 | 360 | if(!(deleting.status===204)){ |
361 | // Not fatal: the file exists at destination; warn but keep going. | ||
362 | console.warn('Delete original failed', deleting.status); | ||
363 | } | ||
![]() |
499.2 | 364 | |
![]() |
499.1 | 365 | return true; |
366 | } | ||
367 | |||
368 | // Wire up per-card search boxes | ||
369 | let timer=null; | ||
370 | document.querySelectorAll('.move-box .move-input').forEach(inp=>{ | ||
371 | const resultsBox = inp.parentElement.querySelector('.move-results'); | ||
![]() |
499.2 | 372 | |
![]() |
499.1 | 373 | inp.addEventListener('input', ()=>{ |
374 | clearTimeout(timer); | ||
375 | const q = inp.value.trim(); | ||
![]() |
499.2 | 376 | if(!q){ |
377 | resultsBox.textContent=''; | ||
378 | return; | ||
379 | } | ||
![]() |
499.1 | 380 | timer = setTimeout(async ()=>{ |
381 | const res = await searchPages(q); | ||
382 | renderResults(resultsBox, res, async (full)=>{ | ||
![]() |
499.2 | 383 | inp.value = full; // Fill the input |
384 | |||
![]() |
499.1 | 385 | // Kick the move |
386 | const filename = inp.getAttribute('data-filename'); | ||
387 | const notice = document.createElement('div'); | ||
![]() |
499.2 | 388 | notice.style.fontSize='12px'; |
389 | notice.style.color='#666'; | ||
390 | notice.textContent='Moving…'; | ||
![]() |
499.1 | 391 | inp.parentElement.appendChild(notice); |
![]() |
499.2 | 392 | |
![]() |
499.1 | 393 | try{ |
394 | await moveAttachment({ | ||
395 | srcSpace: window.SOURCE_SPACE, | ||
![]() |
499.2 | 396 | srcPage: window.SOURCE_PAGE, |
![]() |
499.1 | 397 | filename: filename, |
![]() |
499.2 | 398 | dstFull: full |
![]() |
499.1 | 399 | }); |
400 | notice.textContent = 'Moved ✔ — reloading…'; | ||
401 | setTimeout(()=>location.reload(), 600); | ||
402 | }catch(e){ | ||
403 | notice.style.color = '#b00020'; | ||
404 | notice.textContent = 'Move failed: ' + (e && e.message ? e.message : e); | ||
405 | } | ||
406 | }); | ||
407 | }, 220); | ||
408 | }); | ||
![]() |
499.2 | 409 | |
![]() |
499.1 | 410 | // Close suggestions when clicking out |
![]() |
499.2 | 411 | document.addEventListener('click', (e)=>{ |
412 | if(!inp.parentElement.contains(e.target)) resultsBox.textContent=''; | ||
413 | }); | ||
![]() |
499.1 | 414 | }); |
![]() |
499.2 | 415 | |
![]() |
494.1 | 416 | })(); |
![]() |
493.1 | 417 | </script> |
418 | |||
419 | <style> | ||
![]() |
499.2 | 420 | .video-container:hover{ |
421 | box-shadow:0 4px 8px rgba(0,0,0,0.08); | ||
422 | transition:box-shadow .25s; | ||
423 | } | ||
424 | |||
425 | .btn{ | ||
426 | padding:4px 8px; | ||
427 | border:1px solid #ddd; | ||
428 | background:#f8f9fa; | ||
429 | border-radius:4px; | ||
430 | cursor:pointer; | ||
431 | text-decoration:none; | ||
432 | display:inline-block; | ||
433 | } | ||
434 | |||
435 | .btn:hover{ | ||
436 | background:#e9ecef; | ||
437 | } | ||
438 | |||
439 | .btn-primary{ | ||
440 | background:#007bff; | ||
441 | color:#fff; | ||
442 | border-color:#007bff; | ||
443 | } | ||
444 | |||
445 | .btn-secondary{ | ||
446 | background:#6c757d; | ||
447 | color:#fff; | ||
448 | border-color:#6c757d; | ||
449 | } | ||
450 | |||
451 | .btn-success{ | ||
452 | background:#28a745; | ||
453 | color:#fff; | ||
454 | border-color:#28a745; | ||
455 | } | ||
456 | |||
457 | .btn-sm{ | ||
458 | font-size:12px; | ||
459 | padding:2px 6px; | ||
460 | } | ||
461 | |||
462 | @media (max-width:768px){ | ||
463 | .video-display-grid{ | ||
464 | grid-template-columns:1fr; | ||
465 | } | ||
466 | } | ||
467 | |||
468 | .move-suggest::-webkit-scrollbar{ | ||
469 | width:10px; | ||
470 | height:10px; | ||
471 | } | ||
472 | |||
473 | .move-suggest::-webkit-scrollbar-thumb{ | ||
474 | background:#ccc; | ||
475 | border-radius:6px; | ||
476 | } | ||
![]() |
493.1 | 477 | </style> |
![]() |
499.2 | 478 | |
![]() |
493.1 | 479 | {{/html}} |
![]() |
499.2 | 480 | |
![]() |
494.1 | 481 | {{/cache}} |
![]() |
499.2 | 482 | |
![]() |
493.1 | 483 | {{/velocity}} |