adding pdf and image viewer

This commit is contained in:
DioCrafts
2025-04-02 01:22:05 +02:00
parent 09e37ee677
commit 705cb5b069
20 changed files with 1716 additions and 16 deletions

232
Cargo.lock generated
View File

@@ -17,6 +17,17 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "ahash"
version = "0.8.11"
@@ -72,6 +83,15 @@ version = "1.0.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f"
[[package]]
name = "arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "argon2"
version = "0.5.3"
@@ -315,12 +335,33 @@ version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "bzip2"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47"
dependencies = [
"bzip2-sys",
]
[[package]]
name = "bzip2-sys"
version = "0.1.13+1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "cc"
version = "1.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a"
dependencies = [
"jobserver",
"libc",
"shlex",
]
@@ -345,12 +386,28 @@ dependencies = [
"windows-link",
]
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "constant_time_eq"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -425,6 +482,12 @@ dependencies = [
"typenum",
]
[[package]]
name = "deflate64"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da692b8d1080ea3045efaab14434d40468c3d8657e42abddfffca87b428f4c1b"
[[package]]
name = "der"
version = "0.7.9"
@@ -445,6 +508,17 @@ dependencies = [
"powerfmt",
]
[[package]]
name = "derive_arbitrary"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "digest"
version = "0.10.7"
@@ -724,9 +798,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
"wasm-bindgen",
]
[[package]]
@@ -1141,6 +1217,15 @@ dependencies = [
"hashbrown 0.15.2",
]
[[package]]
name = "inout"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
dependencies = [
"generic-array",
]
[[package]]
name = "ipnet"
version = "2.11.0"
@@ -1153,6 +1238,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "jobserver"
version = "0.1.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a"
dependencies = [
"getrandom 0.3.2",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.77"
@@ -1232,12 +1327,39 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "lzma-sys"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
dependencies = [
"cc",
"libc",
"pkg-config",
]
[[package]]
name = "matchers"
version = "0.1.0"
@@ -1557,6 +1679,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"uuid",
"zip",
]
[[package]]
@@ -1599,6 +1722,16 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
[[package]]
name = "pbkdf2"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2"
dependencies = [
"digest",
"hmac",
]
[[package]]
name = "pem"
version = "3.0.5"
@@ -2168,6 +2301,12 @@ dependencies = [
"rand_core",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "simple_asn1"
version = "0.6.3"
@@ -3402,6 +3541,15 @@ version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "xz2"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
dependencies = [
"lzma-sys",
]
[[package]]
name = "yoke"
version = "0.7.5"
@@ -3492,6 +3640,20 @@ name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.100",
]
[[package]]
name = "zerovec"
@@ -3514,3 +3676,73 @@ dependencies = [
"quote",
"syn 2.0.100",
]
[[package]]
name = "zip"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27c03817464f64e23f6f37574b4fdc8cf65925b5bfd2b0f2aedf959791941f88"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq",
"crc32fast",
"crossbeam-utils",
"deflate64",
"flate2",
"getrandom 0.3.2",
"hmac",
"indexmap",
"lzma-rs",
"memchr",
"pbkdf2",
"sha1",
"time",
"xz2",
"zeroize",
"zopfli",
"zstd",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.15+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb81183ddd97d0c74cedf1d50d85c8d08c1b8b68ee863bdee9e706eedba1a237"
dependencies = [
"cc",
"pkg-config",
]

View File

@@ -14,6 +14,7 @@ tempfile = "3.19.1"
tower = "0.5.2"
tower-http = { version = "0.6.2", features = ["fs", "compression-gzip", "trace", "cors", "add-extension", "request-id"] }
flate2 = "1.1.0"
zip = "2.5.0"
tracing = "0.1.41"
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
chrono = { version = "0.4.40", features = ["serde"] }

View File

@@ -21,4 +21,3 @@ pub use file_path_resolver::FilePathResolver;
pub use file_fs_read_repository::FileFsReadRepository;
pub use file_fs_write_repository::FileFsWriteRepository;
pub use pg::{UserPgRepository, SessionPgRepository};
pub use share_fs_repository::ShareFsRepository;

View File

@@ -5,4 +5,5 @@ pub mod cache_manager;
pub mod file_metadata_cache;
pub mod compression_service;
pub mod buffer_pool;
pub mod trash_cleanup_service;
pub mod trash_cleanup_service;
pub mod zip_service;

View File

@@ -0,0 +1,228 @@
use std::io::{Cursor, Read, Write};
use zip::{ZipWriter, write::SimpleFileOptions};
use thiserror::Error;
use tracing::*;
use crate::{
application::dtos::file_dto::FileDto,
application::dtos::folder_dto::FolderDto,
application::ports::inbound::{FileUseCase, FolderUseCase},
common::errors::{Result, DomainError, ErrorKind},
};
use std::sync::Arc;
/// Error relacionado con la creación de archivos ZIP
#[derive(Debug, Error)]
pub enum ZipError {
#[error("Error de IO: {0}")]
IoError(#[from] std::io::Error),
#[error("Error de ZIP: {0}")]
ZipError(#[from] zip::result::ZipError),
#[error("Error al leer el archivo: {0}")]
FileReadError(String),
#[error("Error al obtener contenido de carpeta: {0}")]
FolderContentsError(String),
#[error("Carpeta no encontrada: {0}")]
FolderNotFound(String),
}
// Implementar From<ZipError> para DomainError para permitir el uso de ?
impl From<ZipError> for DomainError {
fn from(err: ZipError) -> Self {
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
}
}
// Implementar From<zip::result::ZipError> para DomainError directamente
impl From<zip::result::ZipError> for DomainError {
fn from(err: zip::result::ZipError) -> Self {
DomainError::new(ErrorKind::InternalError, "zip_service", err.to_string())
}
}
/// Servicio para crear archivos ZIP
pub struct ZipService {
file_service: Arc<dyn FileUseCase>,
folder_service: Arc<dyn FolderUseCase>,
}
impl ZipService {
/// Crea una nueva instancia del servicio ZIP con una referencia al servicio de archivos
pub fn new(file_service: Arc<dyn FileUseCase>, folder_service: Arc<dyn FolderUseCase>) -> Self {
Self {
file_service,
folder_service,
}
}
/// Crea un archivo ZIP con el contenido de una carpeta y todas sus subcarpetas
/// Retorna los bytes del ZIP
pub async fn create_folder_zip(&self, folder_id: &str, folder_name: &str) -> Result<Vec<u8>> {
info!("Creando ZIP para carpeta: {} (ID: {})", folder_name, folder_id);
// Verificar si la carpeta existe
let folder = match self.folder_service.get_folder(folder_id).await {
Ok(folder) => folder,
Err(e) => {
error!("Error al obtener carpeta {}: {}", folder_id, e);
return Err(ZipError::FolderNotFound(folder_id.to_string()).into());
}
};
// Crear un buffer en memoria para el ZIP
let buf = Cursor::new(Vec::new());
let mut zip = ZipWriter::new(buf);
// Establecer opciones de compresión
let options = SimpleFileOptions::default()
.compression_method(zip::CompressionMethod::Deflated)
.unix_permissions(0o755);
// Objeto para seguir las carpetas procesadas y evitar ciclos
let mut processed_folders = std::collections::HashSet::new();
// Procesamos la carpeta raíz y construimos el ZIP
self.process_folder_recursively(
&mut zip,
&folder,
folder_name,
&options,
&mut processed_folders
).await?;
// Finalizar el ZIP y obtener los bytes
let mut zip_buf = zip.finish()?;
let mut bytes = Vec::new();
match zip_buf.read_to_end(&mut bytes) {
Ok(_) => Ok(bytes),
Err(e) => {
error!("Error al leer ZIP finalizado: {}", e);
Err(ZipError::IoError(e).into())
}
}
}
// Implementación alternativa para evitar recursión en async
async fn process_folder_recursively(
&self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
folder: &FolderDto,
path: &str,
options: &SimpleFileOptions,
processed_folders: &mut std::collections::HashSet<String>
) -> Result<()> {
// Estructura para representar el trabajo pendiente
struct PendingFolder {
folder: FolderDto,
path: String,
}
// Cola de trabajo para procesamiento iterativo
let mut work_queue = vec![PendingFolder {
folder: folder.clone(),
path: path.to_string(),
}];
// Procesar la cola mientras haya elementos
while let Some(current) = work_queue.pop() {
let folder_id = current.folder.id.to_string();
// Evitar ciclos
if processed_folders.contains(&folder_id) {
continue;
}
processed_folders.insert(folder_id.clone());
// Crear la entrada de directorio en el ZIP
let folder_path = format!("{}/", current.path);
match zip.add_directory(&folder_path, *options) {
Ok(_) => debug!("Carpeta agregada al ZIP: {}", folder_path),
Err(e) => {
warn!("No se pudo agregar carpeta al ZIP (puede que ya exista): {}", e);
// Continuamos aunque falle crear el directorio (podría estar duplicado)
}
}
// Agregar archivos de la carpeta al ZIP
let files = match self.file_service.list_files(Some(&folder_id)).await {
Ok(files) => files,
Err(e) => {
error!("Error al listar archivos en carpeta {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar archivos: {}", e)).into());
}
};
// Agregar cada archivo al ZIP
for file in files {
self.add_file_to_zip(zip, &file, &folder_path, options).await?;
}
// Procesar subcarpetas
let subfolders = match self.folder_service.list_folders(Some(&folder_id)).await {
Ok(folders) => folders,
Err(e) => {
error!("Error al listar subcarpetas en {}: {}", folder_id, e);
return Err(ZipError::FolderContentsError(format!("Error al listar subcarpetas: {}", e)).into());
}
};
// Agregar subcarpetas a la cola
for subfolder in subfolders {
let subfolder_path = format!("{}/{}", current.path, subfolder.name);
work_queue.push(PendingFolder {
folder: subfolder,
path: subfolder_path,
});
}
}
Ok(())
}
// Agrega un archivo al ZIP
async fn add_file_to_zip(
&self,
zip: &mut ZipWriter<Cursor<Vec<u8>>>,
file: &FileDto,
folder_path: &str,
options: &SimpleFileOptions,
) -> Result<()> {
let file_path = format!("{}{}", folder_path, file.name);
info!("Agregando archivo al ZIP: {}", file_path);
// Obtener el contenido del archivo
let file_id = file.id.to_string();
let content = match self.file_service.get_file_content(&file_id).await {
Ok(content) => content,
Err(e) => {
error!("Error al leer contenido del archivo {}: {}", file_id, e);
return Err(ZipError::FileReadError(format!("Error al leer archivo {}: {}", file_id, e)).into());
}
};
// Escribir archivo al ZIP
match zip.start_file_from_path(std::path::Path::new(&file_path), *options) {
Ok(_) => {
match zip.write_all(&content) {
Ok(_) => {
debug!("Archivo agregado al ZIP: {}", file_path);
Ok(())
},
Err(e) => {
error!("Error al escribir contenido del archivo {}: {}", file_path, e);
Err(ZipError::IoError(e).into())
}
}
},
Err(e) => {
error!("Error al iniciar archivo en ZIP {}: {}", file_path, e);
Err(ZipError::ZipError(e).into())
}
}
}
}

View File

@@ -196,10 +196,20 @@ impl FileHandler {
Ok(content) => {
// Create base headers
let mut headers = HashMap::new();
headers.insert(
header::CONTENT_DISPOSITION.to_string(),
// Determine if the file should be displayed inline or downloaded
// Images and PDFs should be displayed inline by default, or if inline param is present
let force_inline = params.get("inline").map_or(false, |v| v == "true" || v == "1");
let disposition = if force_inline ||
file.mime_type.starts_with("image/") ||
file.mime_type == "application/pdf" {
format!("inline; filename=\"{}\"", file.name)
} else {
format!("attachment; filename=\"{}\"", file.name)
);
};
headers.insert(header::CONTENT_DISPOSITION.to_string(), disposition);
if should_compress {
// Add content-encoding header for compressed response
@@ -290,10 +300,20 @@ impl FileHandler {
Ok(content) => {
// Create base headers
let mut headers = HashMap::new();
headers.insert(
header::CONTENT_DISPOSITION.to_string(),
// Determine if the file should be displayed inline or downloaded
// Images and PDFs should be displayed inline by default, or if inline param is present
let force_inline = params.get("inline").map_or(false, |v| v == "true" || v == "1");
let disposition = if force_inline ||
file.mime_type.starts_with("image/") ||
file.mime_type == "application/pdf" {
format!("inline; filename=\"{}\"", file.name)
} else {
format!("attachment; filename=\"{}\"", file.name)
);
};
headers.insert(header::CONTENT_DISPOSITION.to_string(), disposition);
if should_compress {
// Add content-encoding header for compressed response

View File

@@ -1,7 +1,8 @@
use std::sync::Arc;
use std::collections::HashMap;
use axum::{
extract::{Path, State, Query},
http::StatusCode,
http::{StatusCode, header, HeaderName, HeaderValue, Response},
response::IntoResponse,
Json,
};
@@ -13,6 +14,7 @@ use crate::common::errors::ErrorKind;
use crate::application::ports::inbound::FolderUseCase;
use crate::common::di::AppState as GlobalAppState;
use crate::interfaces::middleware::auth::AuthUser;
use crate::infrastructure::services::zip_service::ZipService;
type AppState = Arc<FolderService>;
@@ -213,4 +215,79 @@ impl FolderHandler {
}
}
}
/// Downloads a folder as a ZIP file
pub async fn download_folder_zip(
State(state): State<GlobalAppState>,
Path(id): Path<String>,
Query(_params): Query<HashMap<String, String>>,
) -> impl IntoResponse {
tracing::info!("Downloading folder as ZIP: {}", id);
// Get folder information first to check it exists and get name
let folder_service = &state.applications.folder_service;
let file_service = &state.applications.file_service;
match folder_service.get_folder(&id).await {
Ok(folder) => {
tracing::info!("Preparing ZIP for folder: {} ({})", folder.name, id);
// Create ZIP service with the required services
let zip_service = ZipService::new(
file_service.clone(),
folder_service.clone()
);
// Create the ZIP file
match zip_service.create_folder_zip(&id, &folder.name).await {
Ok(zip_data) => {
tracing::info!("ZIP file created successfully, size: {} bytes", zip_data.len());
// Setup headers for download
let filename = format!("{}.zip", folder.name);
let content_disposition = format!("attachment; filename=\"{}\"", filename);
// Build response with the ZIP data
let mut headers = HashMap::new();
headers.insert(header::CONTENT_TYPE.to_string(), "application/zip".to_string());
headers.insert(header::CONTENT_DISPOSITION.to_string(), content_disposition);
headers.insert(header::CONTENT_LENGTH.to_string(), zip_data.len().to_string());
// Build the response
let mut response = Response::builder()
.status(StatusCode::OK)
.body(axum::body::Body::from(zip_data))
.unwrap();
// Add headers to response
for (name, value) in headers {
response.headers_mut().insert(
HeaderName::from_bytes(name.as_bytes()).unwrap(),
HeaderValue::from_str(&value).unwrap()
);
}
response
},
Err(err) => {
tracing::error!("Error creating ZIP file: {}", err);
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({
"error": format!("Error creating ZIP file: {}", err)
}))).into_response()
}
}
},
Err(err) => {
tracing::error!("Folder not found: {}", err);
let status = match err.kind {
ErrorKind::NotFound => StatusCode::NOT_FOUND,
_ => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(serde_json::json!({
"error": format!("Error finding folder: {}", err)
}))).into_response()
}
}
}
}

View File

@@ -168,6 +168,11 @@ pub fn create_api_routes(
.route("/{id}/move", put(FolderHandler::move_folder))
.with_state(folder_service.clone());
// Special route for ZIP download that requires AppState instead of just FolderService
let folder_zip_router = Router::new()
.route("/{id}/download", get(FolderHandler::download_folder_zip))
.with_state(app_state.clone());
// Create folder operations that use trash separately
let folders_ops_router = Router::new()
.route("/{id}", delete(|
@@ -200,7 +205,7 @@ pub fn create_api_routes(
}));
// Merge the routers
let folders_router = folders_basic_router.merge(folders_ops_router);
let folders_router = folders_basic_router.merge(folders_ops_router).merge(folder_zip_router);
// Create file routes for basic operations and trash-enabled delete
let basic_file_router = Router::new()

170
static/css/fileViewer.css Normal file
View File

@@ -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;
}
}

207
static/css/inlineViewer.css Normal file
View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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');

View 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');
}
}
};

View File

@@ -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}`;
}
});
}

311
static/js/fileViewer.js Normal file
View File

@@ -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();
}
});

329
static/js/inlineViewer.js Normal file
View File

@@ -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();

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -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"
}
}