Krolyxon
Commit ce0f374

ferreniumsrchandlersindex.rs

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