Ugh
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::config::ColumnConfig;
|
||||
use crate::db;
|
||||
use crate::modes::server::AppState;
|
||||
use anyhow::Result;
|
||||
@@ -8,7 +9,6 @@ use axum::{
|
||||
use log::debug;
|
||||
use rusqlite::Connection;
|
||||
use serde::Deserialize;
|
||||
use crate::config::ColumnConfig;
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
@@ -72,8 +72,7 @@ fn default_count() -> usize {
|
||||
/// let app = pages::add_routes(axum::Router::new());
|
||||
/// ```
|
||||
pub fn add_routes(app: axum::Router<AppState>) -> axum::Router<AppState> {
|
||||
app
|
||||
.route("/", axum::routing::get(list_items))
|
||||
app.route("/", axum::routing::get(list_items))
|
||||
.route("/item/{item_id}", axum::routing::get(show_item))
|
||||
.route("/style.css", axum::routing::get(style_css))
|
||||
}
|
||||
@@ -84,9 +83,9 @@ async fn list_items(
|
||||
) -> Result<Response, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
let settings = &state.settings;
|
||||
|
||||
|
||||
let result = build_item_list(&conn, ¶ms, &settings.list_format);
|
||||
|
||||
|
||||
match result {
|
||||
Ok(html) => {
|
||||
// Build response with explicit Content-Length
|
||||
@@ -96,23 +95,28 @@ async fn list_items(
|
||||
.body(axum::body::Body::from(html))
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
},
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[ColumnConfig]) -> Result<String> {
|
||||
let tags: Vec<String> = params.tags
|
||||
fn build_item_list(
|
||||
conn: &Connection,
|
||||
params: &ListQueryParams,
|
||||
columns: &[ColumnConfig],
|
||||
) -> Result<String> {
|
||||
let tags: Vec<String> = 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" {
|
||||
@@ -120,7 +124,7 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} 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());
|
||||
@@ -129,29 +133,29 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
|
||||
|
||||
// Get tags and meta for all items in the page
|
||||
let item_ids: Vec<i64> = page_items.iter().filter_map(|item| item.id).collect();
|
||||
let tags_map = db::get_tags_for_items(conn, &item_ids)?;
|
||||
let meta_map = db::get_meta_for_items(conn, &item_ids)?;
|
||||
|
||||
|
||||
// Debug: print number of tags per item
|
||||
for item_id in &item_ids {
|
||||
if let Some(tags) = tags_map.get(item_id) {
|
||||
debug!("Item {} has {} tags: {:?}", item_id, tags.len(), tags);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let mut html = String::new();
|
||||
html.push_str("<html><head><title>Keep - Items</title>");
|
||||
html.push_str("<link rel=\"stylesheet\" href=\"/style.css\">");
|
||||
html.push_str("</head><body>");
|
||||
html.push_str("<h1>Items</h1>");
|
||||
html.push_str("<p><a href=\"/swagger\">API Documentation</a></p>");
|
||||
|
||||
|
||||
// Add recent tags section using the items we already have
|
||||
html.push_str("<h2>Recent Tags</h2>");
|
||||
|
||||
|
||||
// Collect all tags from all items, keeping track of their timestamps
|
||||
let mut all_tags_with_time: Vec<(String, chrono::DateTime<chrono::Utc>)> = Vec::new();
|
||||
for item in &sorted_items {
|
||||
@@ -163,10 +167,10 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Sort by timestamp descending (most recent first)
|
||||
all_tags_with_time.sort_by(|a, b| b.1.cmp(&a.1));
|
||||
|
||||
|
||||
// Get unique tags in order of most recent appearance
|
||||
let mut seen = std::collections::HashSet::new();
|
||||
let mut recent_tags = Vec::new();
|
||||
@@ -179,20 +183,23 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if recent_tags.is_empty() {
|
||||
html.push_str("<p>No tags found</p>");
|
||||
} else {
|
||||
html.push_str("<p>");
|
||||
for tag in recent_tags {
|
||||
html.push_str(&format!("<a href=\"/?tags={}\" style=\"margin-right: 8px;\">{}</a>", tag, tag));
|
||||
html.push_str(&format!(
|
||||
"<a href=\"/?tags={}\" style=\"margin-right: 8px;\">{}</a>",
|
||||
tag, tag
|
||||
));
|
||||
}
|
||||
html.push_str("</p>");
|
||||
}
|
||||
|
||||
|
||||
// Start table
|
||||
html.push_str("<table>");
|
||||
|
||||
|
||||
// Table headers
|
||||
html.push_str("<tr>");
|
||||
for column in columns {
|
||||
@@ -200,19 +207,21 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
html.push_str("<th>Actions</th>");
|
||||
html.push_str("</tr>");
|
||||
|
||||
|
||||
// Table rows
|
||||
for item in page_items {
|
||||
let item_id = item.id.unwrap_or(0);
|
||||
let tags = tags_map.get(&item_id).cloned().unwrap_or_default();
|
||||
let meta: HashMap<String, String> = meta_map.get(&item_id)
|
||||
let meta: HashMap<String, String> = meta_map
|
||||
.get(&item_id)
|
||||
.map(|metas| {
|
||||
metas.iter()
|
||||
metas
|
||||
.iter()
|
||||
.map(|(name, value)| (name.clone(), value.clone()))
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
html.push_str("<tr>");
|
||||
for column in columns {
|
||||
let value = match column.name.as_str() {
|
||||
@@ -220,16 +229,17 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
let id_value = item.id.map(|id| id.to_string()).unwrap_or_default();
|
||||
// Make the ID a link to the item details page
|
||||
format!("<a href=\"/item/{}\">{}</a>", item_id, id_value)
|
||||
},
|
||||
}
|
||||
"time" => item.ts.format("%Y-%m-%d %H:%M:%S").to_string(),
|
||||
"size" => item.size.map(|s| s.to_string()).unwrap_or_default(),
|
||||
"tags" => {
|
||||
// Make sure we're using all tags for the item
|
||||
let tag_links: Vec<String> = tags.iter()
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.collect();
|
||||
tag_links.join(", ")
|
||||
},
|
||||
}
|
||||
_ => {
|
||||
if column.name.starts_with("meta:") {
|
||||
let meta_key = &column.name[5..];
|
||||
@@ -239,7 +249,7 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// Apply max_len if specified, but skip for tags column to avoid truncating HTML
|
||||
let display_value = if column.name == "tags" {
|
||||
value
|
||||
@@ -257,36 +267,41 @@ fn build_item_list(conn: &Connection, params: &ListQueryParams, columns: &[Colum
|
||||
} else {
|
||||
value
|
||||
};
|
||||
|
||||
|
||||
// Apply alignment
|
||||
let align_style = match column.align {
|
||||
crate::config::ColumnAlignment::Left => "text-align: left;",
|
||||
crate::config::ColumnAlignment::Right => "text-align: right;",
|
||||
crate::config::ColumnAlignment::Center => "text-align: center;",
|
||||
};
|
||||
|
||||
html.push_str(&format!("<td style=\"{}\">{}</td>", align_style, display_value));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<td style=\"{}\">{}</td>",
|
||||
align_style, display_value
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
// Actions column
|
||||
html.push_str(&format!(
|
||||
"<td><a href=\"/item/{}\">View</a> | <a href=\"/api/item/{}/content\">Download</a></td>",
|
||||
item_id, item_id
|
||||
));
|
||||
|
||||
|
||||
html.push_str("</tr>");
|
||||
}
|
||||
|
||||
|
||||
html.push_str("</table>");
|
||||
|
||||
|
||||
// Add pagination info
|
||||
html.push_str(&format!("<p>Showing {} to {} of {} items</p>",
|
||||
start + 1,
|
||||
std::cmp::min(end, sorted_items.len()),
|
||||
sorted_items.len()));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<p>Showing {} to {} of {} items</p>",
|
||||
start + 1,
|
||||
std::cmp::min(end, sorted_items.len()),
|
||||
sorted_items.len()
|
||||
));
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
@@ -344,9 +359,9 @@ async fn show_item(
|
||||
Path(id): Path<i64>,
|
||||
) -> Result<Response, Html<String>> {
|
||||
let conn = state.db.lock().await;
|
||||
|
||||
|
||||
let result = build_item_details(&conn, id);
|
||||
|
||||
|
||||
match result {
|
||||
Ok(html) => {
|
||||
// Build response with explicit Content-Length
|
||||
@@ -356,7 +371,7 @@ async fn show_item(
|
||||
.body(axum::body::Body::from(html))
|
||||
.map_err(|_| Html("<html><body>Internal Server Error</body></html>".to_string()))?;
|
||||
Ok(response)
|
||||
},
|
||||
}
|
||||
Err(e) => Err(Html(format!("<html><body>Error: {}</body></html>", e))),
|
||||
}
|
||||
}
|
||||
@@ -366,51 +381,70 @@ fn build_item_details(conn: &Connection, id: i64) -> Result<String> {
|
||||
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(&format!("<html><head><title>Keep - Item #{}</title>", id));
|
||||
html.push_str("<link rel=\"stylesheet\" href=\"/style.css\">");
|
||||
html.push_str("</head><body>");
|
||||
html.push_str(&format!("<h1>Item #{}</h1>", id));
|
||||
|
||||
|
||||
// Single table for all details
|
||||
html.push_str("<table>");
|
||||
html.push_str(&format!("<tr><th>ID</th><td>{}</td></tr>", item.id.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><th>Timestamp</th><td>{}</td></tr>", item.ts.format("%Y-%m-%d %H:%M:%S")));
|
||||
html.push_str(&format!("<tr><th>Size</th><td>{}</td></tr>", item.size.unwrap_or(0)));
|
||||
html.push_str(&format!("<tr><th>Compression</th><td>{}</td></tr>", item.compression));
|
||||
|
||||
html.push_str(&format!(
|
||||
"<tr><th>ID</th><td>{}</td></tr>",
|
||||
item.id.unwrap_or(0)
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Timestamp</th><td>{}</td></tr>",
|
||||
item.ts.format("%Y-%m-%d %H:%M:%S")
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Size</th><td>{}</td></tr>",
|
||||
item.size.unwrap_or(0)
|
||||
));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>Compression</th><td>{}</td></tr>",
|
||||
item.compression
|
||||
));
|
||||
|
||||
// Tags row
|
||||
html.push_str("<tr><th>Tags</th><td>");
|
||||
if tags.is_empty() {
|
||||
html.push_str("No tags");
|
||||
} else {
|
||||
let tag_links: Vec<String> = tags.iter()
|
||||
let tag_links: Vec<String> = tags
|
||||
.iter()
|
||||
.map(|t| format!("<a href=\"/?tags={}\">{}</a>", t.name, t.name))
|
||||
.collect();
|
||||
html.push_str(&tag_links.join(", "));
|
||||
}
|
||||
html.push_str("</td></tr>");
|
||||
|
||||
|
||||
// Metadata rows
|
||||
if metas.is_empty() {
|
||||
html.push_str("<tr><th>Metadata</th><td>No metadata</td></tr>");
|
||||
} else {
|
||||
for meta in metas {
|
||||
html.push_str(&format!("<tr><th>{}</th><td>{}</td></tr>", meta.name, meta.value));
|
||||
html.push_str(&format!(
|
||||
"<tr><th>{}</th><td>{}</td></tr>",
|
||||
meta.name, meta.value
|
||||
));
|
||||
}
|
||||
}
|
||||
html.push_str("</table>");
|
||||
|
||||
|
||||
// Links
|
||||
html.push_str("<h2>Actions</h2>");
|
||||
html.push_str(&format!("<p><a href=\"/api/item/{}/content\">Download Content</a></p>", id));
|
||||
html.push_str(&format!(
|
||||
"<p><a href=\"/api/item/{}/content\">Download Content</a></p>",
|
||||
id
|
||||
));
|
||||
html.push_str("<p><a href=\"/\">Back to list</a></p>");
|
||||
|
||||
|
||||
html.push_str("</body></html>");
|
||||
|
||||
|
||||
Ok(html)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user