document.addEventListener('alpine:init', () => { Alpine.data('library', () => ({ currentPath: '', parentPath: null, entries: [], selected: null, loading: false, playerLoading: false, playerLoadingCompact: false, error: '', playerController: null, viewMode: 'grid', infoOpen: false, infoLoading: false, infoError: '', diagnostics: null, previewFrames: {}, previewUrls: {}, previewStates: {}, previewTimers: {}, previewActive: {}, previewImages: {}, previewGeneration: 0, init() { try { this.viewMode = localStorage.getItem('lan-play:view') || 'grid'; } catch (_) {} this.locationHandler = () => { this.restoreLocation(); }; this.resizeHandler = () => { this.scheduleMarqueeCheck(); }; window.addEventListener('popstate', this.locationHandler); window.addEventListener('resize', this.resizeHandler); }, destroy() { this.stopAllPreviews(); this.cancelPreviewLoads(); window.removeEventListener('popstate', this.locationHandler); window.removeEventListener('resize', this.resizeHandler); if (this.marqueeFrame) window.cancelAnimationFrame(this.marqueeFrame); if (this.playerController) this.playerController.dispose(); }, async restoreLocation() { const location = window.LanPlayLocation.read(); if (this.playerController) await this.playerController.stop(); this.selected = null; await this.browse(location.path, false); if (location.play) { await this.play({ path: location.play, name: window.LanPlayLocation.fileName(location.play), kind: 'media' }, false); } }, async browse(path, updateLocation = true) { this.stopAllPreviews(); this.cancelPreviewLoads(); this.loading = true; this.error = ''; try { const data = await window.LanPlayApi.browse(path); this.currentPath = data.path; this.parentPath = data.parent; this.entries = data.entries; if (updateLocation) window.LanPlayLocation.write(data.path, null, false); await this.$nextTick(); this.preloadPreviews(data.entries); this.scheduleMarqueeCheck(); } catch (error) { this.error = error.message; } finally { this.loading = false; } }, async play(entry, updateLocation = true) { this.stopAllPreviews(); this.enterFullscreen(); this.error = ''; this.selected = entry; this.playerLoading = true; if (updateLocation) window.LanPlayLocation.write(this.currentPath, entry.path, false); await this.$nextTick(); if (!this.playerController) { this.playerController = new window.LanPlayPlayer( this.$refs.video, this.$refs.videoWrapper, (message) => { this.error = message; }, (loading, compact = false) => { this.playerLoading = loading; this.playerLoadingCompact = loading && compact; }, () => { this.playNext(); } ); } this.playerController.setNextAvailable(Boolean(this.nextEntry())); try { await this.playerController.play(entry); } catch (error) { this.error = error.message; } finally { this.playerLoading = false; this.playerLoadingCompact = false; } }, nextEntry() { if (!this.selected) return null; const media = this.entries.filter((entry) => entry.kind === 'media'); const index = media.findIndex((entry) => entry.path === this.selected.path); return index >= 0 ? media[index + 1] || null : null; }, async playNext() { const next = this.nextEntry(); if (next) await this.play(next); }, mediaTitle() { if (!this.selected || !this.selected.name) return ''; return this.selected.name.replace(/\.[^.]+$/, ''); }, setViewMode(mode) { this.viewMode = mode === 'table' ? 'table' : 'grid'; try { localStorage.setItem('lan-play:view', this.viewMode); } catch (_) {} this.$nextTick(() => this.scheduleMarqueeCheck()); }, scheduleMarqueeCheck() { if (this.marqueeFrame) window.cancelAnimationFrame(this.marqueeFrame); this.marqueeFrame = window.requestAnimationFrame(() => { this.marqueeFrame = null; document.querySelectorAll('.entry-name').forEach((container) => { if (container.clientWidth <= 0) return; const text = container.querySelector('.entry-name-track'); container.classList.toggle('is-overflowing', Boolean(text) && text.scrollWidth > container.clientWidth + 1); }); }); }, openEntry(entry) { return entry.kind === 'directory' ? this.browse(entry.path) : this.play(entry); }, async openInfo() { this.infoOpen = true; this.infoLoading = true; this.infoError = ''; try { const server = await window.LanPlayApi.info(); let dashVersion = 'Unknown'; try { dashVersion = dashjs.MediaPlayer().create().getVersion(); } catch (_) {} this.diagnostics = { ...server, interface: '20', dashjs: dashVersion, browser: navigator.userAgent, display: `${window.screen.width}×${window.screen.height} @ ${window.devicePixelRatio || 1}x`, fullscreen: Boolean(document.documentElement.requestFullscreen || document.documentElement.webkitRequestFullscreen), mediaSource: 'MediaSource' in window }; } catch (error) { this.infoError = error.message; } finally { this.infoLoading = false; } }, closeInfo() { this.infoOpen = false; }, previewStyle(entry) { if (this.previewStates[entry.path] !== 'ready') return ''; const url = this.previewUrls[entry.path]; if (!url) return ''; const frame = this.previewFrames[entry.path] || 0; const column = frame % 5; const row = Math.floor(frame / 5); return `background-image:url("${url}");background-position:${column * 25}% ${row * 100}%`; }, startPreview(entry) { if (!entry || entry.kind !== 'media') return; this.previewActive[entry.path] = true; this.stopPreview(entry); this.previewActive[entry.path] = true; if (this.previewStates[entry.path] !== 'ready') return; this.previewFrames[entry.path] = 0; this.previewTimers[entry.path] = window.setInterval(() => { this.previewFrames[entry.path] = ((this.previewFrames[entry.path] || 0) + 1) % 10; }, 500); }, stopPreview(entry) { if (!entry) return; this.previewActive[entry.path] = false; const timer = this.previewTimers[entry.path]; if (timer) window.clearInterval(timer); delete this.previewTimers[entry.path]; this.previewFrames[entry.path] = 0; }, stopAllPreviews() { Object.values(this.previewTimers).forEach((timer) => window.clearInterval(timer)); this.previewTimers = {}; this.previewActive = {}; }, preloadPreviews(entries) { const generation = this.previewGeneration; entries.filter((entry) => entry.kind === 'media').forEach((entry) => { const path = entry.path; const url = `/api/preview?path=${encodeURIComponent(path)}`; const image = new Image(); this.previewUrls[path] = url; this.previewStates[path] = 'loading'; this.previewImages[path] = image; image.onload = () => { if (generation !== this.previewGeneration) return; this.previewStates[path] = 'ready'; delete this.previewImages[path]; if (this.previewActive[path]) this.startPreview(entry); }; image.onerror = () => { if (generation !== this.previewGeneration) return; this.previewStates[path] = 'error'; delete this.previewImages[path]; }; image.src = url; }); }, cancelPreviewLoads() { this.previewGeneration += 1; Object.values(this.previewImages).forEach((image) => { image.onload = null; image.onerror = null; image.src = ''; }); this.previewImages = {}; this.previewFrames = {}; this.previewUrls = {}; this.previewStates = {}; }, enterFullscreen() { if (document.fullscreenElement || document.webkitFullscreenElement) return; const root = document.documentElement; const request = root.requestFullscreen || root.webkitRequestFullscreen; if (request) { try { const result = request.call(root); if (result && result.catch) result.catch(() => {}); } catch (_) {} } }, exitFullscreen() { if (!document.fullscreenElement && !document.webkitFullscreenElement) return; const exit = document.exitFullscreen || document.webkitExitFullscreen; if (exit) { try { const result = exit.call(document); if (result && result.catch) result.catch(() => {}); } catch (_) {} } }, async closePlayer(updateLocation = true) { this.exitFullscreen(); this.selected = null; this.playerLoading = false; this.playerLoadingCompact = false; if (updateLocation) window.LanPlayLocation.write(this.currentPath, null, false); await this.$nextTick(); if (this.playerController) this.playerController.stop(); } })); });