' +
80 | // If video description is found, use it, otherwise fallback to generic description element.
81 | (feedItem.querySelector('.enclosure-description')?.innerHTML.trim() ||
82 | feedItem.querySelector('article div.text')?.innerHTML.trim() || '') +
83 | '
',
84 | video_youtube_url: youubeUrl,
85 | video_invidious_redirect_url: `${youtubeId ? invidiousRedirectPrefixUrl + youtubeId : ''}`
86 | };
87 | }
88 |
89 | function createModalWithData(data) {
90 | // Create custom modal
91 | let modal = document.getElementById('youlagTheaterModal');
92 |
93 | if (!modal) {
94 | modal = document.createElement('div');
95 | modal.id = 'youlagTheaterModal';
96 | modal.innerHTML = `
104 |
105 |
109 |
110 |
111 |
112 |

113 |
114 |
115 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
168 |
169 |
170 |
171 |
172 | ${data.video_description}
173 |
174 |
175 |
176 |
177 |
178 | `;
179 |
180 |
181 | if (!youtubeId) {
182 | // Not a video feed item
183 | modal.classList.add('youlag-modal-feed-item--text');
184 | let iframeContainer = document.querySelector('.youlag-iframe-container');
185 | if (iframeContainer) {
186 | document.querySelector('.youlag-iframe-container').remove();
187 | }
188 | }
189 |
190 |
191 | container.querySelector(`#${modalCloseIdName}`)?.addEventListener('click', closeModal);
192 | container.querySelector(`#${modalMinimizeIdName}`)?.addEventListener('click', togglePipMode);
193 | container.querySelector(`#${modalToggleFavoriteIdName}`)?.addEventListener('click', (e) => {
194 | // Toggle favorites state in background
195 | e.preventDefault();
196 | toggleFavorite(data.favorite_toggle_url, container, data.feedItemEl);
197 | });
198 |
199 | // Push a new state to the history, to allow modal close when routing back.
200 | history.pushState({ modalOpen: true }, '', '');
201 |
202 | // Close theater modal on Esc key
203 | document.addEventListener('keydown', (event) => {
204 | if (event.key === 'Escape') {
205 | closeModal();
206 | }
207 | });
208 |
209 | window.addEventListener('popstate', closeModal);
210 | }
211 |
212 | function toggleFavorite(url, container, feedItemEl) {
213 | const favoriteButton = container.querySelector(`#${modalToggleFavoriteIdName}`);
214 | if (!favoriteButton) return;
215 |
216 | fetch(url, { method: 'GET' })
217 | .then(response => {
218 | if (response.ok) {
219 | // Toggle favorite classes and icons
220 | const currentlyTrue = favoriteButton.classList.contains(`${modalFavoriteClassName}--true`);
221 | const bookmarkIcon = feedItemEl.querySelector('.item-element.bookmark img.icon');
222 | favoriteButton.classList.remove(`${modalFavoriteClassName}--${currentlyTrue}`);
223 | favoriteButton.classList.add(`${modalFavoriteClassName}--${!currentlyTrue}`);
224 |
225 | if (currentlyTrue) {
226 | feedItemEl.classList.remove('favorite');
227 | if (bookmarkIcon) {
228 | bookmarkIcon.src = '../themes/Mapco/icons/non-starred.svg';
229 | }
230 | } else {
231 | feedItemEl.classList.add('favorite');
232 | if (bookmarkIcon) {
233 | bookmarkIcon.src = '../themes/Mapco/icons/starred.svg';
234 | }
235 | }
236 | } else {
237 | console.error('Failed to toggle favorite status');
238 | }
239 | })
240 | .catch(error => console.error('Error:', error));
241 | }
242 |
243 | function closeModal() {
244 | const modal = document.getElementById('youlagTheaterModal');
245 | if (modal) modal.remove();
246 | if (history.state && history.state.modalOpen) {
247 | history.back();
248 | }
249 | setModePip(false);
250 | setModeFullscreen(false);
251 | }
252 |
253 | function togglePipMode() {
254 | if (modePip) {
255 | setModePip(false);
256 | setModeFullscreen(true);
257 | }
258 | else {
259 | setModePip(true);
260 | setModeFullscreen(false);
261 | }
262 | }
263 |
264 | function setModePip(state) {
265 | if (state === true) {
266 | document.body.classList.add('youlag-mode--pip');
267 | modePip = true;
268 | modeFullscreen = false;
269 | }
270 | else if (state === false) {
271 | document.body.classList.remove('youlag-mode--pip');
272 | modePip = false;
273 | }
274 | }
275 |
276 | function setModeFullscreen(state) {
277 | if (state === true) {
278 | document.body.classList.add('youlag-mode--fullscreen');
279 | document.body.classList.remove('youlag-mode--pip');
280 | modeFullscreen = true;
281 | modePip = false;
282 | }
283 | else if (state === false) {
284 | document.body.classList.remove('youlag-mode--fullscreen');
285 | modeFullscreen = false;
286 | }
287 | }
288 |
289 | function setupClickListener() {
290 | const streamContainer = document.querySelector('#stream');
291 |
292 | if (streamContainer) {
293 | streamContainer.addEventListener('click', (event) => {
294 | // Prevent activation if clicked element is inside .flux_header li.
295 | // These are the feed item actions buttons.
296 | if (event.target.closest('div[data-feed] .flux_header li.manage')) return;
297 | if (event.target.closest('div[data-feed] .flux_header li.labels')) return;
298 | if (event.target.closest('div[data-feed] .flux_header li.share')) return;
299 | if (event.target.closest('div[data-feed] .flux_header li.link')) return;
300 | if (event.target.closest('div[data-feed] .flux_header .website a[href^="./?get=f_"]')) return;
301 | const target = event.target.closest('div[data-feed]');
302 |
303 | if (target) {
304 | handleActiveRssItem(event);
305 | collapseBackgroundFeedItem(target);
306 | }
307 | });
308 | }
309 | }
310 |
311 | function collapseBackgroundFeedItem(target) {
312 | // Workaround: If user has YouTube Video Feed extension installed, prevent it from showing the default embedded
313 | // in favor of Youlag theater view modal. This collapses down the original feed item that activates by FreshRSS clickevent.
314 |
315 | const feedItem = target;
316 | let isActive = feedItem.classList.contains('active') && feedItem.classList.contains('current');
317 | const iframes = feedItem.querySelectorAll('iframe');
318 |
319 | if (iframes || youtubeExtensionInstalled) {
320 | iframes.forEach(iframe => {
321 | // Disable iframes to prevent autoplay
322 | const src = iframe.getAttribute('src');
323 | if (src) {
324 | iframe.setAttribute('data-original', src);
325 | iframe.setAttribute('src', '');
326 | }
327 | });
328 | }
329 |
330 | if (isActive) {
331 | // Collapse the feed item
332 | feedItem.classList.remove('active');
333 | feedItem.classList.remove('current');
334 | }
335 | }
336 |
337 | function init() {
338 | setupClickListener();
339 | removeYoulagLoadingState();
340 | youlagScriptLoaded = true;
341 | }
342 |
343 | function removeYoulagLoadingState() {
344 | // By default, the youlag CSS is set to a loading state.
345 | // This will remove the loading state when the script is ready.
346 | document.body.classList.add('youlag-loaded');
347 | }
348 |
349 | function initFallback() {
350 | if (document.readyState === 'complete' || document.readyState === 'interactive' || youlagScriptLoaded === true) {
351 | init();
352 | } else {
353 | document.addEventListener('DOMContentLoaded', init);
354 | window.addEventListener('load', init);
355 | }
356 | }
357 |
358 | // Fallback interval check
359 | const checkInitInterval = setInterval(() => {
360 | if (document.readyState === 'complete' || youlagScriptLoaded === true) {
361 | init();
362 | clearInterval(checkInitInterval);
363 | }
364 | }, 1000);
365 |
366 | // Ensure init runs
367 | initFallback();
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.