✨ document chrono view
This commit is contained in:
parent
e2b755aeaf
commit
64956bf2f2
4 changed files with 152 additions and 10 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -202,7 +202,7 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
|
|||
[[package]]
|
||||
name = "based"
|
||||
version = "0.1.0"
|
||||
source = "git+https://git.hydrar.de/jmarya/based?branch=ui#c05d0dcc0a46e721e8664a15e6e2f264aa8d4b53"
|
||||
source = "git+https://git.hydrar.de/jmarya/based?branch=ui#12e709d722e7fe9d61ca3d76b3642ff2ef5301c2"
|
||||
dependencies = [
|
||||
"bcrypt",
|
||||
"chrono",
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use std::{collections::HashSet, path::PathBuf};
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
path::PathBuf,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
blacklist::{check_blacklist, check_blacklist_path},
|
||||
|
@ -9,6 +12,7 @@ use crate::{
|
|||
mod document;
|
||||
mod domain;
|
||||
use based::get_pg;
|
||||
use chrono::NaiveDate;
|
||||
pub use document::Document;
|
||||
pub use domain::*;
|
||||
|
||||
|
@ -292,3 +296,25 @@ pub async fn index_document(doc: &Document) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct DocumentIndex {}
|
||||
|
||||
impl DocumentIndex {
|
||||
pub async fn get_documents_of_day(day: NaiveDate) -> HashMap<String, Vec<String>> {
|
||||
let res: Vec<(String, String)> =
|
||||
sqlx::query_as("SELECT domain, path FROM document_index WHERE version = $1")
|
||||
.bind(day)
|
||||
.fetch_all(get_pg!())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut ret = HashMap::new();
|
||||
|
||||
for (domain, path) in res {
|
||||
let d: &mut Vec<String> = ret.entry(domain).or_default();
|
||||
d.push(path);
|
||||
}
|
||||
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use based::asset::AssetRoutes;
|
||||
use based::get_pg;
|
||||
use based::ui::components::Shell;
|
||||
use based::ui::components::{NavBar, Shell};
|
||||
use based::ui::prelude::*;
|
||||
use rocket::routes;
|
||||
use webarc::ai::EmbedStore;
|
||||
|
@ -57,7 +57,8 @@ async fn main() {
|
|||
pages::domain_info_route,
|
||||
pages::favicon_route,
|
||||
pages::vector_search,
|
||||
pages::render_txt_website
|
||||
pages::render_txt_website,
|
||||
pages::timeline_route
|
||||
],
|
||||
)
|
||||
.manage(arc)
|
||||
|
@ -189,6 +190,7 @@ pub fn get_shell() -> Shell {
|
|||
.color(Zinc::_950),
|
||||
)
|
||||
.use_ui()
|
||||
.with_navbar(NavBar("Web Archive"))
|
||||
}
|
||||
|
||||
// TODO : archive cleanup code
|
||||
|
|
126
src/pages/mod.rs
126
src/pages/mod.rs
|
@ -1,19 +1,30 @@
|
|||
use std::{io::Read, path::PathBuf, sync::Arc};
|
||||
use std::{collections::HashMap, io::Read, path::PathBuf, sync::Arc};
|
||||
|
||||
use based::{
|
||||
page,
|
||||
request::{
|
||||
api::GeneratedPager, assets::DataResponse, respond_json, RequestContext, StringResponse,
|
||||
api::GeneratedPager, assets::DataResponse, respond_html, respond_json, RequestContext,
|
||||
StringResponse,
|
||||
},
|
||||
ui::{
|
||||
components::{
|
||||
prelude::{InfinityScroll, Timeline, TimelineElement},
|
||||
ColoredSpinner, Search, Shell,
|
||||
},
|
||||
primitives::flex::Column,
|
||||
UIWidget,
|
||||
},
|
||||
ui::components::{Search, Shell},
|
||||
};
|
||||
use chrono::NaiveDate;
|
||||
use maud::{html, PreEscaped};
|
||||
use rocket::{get, request::FromSegments, State};
|
||||
|
||||
pub mod component;
|
||||
use based::ui::prelude::*;
|
||||
use component::*;
|
||||
use serde_json::json;
|
||||
|
||||
use webarc::archive::Document;
|
||||
use webarc::archive::{Document, DocumentIndex};
|
||||
use webarc::{
|
||||
ai::{generate_embedding, remove_data_urls, EmbedStore, SearchResult},
|
||||
archive::{extract_domains, WebsiteArchive},
|
||||
|
@ -21,10 +32,113 @@ use webarc::{
|
|||
render_page,
|
||||
};
|
||||
|
||||
// TODO : Implement archive timeline page (chrono sorted documents)
|
||||
|
||||
const SEARCH_BAR_STYLE: &str = "w-full px-4 mb-4 py-2 text-white bg-black border-2 border-neon-blue placeholder-neon-blue focus:ring-2 focus:ring-neon-pink focus:outline-none font-mono text-lg";
|
||||
|
||||
pub fn WebsiteIcon(domain: &str) -> PreEscaped<String> {
|
||||
html! {
|
||||
h2 class="text-xl font-bold mb-4 -ml-2 flex items-center w-fit" {
|
||||
img class="p-2" src=(format!("/favicon/{domain}")) {};
|
||||
a href=(format!("/d/{domain}")) { (domain) };
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn build_timeline(
|
||||
domains: HashMap<String, Vec<String>>,
|
||||
the_day: NaiveDate,
|
||||
vanish: bool,
|
||||
) -> PreEscaped<String> {
|
||||
let mut tl = Timeline();
|
||||
|
||||
let yesterday = the_day - chrono::Duration::days(1);
|
||||
|
||||
let mut sorted_keys: Vec<_> = domains.keys().collect();
|
||||
sorted_keys.sort();
|
||||
|
||||
for key in sorted_keys {
|
||||
let (domain, paths) = (key, domains.get(key).unwrap());
|
||||
tl = tl.add_element(TimelineElement::new(
|
||||
&the_day.to_string(),
|
||||
WebsiteIcon(domain),
|
||||
Column(
|
||||
paths
|
||||
.into_iter()
|
||||
.map(|path| Link(&format!("/s/{domain}/{path}"), Text(&path).large()))
|
||||
.collect(),
|
||||
)
|
||||
.gap(ScreenValue::_2),
|
||||
));
|
||||
}
|
||||
|
||||
tl = tl.add_after(InfinityScroll(
|
||||
ColoredSpinner(Purple::_500),
|
||||
&format!("/timeline?before={}", yesterday.to_string()),
|
||||
));
|
||||
|
||||
if vanish {
|
||||
tl = tl.vanish();
|
||||
}
|
||||
|
||||
tl.render_with_class("")
|
||||
}
|
||||
|
||||
pub async fn get_domains(before: &str) -> HashMap<String, Vec<String>> {
|
||||
let today = NaiveDate::parse_from_str(before, "%Y-%m-%d").unwrap();
|
||||
DocumentIndex::get_documents_of_day(today).await
|
||||
}
|
||||
|
||||
pub async fn get_domains_lookback(
|
||||
before: &str,
|
||||
mut days: u64,
|
||||
) -> (HashMap<String, Vec<String>>, NaiveDate) {
|
||||
let mut the_day = NaiveDate::parse_from_str(before, "%Y-%m-%d").unwrap();
|
||||
while days > 0 {
|
||||
let domains = get_domains(&the_day.to_string()).await;
|
||||
|
||||
if !domains.is_empty() {
|
||||
return (domains, the_day);
|
||||
}
|
||||
|
||||
the_day -= chrono::Duration::days(1);
|
||||
days -= 1;
|
||||
}
|
||||
|
||||
(HashMap::new(), the_day)
|
||||
}
|
||||
|
||||
#[get("/timeline?<before>")]
|
||||
pub async fn timeline_route(
|
||||
ctx: RequestContext,
|
||||
shell: &State<Shell>,
|
||||
before: Option<&str>,
|
||||
) -> StringResponse {
|
||||
let today = chrono::Local::now().date_naive();
|
||||
|
||||
let (domains, day) = if let Some(before) = before {
|
||||
get_domains_lookback(before, 30 * 12).await
|
||||
} else {
|
||||
get_domains_lookback(&today.to_string(), 30 * 12).await
|
||||
};
|
||||
|
||||
if domains.is_empty() {
|
||||
return respond_html(Nothing().0);
|
||||
}
|
||||
|
||||
if ctx.is_htmx && !ctx.htmx_redirect {
|
||||
return respond_html(build_timeline(domains, day, true).0);
|
||||
}
|
||||
|
||||
let tl = Screen::medium(Padding(Nothing()).all(ScreenValue::_6))
|
||||
.on(Column(vec![
|
||||
Margin(Text("Document Timeline")._3xl().extrabold())
|
||||
.left(ScreenValue::_6)
|
||||
.render_with_class(""),
|
||||
build_timeline(domains, day, false),
|
||||
]))
|
||||
.render_with_class("");
|
||||
page!(shell, ctx, "Timeline", tl)
|
||||
}
|
||||
|
||||
/// Get the favicon of a domain
|
||||
#[get("/favicon/<domain>")]
|
||||
pub async fn favicon_route(domain: &str) -> Option<DataResponse> {
|
||||
|
|
Loading…
Add table
Reference in a new issue