Krolyxon
Commit 18ca21c

ferreniumsrchandlersindex.rs

159 lines · 4.2 KB · Rust

1//! `/` — repository index.
2
3use std::sync::Arc;
4
5use askama::Template;
6use axum::extract::{Query, State};
7use axum::http::StatusCode;
8use axum::response::Response;
9use serde::Deserialize;
10
11use super::{Layout, render};
12use crate::index::Repo;
13use crate::render::{encode_path, time};
14use crate::state::AppState;
15
16/// Repos pushed within this window get the activity dot.
17const FRESH_SECS: i64 = 24 * 3600;
18
19#[derive(Debug, Default, Deserialize)]
20pub struct IndexQuery {
21 #[serde(default)]
22 q: String,
23 #[serde(default)]
24 sort: String,
25}
26
27pub struct Row {
28 pub name: String,
29 pub url: String,
30 pub description: Option<String>,
31 pub age: Option<String>,
32 pub iso: Option<String>,
33 pub fresh: bool,
34 /// Main language, once the background statistics have it.
35 pub language: Option<&'static str>,
36 /// Lowercased text the client-side filter matches against.
37 pub search: String,
38}
39
40pub struct Section {
41 pub name: Option<String>,
42 pub rows: Vec<Row>,
43}
44
45#[derive(Template)]
46#[template(path = "index.html")]
47pub struct IndexPage {
48 pub layout: Layout,
49 pub description: String,
50 /// Visible repositories in total.
51 pub total: usize,
52 /// Repositories matching the filter.
53 pub shown: usize,
54 pub q: String,
55 pub by_activity: bool,
56 pub sort_name_url: String,
57 pub sort_activity_url: String,
58 pub sections: Vec<Section>,
59}
60
61pub async fn index(
62 State(state): State<Arc<AppState>>,
63 Query(query): Query<IndexQuery>,
64) -> Response {
65 let config = state.config.load();
66 let index = state.index.load();
67 let by_activity = query.sort == "activity";
68 let q = query.q.trim().to_string();
69 let terms: Vec<String> = q.split_whitespace().map(str::to_lowercase).collect();
70 let now = time::now();
71 let base = &config.server.base_path;
72
73 let mut repos: Vec<&Arc<Repo>> = index
74 .repos
75 .values()
76 .filter(|repo| {
77 let text = search_text(repo);
78 terms.iter().all(|t| text.contains(t.as_str()))
79 })
80 .collect();
81 let shown = repos.len();
82
83 if by_activity {
84 repos.sort_by(|a, b| {
85 b.meta
86 .last_activity
87 .cmp(&a.meta.last_activity)
88 .then_with(|| a.name.cmp(&b.name))
89 });
90 } else {
91 repos.sort_by_cached_key(|r| {
92 (
93 r.meta.section.clone().map(|s| s.to_lowercase()),
94 r.name.to_lowercase(),
95 )
96 });
97 }
98
99 let row = |repo: &Arc<Repo>| Row {
100 name: repo.name.clone(),
101 url: format!("{base}{}", encode_path(&repo.name)),
102 description: repo.meta.description.clone(),
103 age: repo.meta.last_activity.map(|t| time::relative(t, now)),
104 iso: repo.meta.last_activity.map(time::iso),
105 fresh: repo
106 .meta
107 .last_activity
108 .is_some_and(|t| now - t < FRESH_SECS),
109 language: state.warmer.primary(&repo.name),
110 search: search_text(repo),
111 };
112
113 let mut sections: Vec<Section> = Vec::new();
114 for repo in repos {
115 // Sections only apply to the name ordering; activity is one list.
116 let section = if by_activity {
117 None
118 } else {
119 repo.meta.section.clone()
120 };
121 match sections.last_mut() {
122 Some(last) if last.name == section => last.rows.push(row(repo)),
123 _ => sections.push(Section {
124 name: section,
125 rows: vec![row(repo)],
126 }),
127 }
128 }
129
130 let sort_url = |sort: &str| {
131 let mut url = format!("{base}?sort={sort}");
132 if !q.is_empty() {
133 url.push_str("&q=");
134 url.push_str(&encode_path(&q));
135 }
136 url
137 };
138 let page = IndexPage {
139 layout: Layout::new(&state, &config, None),
140 description: config.site.description.clone(),
141 total: index.repos.len(),
142 shown,
143 sort_name_url: sort_url("name"),
144 sort_activity_url: sort_url("activity"),
145 q,
146 by_activity,
147 sections,
148 };
149 render(StatusCode::OK, &page)
150}
151
152fn search_text(repo: &Repo) -> String {
153 let mut text = repo.name.to_lowercase();
154 if let Some(d) = &repo.meta.description {
155 text.push(' ');
156 text.push_str(&d.to_lowercase());
157 }
158 text
159}