263 lines
9.2 KiB
JavaScript
263 lines
9.2 KiB
JavaScript
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();
|
||
}
|
||
}));
|
||
});
|