diff --git a/static/css/fileViewer.css b/static/css/fileViewer.css
index 5a6f5037..51f3e51c 100644
--- a/static/css/fileViewer.css
+++ b/static/css/fileViewer.css
@@ -123,6 +123,25 @@
border: none;
}
+/* Text viewer styles */
+.file-viewer-text-content {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 16px 24px;
+ font-family: 'Courier New', Consolas, Monaco, monospace;
+ font-size: 14px;
+ line-height: 1.6;
+ color: #2d3748;
+ background-color: #fff;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ box-sizing: border-box;
+ text-align: left;
+ tab-size: 4;
+}
+
/* Loader */
.file-viewer-loader {
position: absolute;
diff --git a/static/css/inlineViewer.css b/static/css/inlineViewer.css
index 0f8ee1d0..2dcb0bc9 100644
--- a/static/css/inlineViewer.css
+++ b/static/css/inlineViewer.css
@@ -193,6 +193,25 @@
margin: 0 0 16px;
}
+/* Text viewer */
+.inline-viewer-text-content {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ padding: 16px 24px;
+ font-family: 'Courier New', Consolas, Monaco, monospace;
+ font-size: 14px;
+ line-height: 1.6;
+ color: #2d3748;
+ background-color: #fff;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ box-sizing: border-box;
+ text-align: left;
+ tab-size: 4;
+}
+
/* Responsive adjustments */
@media (max-width: 768px) {
.inline-viewer-content {
diff --git a/static/js/contextMenus.js b/static/js/contextMenus.js
index 50f9a313..08281a4e 100644
--- a/static/js/contextMenus.js
+++ b/static/js/contextMenus.js
@@ -88,9 +88,8 @@ const contextMenus = {
})
.then(response => response.json())
.then(fileDetails => {
- // Check if viewable file type
- if ((fileDetails.mime_type && fileDetails.mime_type.startsWith('image/')) ||
- (fileDetails.mime_type && fileDetails.mime_type === 'application/pdf')) {
+ // Check if viewable file type (images, PDFs, text files)
+ if (window.ui && window.ui.isViewableFile(fileDetails)) {
// Open with inline viewer
if (window.inlineViewer) {
window.inlineViewer.openFile(fileDetails);
diff --git a/static/js/favorites.js b/static/js/favorites.js
index 5d37ac17..08ea4842 100644
--- a/static/js/favorites.js
+++ b/static/js/favorites.js
@@ -609,9 +609,13 @@ const favorites = {
Modified ${formattedDate.split(' ')[0]}
`;
- // Download on click
+ // View or download on click
fileGridElement.addEventListener('click', () => {
- window.location.href = `/api/files/${file.id}`;
+ if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
+ window.inlineViewer.openFile(file);
+ } else if (window.fileOps) {
+ window.fileOps.downloadFile(file.id, file.name);
+ }
});
// Context menu
@@ -654,9 +658,13 @@ const favorites = {
${formattedDate}
`;
- // Download on click
+ // View or download on click
fileListElement.addEventListener('click', () => {
- window.location.href = `/api/files/${file.id}`;
+ if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
+ window.inlineViewer.openFile(file);
+ } else if (window.fileOps) {
+ window.fileOps.downloadFile(file.id, file.name);
+ }
});
// Context menu
diff --git a/static/js/fileRenderer.js b/static/js/fileRenderer.js
index 4282f60e..27c1a28a 100644
--- a/static/js/fileRenderer.js
+++ b/static/js/fileRenderer.js
@@ -304,8 +304,10 @@ class FileRenderer {
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}`;
+ // Fallback to authenticated download if viewer is not available
+ if (window.fileOps) {
+ window.fileOps.downloadFile(item.id, item.name);
+ }
}
});
}
@@ -448,8 +450,10 @@ class FileRenderer {
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}`;
+ // Fallback to authenticated download if viewer is not available
+ if (window.fileOps) {
+ window.fileOps.downloadFile(item.id, item.name);
+ }
}
});
}
diff --git a/static/js/fileViewer.js b/static/js/fileViewer.js
index b214c399..3b6a2d84 100644
--- a/static/js/fileViewer.js
+++ b/static/js/fileViewer.js
@@ -128,6 +128,9 @@ class FileViewer {
} else if (fileData.mime_type && fileData.mime_type === 'application/pdf') {
console.log('FileViewer: Loading PDF viewer');
this.loadPdfViewer(fileData.id, viewerArea);
+ } else if (fileData.mime_type && this.isTextViewable(fileData.mime_type)) {
+ console.log('FileViewer: Loading text viewer');
+ this.loadTextViewer(fileData.id, viewerArea);
} else {
console.log('FileViewer: Unsupported file type', fileData.mime_type);
// For unsupported files, show download prompt
@@ -141,25 +144,37 @@ class FileViewer {
* @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 = '';
container.appendChild(loader);
- // When image loads, remove loader
- img.onload = () => {
- container.removeChild(loader);
- };
-
- // Add image to container
- container.appendChild(img);
+ // Fetch image with auth header and create blob URL
+ this.fetchFileAsBlob(fileId).then(blob => {
+ const blobUrl = URL.createObjectURL(blob);
+ this.currentBlobUrl = blobUrl;
+
+ const img = document.createElement('img');
+ img.className = 'file-viewer-image';
+ img.src = blobUrl;
+ img.alt = this.fileData.name;
+
+ img.onload = () => {
+ if (loader.parentNode) container.removeChild(loader);
+ };
+
+ img.onerror = () => {
+ if (loader.parentNode) container.removeChild(loader);
+ this.showErrorMessage(container);
+ };
+
+ container.appendChild(img);
+ }).catch(error => {
+ console.error('Error loading image:', error);
+ if (loader.parentNode) container.removeChild(loader);
+ this.showErrorMessage(container);
+ });
// Add zoom controls to toolbar
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
@@ -220,25 +235,87 @@ class FileViewer {
* @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 = '';
container.appendChild(loader);
- // When iframe loads, remove loader
- iframe.onload = () => {
- container.removeChild(loader);
- };
+ // Fetch PDF with auth header and create blob URL
+ this.fetchFileAsBlob(fileId).then(blob => {
+ const blobUrl = URL.createObjectURL(blob);
+ this.currentBlobUrl = blobUrl;
+
+ const iframe = document.createElement('iframe');
+ iframe.className = 'file-viewer-pdf';
+ iframe.src = blobUrl;
+ iframe.title = this.fileData.name;
+
+ iframe.onload = () => {
+ if (loader.parentNode) container.removeChild(loader);
+ };
+
+ container.appendChild(iframe);
+ }).catch(error => {
+ console.error('Error loading PDF:', error);
+ if (loader.parentNode) container.removeChild(loader);
+ this.showErrorMessage(container);
+ });
+ }
+
+ /**
+ * Load the text viewer
+ */
+ async loadTextViewer(fileId, container) {
+ const loader = document.createElement('div');
+ loader.className = 'file-viewer-loader';
+ loader.innerHTML = '';
+ container.appendChild(loader);
- // Add iframe to container
- container.appendChild(iframe);
+ try {
+ const token = localStorage.getItem('oxicloud_token');
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
+ const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
+
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+
+ const text = await response.text();
+ if (loader.parentNode) container.removeChild(loader);
+
+ const pre = document.createElement('pre');
+ pre.className = 'file-viewer-text-content';
+ pre.textContent = text;
+ container.appendChild(pre);
+ } catch (error) {
+ console.error('Error loading text:', error);
+ if (loader.parentNode) container.removeChild(loader);
+ this.showErrorMessage(container);
+ }
+ }
+
+ /**
+ * Check if a MIME type is text-viewable
+ */
+ isTextViewable(mimeType) {
+ if (!mimeType) return false;
+ if (mimeType.startsWith('text/')) return true;
+ const textTypes = [
+ 'application/json', 'application/xml', 'application/javascript',
+ 'application/x-sh', 'application/x-yaml', 'application/toml',
+ 'application/x-toml', 'application/sql',
+ ];
+ return textTypes.includes(mimeType);
+ }
+
+ /**
+ * Fetch a file as blob with auth headers
+ */
+ async fetchFileAsBlob(fileId) {
+ const token = localStorage.getItem('oxicloud_token');
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
+ const response = await fetch(`/api/files/${fileId}?inline=true`, { headers });
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
+ return response.blob();
}
/**
@@ -251,10 +328,10 @@ class FileViewer {
message.innerHTML = `
- ${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'Este tipo de archivo no se puede previsualizar.'}
+ ${window.i18n ? window.i18n.t('viewer.unsupported_file') : 'This file type cannot be previewed.'}
`;
@@ -272,14 +349,39 @@ class FileViewer {
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);
+ // Download with auth headers
+ const token = localStorage.getItem('oxicloud_token');
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
+
+ fetch(`/api/files/${this.fileData.id}`, { headers })
+ .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 = this.fileData.name;
+ document.body.appendChild(link);
+ link.click();
+ document.body.removeChild(link);
+ URL.revokeObjectURL(url);
+ })
+ .catch(err => console.error('Download error:', err));
+ }
+
+ /**
+ * Show error message
+ */
+ showErrorMessage(container) {
+ const message = document.createElement('div');
+ message.className = 'file-viewer-unsupported';
+ message.innerHTML = `
+
+ Error loading the file. Try downloading it directly.
+ `;
+ container.appendChild(message);
}
/**
@@ -290,6 +392,12 @@ class FileViewer {
this.fileData = null;
this.viewerContainer.classList.remove('active');
+ // Clean up blob URL if exists
+ if (this.currentBlobUrl) {
+ URL.revokeObjectURL(this.currentBlobUrl);
+ this.currentBlobUrl = null;
+ }
+
// Reset toolbar (remove zoom controls)
const toolbar = this.viewerContainer.querySelector('.file-viewer-toolbar');
const downloadBtn = toolbar.querySelector('.file-viewer-download');
diff --git a/static/js/inlineViewer.js b/static/js/inlineViewer.js
index 7ca728b7..a605f5bd 100644
--- a/static/js/inlineViewer.js
+++ b/static/js/inlineViewer.js
@@ -133,6 +133,19 @@ class InlineViewer {
// Create PDF viewer using object tag with blob URL
this.createBlobUrlViewer(file, 'pdf', container, loader);
}
+ else if (file.mime_type && this.isTextViewable(file.mime_type)) {
+ // Hide zoom controls for text files
+ controls.style.display = 'none';
+
+ // Show loading indicator
+ const loader = document.createElement('div');
+ loader.className = 'inline-viewer-loader';
+ loader.innerHTML = '';
+ container.appendChild(loader);
+
+ // Create text viewer using authenticated fetch
+ this.createTextViewer(file, container, loader);
+ }
else {
// Hide zoom controls for unsupported files
controls.style.display = 'none';
@@ -143,8 +156,8 @@ class InlineViewer {
message.innerHTML = `
-
Este tipo de archivo no puede ser previsualizado.
-
Haz clic en "Descargar" para obtener el archivo.
+
This file type cannot be previewed.
+
Click "Download" to get the file.
`;
container.appendChild(message);
@@ -154,6 +167,63 @@ class InlineViewer {
modal.classList.add('active');
}
+ // Check if a MIME type is text-viewable
+ isTextViewable(mimeType) {
+ if (!mimeType) return false;
+ if (mimeType.startsWith('text/')) return true;
+ const textTypes = [
+ 'application/json',
+ 'application/xml',
+ 'application/javascript',
+ 'application/x-sh',
+ 'application/x-yaml',
+ 'application/toml',
+ 'application/x-toml',
+ 'application/sql',
+ ];
+ return textTypes.includes(mimeType);
+ }
+
+ // Creates a text viewer using authenticated fetch
+ async createTextViewer(file, container, loader) {
+ try {
+ console.log('Creating text viewer for:', file.name);
+
+ const token = localStorage.getItem('oxicloud_token');
+ const headers = token ? { 'Authorization': `Bearer ${token}` } : {};
+
+ const response = await fetch(`/api/files/${file.id}?inline=true`, { headers });
+
+ if (!response.ok) {
+ throw new Error(`Error fetching file: ${response.status} ${response.statusText}`);
+ }
+
+ const text = await response.text();
+
+ // Remove loader
+ if (loader && 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 && loader.parentNode) {
+ loader.parentNode.removeChild(loader);
+ }
+
+ this.showErrorMessage(container);
+ }
+ }
+
// Creates a viewer using a Blob URL to avoid content-disposition header
async createBlobUrlViewer(file, type, container, loader) {
try {
@@ -269,8 +339,8 @@ class InlineViewer {
message.innerHTML = `
-
Error al cargar el archivo.
-
Intenta descargarlo directamente.
+
Error loading the file.
+
Try downloading it directly.
`;
container.appendChild(message);
diff --git a/static/js/recent.js b/static/js/recent.js
index 8ecac23a..097d8e52 100644
--- a/static/js/recent.js
+++ b/static/js/recent.js
@@ -207,9 +207,13 @@ const recent = {
Accessed ${formattedDate.split(' ')[0]}
`;
- // Download on click
+ // View or download on click
fileGridElement.addEventListener('click', () => {
- window.location.href = `/api/files/${file.id}`;
+ if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
+ window.inlineViewer.openFile(file);
+ } else if (window.fileOps) {
+ window.fileOps.downloadFile(file.id, file.name);
+ }
// Dispatch custom event to update recent files
document.dispatchEvent(new CustomEvent('file-accessed', {
@@ -257,9 +261,13 @@ const recent = {
${formattedDate}
`;
- // Download on click
+ // View or download on click
fileListElement.addEventListener('click', () => {
- window.location.href = `/api/files/${file.id}`;
+ if (window.ui && window.ui.isViewableFile(file) && window.inlineViewer) {
+ window.inlineViewer.openFile(file);
+ } else if (window.fileOps) {
+ window.fileOps.downloadFile(file.id, file.name);
+ }
// Dispatch custom event to update recent files
document.dispatchEvent(new CustomEvent('file-accessed', {
diff --git a/static/js/ui.js b/static/js/ui.js
index daefedf0..4ccabb1e 100644
--- a/static/js/ui.js
+++ b/static/js/ui.js
@@ -434,6 +434,24 @@ const ui = {
}
},
+ /**
+ * Check if a file can be previewed in the viewer
+ * @param {Object} file - File object with mime_type property
+ * @returns {boolean}
+ */
+ isViewableFile(file) {
+ if (!file || !file.mime_type) return false;
+ if (file.mime_type.startsWith('image/')) return true;
+ if (file.mime_type === 'application/pdf') return true;
+ if (file.mime_type.startsWith('text/')) return true;
+ const textTypes = [
+ 'application/json', 'application/xml', 'application/javascript',
+ 'application/x-sh', 'application/x-yaml', 'application/toml',
+ 'application/x-toml', 'application/sql',
+ ];
+ return textTypes.includes(file.mime_type);
+ },
+
/**
* Show notification
* @param {string} title - Notification title
@@ -957,17 +975,16 @@ const ui = {
}
// Check if it's a viewable file type
- if ((file.mime_type && file.mime_type.startsWith('image/')) ||
- (file.mime_type && file.mime_type === 'application/pdf')) {
+ if (this.isViewableFile(file)) {
if (window.inlineViewer) {
window.inlineViewer.openFile(file);
} else if (window.fileViewer) {
window.fileViewer.open(file);
} else {
- window.location.href = `/api/files/${file.id}`;
+ window.fileOps.downloadFile(file.id, file.name);
}
} else {
- window.location.href = `/api/files/${file.id}`;
+ window.fileOps.downloadFile(file.id, file.name);
}
});
@@ -1051,8 +1068,7 @@ const ui = {
}
// Check if it's a viewable file type
- if ((file.mime_type && file.mime_type.startsWith('image/')) ||
- (file.mime_type && file.mime_type === 'application/pdf')) {
+ if (this.isViewableFile(file)) {
// Open in the inline viewer
if (window.inlineViewer) {
window.inlineViewer.openFile(file);
@@ -1061,11 +1077,11 @@ const ui = {
window.fileViewer.open(file);
} else {
// No viewer available, download directly
- window.location.href = `/api/files/${file.id}`;
+ window.fileOps.downloadFile(file.id, file.name);
}
} else {
// For other file types, download directly
- window.location.href = `/api/files/${file.id}`;
+ window.fileOps.downloadFile(file.id, file.name);
}
});