adding pdf and image viewer
This commit is contained in:
@@ -0,0 +1,170 @@
|
||||
/* OxiCloud File Viewer Styles */
|
||||
|
||||
.file-viewer-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
z-index: 3000;
|
||||
display: none !important;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.file-viewer-container.active {
|
||||
display: flex !important;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.file-viewer-content {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.file-viewer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 15px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.file-viewer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.file-viewer-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #718096;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.file-viewer-close:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.file-viewer-area {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: #f8f9fa;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.file-viewer-toolbar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
padding: 10px 20px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.file-viewer-toolbar button {
|
||||
background: none;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 4px;
|
||||
padding: 8px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #4a5568;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.file-viewer-toolbar button:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #2d3748;
|
||||
}
|
||||
|
||||
/* Image viewer styles */
|
||||
.file-viewer-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transform-origin: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* PDF viewer styles */
|
||||
.file-viewer-pdf {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.file-viewer-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 24px;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
/* Unsupported file message */
|
||||
.file-viewer-unsupported {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
max-width: 500px;
|
||||
}
|
||||
|
||||
.file-viewer-unsupported i {
|
||||
font-size: 48px;
|
||||
color: #a0aec0;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.file-viewer-unsupported p {
|
||||
margin-bottom: 20px;
|
||||
color: #4a5568;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.file-viewer-unsupported .download-btn {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
/* Responsive styles */
|
||||
@media (max-width: 768px) {
|
||||
.file-viewer-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.file-viewer-toolbar {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
/* OxiCloud Inline Viewer Styles */
|
||||
|
||||
.inline-viewer-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 9999;
|
||||
background-color: rgba(0, 0, 0, 0.85);
|
||||
display: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.inline-viewer-modal.active {
|
||||
display: flex !important;
|
||||
opacity: 1;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.inline-viewer-content {
|
||||
width: 90%;
|
||||
height: 90%;
|
||||
max-width: 1200px;
|
||||
background-color: #fff;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.inline-viewer-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.inline-viewer-title {
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
color: #2d3748;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.inline-viewer-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 18px;
|
||||
cursor: pointer;
|
||||
color: #718096;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.inline-viewer-close:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #4a5568;
|
||||
}
|
||||
|
||||
.inline-viewer-container {
|
||||
flex-grow: 1;
|
||||
overflow: auto;
|
||||
background-color: #f0f3f7;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.inline-viewer-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
background-color: #f8f9fa;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.inline-viewer-download {
|
||||
background-color: #ff5e3a;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
padding: 8px 16px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.inline-viewer-download:hover {
|
||||
background-color: #e74c3c;
|
||||
}
|
||||
|
||||
.inline-viewer-controls {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.inline-viewer-controls button {
|
||||
background-color: #f1f5f9;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 4px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: #64748b;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.inline-viewer-controls button:hover {
|
||||
background-color: #e2e8f0;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* Image viewer */
|
||||
.inline-viewer-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
transform-origin: center;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
/* PDF viewer */
|
||||
.inline-viewer-pdf,
|
||||
.inline-viewer-pdf-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Only show fallback if object fails */
|
||||
.inline-viewer-pdf + .inline-viewer-pdf-fallback {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.inline-viewer-pdf:not([data]),
|
||||
.inline-viewer-pdf[data=""] + .inline-viewer-pdf-fallback {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Loading indicator */
|
||||
.inline-viewer-loader {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
font-size: 36px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Error and unsupported message */
|
||||
.inline-viewer-message {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
max-width: 400px;
|
||||
}
|
||||
|
||||
.inline-viewer-icon {
|
||||
font-size: 64px;
|
||||
color: #cbd5e1;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.inline-viewer-text {
|
||||
color: #64748b;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.inline-viewer-text p {
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.inline-viewer-content {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.inline-viewer-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,14 @@
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<link rel="stylesheet" href="/css/inlineViewer.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
|
||||
<link rel="preconnect" href="https://cdnjs.cloudflare.com" crossorigin>
|
||||
|
||||
<!-- Scripts -->
|
||||
<script src="/js/i18n.js"></script>
|
||||
<script src="/js/languageSelector.js"></script>
|
||||
<script src="/js/inlineViewer.js"></script>
|
||||
<script src="/js/fileRenderer.js"></script>
|
||||
<script src="/js/fileSharing.js"></script>
|
||||
<script src="/js/contextMenus.js"></script>
|
||||
|
||||
@@ -56,6 +56,15 @@ function initApp() {
|
||||
console.log('Using standard file rendering');
|
||||
}
|
||||
|
||||
// Check if inline viewer is initialized
|
||||
if (window.inlineViewer) {
|
||||
console.log('Inline viewer is available');
|
||||
} else {
|
||||
console.warn('Inline viewer not initialized yet, will initialize it now');
|
||||
// Create inline viewer if not already created
|
||||
window.inlineViewer = new InlineViewer();
|
||||
}
|
||||
|
||||
// Wait for translations to load before checking authentication
|
||||
if (window.i18n && window.i18n.isLoaded && window.i18n.isLoaded()) {
|
||||
// Translations already loaded, proceed with authentication
|
||||
|
||||
@@ -10,6 +10,16 @@ const contextMenus = {
|
||||
*/
|
||||
assignMenuEvents() {
|
||||
// Folder context menu options
|
||||
document.getElementById('download-folder-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFolder) {
|
||||
window.fileOps.downloadFolder(
|
||||
window.app.contextMenuTargetFolder.id,
|
||||
window.app.contextMenuTargetFolder.name
|
||||
);
|
||||
}
|
||||
window.ui.closeContextMenu();
|
||||
});
|
||||
|
||||
document.getElementById('rename-folder-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFolder) {
|
||||
this.showRenameDialog(window.app.contextMenuTargetFolder);
|
||||
@@ -42,6 +52,16 @@ const contextMenus = {
|
||||
});
|
||||
|
||||
// File context menu options
|
||||
document.getElementById('download-file-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFile) {
|
||||
window.fileOps.downloadFile(
|
||||
window.app.contextMenuTargetFile.id,
|
||||
window.app.contextMenuTargetFile.name
|
||||
);
|
||||
}
|
||||
window.ui.closeFileContextMenu();
|
||||
});
|
||||
|
||||
document.getElementById('move-file-option').addEventListener('click', () => {
|
||||
if (window.app.contextMenuTargetFile) {
|
||||
this.showMoveDialog(window.app.contextMenuTargetFile, 'file');
|
||||
|
||||
@@ -459,6 +459,47 @@ const fileOps = {
|
||||
window.ui.showNotification('Error', 'Error al vaciar la papelera');
|
||||
return false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Descargar un archivo
|
||||
* @param {string} fileId - ID del archivo
|
||||
* @param {string} fileName - Nombre del archivo
|
||||
*/
|
||||
downloadFile(fileId, fileName) {
|
||||
// Create a link and trigger download
|
||||
const link = document.createElement('a');
|
||||
link.href = `/api/files/${fileId}`;
|
||||
link.download = fileName;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
},
|
||||
|
||||
/**
|
||||
* Descargar una carpeta como ZIP
|
||||
* @param {string} folderId - ID de la carpeta
|
||||
* @param {string} folderName - Nombre de la carpeta
|
||||
*/
|
||||
async downloadFolder(folderId, folderName) {
|
||||
try {
|
||||
// Show notification to user
|
||||
window.ui.showNotification('Preparando descarga', 'Preparando la carpeta para descargar...');
|
||||
|
||||
// Request the server to create a ZIP of the folder
|
||||
// Since the API might not support this directly, we will simply download with zip parameter
|
||||
const link = document.createElement('a');
|
||||
link.href = `/api/folders/${folderId}/download?format=zip`;
|
||||
link.download = `${folderName}.zip`;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} catch (error) {
|
||||
console.error('Error downloading folder:', error);
|
||||
window.ui.showNotification('Error', 'Error al descargar la carpeta');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -291,9 +291,22 @@ class FileRenderer {
|
||||
});
|
||||
});
|
||||
|
||||
// Click event (download)
|
||||
elem.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
// Click event (preview)
|
||||
elem.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
console.log('File clicked:', item);
|
||||
|
||||
// Open file in our new inline viewer
|
||||
if (window.inlineViewer) {
|
||||
console.log('Using inline viewer');
|
||||
window.inlineViewer.openFile(item);
|
||||
} else {
|
||||
console.warn('Inline viewer not available, downloading directly');
|
||||
// Fallback to direct download if viewer is not available
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -422,9 +435,22 @@ class FileRenderer {
|
||||
});
|
||||
});
|
||||
|
||||
// Click event (download)
|
||||
elem.addEventListener('click', () => {
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
// Click event (preview)
|
||||
elem.addEventListener('click', (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
console.log('File clicked:', item);
|
||||
|
||||
// Open file in our new inline viewer
|
||||
if (window.inlineViewer) {
|
||||
console.log('Using inline viewer');
|
||||
window.inlineViewer.openFile(item);
|
||||
} else {
|
||||
console.warn('Inline viewer not available, downloading directly');
|
||||
// Fallback to direct download if viewer is not available
|
||||
window.location.href = `/api/files/${item.id}`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
/**
|
||||
* OxiCloud File Viewer Module
|
||||
* Provides integrated viewers for images, PDFs and other file types
|
||||
*/
|
||||
|
||||
class FileViewer {
|
||||
constructor() {
|
||||
this.viewerContainer = null;
|
||||
this.fileData = null;
|
||||
this.isOpen = false;
|
||||
|
||||
// Create the viewer container on initialization
|
||||
this.createViewerContainer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the viewer container DOM element
|
||||
*/
|
||||
createViewerContainer() {
|
||||
console.log('Creating file viewer container');
|
||||
|
||||
// Check if container already exists
|
||||
if (document.getElementById('file-viewer-container')) {
|
||||
console.log('Viewer container already exists');
|
||||
this.viewerContainer = document.getElementById('file-viewer-container');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create viewer container
|
||||
this.viewerContainer = document.createElement('div');
|
||||
this.viewerContainer.id = 'file-viewer-container';
|
||||
this.viewerContainer.className = 'file-viewer-container';
|
||||
|
||||
// Create viewer content
|
||||
const viewerContent = document.createElement('div');
|
||||
viewerContent.className = 'file-viewer-content';
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'file-viewer-header';
|
||||
|
||||
const title = document.createElement('div');
|
||||
title.className = 'file-viewer-title';
|
||||
title.textContent = 'File Viewer';
|
||||
header.appendChild(title);
|
||||
|
||||
const closeBtn = document.createElement('button');
|
||||
closeBtn.className = 'file-viewer-close';
|
||||
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||
closeBtn.addEventListener('click', () => {
|
||||
console.log('Close button clicked');
|
||||
this.close();
|
||||
});
|
||||
header.appendChild(closeBtn);
|
||||
|
||||
// Create viewer area
|
||||
const viewerArea = document.createElement('div');
|
||||
viewerArea.className = 'file-viewer-area';
|
||||
|
||||
// Create toolbar
|
||||
const toolbar = document.createElement('div');
|
||||
toolbar.className = 'file-viewer-toolbar';
|
||||
|
||||
const downloadBtn = document.createElement('button');
|
||||
downloadBtn.className = 'file-viewer-download';
|
||||
downloadBtn.innerHTML = '<i class="fas fa-download"></i>';
|
||||
downloadBtn.addEventListener('click', () => this.downloadFile());
|
||||
toolbar.appendChild(downloadBtn);
|
||||
|
||||
// Assemble the viewer
|
||||
viewerContent.appendChild(header);
|
||||
viewerContent.appendChild(viewerArea);
|
||||
viewerContent.appendChild(toolbar);
|
||||
this.viewerContainer.appendChild(viewerContent);
|
||||
|
||||
// Add to the document
|
||||
if (document.body) {
|
||||
console.log('Adding viewer container to body');
|
||||
document.body.appendChild(this.viewerContainer);
|
||||
} else {
|
||||
console.warn('Document body not ready, will add viewer later');
|
||||
// Try to add it after document is ready
|
||||
setTimeout(() => {
|
||||
if (document.body) {
|
||||
console.log('Adding viewer container to body (delayed)');
|
||||
document.body.appendChild(this.viewerContainer);
|
||||
} else {
|
||||
console.error('Document body still not available after delay');
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// Add event listeners for keyboard navigation
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (this.isOpen && e.key === 'Escape') {
|
||||
console.log('Escape key pressed, closing viewer');
|
||||
this.close();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Viewer container created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the viewer with the specified file
|
||||
* @param {Object} fileData - File data with id, name, mime_type
|
||||
*/
|
||||
async open(fileData) {
|
||||
console.log('FileViewer: Opening file', fileData);
|
||||
this.fileData = fileData;
|
||||
this.isOpen = true;
|
||||
|
||||
// Reset the viewer area
|
||||
const viewerArea = this.viewerContainer.querySelector('.file-viewer-area');
|
||||
viewerArea.innerHTML = '';
|
||||
|
||||
// Set the title
|
||||
const title = this.viewerContainer.querySelector('.file-viewer-title');
|
||||
title.textContent = fileData.name;
|
||||
|
||||
// Show the container
|
||||
this.viewerContainer.classList.add('active');
|
||||
|
||||
// Determine content type and load appropriate viewer
|
||||
if (fileData.mime_type && fileData.mime_type.startsWith('image/')) {
|
||||
console.log('FileViewer: Loading image viewer');
|
||||
this.loadImageViewer(fileData.id, viewerArea);
|
||||
} else if (fileData.mime_type && fileData.mime_type === 'application/pdf') {
|
||||
console.log('FileViewer: Loading PDF viewer');
|
||||
this.loadPdfViewer(fileData.id, viewerArea);
|
||||
} else {
|
||||
console.log('FileViewer: Unsupported file type', fileData.mime_type);
|
||||
// For unsupported files, show download prompt
|
||||
this.showUnsupportedFileMessage(viewerArea);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the image viewer
|
||||
* @param {string} fileId - ID of the file to view
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
loadImageViewer(fileId, container) {
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
img.className = 'file-viewer-image';
|
||||
img.src = `/api/files/${fileId}`;
|
||||
img.alt = this.fileData.name;
|
||||
|
||||
// Create loader
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'file-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// When image loads, remove loader
|
||||
img.onload = () => {
|
||||
container.removeChild(loader);
|
||||
};
|
||||
|
||||
// Add image to container
|
||||
container.appendChild(img);
|
||||
|
||||
// Add zoom controls to toolbar
|
||||
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
|
||||
|
||||
const zoomInBtn = document.createElement('button');
|
||||
zoomInBtn.className = 'file-viewer-zoom-in';
|
||||
zoomInBtn.innerHTML = '<i class="fas fa-search-plus"></i>';
|
||||
zoomInBtn.addEventListener('click', () => this.zoomImage(1.2));
|
||||
toolbar.appendChild(zoomInBtn);
|
||||
|
||||
const zoomOutBtn = document.createElement('button');
|
||||
zoomOutBtn.className = 'file-viewer-zoom-out';
|
||||
zoomOutBtn.innerHTML = '<i class="fas fa-search-minus"></i>';
|
||||
zoomOutBtn.addEventListener('click', () => this.zoomImage(0.8));
|
||||
toolbar.appendChild(zoomOutBtn);
|
||||
|
||||
const resetZoomBtn = document.createElement('button');
|
||||
resetZoomBtn.className = 'file-viewer-zoom-reset';
|
||||
resetZoomBtn.innerHTML = '<i class="fas fa-expand"></i>';
|
||||
resetZoomBtn.addEventListener('click', () => this.resetZoom());
|
||||
toolbar.appendChild(resetZoomBtn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom the image
|
||||
* @param {number} factor - Zoom factor
|
||||
*/
|
||||
zoomImage(factor) {
|
||||
const img = this.viewerContainer.querySelector('.file-viewer-image');
|
||||
if (!img) return;
|
||||
|
||||
// Get current scale
|
||||
let scale = img.style.transform ?
|
||||
parseFloat(img.style.transform.replace('scale(', '').replace(')', '')) : 1;
|
||||
|
||||
// Apply new scale
|
||||
scale *= factor;
|
||||
|
||||
// Limit scale range
|
||||
scale = Math.max(0.5, Math.min(5, scale));
|
||||
|
||||
img.style.transform = `scale(${scale})`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset image zoom
|
||||
*/
|
||||
resetZoom() {
|
||||
const img = this.viewerContainer.querySelector('.file-viewer-image');
|
||||
if (!img) return;
|
||||
|
||||
img.style.transform = 'scale(1)';
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the PDF viewer
|
||||
* @param {string} fileId - ID of the file to view
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
loadPdfViewer(fileId, container) {
|
||||
// Create iframe for PDF viewer
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'file-viewer-pdf';
|
||||
iframe.src = `/api/files/${fileId}`;
|
||||
iframe.title = this.fileData.name;
|
||||
|
||||
// Create loader
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'file-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// When iframe loads, remove loader
|
||||
iframe.onload = () => {
|
||||
container.removeChild(loader);
|
||||
};
|
||||
|
||||
// Add iframe to container
|
||||
container.appendChild(iframe);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show message for unsupported file types
|
||||
* @param {HTMLElement} container - Container element to render into
|
||||
*/
|
||||
showUnsupportedFileMessage(container) {
|
||||
const message = document.createElement('div');
|
||||
message.className = 'file-viewer-unsupported';
|
||||
|
||||
message.innerHTML = `
|
||||
<i class="fas fa-file-download"></i>
|
||||
<p>${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'Este tipo de archivo no se puede previsualizar.'}</p>
|
||||
<button class="btn btn-primary download-btn">
|
||||
<i class="fas fa-download"></i>
|
||||
${window.i18n ? window.i18n.t('viewer.download_file') : 'Descargar archivo'}
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Add download button click event
|
||||
message.querySelector('.download-btn').addEventListener('click', () => {
|
||||
this.downloadFile();
|
||||
});
|
||||
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the current file
|
||||
*/
|
||||
downloadFile() {
|
||||
if (!this.fileData) return;
|
||||
|
||||
// Create a link and simulate click
|
||||
const link = document.createElement('a');
|
||||
link.href = `/api/files/${this.fileData.id}`;
|
||||
link.download = this.fileData.name;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the viewer
|
||||
*/
|
||||
close() {
|
||||
this.isOpen = false;
|
||||
this.fileData = null;
|
||||
this.viewerContainer.classList.remove('active');
|
||||
|
||||
// Reset toolbar (remove zoom controls)
|
||||
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
|
||||
const downloadBtn = toolbar.querySelector('.file-viewer-download');
|
||||
toolbar.innerHTML = '';
|
||||
toolbar.appendChild(downloadBtn);
|
||||
}
|
||||
}
|
||||
|
||||
// Create the file viewer immediately and make it accessible globally
|
||||
window.fileViewer = new FileViewer();
|
||||
|
||||
// Ensure the file viewer is available when the DOM is ready
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
console.log('FileViewer initialized:', window.fileViewer ? 'Yes' : 'No');
|
||||
if (!window.fileViewer) {
|
||||
console.warn('Re-initializing fileViewer as it was not properly set');
|
||||
window.fileViewer = new FileViewer();
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,329 @@
|
||||
/**
|
||||
* OxiCloud Inline Viewer
|
||||
* A simpler approach to viewing files that doesn't rely on complex DOM manipulation
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Create modal container
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'inline-viewer-modal';
|
||||
modal.className = 'inline-viewer-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="inline-viewer-content">
|
||||
<div class="inline-viewer-header">
|
||||
<div class="inline-viewer-title">File Viewer</div>
|
||||
<button class="inline-viewer-close"><i class="fas fa-times"></i></button>
|
||||
</div>
|
||||
<div class="inline-viewer-container"></div>
|
||||
<div class="inline-viewer-toolbar">
|
||||
<button class="inline-viewer-download"><i class="fas fa-download"></i> Download</button>
|
||||
<div class="inline-viewer-controls">
|
||||
<button class="inline-viewer-zoom-out" title="Zoom Out"><i class="fas fa-search-minus"></i></button>
|
||||
<button class="inline-viewer-zoom-reset" title="Reset Zoom"><i class="fas fa-expand"></i></button>
|
||||
<button class="inline-viewer-zoom-in" title="Zoom In"><i class="fas fa-search-plus"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Add to document
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Add event listeners
|
||||
modal.querySelector('.inline-viewer-close').addEventListener('click', () => {
|
||||
this.closeViewer();
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-download').addEventListener('click', () => {
|
||||
if (this.currentFile) {
|
||||
this.downloadFile(this.currentFile);
|
||||
}
|
||||
});
|
||||
|
||||
// Add zoom controls for images
|
||||
modal.querySelector('.inline-viewer-zoom-in').addEventListener('click', () => {
|
||||
this.zoomImage(1.2);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-out').addEventListener('click', () => {
|
||||
this.zoomImage(0.8);
|
||||
});
|
||||
|
||||
modal.querySelector('.inline-viewer-zoom-reset').addEventListener('click', () => {
|
||||
this.resetZoom();
|
||||
});
|
||||
|
||||
// Close on ESC key
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && modal.classList.contains('active')) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
// Click outside to close
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
this.closeViewer();
|
||||
}
|
||||
});
|
||||
|
||||
console.log('Inline viewer initialized');
|
||||
}
|
||||
|
||||
openFile(file) {
|
||||
console.log('Opening file:', file);
|
||||
this.currentFile = file;
|
||||
|
||||
// Get container
|
||||
const modal = document.getElementById('inline-viewer-modal');
|
||||
const container = modal.querySelector('.inline-viewer-container');
|
||||
const title = modal.querySelector('.inline-viewer-title');
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Set title
|
||||
title.textContent = file.name;
|
||||
|
||||
// Set controls visibility
|
||||
const controls = modal.querySelector('.inline-viewer-controls');
|
||||
|
||||
// Show viewer based on file type
|
||||
if (file.mime_type && file.mime_type.startsWith('image/')) {
|
||||
// Show zoom controls
|
||||
controls.style.display = 'flex';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create image viewer using a blob URL
|
||||
this.createBlobUrlViewer(file, 'image', container, loader);
|
||||
}
|
||||
else if (file.mime_type && file.mime_type === 'application/pdf') {
|
||||
// Hide zoom controls for PDFs
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show loading indicator
|
||||
const loader = document.createElement('div');
|
||||
loader.className = 'inline-viewer-loader';
|
||||
loader.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
container.appendChild(loader);
|
||||
|
||||
// Create PDF viewer using object tag with blob URL
|
||||
this.createBlobUrlViewer(file, 'pdf', container, loader);
|
||||
}
|
||||
else {
|
||||
// Hide zoom controls for unsupported files
|
||||
controls.style.display = 'none';
|
||||
|
||||
// Show unsupported file message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-file"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Este tipo de archivo no puede ser previsualizado.</p>
|
||||
<p>Haz clic en "Descargar" para obtener el archivo.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
// Show modal
|
||||
modal.classList.add('active');
|
||||
}
|
||||
|
||||
// Creates a viewer using a Blob URL to avoid content-disposition header
|
||||
async createBlobUrlViewer(file, type, container, loader) {
|
||||
try {
|
||||
console.log('Creating blob URL viewer for:', file.name, 'type:', type);
|
||||
|
||||
// Use XMLHttpRequest instead of fetch to get better control over the response
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('GET', `/api/files/${file.id}?inline=true`, true);
|
||||
xhr.responseType = 'blob';
|
||||
|
||||
// Create a promise to handle the XHR
|
||||
const response = await new Promise((resolve, reject) => {
|
||||
xhr.onload = function() {
|
||||
if (this.status >= 200 && this.status < 300) {
|
||||
resolve(this.response);
|
||||
} else {
|
||||
reject(new Error(`Error fetching file: ${this.status} ${this.statusText}`));
|
||||
}
|
||||
};
|
||||
|
||||
xhr.onerror = function() {
|
||||
reject(new Error('Network error'));
|
||||
};
|
||||
|
||||
xhr.send();
|
||||
});
|
||||
|
||||
// Create blob URL from response
|
||||
const blob = response;
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
|
||||
console.log('Created blob URL:', blobUrl.substring(0, 30) + '...');
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
if (type === 'image') {
|
||||
console.log('Creating image viewer');
|
||||
// Create image element
|
||||
const img = document.createElement('img');
|
||||
img.className = 'inline-viewer-image';
|
||||
img.src = blobUrl;
|
||||
img.alt = file.name;
|
||||
container.appendChild(img);
|
||||
|
||||
// Add loading indicator until image loads
|
||||
img.style.opacity = 0;
|
||||
img.onload = () => {
|
||||
console.log('Image loaded successfully');
|
||||
img.style.opacity = 1;
|
||||
};
|
||||
|
||||
img.onerror = () => {
|
||||
console.error('Failed to load image');
|
||||
container.removeChild(img);
|
||||
this.showErrorMessage(container);
|
||||
};
|
||||
}
|
||||
else if (type === 'pdf') {
|
||||
console.log('Creating PDF viewer');
|
||||
|
||||
// Create iframe for PDF (more reliable than object tag)
|
||||
const iframe = document.createElement('iframe');
|
||||
iframe.className = 'inline-viewer-pdf';
|
||||
iframe.src = blobUrl;
|
||||
iframe.setAttribute('allowfullscreen', 'true');
|
||||
container.appendChild(iframe);
|
||||
|
||||
// Monitor iframe for loading issues
|
||||
setTimeout(() => {
|
||||
if (!iframe.contentDocument ||
|
||||
iframe.contentDocument.body.innerHTML === '') {
|
||||
console.warn('PDF viewer might be having issues, adding fallback');
|
||||
|
||||
// Add fallback embed
|
||||
const embed = document.createElement('embed');
|
||||
embed.className = 'inline-viewer-pdf-fallback';
|
||||
embed.type = 'application/pdf';
|
||||
embed.src = blobUrl;
|
||||
container.appendChild(embed);
|
||||
}
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
// Store blob URL for cleaning up later
|
||||
this.currentBlobUrl = blobUrl;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('Error creating blob URL viewer:', error);
|
||||
|
||||
// Remove loader
|
||||
if (loader && loader.parentNode) {
|
||||
loader.parentNode.removeChild(loader);
|
||||
}
|
||||
|
||||
this.showErrorMessage(container);
|
||||
}
|
||||
},
|
||||
|
||||
// Helper to show error message
|
||||
showErrorMessage(container) {
|
||||
// Show error message
|
||||
const message = document.createElement('div');
|
||||
message.className = 'inline-viewer-message';
|
||||
message.innerHTML = `
|
||||
<div class="inline-viewer-icon"><i class="fas fa-exclamation-triangle"></i></div>
|
||||
<div class="inline-viewer-text">
|
||||
<p>Error al cargar el archivo.</p>
|
||||
<p>Intenta descargarlo directamente.</p>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(message);
|
||||
}
|
||||
|
||||
closeViewer() {
|
||||
// Get modal
|
||||
const modal = document.getElementById('inline-viewer-modal');
|
||||
|
||||
// Hide modal
|
||||
modal.classList.remove('active');
|
||||
|
||||
// Clean up blob URL if exists
|
||||
if (this.currentBlobUrl) {
|
||||
URL.revokeObjectURL(this.currentBlobUrl);
|
||||
this.currentBlobUrl = null;
|
||||
}
|
||||
|
||||
// Clear references
|
||||
this.currentFile = null;
|
||||
}
|
||||
|
||||
downloadFile(file) {
|
||||
// Create a link and click it
|
||||
const link = document.createElement('a');
|
||||
link.href = `/api/files/${file.id}`;
|
||||
link.download = file.name;
|
||||
link.target = '_blank';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
|
||||
zoomImage(factor) {
|
||||
const container = document.querySelector('.inline-viewer-container');
|
||||
const img = 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 = scale;
|
||||
|
||||
// Apply scale
|
||||
img.style.transform = `scale(${scale})`;
|
||||
}
|
||||
|
||||
resetZoom() {
|
||||
const container = document.querySelector('.inline-viewer-container');
|
||||
const img = container.querySelector('.inline-viewer-image');
|
||||
|
||||
if (!img) return;
|
||||
|
||||
// Reset scale
|
||||
img.dataset.scale = 1.0;
|
||||
img.style.transform = 'scale(1.0)';
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize viewer
|
||||
window.inlineViewer = new InlineViewer();
|
||||
@@ -15,6 +15,9 @@ const ui = {
|
||||
folderMenu.className = 'context-menu';
|
||||
folderMenu.id = 'folder-context-menu';
|
||||
folderMenu.innerHTML = `
|
||||
<div class="context-menu-item" id="download-folder-option">
|
||||
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span>
|
||||
</div>
|
||||
<div class="context-menu-item" id="rename-folder-option">
|
||||
<i class="fas fa-edit"></i> <span data-i18n="actions.rename">Renombrar</span>
|
||||
</div>
|
||||
@@ -37,6 +40,9 @@ const ui = {
|
||||
fileMenu.className = 'context-menu';
|
||||
fileMenu.id = 'file-context-menu';
|
||||
fileMenu.innerHTML = `
|
||||
<div class="context-menu-item" id="download-file-option">
|
||||
<i class="fas fa-download"></i> <span data-i18n="actions.download">Descargar</span>
|
||||
</div>
|
||||
<div class="context-menu-item" id="share-file-option">
|
||||
<i class="fas fa-share-alt"></i> <span data-i18n="actions.share">Compartir</span>
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
"move": "Move to...",
|
||||
"move_to": "Move to",
|
||||
"delete": "Delete",
|
||||
"download": "Download",
|
||||
"cancel": "Cancel",
|
||||
"confirm": "Confirm",
|
||||
"share": "Share",
|
||||
@@ -243,5 +244,12 @@
|
||||
"admin_password": "Admin password",
|
||||
"create_admin": "Create administrator",
|
||||
"back_to_login": "Already set up?"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "This file type cannot be previewed.",
|
||||
"download_file": "Download file",
|
||||
"zoom_in": "Zoom in",
|
||||
"zoom_out": "Zoom out",
|
||||
"zoom_reset": "Reset zoom"
|
||||
}
|
||||
}
|
||||
@@ -137,6 +137,7 @@
|
||||
"move": "Mover a...",
|
||||
"move_to": "Mover a",
|
||||
"delete": "Eliminar",
|
||||
"download": "Descargar",
|
||||
"cancel": "Cancelar",
|
||||
"confirm": "Confirmar",
|
||||
"share": "Compartir",
|
||||
@@ -243,5 +244,12 @@
|
||||
"admin_password": "Contraseña administrador",
|
||||
"create_admin": "Crear administrador",
|
||||
"back_to_login": "¿Ya está configurado?"
|
||||
},
|
||||
"viewer": {
|
||||
"unsupported_file": "Este tipo de archivo no se puede previsualizar.",
|
||||
"download_file": "Descargar archivo",
|
||||
"zoom_in": "Acercar",
|
||||
"zoom_out": "Alejar",
|
||||
"zoom_reset": "Restablecer zoom"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user