Compare commits
	
		
			3 Commits 
		
	
	
		
			32d70daab2
			...
			a57e301d16
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						a57e301d16 | |
| 
							
							
								
									
								
								 | 
						68b2b1beb4 | |
| 
							
							
								
									
								
								 | 
						e8e0c94937 | 
| 
						 | 
				
			
			@ -1260,6 +1260,7 @@ dependencies = [
 | 
			
		|||
 "http-body-util",
 | 
			
		||||
 "rand",
 | 
			
		||||
 "serde",
 | 
			
		||||
 "serde_urlencoded",
 | 
			
		||||
 "tera",
 | 
			
		||||
 "tokio",
 | 
			
		||||
 "tower-http",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								Justfile
								
								
								
								
							
							
						
						
									
										2
									
								
								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 \
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										10
									
								
								otter.toml
								
								
								
								
							
							
						
						
									
										10
									
								
								otter.toml
								
								
								
								
							| 
						 | 
				
			
			@ -1,9 +1,9 @@
 | 
			
		|||
data_dir = "./data"
 | 
			
		||||
 | 
			
		||||
[net]
 | 
			
		||||
# type = "tcp"
 | 
			
		||||
# domain = "127.0.0.1"
 | 
			
		||||
# port = 8080
 | 
			
		||||
type = "tcp"
 | 
			
		||||
domain = "127.0.0.1"
 | 
			
		||||
port = 8080
 | 
			
		||||
 | 
			
		||||
type = "unix"
 | 
			
		||||
path = "./otter.socket"
 | 
			
		||||
# type = "unix"
 | 
			
		||||
# path = "./otter.socket"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,8 +6,8 @@ use std::path::PathBuf;
 | 
			
		|||
 | 
			
		||||
use clap::{Args, Parser, Subcommand, ValueEnum};
 | 
			
		||||
use figment::{
 | 
			
		||||
    providers::{Env, Format, Serialized, Toml},
 | 
			
		||||
    Figment,
 | 
			
		||||
    providers::{Env, Format, Serialized, Toml},
 | 
			
		||||
};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,20 +1,20 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Router,
 | 
			
		||||
    extract::{Path, State},
 | 
			
		||||
    routing::post,
 | 
			
		||||
    Router,
 | 
			
		||||
};
 | 
			
		||||
use axum_extra::{
 | 
			
		||||
    extract::{cookie::Cookie, CookieJar},
 | 
			
		||||
    headers::{authorization::Basic, Authorization, UserAgent},
 | 
			
		||||
    TypedHeader,
 | 
			
		||||
    extract::{CookieJar, cookie::Cookie},
 | 
			
		||||
    headers::{Authorization, UserAgent, authorization::Basic},
 | 
			
		||||
};
 | 
			
		||||
use cookie::time::Duration;
 | 
			
		||||
use gpodder::AuthErr;
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::SESSION_ID_COOKIE,
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router() -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,18 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
    extract::{Path, State},
 | 
			
		||||
    middleware,
 | 
			
		||||
    routing::{get, post},
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::{
 | 
			
		||||
        auth_api_middleware,
 | 
			
		||||
        format::{Format, StringWithFormat},
 | 
			
		||||
        models,
 | 
			
		||||
    },
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,14 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
    extract::{Path, Query, State},
 | 
			
		||||
    middleware,
 | 
			
		||||
    routing::post,
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
};
 | 
			
		||||
use chrono::DateTime;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::{
 | 
			
		||||
        auth_api_middleware,
 | 
			
		||||
| 
						 | 
				
			
			@ -15,7 +16,6 @@ use crate::server::{
 | 
			
		|||
        models,
 | 
			
		||||
        models::UpdatedUrlsResponse,
 | 
			
		||||
    },
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,19 +1,19 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
    extract::{Path, Query, State},
 | 
			
		||||
    middleware,
 | 
			
		||||
    routing::post,
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::{
 | 
			
		||||
        auth_api_middleware,
 | 
			
		||||
        format::{Format, StringWithFormat},
 | 
			
		||||
        models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
 | 
			
		||||
    },
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,18 +1,18 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
    extract::{Path, State},
 | 
			
		||||
    middleware,
 | 
			
		||||
    routing::get,
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::{
 | 
			
		||||
        auth_api_middleware,
 | 
			
		||||
        format::{Format, StringWithFormat},
 | 
			
		||||
        models::{SyncStatus, SyncStatusDelta},
 | 
			
		||||
    },
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,8 +1,8 @@
 | 
			
		|||
use std::ops::Deref;
 | 
			
		||||
 | 
			
		||||
use serde::{
 | 
			
		||||
    de::{value::StrDeserializer, Visitor},
 | 
			
		||||
    Deserialize,
 | 
			
		||||
    de::{Visitor, value::StrDeserializer},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Debug, PartialEq, Eq)]
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -4,16 +4,16 @@ mod models;
 | 
			
		|||
mod simple;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    RequestExt, Router,
 | 
			
		||||
    extract::{Request, State},
 | 
			
		||||
    http::{header::WWW_AUTHENTICATE, HeaderName, HeaderValue, StatusCode},
 | 
			
		||||
    http::{HeaderName, HeaderValue, StatusCode, header::WWW_AUTHENTICATE},
 | 
			
		||||
    middleware::Next,
 | 
			
		||||
    response::{IntoResponse, Response},
 | 
			
		||||
    RequestExt, Router,
 | 
			
		||||
};
 | 
			
		||||
use axum_extra::{
 | 
			
		||||
    extract::{cookie::Cookie, CookieJar},
 | 
			
		||||
    headers::{authorization::Basic, Authorization},
 | 
			
		||||
    TypedHeader,
 | 
			
		||||
    extract::{CookieJar, cookie::Cookie},
 | 
			
		||||
    headers::{Authorization, authorization::Basic},
 | 
			
		||||
};
 | 
			
		||||
use tower_http::set_header::SetResponseHeaderLayer;
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,14 +1,14 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
    extract::{Path, State},
 | 
			
		||||
    middleware,
 | 
			
		||||
    routing::get,
 | 
			
		||||
    Extension, Json, Router,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::server::{
 | 
			
		||||
    Context,
 | 
			
		||||
    error::{AppError, AppResult},
 | 
			
		||||
    gpodder::{auth_api_middleware, format::StringWithFormat},
 | 
			
		||||
    Context,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,12 +6,12 @@ mod web;
 | 
			
		|||
use std::sync::Arc;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    Router,
 | 
			
		||||
    body::Body,
 | 
			
		||||
    extract::Request,
 | 
			
		||||
    http::StatusCode,
 | 
			
		||||
    middleware::Next,
 | 
			
		||||
    response::{IntoResponse, Response},
 | 
			
		||||
    Router,
 | 
			
		||||
};
 | 
			
		||||
use http_body_util::BodyExt;
 | 
			
		||||
use tower_http::trace::TraceLayer;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
use std::io::Cursor;
 | 
			
		||||
 | 
			
		||||
use axum::{routing::get, Router};
 | 
			
		||||
use axum_extra::{headers::Range, TypedHeader};
 | 
			
		||||
use axum::{Router, routing::get};
 | 
			
		||||
use axum_extra::{TypedHeader, headers::Range};
 | 
			
		||||
use axum_range::{KnownSize, Ranged};
 | 
			
		||||
 | 
			
		||||
use super::Context;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,17 +1,19 @@
 | 
			
		|||
mod sessions;
 | 
			
		||||
 | 
			
		||||
use axum::{
 | 
			
		||||
    Form, RequestExt, Router,
 | 
			
		||||
    extract::{Request, State},
 | 
			
		||||
    http::{HeaderMap, HeaderName, HeaderValue, header},
 | 
			
		||||
    middleware::{self, Next},
 | 
			
		||||
    http::HeaderMap,
 | 
			
		||||
    middleware::Next,
 | 
			
		||||
    response::{IntoResponse, Redirect, Response},
 | 
			
		||||
    routing::{get, post},
 | 
			
		||||
};
 | 
			
		||||
use axum_extra::{TypedHeader, extract::CookieJar, headers::UserAgent};
 | 
			
		||||
use cookie::{Cookie, time::Duration};
 | 
			
		||||
use gpodder::{AuthErr, Session};
 | 
			
		||||
use serde::Deserialize;
 | 
			
		||||
use serde::{Deserialize, Serialize};
 | 
			
		||||
 | 
			
		||||
use crate::web::{Page, TemplateExt, TemplateResponse, View};
 | 
			
		||||
use crate::web::{Page, Query, TemplateExt, TemplateResponse, ToQuery, View};
 | 
			
		||||
 | 
			
		||||
use super::{
 | 
			
		||||
    Context,
 | 
			
		||||
| 
						 | 
				
			
			@ -20,13 +22,50 @@ use super::{
 | 
			
		|||
 | 
			
		||||
const SESSION_ID_COOKIE: &str = "sessionid";
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Clone)]
 | 
			
		||||
#[serde(default)]
 | 
			
		||||
pub struct Pagination {
 | 
			
		||||
    page: u32,
 | 
			
		||||
    per_page: u32,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<Pagination> for gpodder::Page {
 | 
			
		||||
    fn from(value: Pagination) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            page: value.page,
 | 
			
		||||
            per_page: value.per_page,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Default for Pagination {
 | 
			
		||||
    fn default() -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            page: 0,
 | 
			
		||||
            per_page: 1,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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<Context> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/sessions", get(get_sessions))
 | 
			
		||||
        .route_layer(axum::middleware::from_fn_with_state(
 | 
			
		||||
            ctx.clone(),
 | 
			
		||||
            auth_web_middleware,
 | 
			
		||||
        ))
 | 
			
		||||
        .route("/", get(get_index))
 | 
			
		||||
        // .layer(middleware::from_fn_with_state(
 | 
			
		||||
        //     ctx.clone(),
 | 
			
		||||
| 
						 | 
				
			
			@ -36,6 +75,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		|||
        // loop
 | 
			
		||||
        .route("/login", get(get_login).post(post_login))
 | 
			
		||||
        .route("/logout", post(post_logout))
 | 
			
		||||
        .merge(sessions::router(ctx.clone()))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async fn get_index(
 | 
			
		||||
| 
						 | 
				
			
			@ -166,14 +206,3 @@ pub async fn auth_web_middleware(
 | 
			
		|||
        Err(err) => err.into_response(),
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn get_sessions(
 | 
			
		||||
    State(ctx): State<Context>,
 | 
			
		||||
    headers: HeaderMap,
 | 
			
		||||
) -> TemplateResponse<Page<View>> {
 | 
			
		||||
    View::Sessions(Vec::new())
 | 
			
		||||
        .page(&headers)
 | 
			
		||||
        .headers(&headers)
 | 
			
		||||
        .authenticated(true)
 | 
			
		||||
        .response(&ctx.tera)
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,42 @@
 | 
			
		|||
use axum::{
 | 
			
		||||
    Extension, Router,
 | 
			
		||||
    extract::{Query, State},
 | 
			
		||||
    http::HeaderMap,
 | 
			
		||||
    routing::get,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
use crate::{
 | 
			
		||||
    server::{Context, error::AppResult},
 | 
			
		||||
    web::{Page, TemplateExt, TemplateResponse, ToQuery, View},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
pub fn router(ctx: Context) -> Router<Context> {
 | 
			
		||||
    Router::new()
 | 
			
		||||
        .route("/sessions", get(get_sessions))
 | 
			
		||||
        .route_layer(axum::middleware::from_fn_with_state(
 | 
			
		||||
            ctx.clone(),
 | 
			
		||||
            super::auth_web_middleware,
 | 
			
		||||
        ))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
pub async fn get_sessions(
 | 
			
		||||
    State(ctx): State<Context>,
 | 
			
		||||
    headers: HeaderMap,
 | 
			
		||||
    Extension(user): Extension<gpodder::User>,
 | 
			
		||||
    Query(page): Query<super::Pagination>,
 | 
			
		||||
) -> AppResult<TemplateResponse<Page<View>>> {
 | 
			
		||||
    let next_page = page.next_page();
 | 
			
		||||
    let sessions =
 | 
			
		||||
        tokio::task::spawn_blocking(move || ctx.store.paginated_sessions(&user, page.into()))
 | 
			
		||||
            .await
 | 
			
		||||
            .unwrap()?;
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
        .response(&ctx.tera))
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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";
 | 
			
		||||
| 
						 | 
				
			
			@ -22,7 +24,7 @@ pub trait Template {
 | 
			
		|||
    /// Render the template using the given Tera instance.
 | 
			
		||||
    ///
 | 
			
		||||
    /// Templates are expected to manage their own context requirements if needed.
 | 
			
		||||
    fn render(&self, tera: &tera::Tera) -> tera::Result<String>;
 | 
			
		||||
    fn render(self, tera: &tera::Tera) -> tera::Result<String>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Useful additional functions on sized Template implementors
 | 
			
		||||
| 
						 | 
				
			
			@ -80,7 +82,7 @@ pub fn initialize_tera() -> tera::Result<tera::Tera> {
 | 
			
		|||
            include_str!("templates/views/login.html"),
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            View::Sessions(Vec::new()).template(),
 | 
			
		||||
            View::Sessions(Vec::new(), None).template(),
 | 
			
		||||
            include_str!("templates/views/sessions.html"),
 | 
			
		||||
        ),
 | 
			
		||||
    ])?;
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -18,7 +18,7 @@ impl<T: Template> Template for Page<T> {
 | 
			
		|||
        self.template.template()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
 | 
			
		||||
    fn render(self, tera: &tera::Tera) -> tera::Result<String> {
 | 
			
		||||
        let inner = self.template.render(tera)?;
 | 
			
		||||
 | 
			
		||||
        if self.wrap_with_base {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -9,10 +9,20 @@
 | 
			
		|||
        </tr>
 | 
			
		||||
    </thead>
 | 
			
		||||
    <tbody>
 | 
			
		||||
        {% for session in sessions %}
 | 
			
		||||
        <tr>
 | 
			
		||||
            <th>Firefox</th>
 | 
			
		||||
            <th>yesterday</th>
 | 
			
		||||
            <th>{{ session.user_agent }}</th>
 | 
			
		||||
            <th>{{ session.last_seen }}</th>
 | 
			
		||||
            <th>Remove</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
        {% endfor %}
 | 
			
		||||
        {% if next_page_query %}
 | 
			
		||||
        <tr 
 | 
			
		||||
            hx-get="/sessions?{{ next_page_query }}"
 | 
			
		||||
            hx-trigger="revealed"
 | 
			
		||||
            hx-swap="outerHTML"
 | 
			
		||||
            hx-select="table > tbody > tr"
 | 
			
		||||
        ></tr>
 | 
			
		||||
        {% endif %}
 | 
			
		||||
    </tbody>
 | 
			
		||||
</table>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,9 +1,18 @@
 | 
			
		|||
use super::Template;
 | 
			
		||||
use chrono::{DateTime, Utc};
 | 
			
		||||
use serde::Serialize;
 | 
			
		||||
 | 
			
		||||
use super::{Query, Template};
 | 
			
		||||
 | 
			
		||||
pub enum View {
 | 
			
		||||
    Index,
 | 
			
		||||
    Login,
 | 
			
		||||
    Sessions(Vec<gpodder::Session>),
 | 
			
		||||
    Sessions(Vec<gpodder::Session>, Option<Query>),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize)]
 | 
			
		||||
struct Session {
 | 
			
		||||
    user_agent: Option<String>,
 | 
			
		||||
    last_seen: DateTime<Utc>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl Template for View {
 | 
			
		||||
| 
						 | 
				
			
			@ -11,19 +20,37 @@ 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",
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
 | 
			
		||||
    fn render(self, tera: &tera::Tera) -> tera::Result<String> {
 | 
			
		||||
        let mut ctx = tera::Context::new();
 | 
			
		||||
        let template = self.template();
 | 
			
		||||
 | 
			
		||||
        // match self {
 | 
			
		||||
        //     Self::Sessions(sessions) => {
 | 
			
		||||
        //         ctx.insert("sessions", sessions);
 | 
			
		||||
        //     }
 | 
			
		||||
        // };
 | 
			
		||||
        match self {
 | 
			
		||||
            Self::Sessions(sessions, query) => {
 | 
			
		||||
                ctx.insert(
 | 
			
		||||
                    "sessions",
 | 
			
		||||
                    &sessions.into_iter().map(Session::from).collect::<Vec<_>>(),
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
        tera.render(self.template(), &ctx)
 | 
			
		||||
                if let Some(query) = query {
 | 
			
		||||
                    ctx.insert("next_page_query", &query.encode());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            _ => {}
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        tera.render(template, &ctx)
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<gpodder::Session> for Session {
 | 
			
		||||
    fn from(value: gpodder::Session) -> Self {
 | 
			
		||||
        Self {
 | 
			
		||||
            user_agent: value.user_agent,
 | 
			
		||||
            last_seen: value.last_seen,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue