0 Votes

Changes for page Uncategorized Videos

Last modified by Ryan C on 2025/09/10 07:29

From version 493.1
edited by Ryan C
on 2025/09/10 03:49
Change comment: There is no comment for this version
To version 494.1
edited by Ryan C
on 2025/09/10 03:59
Change comment: There is no comment for this version

Summary

Details

Page properties
Content
... ... @@ -1,5 +1,5 @@
1 1  {{velocity}}
2 -## Collect video attachments on the current page (safe getters only)
2 +## Gather video attachments on the current page
3 3  #set($videoExtensions = ['mp4','webm','ogg','avi','mov','wmv','flv','m4v'])
4 4  #set($videos = [])
5 5  #foreach($att in $doc.getAttachmentList())
... ... @@ -13,158 +13,177 @@
13 13   #end
14 14  #end
15 15  
16 -#if($videos.size() > 0)
16 +{{cache id="vid-list-$doc.fullName" timeToLive="21600"}}## 6h cache of rendered HTML
17 17  {{html wiki="false" clean="false"}}
18 18  <div id="xwiki-video-manager" style="margin:20px 0;">
19 - <h2>📹 Video Manager for: ${escapetool.xml($doc.fullName)}</h2>
19 + <h2>📹 Videos on: ${escapetool.xml($doc.fullName)}</h2>
20 20  
21 - <!-- Management panel only in edit action -->
22 - #if($xcontext.action == 'edit')
23 - <div class="video-management-panel" style="background:#f8f9fa;border:1px solid #dee2e6;border-radius:8px;padding:15px;margin-bottom:20px;">
24 - <h3>🛠️ Video Management</h3>
25 - <div style="margin-bottom:10px;">
26 - <button onclick="toggleVideoManager()" class="btn btn-primary">Manage Videos</button>
27 - <button onclick="exportVideoList()" class="btn btn-secondary">Export Video List</button>
28 - </div>
29 - <div id="video-manager-controls" style="display:none;">
30 - <h4>Move Videos to Another Page:</h4>
31 - <input type="text" id="target-page" placeholder="Space.PageName" style="width:300px;margin-right:10px;">
32 - <button onclick="moveSelectedVideos()" class="btn btn-warning">Move Selected</button>
33 - <div style="margin-top:10px;font-size:12px;color:#666;">
34 - Select videos below and enter target page (e.g., "Main.MyPage")
35 - </div>
36 - </div>
21 + #if($videos.size() == 0)
22 + <div style="text-align:center;padding:40px;background:#f8f9fa;border-radius:8px;">
23 + <h3>No Videos Found</h3>
24 + <p>Attach video files to this page to see them here.</p>
37 37   </div>
38 - #end
26 + #else
27 + ## ---- Settings
28 + <script>
29 + window.VID_CHUNK_SIZE = 48; // how many lightweight cards per chunk
30 + window.VID_LAZY_MARGIN = '600px';// start loading a bit before entering view
31 + </script>
39 39  
40 - <!-- Display grid -->
41 - <div class="video-display-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(400px,1fr));gap:20px;">
42 - #set($i = 0)
43 - #foreach($att in $videos)
44 - #set($i = $i + 1)
45 - #set($filename = $att.getFilename())
46 - #set($lname = $filename.toLowerCase())
47 - #set($url = $doc.getAttachmentURL($filename))
48 - #set($vid = "video_${i}")
33 + <div id="video-chunks">
34 + #set($i = 0)
35 + #set($chunkIndex = 0)
36 + #foreach($att in $videos)
37 + #set($i = $i + 1)
38 + #set($filename = $att.getFilename())
39 + #set($lname = $filename.toLowerCase())
40 + #set($url = $doc.getAttachmentURL($filename))
49 49  
50 - ## MIME type from extension (basic)
51 - #set($videoType = "video/mp4")
52 - #if($lname.endsWith(".webm"))
53 - #set($videoType = "video/webm")
54 - #elseif($lname.endsWith(".ogg"))
55 - #set($videoType = "video/ogg")
56 - #elseif($lname.endsWith(".avi"))
57 - #set($videoType = "video/x-msvideo")
58 - #elseif($lname.endsWith(".mov"))
59 - #set($videoType = "video/quicktime")
60 - #elseif($lname.endsWith(".wmv"))
61 - #set($videoType = "video/x-ms-wmv")
62 - #elseif($lname.endsWith(".flv"))
63 - #set($videoType = "video/x-flv")
64 - #elseif($lname.endsWith(".m4v"))
42 + ## decide MIME type (lightweight, used later when creating <video>)
65 65   #set($videoType = "video/mp4")
66 - #end
44 + #if($lname.endsWith(".webm"))
45 + #set($videoType = "video/webm")
46 + #elseif($lname.endsWith(".ogg"))
47 + #set($videoType = "video/ogg")
48 + #elseif($lname.endsWith(".avi"))
49 + #set($videoType = "video/x-msvideo")
50 + #elseif($lname.endsWith(".mov"))
51 + #set($videoType = "video/quicktime")
52 + #elseif($lname.endsWith(".wmv"))
53 + #set($videoType = "video/x-ms-wmv")
54 + #elseif($lname.endsWith(".flv"))
55 + #set($videoType = "video/x-flv")
56 + #elseif($lname.endsWith(".m4v"))
57 + #set($videoType = "video/mp4")
58 + #end
67 67  
68 - <div class="video-container" style="border:1px solid #ddd;border-radius:8px;padding:15px;background:#fff;">
69 - <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:10px;">
70 - <h4 style="margin:0;flex-grow:1;">${escapetool.xml($filename)}</h4>
71 - #if($xcontext.action == 'edit')
72 - <input type="checkbox" class="video-selector" data-video="${escapetool.xml($filename)}" style="margin-left:10px;">
73 - #end
74 - </div>
60 + ## open chunk wrapper when starting a new chunk
61 + #if($i == 1 || ($i - 1) % 48 == 0)
62 + #set($chunkIndex = $chunkIndex + 1)
63 + <div class="vid-chunk" data-chunk="$chunkIndex" style="display: #if($chunkIndex == 1) block #else none #end;">
64 + <div class="video-display-grid" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:20px;">
65 + #end
75 75  
76 - <video id="${vid}" controls preload="metadata" style="width:100%;max-width:100%;border-radius:4px;">
77 - <source src="${url}" type="${videoType}">
78 - Your browser does not support the video tag.
79 - </video>
67 + ## lightweight card (NO <video> yet)
68 + <div class="video-container" style="border:1px solid #ddd;border-radius:8px;padding:12px;background:#fff;">
69 + <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
70 + <h4 style="margin:0;flex:1;min-width:0;">${escapetool.xml($filename)}</h4>
71 + #if($xcontext.action == 'edit')
72 + <input type="checkbox" class="video-selector" data-video="${escapetool.xml($filename)}" title="Select for bulk actions">
73 + #end
74 + </div>
80 80  
81 - <div class="video-controls" style="margin-top:10px;display:flex;flex-wrap:wrap;gap:5px;">
82 - <button onclick="playPause('${vid}')" class="btn btn-sm btn-primary">⏯️ Play/Pause</button>
83 - <button onclick="skipTime('${vid}', -10)" class="btn btn-sm btn-secondary">⏪ -10s</button>
84 - <button onclick="skipTime('${vid}', 10)" class="btn btn-sm btn-secondary">⏩ +10s</button>
85 - <button onclick="changeSpeed('${vid}', 0.5)" class="btn btn-sm btn-info">0.5x</button>
86 - <button onclick="changeSpeed('${vid}', 1)" class="btn btn-sm btn-info">1x</button>
87 - <button onclick="changeSpeed('${vid}', 1.5)" class="btn btn-sm btn-info">1.5x</button>
88 - <button onclick="changeSpeed('${vid}', 2)" class="btn btn-sm btn-info">2x</button>
89 - <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
90 - </div>
76 + <!-- Placeholder; the real <video> is created on demand -->
77 + <div class="video-frame"
78 + data-src="${url}"
79 + data-type="${videoType}"
80 + data-name="${escapetool.xml($filename)}"
81 + style="width:100%;aspect-ratio:16/9;background:#f3f3f3;border-radius:4px;display:flex;align-items:center;justify-content:center;cursor:pointer;">
82 + <button class="btn btn-sm btn-primary" type="button">Load &amp; Play</button>
83 + </div>
91 91  
92 - <div class="video-info" style="margin-top:10px;font-size:12px;color:#666;">
93 - <div>Size: $att.getLongSize() bytes</div>
94 - <div>Modified: $xwiki.formatDate($att.getDate())</div>
95 - <div id="${vid}_duration">Duration: Loading...</div>
85 + <div class="video-controls" style="margin-top:8px;display:flex;flex-wrap:wrap;gap:6px;">
86 + <a href="${url}" download="${escapetool.xml($filename)}" class="btn btn-sm btn-success">📥 Download</a>
87 + <span class="vid-duration" style="font-size:12px;color:#666;">Duration: —</span>
88 + </div>
96 96   </div>
97 - </div>
98 - #end
99 - </div>
90 +
91 + ## close chunk wrapper when reaching size or end
92 + #if(($i % 48 == 0) || $foreach.last)
93 + </div>
94 + #if(!$foreach.last)
95 + <div style="text-align:center;margin:12px 0 28px;">
96 + <button class="btn btn-secondary load-more" data-next="$mathtool.add($chunkIndex,1)">Load more</button>
97 + </div>
98 + #end
99 + </div>
100 + #end
101 + #end
102 + </div>
103 + #end
100 100  </div>
101 101  
102 102  <script>
103 -function playPause(id){const v=document.getElementById(id);if(v){v.paused?v.play():v.pause();}}
104 -function skipTime(id,s){const v=document.getElementById(id);if(v){v.currentTime+=s;}}
105 -function changeSpeed(id,sp){const v=document.getElementById(id);if(v){v.playbackRate=sp;}}
106 -document.addEventListener('DOMContentLoaded',function(){
107 - document.querySelectorAll('video').forEach(function(v){
108 - v.addEventListener('loadedmetadata',function(){
109 - var d=Math.round(v.duration)||0,m=Math.floor(d/60),s=d%60;
110 - var e=document.getElementById(v.id+'_duration');
111 - if(e){e.textContent='Duration: '+m+':' + String(s).padStart(2,'0');}
107 +(function(){
108 + // Utility: create a <video preload="none"> only when needed
109 + function mountVideo(frame){
110 + if(frame.getAttribute('data-mounted') === '1') return;
111 + const src = frame.getAttribute('data-src');
112 + const type = frame.getAttribute('data-type') || 'video/mp4';
113 + const name = frame.getAttribute('data-name') || 'video';
114 +
115 + const v = document.createElement('video');
116 + v.setAttribute('controls','');
117 + v.setAttribute('preload','none'); // don't fetch until user interacts or we call load()
118 + v.style.width = '100%';
119 + v.style.maxWidth = '100%';
120 + v.style.borderRadius = '4px';
121 +
122 + const s = document.createElement('source');
123 + s.src = src;
124 + s.type = type;
125 + v.appendChild(s);
126 +
127 + // when metadata arrives (after load()), fill duration text
128 + v.addEventListener('loadedmetadata', function(){
129 + const d = Math.round(v.duration||0);
130 + const mm = Math.floor(d/60), ss = String(d%60).padStart(2,'0');
131 + const dur = frame.parentElement.querySelector('.vid-duration');
132 + if(dur) dur.textContent = 'Duration: ' + mm + ':' + ss;
112 112   });
134 +
135 + frame.replaceChildren(v);
136 + frame.setAttribute('data-mounted','1');
137 + // We don't call v.load() here; it will start when the user clicks play.
138 + }
139 +
140 + // Click-to-load for each placeholder
141 + document.querySelectorAll('.video-frame').forEach(function(f){
142 + f.addEventListener('click', function(){
143 + mountVideo(f);
144 + const v = f.querySelector('video');
145 + if(v) v.play().catch(()=>{});
146 + }, {once:false});
113 113   });
114 -});
115 -function toggleVideoManager(){
116 - var c=document.getElementById('video-manager-controls');
117 - if(c){c.style.display=(c.style.display==='none'||!c.style.display)?'block':'none';}
118 -}
119 -function moveSelectedVideos(){
120 - const sel=[...document.querySelectorAll('.video-selector:checked')].map(x=>x.getAttribute('data-video'));
121 - const target=(document.getElementById('target-page')||{}).value||'';
122 - if(!target){alert('Please enter a target page (e.g., "Main.MyPage")');return;}
123 - if(!sel.length){alert('Please select at least one video to move.');return;}
124 - if(confirm('Move '+sel.length+' video(s) to '+target+'?\n\nVideos: '+sel.join(', '))){
125 - alert('Server-side move not implemented.\nSelected: '+sel.join(', ')+'\nTarget: '+target);
148 +
149 + // IntersectionObserver to auto-mount when approaching viewport
150 + if('IntersectionObserver' in window){
151 + const io = new IntersectionObserver((entries)=>{
152 + entries.forEach(e=>{
153 + if(e.isIntersecting){
154 + mountVideo(e.target);
155 + // don't auto-play on scroll; user can play if desired
156 + io.unobserve(e.target);
157 + }
158 + });
159 + }, { rootMargin: (window.VID_LAZY_MARGIN||'600px') });
160 + document.querySelectorAll('.video-frame').forEach(el=>io.observe(el));
126 126   }
127 -}
128 -function exportVideoList(){
129 - const titles=[...document.querySelectorAll('.video-container h4')].map(h=>h.textContent);
130 - const txt='Video List from '+window.location.href+'\n\n'+titles.join('\n');
131 - const blob=new Blob([txt],{type:'text/plain'});const url=URL.createObjectURL(blob);
132 - const a=document.createElement('a');a.href=url;a.download='video-list.txt';
133 - document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);
134 -}
135 -document.addEventListener('keydown',function(e){
136 - const v=document.querySelector('video:hover, video:focus'); if(!v) return;
137 - switch(e.code){
138 - case 'Space': e.preventDefault(); v.paused?v.play():v.pause(); break;
139 - case 'ArrowLeft': e.preventDefault(); v.currentTime-=10; break;
140 - case 'ArrowRight': e.preventDefault(); v.currentTime+=10; break;
141 - }
142 -});
162 +
163 + // Chunk loader (reveals next batch only when user asks)
164 + document.addEventListener('click', function(ev){
165 + const b = ev.target.closest('.load-more');
166 + if(!b) return;
167 + const next = b.getAttribute('data-next');
168 + const nxt = document.querySelector('.vid-chunk[data-chunk="'+ next +'"]');
169 + if(nxt){ nxt.style.display = 'block'; b.parentElement.style.display = 'none'; }
170 + });
171 +
172 + // Basic button styles (same as before)
173 +})();
143 143  </script>
144 144  
145 145  <style>
146 -.video-container:hover{box-shadow:0 4px 8px rgba(0,0,0,0.1);transition:box-shadow .3s ease;}
177 +.video-container:hover{box-shadow:0 4px 8px rgba(0,0,0,0.08);transition:box-shadow .25s;}
147 147  .btn{padding:4px 8px;border:1px solid #ddd;background:#f8f9fa;border-radius:4px;cursor:pointer;text-decoration:none;display:inline-block;}
148 148  .btn:hover{background:#e9ecef;}
149 149  .btn-primary{background:#007bff;color:#fff;border-color:#007bff;}
150 150  .btn-secondary{background:#6c757d;color:#fff;border-color:#6c757d;}
151 151  .btn-success{background:#28a745;color:#fff;border-color:#28a745;}
152 -.btn-warning{background:#ffc107;color:#212529;border-color:#ffc107;}
153 -.btn-info{background:#17a2b8;color:#fff;border-color:#17a2b8;}
154 154  .btn-sm{font-size:12px;padding:2px 6px;}
155 -@media (max-width:768px){.video-display-grid{grid-template-columns:1fr}.video-controls{justify-content:center}}
184 +@media (max-width:768px){.video-display-grid{grid-template-columns:1fr}}
156 156  </style>
157 157  {{/html}}
158 -#else
159 -{{html wiki="false" clean="false"}}
160 -<div style="text-align:center;padding:40px;background:#f8f9fa;border-radius:8px;">
161 - <h3>📹 No Videos Found</h3>
162 - <p>No video files are attached to this page.</p>
163 - #if($xcontext.action == 'edit')
164 - <p>You can upload video files using the attachment feature in XWiki.</p>
165 - #end
166 -</div>
167 -{{/html}}
168 -#end
187 +{{/cache}}
169 169  {{/velocity}}
170 170