Files
lan-play/web/js/library.js
T

263 lines
9.2 KiB
JavaScript
Raw Normal View History

2026-08-05 12:32:52 +01:00
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();
}
}));
});