dashboard file preview: markdown tabs + raster image rendering
Follow-up to #188. Two additions to the side-panel file preview: - Markdown files get a rendered/plain tabbed view (was: always rendered, no way to see source) — same tab pattern as SVG. - Raster images (png/jpg/gif/webp/bmp/ico/avif) render as an <img>. /api/state-file previously from_utf8_lossy-stringified every file and served text/plain, which corrupts binary; it now serves image files as raw bytes with their real content-type (over-cap images are rejected, not truncated — a clipped binary is corrupt). buildSvgPanel generalised to buildTabbedPreview, shared by SVG + markdown. .svg-host/.svg-render renamed .preview-host/.img-preview since they now back images + md too. closes #192
This commit is contained in:
parent
0884a54960
commit
f42ba9b561
4 changed files with 107 additions and 42 deletions
|
|
@ -1292,6 +1292,20 @@ async fn get_state_file(
|
|||
Ok(b) => b,
|
||||
Err(e) => return error_response(&format!("state-file: read {}: {e}", canonical.display())),
|
||||
};
|
||||
// Raster images: serve the raw bytes with their real content-type
|
||||
// so the dashboard can render them in an <img>. Not truncated —
|
||||
// a clipped binary is corrupt, so over-cap images are rejected
|
||||
// instead. (SVG stays on the text path: it's text, and the client
|
||||
// renders it via a data: URI.)
|
||||
if let Some(ct) = image_content_type(&canonical) {
|
||||
if bytes.len() > MAX_BYTES {
|
||||
return error_response(&format!(
|
||||
"state-file: image {} is {size} bytes, over the {MAX_BYTES}-byte preview cap",
|
||||
canonical.display()
|
||||
));
|
||||
}
|
||||
return ([("content-type", ct)], bytes).into_response();
|
||||
}
|
||||
let truncated = bytes.len() > MAX_BYTES;
|
||||
let body_bytes = if truncated { &bytes[..MAX_BYTES] } else { &bytes[..] };
|
||||
let mut body = String::from_utf8_lossy(body_bytes).into_owned();
|
||||
|
|
@ -1302,6 +1316,24 @@ async fn get_state_file(
|
|||
([("content-type", "text/plain; charset=utf-8")], body).into_response()
|
||||
}
|
||||
|
||||
/// Content-type for a raster image the dashboard can preview in an
|
||||
/// `<img>`, keyed off the file extension. `None` for non-image, SVG,
|
||||
/// and text files (SVG is served on the text path and rendered
|
||||
/// client-side via a `data:` URI).
|
||||
fn image_content_type(path: &Path) -> Option<&'static str> {
|
||||
let ext = path.extension()?.to_str()?.to_ascii_lowercase();
|
||||
Some(match ext.as_str() {
|
||||
"png" => "image/png",
|
||||
"jpg" | "jpeg" => "image/jpeg",
|
||||
"gif" => "image/gif",
|
||||
"webp" => "image/webp",
|
||||
"bmp" => "image/bmp",
|
||||
"ico" => "image/x-icon",
|
||||
"avif" => "image/avif",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
async fn api_reminders(State(state): State<AppState>) -> Response {
|
||||
match state.coord.broker.list_pending_reminders() {
|
||||
Ok(rows) => axum::Json(rows).into_response(),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue