diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | migrations/0002_custom_theme_setting.sql | 3 | ||||
-rw-r--r-- | src/database/settings.rs | 19 | ||||
-rw-r--r-- | src/frontend/mod.rs | 22 | ||||
-rw-r--r-- | src/frontend/onboarding.rs | 2 | ||||
-rw-r--r-- | src/indieauth/mod.rs | 15 | ||||
-rw-r--r-- | src/login.rs | 28 | ||||
-rw-r--r-- | templates/Cargo.toml | 1 | ||||
-rw-r--r-- | templates/src/lib.rs | 1 | ||||
-rw-r--r-- | templates/src/templates.rs | 6 | ||||
-rw-r--r-- | templates/src/themes.rs | 25 |
11 files changed, 106 insertions, 17 deletions
diff --git a/Cargo.lock b/Cargo.lock index 783ec4f..f4137d3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1924,6 +1924,7 @@ dependencies = [ "markup", "microformats", "rand", + "serde", "serde_json", "time", "walkdir", diff --git a/migrations/0002_custom_theme_setting.sql b/migrations/0002_custom_theme_setting.sql new file mode 100644 index 0000000..a258e82 --- /dev/null +++ b/migrations/0002_custom_theme_setting.sql @@ -0,0 +1,3 @@ +ALTER TABLE kittybox.users +ADD COLUMN custom_css JSONB NOT NULL DEFAULT '""'::jsonb, +ADD COLUMN theme JSONB NOT NULL DEFAULT '"kittybox"'::jsonb; diff --git a/src/database/settings.rs b/src/database/settings.rs index 792a155..77e5821 100644 --- a/src/database/settings.rs +++ b/src/database/settings.rs @@ -1,3 +1,5 @@ +pub use kittybox_frontend_renderer::themes::ThemeName; + mod private { pub trait Sealed {} } @@ -61,3 +63,20 @@ impl Setting for Webring { Self(data) } } + +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] +/// Theme setting for Kittybox, specifying the stylesheet used for laying out the website. +pub struct Theme(ThemeName); +impl private::Sealed for Theme {} +impl Setting for Theme { + type Data = ThemeName; + const ID: &'static str = "theme"; + + fn into_inner(self) -> Self::Data { + self.0 + } + + fn new(data: Self::Data) -> Self { + Self(data) + } +} diff --git a/src/frontend/mod.rs b/src/frontend/mod.rs index 94b8aa7..a05c91d 100644 --- a/src/frontend/mod.rs +++ b/src/frontend/mod.rs @@ -1,4 +1,4 @@ -use crate::database::{Storage, StorageError}; +use crate::database::{settings::Setting, Storage, StorageError}; use axum::{ extract::{Query, State}, http::{StatusCode, Uri}, @@ -271,11 +271,13 @@ pub async fn homepage<D: Storage>( // other requests anyway if it's serious...) // // btw is it more efficient to fetch these in parallel? - let (blogname, webring, channels) = tokio::join!( + let (blogname, webring, theme, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&hcard_url) .map(Result::unwrap_or_default), db.get_setting::<crate::database::settings::Webring>(&hcard_url) .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&hcard_url) + .map(Result::unwrap_or_default), db.get_channels(&hcard_url).map(|i| i.unwrap_or_default()) ); @@ -293,6 +295,7 @@ pub async fn homepage<D: Storage>( title: blogname.as_ref(), blog_name: blogname.as_ref(), feeds: channels, + theme: theme.into_inner(), user: session.as_deref(), content: MainPage { feed: &hfeed, @@ -319,9 +322,11 @@ pub async fn homepage<D: Storage>( } else { error!("Error while fetching h-card and/or h-feed: {}", err); // Return the error - let (blogname, channels) = tokio::join!( + let (blogname, theme, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&hcard_url) .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&hcard_url) + .map(Result::unwrap_or_default), db.get_channels(&hcard_url).map(|i| i.unwrap_or_default()) ); @@ -332,6 +337,7 @@ pub async fn homepage<D: Storage>( title: blogname.as_ref(), blog_name: blogname.as_ref(), feeds: channels, + theme: theme.into_inner(), user: session.as_deref(), content: ErrorPage { code: err.code(), @@ -361,9 +367,11 @@ pub async fn catchall<D: Storage>( match get_post_from_database(&db, path.as_str(), query.after, user).await { Ok((post, cursor)) => { - let (blogname, channels) = tokio::join!( + let (blogname, theme, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&host) .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&host) + .map(Result::unwrap_or_default), db.get_channels(&host).map(|i| i.unwrap_or_default()) ); let mut headers = axum::http::HeaderMap::new(); @@ -411,6 +419,7 @@ pub async fn catchall<D: Storage>( title: blogname.as_ref(), blog_name: blogname.as_ref(), feeds: channels, + theme: theme.into_inner(), user: session.as_deref(), content: match post.pointer("/type/0").and_then(|i| i.as_str()) { Some("h-entry") => Entry { @@ -434,9 +443,11 @@ pub async fn catchall<D: Storage>( .into_response() } Err(err) => { - let (blogname, channels) = tokio::join!( + let (blogname, theme, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&host) .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&host) + .map(Result::unwrap_or_default), db.get_channels(&host).map(|i| i.unwrap_or_default()) ); ( @@ -449,6 +460,7 @@ pub async fn catchall<D: Storage>( title: blogname.as_ref(), blog_name: blogname.as_ref(), feeds: channels, + theme: theme.into_inner(), user: session.as_deref(), content: ErrorPage { code: err.code(), diff --git a/src/frontend/onboarding.rs b/src/frontend/onboarding.rs index 3b53911..3caa458 100644 --- a/src/frontend/onboarding.rs +++ b/src/frontend/onboarding.rs @@ -21,6 +21,7 @@ pub async fn get() -> Html<String> { title: "Kittybox - Onboarding", blog_name: "Kittybox", feeds: vec![], + theme: Default::default(), user: None, content: OnboardingPage {}.to_string(), } @@ -150,6 +151,7 @@ pub async fn post<D: Storage + 'static>( title: "Kittybox - Onboarding", blog_name: "Kittybox", feeds: vec![], + theme: Default::default(), user: None, content: ErrorPage { code: err.code(), diff --git a/src/indieauth/mod.rs b/src/indieauth/mod.rs index 5cdbf05..af49a7f 100644 --- a/src/indieauth/mod.rs +++ b/src/indieauth/mod.rs @@ -1,4 +1,4 @@ -use crate::database::Storage; +use crate::database::{settings::Setting, Storage}; use axum::{ extract::{Form, FromRef, Json, Query, State}, http::StatusCode, @@ -13,6 +13,7 @@ use axum_extra::{ headers::{authorization::Bearer, Authorization, ContentType, HeaderMapExt}, TypedHeader, }; +use futures::FutureExt; use kittybox_indieauth::{ AuthorizationRequest, AuthorizationResponse, ClientMetadata, Error, ErrorKind, GrantRequest, GrantResponse, GrantType, IntrospectionEndpointAuthMethod, Metadata, PKCEMethod, Profile, @@ -319,12 +320,20 @@ async fn authorization_endpoint_get<A: AuthBackend, D: Storage + 'static>( { request.scope.replace(Scopes::new(vec![Scope::Profile])); } + let (blog_name, theme, channels) = tokio::join!( + db.get_setting::<crate::database::settings::SiteName>(&me) + .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&me) + .map(Result::unwrap_or_default), + db.get_channels(&me).map(|i| i.unwrap_or_default()) + ); Html( kittybox_frontend_renderer::Template { title: "Confirm sign-in via IndieAuth", - blog_name: "Kittybox", - feeds: vec![], + blog_name: &blog_name.as_ref(), + feeds: channels, + theme: theme.into_inner(), user: None, content: kittybox_frontend_renderer::AuthorizationRequestPage { request, diff --git a/src/login.rs b/src/login.rs index 3038d9c..1b6a6f3 100644 --- a/src/login.rs +++ b/src/login.rs @@ -23,7 +23,7 @@ use kittybox_frontend_renderer::{LoginPage, LogoutPage, Template}; use kittybox_indieauth::{AuthorizationResponse, Error, GrantType, PKCEVerifier, Scope, Scopes}; use sha2::Digest; -use crate::database::Storage; +use crate::database::{settings::Setting, Storage}; /// Show a login page. async fn get<S: Storage + Send + Sync + 'static>( @@ -32,9 +32,11 @@ async fn get<S: Storage + Send + Sync + 'static>( ) -> impl axum::response::IntoResponse { let hcard_url: url::Url = format!("https://{}/", host).parse().unwrap(); - let (blogname, channels) = tokio::join!( + let (blogname, theme, channels) = tokio::join!( db.get_setting::<crate::database::settings::SiteName>(&hcard_url) .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&hcard_url) + .map(Result::unwrap_or_default), db.get_channels(&hcard_url).map(|i| i.unwrap_or_default()) ); ( @@ -47,6 +49,7 @@ async fn get<S: Storage + Send + Sync + 'static>( title: "Sign in with your website", blog_name: blogname.as_ref(), feeds: channels, + theme: theme.into_inner(), user: None, content: LoginPage {}.to_string(), } @@ -380,14 +383,27 @@ async fn callback( /// of crawlers working with a user's cookies (wget?). If a crawler is /// stupid enough to execute JS and send a POST request though, that's /// on the crawler. -async fn logout_page() -> impl axum::response::IntoResponse { +async fn logout_page<D: Storage + 'static>( + State(db): State<D>, + Host(host): Host, +) -> impl axum::response::IntoResponse { + let me: url::Url = format!("https://{host}/").parse().unwrap(); + let (blog_name, theme, channels) = tokio::join!( + db.get_setting::<crate::database::settings::SiteName>(&me) + .map(Result::unwrap_or_default), + db.get_setting::<crate::database::settings::Theme>(&me) + .map(Result::unwrap_or_default), + db.get_channels(&me).map(|i| i.unwrap_or_default()) + ); + ( StatusCode::OK, [("Content-Type", "text/html")], Template { title: "Signing out...", - blog_name: "Kittybox", - feeds: vec![], + blog_name: blog_name.as_ref(), + theme: theme.into_inner(), + feeds: channels, user: None, content: LogoutPage {}.to_string(), } @@ -501,6 +517,6 @@ where axum::routing::Router::new() .route("/start", axum::routing::get(get::<S>).post(post)) .route("/finish", axum::routing::get(callback)) - .route("/logout", axum::routing::get(logout_page).post(logout)) + .route("/logout", axum::routing::get(logout_page::<S>).post(logout)) .route("/client_metadata", axum::routing::get(client_metadata::<S>)) } diff --git a/templates/Cargo.toml b/templates/Cargo.toml index ca56dfe..de81af5 100644 --- a/templates/Cargo.toml +++ b/templates/Cargo.toml @@ -23,6 +23,7 @@ http = { workspace = true } include_dir = { workspace = true } markup = { workspace = true } serde_json = { workspace = true } +serde = { workspace = true } [dependencies.kittybox-util] version = "0.3.0" diff --git a/templates/src/lib.rs b/templates/src/lib.rs index 0f9f7c6..194a837 100644 --- a/templates/src/lib.rs +++ b/templates/src/lib.rs @@ -10,6 +10,7 @@ mod mf2; pub use mf2::{Entry, Feed, Food, VCard, POSTS_PER_PAGE}; pub mod admin; pub mod assets; +pub mod themes; #[cfg(test)] mod tests { diff --git a/templates/src/templates.rs b/templates/src/templates.rs index 5772b4d..785a4b8 100644 --- a/templates/src/templates.rs +++ b/templates/src/templates.rs @@ -1,16 +1,16 @@ #![allow(clippy::needless_lifetimes)] -use crate::{Feed, VCard}; +use crate::{themes::ThemeName, Feed, VCard}; use http::StatusCode; use kittybox_util::micropub::Channel; markup::define! { - Template<'a>(title: &'a str, blog_name: &'a str, feeds: Vec<Channel>, user: Option<&'a kittybox_indieauth::ProfileUrl>, content: String) { + Template<'a>(title: &'a str, blog_name: &'a str, feeds: Vec<Channel>, theme: ThemeName, user: Option<&'a kittybox_indieauth::ProfileUrl>, content: String) { @markup::doctype() html { head { title { @title } link[rel="preconnect", href="https://fonts.gstatic.com"]; - link[rel="stylesheet", href="/.kittybox/static/style.css"]; + link[rel="stylesheet", href=theme.into_css_link()]; meta[name="viewport", content="initial-scale=1, width=device-width"]; link[rel="micropub", href="/.kittybox/micropub"]; diff --git a/templates/src/themes.rs b/templates/src/themes.rs new file mode 100644 index 0000000..ebc0ed9 --- /dev/null +++ b/templates/src/themes.rs @@ -0,0 +1,25 @@ +#[derive(Debug, Default, serde::Serialize, serde::Deserialize, Clone, Copy, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +/// Choices of themes possible in Kittybox. +pub enum ThemeName { + #[default] + /// Default theme shipped with Kittybox. + Kittybox, + /// "Serious business" theme, typeset like a business memo. + VivianWork, + /// Emulation of the old style websites used back in the day. + Retro, + /// Custom CSS specified by user. + Custom, +} + +impl ThemeName { + pub(crate) fn into_css_link(self) -> &'static str { + match self { + ThemeName::Kittybox => "/.kittybox/static/style.css", + ThemeName::VivianWork => "/.kittybox/static/vivian_work.style.css", + ThemeName::Retro => "/.kittybox/static/retro.style.css", + ThemeName::Custom => "/.kittybox/custom_style.css", + } + } +} |