/**
* OxiCloud Inline Viewer
* A simpler approach to viewing files that doesn't rely on complex DOM manipulation
*/
import { updateHistory } from '../../app/main.js';
import { app } from '../../app/state.js';
import { isTextViewable } from '../../core/formatters.js';
import { wopiEditor } from './wopiEditor.js';
/** @import {FileItem} from '../../core/types.js' */
class InlineViewer {
constructor() {
this.setupViewer();
this.currentFile = null;
}
setupViewer() {
// Create the viewer modal if it doesn't exist
if (document.getElementById('inline-viewer-modal')) {
return;
}
// Verify document.body exists
if (!document.body) {
console.warn('Document body not available yet for inline viewer, will retry later');
setTimeout(() => this.setupViewer(), 200);
return;
}
// Create modal container
const modal = document.createElement('div');
modal.id = 'inline-viewer-modal';
modal.className = 'inline-viewer-modal';
modal.innerHTML = `
This file type cannot be previewed.
Click "Download" to get the file.
`;
container.appendChild(message);
}
// Show modal
modal.classList.add('active');
}
// Creates a text viewer using authenticated fetch
/**
*
* @param {FileItem} file
* @param {HTMLDivElement} container
* @param {*} loader
*/
async createTextViewer(file, container, loader) {
try {
console.log('Creating text viewer for:', file.name);
const response = await fetch(`/api/files/${file.id}?inline=true`, {
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
}
const text = await response.text();
// Remove loader
if (loader?.parentNode) {
loader.parentNode.removeChild(loader);
}
// Create text viewer element
const pre = document.createElement('pre');
pre.className = 'inline-viewer-text-content';
pre.textContent = text;
container.appendChild(pre);
console.log('Text viewer created successfully');
} catch (error) {
console.error('Error creating text viewer:', error);
// Remove loader
if (loader?.parentNode) {
loader.parentNode.removeChild(loader);
}
this.showErrorMessage(container);
}
}
/**
* Creates a viewer using a Blob URL to avoid content-disposition header
* @param {FileItem} file
* @param {string} mediaType
* @param {HTMLDivElement} container
* @param {HTMLDivElement} loader
*/
async createBlobUrlViewer(file, mediaType, container, loader) {
try {
console.log('Creating blob URL viewer for:', file.name, 'type:', mediaType);
// Update loader to show progress bar for large files
let progressBar = /** @type {HTMLElement|null} */ (null);
let progressText = /** @type {HTMLElement|null} */ (null);
if (loader && file.size > 10 * 1024 * 1024) {
// Show progress for files > 10MB
loader.innerHTML = `
Your browser cannot play this audio format.
Click "Download" to save the file.
`;
wrapper.appendChild(msg);
});
container.appendChild(wrapper);
} else {
const video = document.createElement('video');
video.className = 'inline-viewer-video';
video.controls = true;
video.preload = 'metadata';
video.src = blobUrl;
video.setAttribute('playsinline', 'true');
// Fallback message for unsupported codecs
video.addEventListener('error', () => {
console.warn('Video playback error — codec may not be supported');
if (video.parentNode) {
video.parentNode.removeChild(video);
}
const msg = document.createElement('div');
msg.className = 'inline-viewer-message';
msg.innerHTML = `
Your browser cannot play this video format.
Click "Download" to save the file.
`;
container.appendChild(msg);
});
container.appendChild(video);
}
// Store blob URL for cleanup on close
this.currentBlobUrl = blobUrl;
} catch (error) {
console.error(`Error creating ${mediaType} viewer:`, error);
if (loader?.parentNode) {
loader.parentNode.removeChild(loader);
}
this.showErrorMessage(container);
}
}
/**
* Helper to show error message
* @param {HTMLDivElement} container
*/
showErrorMessage(container) {
// Show error message
const message = document.createElement('div');
message.className = 'inline-viewer-message';
message.innerHTML = `
Error loading the file.
Try downloading it directly.
`;
container.appendChild(message);
}
closeViewer() {
// Get modal
const modal = document.getElementById('inline-viewer-modal');
// stops audio/video before closing viewver
const media = /** @type {HTMLMediaElement} */ (modal.querySelector('audio, video'));
if (media && !media.paused) media.pause();
// Hide modal
modal.classList.remove('active');
// Clean up blob URL if exists
if (this.currentBlobUrl) {
URL.revokeObjectURL(this.currentBlobUrl);
this.currentBlobUrl = null;
}
// clear
app.viewFile = null;
updateHistory(false);
// Clear references
this.currentFile = null;
}
/**
*
* @param {FileItem} file
*/
downloadFile(file) {
fetch(`/api/files/${file.id}`, { credentials: 'same-origin' })
.then((res) => {
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.blob();
})
.then((blob) => {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
})
.catch((err) => console.error('Download error:', err));
}
/**
*
* @param {number} factor
* @returns
*/
zoomImage(factor) {
const container = document.querySelector('.inline-viewer-container');
const img = /** @type {HTMLDivElement} */ (container.querySelector('.inline-viewer-image'));
if (!img) return;
// Get current scale
let scale = img.dataset.scale ? parseFloat(img.dataset.scale) : 1.0;
// Apply zoom factor
scale *= factor;
// Limit scale
scale = Math.max(0.1, Math.min(5.0, scale));
// Save scale
img.dataset.scale = String(scale);
// Apply scale
img.style.transform = `scale(${scale})`;
}
resetZoom() {
const container = document.querySelector('.inline-viewer-container');
const img = /** @type {HTMLDivElement} */ (container.querySelector('.inline-viewer-image'));
if (!img) return;
// Reset scale
img.dataset.scale = String(1);
img.style.transform = 'scale(1.0)';
}
}
export const inlineViewer = new InlineViewer();