diff --git a/src/modes/server.rs b/src/modes/server.rs index db98dd4..1a1dea9 100644 --- a/src/modes/server.rs +++ b/src/modes/server.rs @@ -15,6 +15,7 @@ use tower_http::trace::TraceLayer; mod common; mod api; mod docs; +mod pages; pub use common::{ServerConfig, AppState, logging_middleware}; @@ -62,9 +63,10 @@ async fn run_server( .layer(CorsLayer::permissive()) ); - // Add API and documentation routes + // Add API, documentation, and pages routes let app = api::add_routes(app); let app = docs::add_routes(app); + let app = pages::add_routes(app); // Apply state to the router after all routes are added let app = app.with_state(state); diff --git a/src/modes/server/pages.rs b/src/modes/server/pages.rs index e69de29..ed9b89c 100644 --- a/src/modes/server/pages.rs +++ b/src/modes/server/pages.rs @@ -0,0 +1,193 @@ +use crate::db::{self, Item, Tag, Meta}; +use crate::modes::server::AppState; +use anyhow::Result; +use axum::{ + extract::{Path, Query, State}, + response::Html, +}; +use rusqlite::Connection; +use serde::Deserialize; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::Mutex; + +#[derive(Deserialize)] +pub struct ListQueryParams { + #[serde(default = "default_sort")] + sort: String, + #[serde(default)] + tags: Option, + #[serde(default = "default_count")] + count: usize, + #[serde(default)] + start: usize, +} + +fn default_sort() -> String { + "newest".to_string() +} + +fn default_count() -> usize { + 1000 +} + +pub fn add_routes(app: axum::Router) -> axum::Router { + app.route("/", axum::routing::get(list_items)) + .route("/item/:id", axum::routing::get(show_item)) +} + +async fn list_items( + State(state): State, + Query(params): Query, +) -> Result, Html> { + let conn = state.db.lock().await; + + let result = build_item_list(&conn, ¶ms); + + match result { + Ok(html) => Ok(Html(html)), + Err(e) => Err(Html(format!("Error: {}", e))), + } +} + +fn build_item_list(conn: &Connection, params: &ListQueryParams) -> Result { + let tags: Vec = params.tags + .as_ref() + .map(|t| t.split(',').map(|s| s.trim().to_string()).collect()) + .unwrap_or_default(); + + let items = if tags.is_empty() { + db::query_all_items(conn)? + } else { + db::query_tagged_items(conn, &tags)? + }; + + // Sort items + let mut sorted_items = items; + if params.sort == "newest" { + sorted_items.sort_by(|a, b| b.id.cmp(&a.id)); + } else { + sorted_items.sort_by(|a, b| a.id.cmp(&b.id)); + } + + // Apply pagination + let start = params.start; + let end = std::cmp::min(start + params.count, sorted_items.len()); + let page_items = if start < sorted_items.len() { + sorted_items[start..std::cmp::min(end, sorted_items.len())].to_vec() + } else { + vec![] + }; + + // Get tags for all items in the page + let item_ids: Vec = page_items.iter().filter_map(|item| item.id).collect(); + let tags_map = db::get_tags_for_items(conn, &item_ids)?; + + let mut html = String::new(); + html.push_str("Items"); + html.push_str("

Items

"); + html.push_str("
    "); + + for item in page_items { + let item_id = item.id.unwrap_or(0); + let tags_html = if let Some(tags) = tags_map.get(&item_id) { + let tag_names: Vec = tags.iter().map(|t| t.name.clone()).collect(); + if tag_names.is_empty() { + String::new() + } else { + format!(" [{}]", tag_names.join(", ")) + } + } else { + String::new() + }; + + html.push_str(&format!( + "
  • Item #{} - {}{} - Download
  • ", + item_id, + item_id, + item.ts.format("%Y-%m-%d %H:%M:%S"), + tags_html, + item_id + )); + } + + html.push_str("
"); + + // Add pagination info + html.push_str(&format!("

Showing {} to {} of {} items

", + start + 1, + std::cmp::min(end, sorted_items.len()), + sorted_items.len())); + + html.push_str(""); + + Ok(html) +} + +async fn show_item( + State(state): State, + Path(id): Path, +) -> Result, Html> { + let conn = state.db.lock().await; + + let result = build_item_details(&conn, id); + + match result { + Ok(html) => Ok(Html(html)), + Err(e) => Err(Html(format!("Error: {}", e))), + } +} + +fn build_item_details(conn: &Connection, id: i64) -> Result { + let item = match db::get_item(conn, id)? { + Some(item) => item, + None => return Err(anyhow::anyhow!("Item not found")), + }; + + let tags = db::get_item_tags(conn, &item)?; + let metas = db::get_item_meta(conn, &item)?; + + let mut html = String::new(); + html.push_str("Item Details"); + html.push_str(&format!("

Item #{}

", id)); + html.push_str(""); + html.push_str(&format!("", item.id.unwrap_or(0))); + html.push_str(&format!("", item.ts.format("%Y-%m-%d %H:%M:%S"))); + html.push_str(&format!("", item.size.unwrap_or(0))); + html.push_str(&format!("", item.compression)); + html.push_str("
ID:{}
Timestamp:{}
Size:{}
Compression:{}
"); + + // Tags section + html.push_str("

Tags

"); + if tags.is_empty() { + html.push_str("

No tags

"); + } else { + html.push_str("
    "); + for tag in tags { + html.push_str(&format!("
  • {}
  • ", tag.name)); + } + html.push_str("
"); + } + + // Metadata section + html.push_str("

Metadata

"); + if metas.is_empty() { + html.push_str("

No metadata

"); + } else { + html.push_str(""); + html.push_str(""); + for meta in metas { + html.push_str(&format!("", meta.name, meta.value)); + } + html.push_str("
NameValue
{}{}
"); + } + + // Links + html.push_str("

Actions

"); + html.push_str(&format!("

Download Content

", id)); + html.push_str("

Back to list

"); + + html.push_str(""); + + Ok(html) +}