mod html;
mod media;
pub(super) mod paths;
mod window;
use axum::Json;
use axum::extract::{Path as AxumPath, Query, State};
use axum::response::{IntoResponse, Response};
use serde::{Deserialize, Serialize};
use std::path::Path;
use crate::cli::catalog_read::EntryRow;
use crate::cli::settings_api;
use crate::handlers::viewing::directory_tree_nodes;
use crate::integrations::{ZahirFT, delimiter_from_path_for_viewer, file_type_from_metadata_name};
use crate::render::kv_tables::{TreeNodeView, tree_node_to_view, tree_roots_to_lines};
use crate::render::viewers::{pdf_preview, svg_preview, syntect_text};
use crate::utils::file_content_for_viewer;
use super::catalog::entry_row;
use super::error::ApiError;
use super::state::{AppState, current_dir, with_db};
use self::html::content_to_html;
use self::media::{embedded_cover_response, raw_media_response};
use self::paths::{require_rel_path, resolve_entry_disk_path};
use self::window::windowed_text_content_response;
#[derive(Debug, Deserialize)]
pub(super) struct EntryContentQuery {
#[serde(default)]
format: Option<String>,
#[serde(default)]
page: Option<u32>,
#[serde(default)]
offset: Option<u64>,
#[serde(default)]
limit: Option<u64>,
#[serde(default)]
theme: Option<String>,
}
#[derive(Debug, Serialize)]
pub(super) struct EntryContentResponse {
path: String,
category: String,
format: String,
content: String,
#[serde(skip_serializing_if = "Option::is_none")]
page: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
page_count: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
offset: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
byte_len: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
limit: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
total_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
tree: Option<Vec<TreeNodeView>>,
}
pub(super) async fn get_entry_content(
State(state): State<AppState>,
AxumPath(path): AxumPath<String>,
Query(q): Query<EntryContentQuery>,
) -> Result<Response, ApiError> {
let path = require_rel_path(&path)?;
let dir = current_dir(&state)?;
let row = with_db(&state, |conn| entry_row(conn, &path, false))?;
let abs = resolve_entry_disk_path(&dir, &path)?;
let zahir_type = file_type_from_metadata_name(&row.category);
match q.format.as_deref().map(str::trim) {
Some("raw") => return raw_media_response(&abs, zahir_type, q.page),
Some("cover") => return embedded_cover_response(&abs, zahir_type),
_ => {}
}
if let (Some(offset), Some(limit)) = (q.offset, q.limit) {
return windowed_text_content_response(&row, &abs, offset, limit, q.format.as_deref());
}
if abs.is_dir() {
return Ok(directory_tree_content_response(&row, &abs));
}
let text = file_content_for_viewer(&abs, zahir_type).unwrap_or_else(|| "(empty)".into());
let want_html = content_want_html(q.format.as_deref(), zahir_type, &row.path)?;
let pdf_page = q.page.unwrap_or(1).max(1);
let (format, content) = if want_html {
let palette = match q.theme.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
Some(name) => crate::themes::get(Some(name)),
None => settings_api::effective_palette(&dir),
};
(
"html".into(),
content_to_html(&text, &row.path, &abs, zahir_type, palette, Some(pdf_page)),
)
} else {
("text".into(), text)
};
let (page, page_count) = if zahir_type == Some(ZahirFT::Pdf) {
(Some(pdf_page), pdf_preview::pdf_page_count(&abs).ok())
} else {
(None, None)
};
Ok(Json(EntryContentResponse {
path: row.path,
category: row.category,
format,
content,
page,
page_count,
offset: None,
byte_len: None,
limit: None,
total_bytes: None,
tree: None,
})
.into_response())
}
fn directory_tree_content_response(row: &EntryRow, abs: &Path) -> Response {
let roots = directory_tree_nodes(abs);
let tree: Vec<TreeNodeView> = roots.iter().map(tree_node_to_view).collect();
let content = tree_roots_to_lines(&roots).join("\n");
Json(EntryContentResponse {
path: row.path.clone(),
category: row.category.clone(),
format: "tree".into(),
content,
page: None,
page_count: None,
offset: None,
byte_len: None,
limit: None,
total_bytes: None,
tree: Some(tree),
})
.into_response()
}
fn content_want_html(
format: Option<&str>,
zahir_type: Option<ZahirFT>,
path: &str,
) -> Result<bool, ApiError> {
match format.map(str::trim) {
Some("html") => Ok(true),
Some("text") => Ok(false),
None => Ok(matches!(
zahir_type,
Some(
ZahirFT::Markdown
| ZahirFT::Csv
| ZahirFT::Text
| ZahirFT::Image
| ZahirFT::Pdf
| ZahirFT::Video
| ZahirFT::Audio
| ZahirFT::Epub
)
) || zahir_type.is_some_and(syntect_text::uses_syntect_ft)
|| delimiter_from_path_for_viewer(path).is_some()
|| svg_preview::is_svg_path(Path::new(path))),
Some(other) => Err(ApiError::bad_request(format!(
"invalid format {other:?}; expected text|html|raw|cover"
))),
}
}