feature: Implement random theme and theme enum #8

Merged
husky merged 2 commits from features/evie/T155 into develop 2025-03-24 11:48:03 -07:00
17 changed files with 214 additions and 99 deletions

23
Cargo.lock generated
View file

@ -278,6 +278,8 @@ dependencies = [
"rmp-serde",
"serde",
"serde_json",
"strum 0.27.1",
"strum_macros",
"time",
"tokio",
"tower-http",
@ -4146,7 +4148,7 @@ dependencies = [
"serde",
"serde_json",
"sqlx",
"strum",
"strum 0.26.3",
"thiserror 1.0.69",
"time",
"tracing",
@ -4907,6 +4909,25 @@ version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
[[package]]
name = "strum"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f64def088c51c9510a8579e3c5d67c65349dcf755e5479ad3d010aa6454e2c32"
[[package]]
name = "strum_macros"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c77a8c5abcaf0f9ce05d62342b7d298c346515365c36b673df4ebe3ced01fde8"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.100",
]
[[package]]
name = "subtle"
version = "2.6.1"

View file

@ -36,5 +36,7 @@ once_cell = "1.19.0"
chrono = "0.4.33"
rand = "0.8.5"
url_encoded_data = "0.6.1"
strum = "0.27.1"
strum_macros = "0.27.1"
env_logger = "*"

View file

@ -657,4 +657,4 @@ pub async fn admin_user_list(
} else {
Redirect::to("/").into_response()
}
}
}

View file

@ -27,6 +27,7 @@ use tracing::{debug, error};
use tracing::log::warn;
use crate::{Opts, ALPHA, BUILT_ON, GIT_COMMIT, VERSION, YEAR};
use crate::routes::index::FrontpageAnnouncement;
use crate::routes::Themes;
#[derive(Serialize, Debug)]
struct FullAnnouncement {
@ -96,7 +97,7 @@ pub struct AnnouncementTemplate {
built_on: String,
year: String,
alpha: bool,
theme: String,
theme: Themes,
announcement: FullAnnouncement,
}
@ -109,10 +110,10 @@ pub async fn announcement_full(Extension(nats): Extension<Arc<jetstream::Context
built_on: BUILT_ON.to_string(),
year: YEAR.to_string(),
alpha: ALPHA,
theme: "default".to_string(),
theme: Themes::Default,
announcement,
}.into_response()
} else {
StatusCode::NOT_FOUND.into_response()
}
}
}

View file

@ -30,7 +30,7 @@ use tokio::sync::Mutex;
use tracing::error;
use tracing::log::warn;
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
use crate::routes::{authenticate_user, UserInfo};
use crate::routes::{authenticate_user, Themes, UserInfo};
#[derive(Serialize, Debug)]
pub struct FrontpageAnnouncement {
@ -102,7 +102,7 @@ pub fn frontpage_error(error: &str, auth_url: String) -> FrontpageTemplate {
year: YEAR.to_string(),
alpha: ALPHA,
count: WEBSITE_COUNT.load(Ordering::Relaxed),
theme: "default".to_string(),
theme: Themes::Default,
announcement: None,
}
}
@ -185,7 +185,7 @@ pub struct FrontpageTemplate {
year: String,
alpha: bool,
count: u64,
theme: String,
theme: Themes,
announcement: Option<FrontpageAnnouncement>,
}
@ -202,7 +202,7 @@ pub async fn frontpage(
year: YEAR.to_string(),
alpha: ALPHA,
count: WEBSITE_COUNT.load(Ordering::Relaxed),
theme: "default".to_string(),
theme: Themes::Default,
announcement,
}
}
@ -217,7 +217,7 @@ struct IndexTemplate {
year: String,
alpha: bool,
count: u64,
theme: String,
theme: Themes,
announcement: Option<FrontpageAnnouncement>,
}
@ -234,7 +234,7 @@ pub async fn index(
return (jar.remove("token"), frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
}
};
let theme = info.theme.clone();
let theme = info.get_theme();
let announcement = latest_announcement(nats.clone()).await;
@ -260,7 +260,7 @@ pub async fn index(
year: YEAR.to_string(),
alpha: ALPHA,
count: WEBSITE_COUNT.load(Ordering::Relaxed),
theme: "default".to_string(),
theme: Themes::Default,
announcement,
}.into_response()
}

View file

@ -10,7 +10,8 @@
*
* You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fmt::Display;
use std::str::FromStr;
use std::sync::Arc;
use askama::Template;
use askama_axum::IntoResponse;
@ -21,7 +22,13 @@ use asklyphe_common::nats::comms::ServiceResponse;
use async_nats::jetstream;
use axum::http::StatusCode;
use serde::Serialize;
use tracing::error;
use strum::IntoEnumIterator;
use strum_macros::EnumIter;
use time::macros::utc_datetime;
use time::{OffsetDateTime, UtcDateTime};
use tracing::{debug, error};
const RANDOM_THEME_EPOCH: UtcDateTime = utc_datetime!(2025-03-19 00:00);
pub mod search;
pub mod index;
@ -30,7 +37,102 @@ pub mod user_settings;
pub mod admin;
pub mod announcement;
#[derive(Serialize)]
#[derive(Default, EnumIter, PartialEq, Eq, Copy, Clone)]
pub enum Themes {
Classic,
Dark,
#[default]
Default,
Freaky,
Gloss,
Oled,
Water,
Random
}
impl Themes {
pub fn get_all_themes() -> Vec<Themes> {
Self::iter().collect()
}
pub fn display_name(&self) -> String {
match self {
Themes::Classic => {
"classic".to_string()
}
Themes::Dark => {
"dark theme".to_string()
}
Themes::Default => {
"default theme".to_string()
}
Themes::Freaky => {
"freaky".to_string()
}
Themes::Gloss => {
"gloss".to_string()
}
Themes::Oled => {
"lights out".to_string()
}
Themes::Water => {
"water".to_string()
}
Themes::Random => {
"random".to_string()
}
}
}
pub fn internal_name(&self) -> String {
match self {
Themes::Classic => {
"classic".to_string()
}
Themes::Dark => {
"dark".to_string()
}
Themes::Default => {
"default".to_string()
}
Themes::Freaky => {
"freaky".to_string()
}
Themes::Gloss => {
"gloss".to_string()
}
Themes::Oled => {
"oled".to_string()
}
Themes::Water => {
"water".to_string()
}
Themes::Random => {
"random".to_string()
}
}
}
}
impl FromStr for Themes {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"classic" => Ok(Themes::Classic),
"dark" => Ok(Themes::Dark),
"default" => Ok(Themes::Default),
"freaky" => Ok(Themes::Freaky),
"gloss" => Ok(Themes::Gloss),
"oled" => Ok(Themes::Oled),
"water" => Ok(Themes::Water),
"random" => Ok(Themes::Random),
_ => Err(())
}
}
}
#[derive(Serialize, Clone)]
pub struct UserInfo {
pub username: String,
pub email: String,
@ -39,6 +141,27 @@ pub struct UserInfo {
pub administrator: bool,
}
impl UserInfo {
pub fn get_theme(&self) -> Themes {
let theme: Themes = self.theme.parse().unwrap_or_default();
if theme.eq(&Themes::Random) {
let possible_themes = Themes::get_all_themes();
let current_day = UtcDateTime::now();
let rand_value = (((current_day - RANDOM_THEME_EPOCH).as_seconds_f64() / 86400.0) % possible_themes.len() as f64) as usize;
*possible_themes.get(rand_value).unwrap_or(&Themes::Default)
} else {
theme
}
}
pub fn get_true_theme(&self) -> Themes {
self.theme.parse().unwrap()
}
}
pub async fn authenticate_user(nats: Arc<jetstream::Context>, token: String) -> Result<UserInfo, String> {
let response = comms::query_service(
comms::Query::AuthService(AuthServiceQuery {
@ -114,4 +237,4 @@ pub struct NotFoundTemplate;
pub async fn not_found() -> impl IntoResponse {
(StatusCode::NOT_FOUND, NotFoundTemplate).into_response()
}
}

View file

@ -12,7 +12,7 @@
*/
use crate::routes::index::frontpage_error;
use crate::routes::{authenticate_user, UserInfo};
use crate::routes::{authenticate_user, Themes, UserInfo};
use crate::searchbot::{gather_image_results, gather_search_results};
use crate::unit_converter;
use crate::unit_converter::UnitConversion;
@ -111,7 +111,7 @@ struct SearchTemplateJavascript {
built_on: String,
year: String,
alpha: bool,
theme: String,
theme: Themes,
}
pub async fn search_js(
@ -121,7 +121,7 @@ pub async fn search_js(
Extension(opts): Extension<Opts>,
) -> impl IntoResponse {
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplateJavascript {
let theme = info.theme.clone();
let theme = info.get_theme();
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
SearchTemplateJavascript {
info,
@ -175,7 +175,7 @@ pub async fn search_js(
query = query.replace("-complications", "");
}
let theme = info.theme.clone();
let theme = info.get_theme();
let querystr = url_encoded_data::stringify(&[("q", og_query.as_str())]);
SearchTemplateJavascript {
info,
@ -217,7 +217,7 @@ pub struct SearchTemplate {
pub built_on: String,
pub year: String,
pub alpha: bool,
pub theme: String,
pub theme: Themes,
}
pub async fn search_nojs(
@ -227,7 +227,7 @@ pub async fn search_nojs(
Extension(opts): Extension<Opts>,
) -> impl IntoResponse {
fn error_response(query: String, info: UserInfo, error: &str) -> SearchTemplate {
let theme = info.theme.clone();
let theme = info.get_theme();
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
SearchTemplate {
info,
@ -416,7 +416,7 @@ pub struct ImageSearchTemplate {
pub built_on: String,
pub year: String,
pub alpha: bool,
pub theme: String,
pub theme: Themes,
}
pub async fn image_search(
jar: CookieJar,
@ -425,7 +425,7 @@ pub async fn image_search(
Extension(opts): Extension<Opts>,
) -> impl IntoResponse {
fn error_response(query: String, info: UserInfo, error: &str) -> ImageSearchTemplate {
let theme = info.theme.clone();
let theme = info.get_theme();
let querystr = url_encoded_data::stringify(&[("q", query.as_str())]);
ImageSearchTemplate {
info,

View file

@ -27,49 +27,12 @@ use serde::Deserialize;
use tokio::sync::Mutex;
use tracing::error;
use crate::{BUILT_ON, GIT_COMMIT, Opts, ALPHA, VERSION, WEBSITE_COUNT, YEAR};
use crate::routes::{authenticate_user, UserInfo};
pub struct Theme<'a> {
pub value: &'a str,
pub name: &'a str,
}
pub static THEMES: &[Theme] = &[
Theme {
value: "default",
name: "default theme",
},
Theme {
value: "dark",
name: "dark theme",
},
Theme {
value: "oled",
name: "lights out",
},
Theme {
value: "classic",
name: "classic",
},
Theme {
value: "freaky",
name: "freaky",
},
Theme {
value: "water",
name: "water",
},
Theme {
value: "gloss",
name: "gloss",
},
];
use crate::routes::{authenticate_user, Themes, UserInfo};
#[derive(Template)]
#[template(path = "user_settings.html")]
pub struct SettingsTemplate {
themes: &'static [Theme<'static>],
themes: Vec<Themes>,
error: Option<String>,
info: UserInfo,
@ -80,7 +43,8 @@ pub struct SettingsTemplate {
year: String,
alpha: bool,
count: u64,
theme: String,
theme: Themes,
true_theme: Themes,
}
pub async fn user_settings(
@ -96,11 +60,11 @@ pub async fn user_settings(
return (jar.remove("token"), crate::routes::index::frontpage_error(e.as_str(), opts.auth_url.clone())).into_response();
}
};
let theme = info.theme.clone();
let theme = info.get_theme();
SettingsTemplate {
themes: THEMES,
themes: Themes::get_all_themes(),
error: None,
info,
info: info.clone(),
search_query: "".to_string(),
version: VERSION.to_string(),
git_commit: GIT_COMMIT.to_string(),
@ -109,6 +73,7 @@ pub async fn user_settings(
alpha: ALPHA,
count: WEBSITE_COUNT.load(Ordering::Relaxed),
theme,
true_theme: info.get_true_theme(),
}.into_response()
} else {
Redirect::temporary("/").into_response()
@ -126,11 +91,11 @@ pub async fn theme_change_post(
Extension(opts): Extension<Opts>,
Form(input): Form<ThemeChangeForm>,
) -> impl IntoResponse {
fn settings_error(info: UserInfo, theme: String, error: String) -> impl IntoResponse {
fn settings_error(info: UserInfo, theme: Themes, error: String) -> impl IntoResponse {
SettingsTemplate {
themes: THEMES,
themes: Themes::get_all_themes(),
error: Some(error),
info,
info: info.clone(),
search_query: "".to_string(),
version: VERSION.to_string(),
git_commit: GIT_COMMIT.to_string(),
@ -139,6 +104,7 @@ pub async fn theme_change_post(
alpha: ALPHA,
count: WEBSITE_COUNT.load(Ordering::Relaxed),
theme,
true_theme: info.get_true_theme(),
}.into_response()
}
@ -151,10 +117,12 @@ pub async fn theme_change_post(
}
};
let theme = info.theme.clone();
if !THEMES.iter().map(|v| v.value.to_string()).collect::<Vec<String>>().contains(&input.theme.clone().unwrap_or("default".to_string())) {
return settings_error(info, theme, "invalid input, please try again!".to_string()).into_response();
let theme = info.get_theme();
if let Some(theme_input) = &input.theme {
if !Themes::get_all_themes().iter().map(|x| x.internal_name().to_string()).collect::<Vec<String>>().contains(&theme_input) {
return settings_error(info, theme.clone(), "invalid input, please try again!".to_string()).into_response();
}
}
let response = comms::query_service(comms::Query::AuthService(AuthServiceQuery {
@ -200,4 +168,4 @@ pub async fn theme_change_post(
} else {
Redirect::to("/").into_response()
}
}
}

View file

@ -341,7 +341,7 @@ pub async fn gather_search_results(nats: Arc<jetstream::Context>, query: &str, u
search_results.remove(rm - i);
}
let theme = user_info.theme.clone();
let theme = user_info.get_theme();
let querystr = url_encoded_data::stringify(&[("q", query)]);
SearchTemplate {
info: user_info,
@ -489,7 +489,7 @@ pub async fn gather_image_results(nats: Arc<jetstream::Context>, query: &str, us
result.src = format!("/imgproxy?{}", url);
}
let theme = user_info.theme.clone();
let theme = user_info.get_theme();
ImageSearchTemplate {
info: user_info,
error: None,

View file

@ -3,8 +3,8 @@
{% block title %}the best search engine{% endblock %}
{% block head %}
<link rel="stylesheet" href="/static/themes/{{theme}}/frontpage.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/inline-announcement.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/frontpage.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/inline-announcement.css"/>
{% endblock %}
{% block page %}

View file

@ -4,8 +4,8 @@
{% block head %}
<link rel="stylesheet" href="/static/themes/default/home.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/home.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/inline-announcement.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/home.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/inline-announcement.css"/>
{% endblock %}
{% block page %}
@ -71,4 +71,4 @@
</div>
{% include "ui/footer.html" %}
</div>
{% endblock %}
{% endblock %}

View file

@ -4,7 +4,7 @@
{% block head %}
<link rel="stylesheet" href="/static/themes/default/imagesearch.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/imagesearch.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/imagesearch.css"/>
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
{% endblock %}
@ -61,4 +61,4 @@
{% include "ui/footer.html" %}
</div>
{% endblock %}
{% endblock %}

View file

@ -4,7 +4,7 @@
{% block head %}
<link rel="stylesheet" href="/static/themes/default/search.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/search.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/search.css"/>
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
{% endblock %}
@ -97,4 +97,4 @@
{% include "ui/footer.html" %}
</div>
{% endblock %}
{% endblock %}

View file

@ -4,7 +4,7 @@
{% block head %}
<link rel="stylesheet" href="/static/themes/default/search.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/search.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/search.css"/>
{% if search_query == "notnite" %}<link rel="stylesheet" href="/static/creature.css"/>{% endif %}
<script src="/static/js/search.js" defer></script>
{% endblock %}
@ -76,4 +76,4 @@
{% include "ui/footer.html" %}
</div>
{% endblock %}
{% endblock %}

View file

@ -12,11 +12,11 @@
href="/static/osd.xml" />
<link rel="stylesheet" href="/static/themes/default/shell.css" />
<link rel="stylesheet" href="/static/themes/{{theme}}/shell.css" />
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/shell.css" />
{% block head %}{% endblock %}
</head>
<body>
{% block page %}{% endblock %}
</body>
</html>
</html>

View file

@ -4,7 +4,7 @@
{% block head %}
<link rel="stylesheet" href="/static/themes/default/settings.css"/>
<link rel="stylesheet" href="/static/themes/{{theme}}/settings.css"/>
<link rel="stylesheet" href="/static/themes/{{theme.internal_name()}}/settings.css"/>
{% endblock %}
{% block page %}
@ -43,16 +43,15 @@
<div class="settings-row">
<div id="theme" class="settings-section">
<h2>theme</h2>
{% for t in themes %}
{%if theme==t.value%}
<p>your current theme is: "{{t.name}}"</p>
{%endif%}
{% endfor %}
<p>your current theme is: "{{true_theme.display_name()}}"</p>
{% if true_theme.internal_name() != theme.internal_name() %}
<p>today's random theme is {{ theme.display_name() }}</p>
{% endif %}
<form action="/user_settings/set_theme" method="post">
<label for="theme-selector">theme</label>
<select name="theme" id="theme-selector">
{% for t in themes %}
<option value="{{t.value}}" {%if theme==t.value%}selected{%endif%}>{{t.name}}</option>
<option value="{{t.internal_name()}}" {%if true_theme.internal_name()==t.internal_name()%}selected{%endif%}>{{t.display_name()}}</option>
{% endfor %}
</select>
<button type="submit" id="theme-submit">change theme!</button>
@ -65,4 +64,4 @@
{% include "ui/footer.html" %}
</div>
{% endblock %}
{% endblock %}

View file

@ -457,6 +457,7 @@ pub async fn user_count(db: &DatabaseConnection) -> Result<usize, FetchUserError
/// returns the number of users in the database who are admins
pub async fn admin_count(db: &DatabaseConnection) -> Result<usize, FetchUserError> {
// dont fucking touch this, i don't know why it works but it does, it's actually evil
// note: doesn't work
Ok(user::Entity::find().filter(user::Column::Flags.into_expr().binary(BinOper::LShift, Expr::value(63 - 2)).lt(1 << (63 - 2)))
.count(db).await.map_err(|e| {
error!("DATABASE ERROR WHILE ADMINCOUNT: {e}");
@ -482,4 +483,4 @@ pub async fn email_list(db: &DatabaseConnection) -> Result<Vec<(String, String)>
})?.into_iter().map(|v| {
(v.username, v.email)
}).collect())
}
}