diff --git a/Cargo.lock b/Cargo.lock index 630c663..bc750cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1260,6 +1260,7 @@ dependencies = [ "http-body-util", "rand", "serde", + "serde_urlencoded", "tera", "tokio", "tower-http", diff --git a/Justfile b/Justfile index c19ef20..1280487 100644 --- a/Justfile +++ b/Justfile @@ -46,7 +46,7 @@ run: --log debug doc: - cargo doc --workspace --frozen + cargo doc --workspace --frozen --open publish-release-binaries tag: build-release-static curl \ diff --git a/otter/Cargo.toml b/otter/Cargo.toml index 5b98adc..0c3bf8f 100644 --- a/otter/Cargo.toml +++ b/otter/Cargo.toml @@ -25,3 +25,4 @@ http-body-util = "0.1.3" tokio = { version = "1.43.0", features = ["full"] } tracing-subscriber = "0.3.19" tera = "1.20.0" +serde_urlencoded = "0.7.1" diff --git a/otter/src/server/web/mod.rs b/otter/src/server/web/mod.rs index f924f9b..dac3229 100644 --- a/otter/src/server/web/mod.rs +++ b/otter/src/server/web/mod.rs @@ -13,7 +13,7 @@ use cookie::{Cookie, time::Duration}; use gpodder::{AuthErr, Session}; use serde::{Deserialize, Serialize}; -use crate::web::{Page, TemplateExt, TemplateResponse, View}; +use crate::web::{Page, Query, TemplateExt, TemplateResponse, ToQuery, View}; use super::{ Context, @@ -22,7 +22,7 @@ use super::{ const SESSION_ID_COOKIE: &str = "sessionid"; -#[derive(Deserialize, Serialize)] +#[derive(Deserialize, Clone)] #[serde(default)] pub struct Pagination { page: u32, @@ -47,6 +47,23 @@ impl Default for Pagination { } } +impl ToQuery for Pagination { + fn to_query(self) -> Query { + Query::default() + .parameter("page", self.page) + .parameter("per_page", self.per_page) + } +} + +impl Pagination { + pub fn next_page(&self) -> Self { + Self { + page: self.page + 1, + per_page: self.per_page, + } + } +} + pub fn router(ctx: Context) -> Router { Router::new() .route("/", get(get_index)) diff --git a/otter/src/server/web/sessions.rs b/otter/src/server/web/sessions.rs index 9020bab..611372a 100644 --- a/otter/src/server/web/sessions.rs +++ b/otter/src/server/web/sessions.rs @@ -7,7 +7,7 @@ use axum::{ use crate::{ server::{Context, error::AppResult}, - web::{Page, TemplateExt, TemplateResponse, View}, + web::{Page, TemplateExt, TemplateResponse, ToQuery, View}, }; pub fn router(ctx: Context) -> Router { @@ -25,12 +25,16 @@ pub async fn get_sessions( Extension(user): Extension, Query(page): Query, ) -> AppResult>> { + let next_page = page.next_page(); let sessions = tokio::task::spawn_blocking(move || ctx.store.paginated_sessions(&user, page.into())) .await .unwrap()?; - Ok(View::Sessions(sessions) + let next_page_query = + (sessions.len() == next_page.per_page as usize).then_some(next_page.to_query()); + + Ok(View::Sessions(sessions, next_page_query) .page(&headers) .headers(&headers) .authenticated(true) diff --git a/otter/src/web/mod.rs b/otter/src/web/mod.rs index 05d66e2..f99ff53 100644 --- a/otter/src/web/mod.rs +++ b/otter/src/web/mod.rs @@ -1,4 +1,5 @@ mod page; +mod query; mod view; use std::sync::Arc; @@ -10,6 +11,7 @@ use axum::{ }; pub use page::Page; +pub use query::{Query, ToQuery}; pub use view::View; const BASE_TEMPLATE: &str = "base.html"; @@ -80,7 +82,7 @@ pub fn initialize_tera() -> tera::Result { include_str!("templates/views/login.html"), ), ( - View::Sessions(Vec::new()).template(), + View::Sessions(Vec::new(), None).template(), include_str!("templates/views/sessions.html"), ), ])?; diff --git a/otter/src/web/query.rs b/otter/src/web/query.rs new file mode 100644 index 0000000..409afd5 --- /dev/null +++ b/otter/src/web/query.rs @@ -0,0 +1,37 @@ +/// Represents a list of query parameters +#[derive(Default)] +pub struct Query(Vec<(String, String)>); + +impl Query { + /// Combine two queries into one + pub fn join(mut self, other: impl ToQuery) -> Self { + let mut other = other.to_query(); + self.0.append(&mut other.0); + + self + } + + /// Convert the query into a url-encoded query parameter string + pub fn encode(self) -> String { + // TODO is this unwrap safe? + serde_urlencoded::to_string(&self.0).unwrap() + } + + /// Builder-style method that appends a parameter to the query + pub fn parameter(mut self, key: impl ToString, value: impl ToString) -> Self { + self.0.push((key.to_string(), value.to_string())); + + self + } +} + +/// Allows objects to be converted into queries +pub trait ToQuery { + fn to_query(self) -> Query; +} + +impl ToQuery for Query { + fn to_query(self) -> Query { + self + } +} diff --git a/otter/src/web/templates/views/sessions.html b/otter/src/web/templates/views/sessions.html index f160d41..1a59e34 100644 --- a/otter/src/web/templates/views/sessions.html +++ b/otter/src/web/templates/views/sessions.html @@ -16,5 +16,13 @@ Remove {% endfor %} + {% if next_page_query %} + + {% endif %} diff --git a/otter/src/web/view.rs b/otter/src/web/view.rs index e1a0f59..5636a16 100644 --- a/otter/src/web/view.rs +++ b/otter/src/web/view.rs @@ -1,12 +1,12 @@ use chrono::{DateTime, Utc}; use serde::Serialize; -use super::Template; +use super::{Query, Template}; pub enum View { Index, Login, - Sessions(Vec), + Sessions(Vec, Option), } #[derive(Serialize)] @@ -20,7 +20,7 @@ impl Template for View { match self { Self::Index => "views/index.html", Self::Login => "views/login.html", - Self::Sessions(_) => "views/sessions.html", + Self::Sessions(..) => "views/sessions.html", } } @@ -29,11 +29,15 @@ impl Template for View { let template = self.template(); match self { - Self::Sessions(sessions) => { + Self::Sessions(sessions, query) => { ctx.insert( "sessions", &sessions.into_iter().map(Session::from).collect::>(), ); + + if let Some(query) = query { + ctx.insert("next_page_query", &query.encode()); + } } _ => {} };