Basic settings support
This commit is contained in:
parent
239258e324
commit
4960527af3
10 changed files with 189 additions and 20 deletions
|
|
@ -4,9 +4,9 @@ Sam's small image board. Currently a WIP.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Config
|
|
||||||
- [ ] Video support
|
- [ ] Video support
|
||||||
- [ ] Cleanup/fixup background tasks
|
- [ ] Cleanup/fixup background tasks
|
||||||
|
- [ ] User management
|
||||||
- [ ] CSS
|
- [ ] CSS
|
||||||
- [ ] Cleanup, CLI, env vars, logging, better errors...
|
- [ ] Cleanup, CLI, env vars, logging, better errors...
|
||||||
|
|
||||||
|
|
|
||||||
1
src/config.rs
Normal file
1
src/config.rs
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME";
|
||||||
21
src/lib.rs
21
src/lib.rs
|
|
@ -1,4 +1,5 @@
|
||||||
pub(crate) mod auth;
|
pub(crate) mod auth;
|
||||||
|
pub(crate) mod config;
|
||||||
pub(crate) mod entities;
|
pub(crate) mod entities;
|
||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub(crate) mod query;
|
pub(crate) mod query;
|
||||||
|
|
@ -13,20 +14,22 @@ use axum::{
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
use axum_login::AuthManagerLayerBuilder;
|
use axum_login::AuthManagerLayerBuilder;
|
||||||
|
use entities::{prelude::SameyConfig, samey_config};
|
||||||
use password_auth::generate_hash;
|
use password_auth::generate_hash;
|
||||||
use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait};
|
use sea_orm::{ActiveValue::Set, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
use tokio::fs;
|
use tokio::{fs, sync::RwLock};
|
||||||
use tower_http::services::ServeDir;
|
use tower_http::services::ServeDir;
|
||||||
use tower_sessions::SessionManagerLayer;
|
use tower_sessions::SessionManagerLayer;
|
||||||
|
|
||||||
use crate::auth::{Backend, SessionStorage};
|
use crate::auth::{Backend, SessionStorage};
|
||||||
|
use crate::config::APPLICATION_NAME_KEY;
|
||||||
use crate::entities::{prelude::SameyUser, samey_user};
|
use crate::entities::{prelude::SameyUser, samey_user};
|
||||||
pub use crate::error::SameyError;
|
pub use crate::error::SameyError;
|
||||||
use crate::views::{
|
use crate::views::{
|
||||||
add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post,
|
add_post_source, add_post_to_pool, change_pool_visibility, create_pool, delete_post,
|
||||||
edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout,
|
edit_post_details, get_full_media, get_media, get_pools, get_pools_page, index, login, logout,
|
||||||
post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag,
|
post_details, posts, posts_page, remove_field, remove_pool_post, search_tags, select_tag,
|
||||||
sort_pool, submit_post_details, upload, view_pool, view_post,
|
settings, sort_pool, submit_post_details, update_settings, upload, view_pool, view_post,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
pub(crate) const NEGATIVE_PREFIX: &str = "-";
|
||||||
|
|
@ -36,6 +39,7 @@ pub(crate) const RATING_PREFIX: &str = "rating:";
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
files_dir: Arc<String>,
|
files_dir: Arc<String>,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
|
application_name: Arc<RwLock<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
|
|
@ -56,9 +60,18 @@ pub async fn create_user(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Router, SameyError> {
|
pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Router, SameyError> {
|
||||||
|
let application_name = match SameyConfig::find()
|
||||||
|
.filter(samey_config::Column::Key.eq(APPLICATION_NAME_KEY))
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(row) => row.data.as_str().unwrap_or("Samey").to_owned(),
|
||||||
|
None => "Samey".to_owned(),
|
||||||
|
};
|
||||||
let state = AppState {
|
let state = AppState {
|
||||||
files_dir: Arc::new(files_dir.into()),
|
files_dir: Arc::new(files_dir.into()),
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
|
application_name: Arc::new(RwLock::new(application_name)),
|
||||||
};
|
};
|
||||||
fs::create_dir_all(files_dir).await?;
|
fs::create_dir_all(files_dir).await?;
|
||||||
|
|
||||||
|
|
@ -96,6 +109,8 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
|
||||||
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
||||||
.route("/pool/{pool_id}/sort", put(sort_pool))
|
.route("/pool/{pool_id}/sort", put(sort_pool))
|
||||||
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
||||||
|
// Settings routes
|
||||||
|
.route("/settings", get(settings).put(update_settings))
|
||||||
// Search routes
|
// Search routes
|
||||||
.route("/posts", get(posts))
|
.route("/posts", get(posts))
|
||||||
.route("/posts/{page}", get(posts_page))
|
.route("/posts/{page}", get(posts_page))
|
||||||
|
|
|
||||||
144
src/views.rs
144
src/views.rs
|
|
@ -1,5 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashSet,
|
any::Any,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufReader, Seek, Write},
|
io::{BufReader, Seek, Write},
|
||||||
num::NonZero,
|
num::NonZero,
|
||||||
|
|
@ -26,9 +27,14 @@ use tokio::task::spawn_blocking;
|
||||||
use crate::{
|
use crate::{
|
||||||
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
AppState, NEGATIVE_PREFIX, RATING_PREFIX,
|
||||||
auth::{AuthSession, Credentials, User},
|
auth::{AuthSession, Credentials, User},
|
||||||
|
config::APPLICATION_NAME_KEY,
|
||||||
entities::{
|
entities::{
|
||||||
prelude::{SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag, SameyTagPost},
|
prelude::{
|
||||||
samey_pool, samey_pool_post, samey_post, samey_post_source, samey_tag, samey_tag_post,
|
SameyConfig, SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag,
|
||||||
|
SameyTagPost,
|
||||||
|
},
|
||||||
|
samey_config, samey_pool, samey_pool_post, samey_post, samey_post_source, samey_tag,
|
||||||
|
samey_tag_post,
|
||||||
},
|
},
|
||||||
error::SameyError,
|
error::SameyError,
|
||||||
query::{
|
query::{
|
||||||
|
|
@ -43,12 +49,20 @@ const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
struct IndexTemplate {
|
struct IndexTemplate {
|
||||||
|
application_name: String,
|
||||||
user: Option<User>,
|
user: Option<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn index(auth_session: AuthSession) -> Result<impl IntoResponse, SameyError> {
|
pub(crate) async fn index(
|
||||||
|
State(AppState {
|
||||||
|
application_name, ..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
IndexTemplate {
|
IndexTemplate {
|
||||||
|
application_name,
|
||||||
user: auth_session.user,
|
user: auth_session.user,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
|
@ -85,7 +99,7 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoRes
|
||||||
// Post upload view
|
// Post upload view
|
||||||
|
|
||||||
pub(crate) async fn upload(
|
pub(crate) async fn upload(
|
||||||
State(AppState { db, files_dir }): State<AppState>,
|
State(AppState { db, files_dir, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
mut multipart: Multipart,
|
mut multipart: Multipart,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
|
@ -370,6 +384,7 @@ pub(crate) async fn select_tag(
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "posts.html")]
|
#[template(path = "posts.html")]
|
||||||
struct PostsTemplate<'a> {
|
struct PostsTemplate<'a> {
|
||||||
|
application_name: String,
|
||||||
tags: Option<Vec<&'a str>>,
|
tags: Option<Vec<&'a str>>,
|
||||||
tags_text: Option<String>,
|
tags_text: Option<String>,
|
||||||
posts: Vec<PostOverview>,
|
posts: Vec<PostOverview>,
|
||||||
|
|
@ -391,11 +406,16 @@ pub(crate) async fn posts(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn posts_page(
|
pub(crate) async fn posts_page(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Query(query): Query<PostsQuery>,
|
Query(query): Query<PostsQuery>,
|
||||||
Path(page): Path<u32>,
|
Path(page): Path<u32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
let tags = query
|
let tags = query
|
||||||
.tags
|
.tags
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -417,6 +437,7 @@ pub(crate) async fn posts_page(
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
PostsTemplate {
|
PostsTemplate {
|
||||||
|
application_name,
|
||||||
tags_text: tags.as_ref().map(|tags| tags.iter().join(" ")),
|
tags_text: tags.as_ref().map(|tags| tags.iter().join(" ")),
|
||||||
tags,
|
tags,
|
||||||
posts,
|
posts,
|
||||||
|
|
@ -439,16 +460,22 @@ pub(crate) async fn get_pools(
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pools.html")]
|
#[template(path = "pools.html")]
|
||||||
struct GetPoolsTemplate {
|
struct GetPoolsTemplate {
|
||||||
|
application_name: String,
|
||||||
pools: Vec<samey_pool::Model>,
|
pools: Vec<samey_pool::Model>,
|
||||||
page: u32,
|
page: u32,
|
||||||
page_count: u64,
|
page_count: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_pools_page(
|
pub(crate) async fn get_pools_page(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(page): Path<u32>,
|
Path(page): Path<u32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
let query = match auth_session.user {
|
let query = match auth_session.user {
|
||||||
None => SameyPool::find().filter(samey_pool::Column::IsPublic.into_simple_expr()),
|
None => SameyPool::find().filter(samey_pool::Column::IsPublic.into_simple_expr()),
|
||||||
Some(user) if user.is_admin => SameyPool::find(),
|
Some(user) if user.is_admin => SameyPool::find(),
|
||||||
|
|
@ -466,6 +493,7 @@ pub(crate) async fn get_pools_page(
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
GetPoolsTemplate {
|
GetPoolsTemplate {
|
||||||
|
application_name,
|
||||||
pools,
|
pools,
|
||||||
page,
|
page,
|
||||||
page_count,
|
page_count,
|
||||||
|
|
@ -504,16 +532,22 @@ pub(crate) async fn create_pool(
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pool.html")]
|
#[template(path = "pool.html")]
|
||||||
struct ViewPoolTemplate {
|
struct ViewPoolTemplate {
|
||||||
|
application_name: String,
|
||||||
pool: samey_pool::Model,
|
pool: samey_pool::Model,
|
||||||
posts: Vec<PoolPost>,
|
posts: Vec<PoolPost>,
|
||||||
can_edit: bool,
|
can_edit: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn view_pool(
|
pub(crate) async fn view_pool(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(pool_id): Path<i32>,
|
Path(pool_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
let pool = SameyPool::find_by_id(pool_id)
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -534,6 +568,7 @@ pub(crate) async fn view_pool(
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ViewPoolTemplate {
|
ViewPoolTemplate {
|
||||||
|
application_name,
|
||||||
pool,
|
pool,
|
||||||
can_edit,
|
can_edit,
|
||||||
posts,
|
posts,
|
||||||
|
|
@ -763,11 +798,93 @@ pub(crate) async fn sort_pool(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Settings views
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "settings.html")]
|
||||||
|
struct SettingsTemplate {
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn settings(
|
||||||
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
if auth_session.user.is_none_or(|user| !user.is_admin) {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
|
|
||||||
|
let config = SameyConfig::find().all(&db).await?;
|
||||||
|
|
||||||
|
let values: HashMap<&str, Box<dyn Any>> = config
|
||||||
|
.iter()
|
||||||
|
.filter_map(|row| match row.key.as_str() {
|
||||||
|
key if key == APPLICATION_NAME_KEY => row
|
||||||
|
.data
|
||||||
|
.as_str()
|
||||||
|
.map::<(&str, Box<dyn Any>), _>(|data| (&row.key, Box::new(data.to_owned()))),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(Html(
|
||||||
|
SettingsTemplate { application_name }.render_with_values(&values)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub(crate) struct UpdateSettingsForm {
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn update_settings(
|
||||||
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
Form(body): Form<UpdateSettingsForm>,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
if auth_session.user.is_none_or(|user| !user.is_admin) {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut configs = vec![];
|
||||||
|
|
||||||
|
if !body.application_name.is_empty() {
|
||||||
|
*application_name.write().await = body.application_name.clone();
|
||||||
|
configs.push(samey_config::ActiveModel {
|
||||||
|
key: Set(APPLICATION_NAME_KEY.into()),
|
||||||
|
data: Set(body.application_name.into()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
SameyConfig::insert_many(configs)
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::column(samey_config::Column::Key)
|
||||||
|
.update_column(samey_config::Column::Data)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(&db)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok("")
|
||||||
|
}
|
||||||
|
|
||||||
// Single post views
|
// Single post views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "view_post.html")]
|
#[template(path = "view_post.html")]
|
||||||
struct ViewPostTemplate {
|
struct ViewPostTemplate {
|
||||||
|
application_name: String,
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
tags: Vec<samey_tag::Model>,
|
tags: Vec<samey_tag::Model>,
|
||||||
tags_text: String,
|
tags_text: String,
|
||||||
|
|
@ -778,10 +895,16 @@ struct ViewPostTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn view_post(
|
pub(crate) async fn view_post(
|
||||||
State(AppState { db, .. }): State<AppState>,
|
State(AppState {
|
||||||
|
db,
|
||||||
|
application_name,
|
||||||
|
..
|
||||||
|
}): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<i32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
|
|
||||||
let post_id = post_id;
|
let post_id = post_id;
|
||||||
let tags = get_tags_for_post(post_id).all(&db).await?;
|
let tags = get_tags_for_post(post_id).all(&db).await?;
|
||||||
let tags_text = tags.iter().map(|tag| &tag.name).join(" ");
|
let tags_text = tags.iter().map(|tag| &tag.name).join(" ");
|
||||||
|
|
@ -851,6 +974,7 @@ pub(crate) async fn view_post(
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ViewPostTemplate {
|
ViewPostTemplate {
|
||||||
|
application_name,
|
||||||
post,
|
post,
|
||||||
tags,
|
tags,
|
||||||
tags_text,
|
tags_text,
|
||||||
|
|
@ -1195,7 +1319,7 @@ pub(crate) async fn get_full_media(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn delete_post(
|
pub(crate) async fn delete_post(
|
||||||
State(AppState { db, files_dir }): State<AppState>,
|
State(AppState { db, files_dir, .. }): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Path(post_id): Path<i32>,
|
Path(post_id): Path<i32>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,12 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<title>Samey</title>
|
<title>{{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
<h1>Samey</h1>
|
<h1>{{ application_name }}</h1>
|
||||||
<article>
|
<article>
|
||||||
<h2>Search</h2>
|
<h2>Search</h2>
|
||||||
<form method="get" action="/posts/1">
|
<form method="get" action="/posts/1">
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,8 @@
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<script src=" https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js "></script>
|
<script src=" https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js "></script>
|
||||||
<title>Pool - {{ pool.name }} - Samey</title>
|
<title>Pool - {{ pool.name }} - {{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
<meta property="og:title" content="{{ pool.name }}" />
|
<meta property="og:title" content="{{ pool.name }}" />
|
||||||
<meta property="og:url" content="/pool/{{ pool.id }}" />
|
<meta property="og:url" content="/pool/{{ pool.id }}" />
|
||||||
<meta property="twitter:title" content="{{ pool.name }}" />
|
<meta property="twitter:title" content="{{ pool.name }}" />
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<title>Pools - Samey</title>
|
<title>Pools - {{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<title>Posts - Samey</title>
|
<title>Posts - {{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<article>
|
<article>
|
||||||
|
|
|
||||||
24
templates/settings.html
Normal file
24
templates/settings.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
|
<title>Settings - {{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>Settings</h1>
|
||||||
|
<form hx-put="/settings" hx-swap="none">
|
||||||
|
<label>Application name</label>
|
||||||
|
<input
|
||||||
|
name="application_name"
|
||||||
|
type="text"
|
||||||
|
value="{{ application_name }}"
|
||||||
|
/>
|
||||||
|
<button>Submit</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
<script src="https://unpkg.com/htmx.org@2.0.4"></script>
|
||||||
<title>Post #{{ post.id }} - Samey</title>
|
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
content="{% if let Some(title) = post.title %}{{ title }}{% endif %}"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue