Initial progress on styling
This commit is contained in:
parent
c650c27825
commit
2c44a69ec3
38 changed files with 748 additions and 412 deletions
100
Cargo.lock
generated
100
Cargo.lock
generated
|
|
@ -1288,6 +1288,15 @@ dependencies = [
|
||||||
"version_check",
|
"version_check",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "getopts"
|
||||||
|
version = "0.2.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-width",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "getrandom"
|
name = "getrandom"
|
||||||
version = "0.2.15"
|
version = "0.2.15"
|
||||||
|
|
@ -2455,6 +2464,25 @@ dependencies = [
|
||||||
"syn 1.0.109",
|
"syn 1.0.109",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark"
|
||||||
|
version = "0.13.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1e8bbe1a966bd2f362681a44f6edce3c2310ac21e4d5067a6e7ec396297a6ea0"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 2.9.0",
|
||||||
|
"getopts",
|
||||||
|
"memchr",
|
||||||
|
"pulldown-cmark-escape",
|
||||||
|
"unicase",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pulldown-cmark-escape"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "007d8adb5ddab6f8e3f491ac63566a7d5002cc7ed73901f72057943fa71ae1ae"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "qoi"
|
name = "qoi"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -2752,6 +2780,41 @@ dependencies = [
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e5fbc0ee50fcb99af7cebb442e5df7b5b45e9460ffa3f8f549cd26b862bec49d"
|
||||||
|
dependencies = [
|
||||||
|
"axum",
|
||||||
|
"rust-embed-impl",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-impl"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bf418c9a2e3f6663ca38b8a7134cc2c2167c9d69688860e8961e3faa731702e"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-embed-utils",
|
||||||
|
"syn 2.0.100",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-embed-utils"
|
||||||
|
version = "8.7.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "08d55b95147fe01265d06b3955db798bdaed52e60e2211c41137701b3aba8e21"
|
||||||
|
dependencies = [
|
||||||
|
"sha2",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rust_decimal"
|
name = "rust_decimal"
|
||||||
version = "1.37.1"
|
version = "1.37.1"
|
||||||
|
|
@ -2858,6 +2921,15 @@ version = "1.0.20"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "same-file"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||||
|
dependencies = [
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "samey"
|
name = "samey"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -2873,8 +2945,11 @@ dependencies = [
|
||||||
"image",
|
"image",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"migration",
|
"migration",
|
||||||
|
"mime_guess",
|
||||||
"password-auth",
|
"password-auth",
|
||||||
|
"pulldown-cmark",
|
||||||
"rand 0.9.0",
|
"rand 0.9.0",
|
||||||
|
"rust-embed",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -3993,6 +4068,12 @@ version = "0.1.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-width"
|
||||||
|
version = "0.1.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "untrusted"
|
name = "untrusted"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
|
@ -4078,6 +4159,16 @@ version = "0.9.5"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "walkdir"
|
||||||
|
version = "2.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
|
||||||
|
dependencies = [
|
||||||
|
"same-file",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasi"
|
name = "wasi"
|
||||||
version = "0.11.0+wasi-snapshot-preview1"
|
version = "0.11.0+wasi-snapshot-preview1"
|
||||||
|
|
@ -4205,6 +4296,15 @@ dependencies = [
|
||||||
"wasite",
|
"wasite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winapi-util"
|
||||||
|
version = "0.1.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||||
|
dependencies = [
|
||||||
|
"windows-sys 0.59.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-core"
|
name = "windows-core"
|
||||||
version = "0.61.0"
|
version = "0.61.0"
|
||||||
|
|
|
||||||
|
|
@ -22,8 +22,11 @@ futures-util = "0.3.31"
|
||||||
image = "0.25.6"
|
image = "0.25.6"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
migration = { path = "migration" }
|
migration = { path = "migration" }
|
||||||
|
mime_guess = "2.0.5"
|
||||||
password-auth = "1.0.0"
|
password-auth = "1.0.0"
|
||||||
|
pulldown-cmark = "0.13.0"
|
||||||
rand = "0.9.0"
|
rand = "0.9.0"
|
||||||
|
rust-embed = { version = "8.7.0", features = ["axum", "debug-embed"] }
|
||||||
sea-orm = { version = "1.1.8", features = [
|
sea-orm = { version = "1.1.8", features = [
|
||||||
"sqlx-sqlite",
|
"sqlx-sqlite",
|
||||||
"runtime-tokio-rustls",
|
"runtime-tokio-rustls",
|
||||||
|
|
|
||||||
13
README.md
13
README.md
|
|
@ -8,10 +8,17 @@ Sam's small image board. Currently a WIP.
|
||||||
|
|
||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [ ] Cleanup/fixup background tasks
|
- [ ] Rename pool
|
||||||
- [ ] User management
|
|
||||||
- [ ] CSS
|
- [ ] CSS
|
||||||
- [ ] Cleanup, CLI, env vars, logging, better errors...
|
- [ ] Logging, better errors...
|
||||||
|
|
||||||
|
### Post-0.1.0 roadmap
|
||||||
|
|
||||||
|
- [ ] Bulk edit tags/Fix tag capitalization
|
||||||
|
- [ ] User management
|
||||||
|
- [ ] Text media
|
||||||
|
- [ ] Cleanup/fixup background tasks
|
||||||
|
- [ ] Migrate to Cot...?
|
||||||
|
|
||||||
## Running in development
|
## Running in development
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,4 +3,4 @@ command = ["cargo", "run"]
|
||||||
background = false
|
background = false
|
||||||
need_stdout = true
|
need_stdout = true
|
||||||
on_change_strategy = "kill_then_restart"
|
on_change_strategy = "kill_then_restart"
|
||||||
watch = ["templates"]
|
watch = ["templates", "static"]
|
||||||
|
|
|
||||||
98
src/lib.rs
98
src/lib.rs
|
|
@ -7,13 +7,19 @@ pub(crate) mod rating;
|
||||||
pub(crate) mod video;
|
pub(crate) mod video;
|
||||||
pub(crate) mod views;
|
pub(crate) mod views;
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::{
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use axum::{
|
use axum::{
|
||||||
Router,
|
Router,
|
||||||
extract::DefaultBodyLimit,
|
extract::DefaultBodyLimit,
|
||||||
|
http::{StatusCode, header::CONTENT_TYPE},
|
||||||
|
response::IntoResponse,
|
||||||
routing::{delete, get, post, put},
|
routing::{delete, get, post, put},
|
||||||
};
|
};
|
||||||
|
use axum_extra::routing::RouterExt;
|
||||||
use axum_login::AuthManagerLayerBuilder;
|
use axum_login::AuthManagerLayerBuilder;
|
||||||
use entities::{prelude::SameyConfig, samey_config};
|
use entities::{prelude::SameyConfig, samey_config};
|
||||||
use password_auth::generate_hash;
|
use password_auth::generate_hash;
|
||||||
|
|
@ -26,19 +32,34 @@ use crate::auth::{Backend, SessionStorage};
|
||||||
use crate::config::APPLICATION_NAME_KEY;
|
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,
|
|
||||||
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,
|
|
||||||
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 = "-";
|
||||||
pub(crate) const RATING_PREFIX: &str = "rating:";
|
pub(crate) const RATING_PREFIX: &str = "rating:";
|
||||||
|
|
||||||
|
#[derive(rust_embed::Embed)]
|
||||||
|
#[folder = "static/"]
|
||||||
|
struct Asset;
|
||||||
|
|
||||||
|
fn assets_router() -> Router {
|
||||||
|
Router::new().route(
|
||||||
|
"/{*file}",
|
||||||
|
get(|uri: axum::http::Uri| async move {
|
||||||
|
let path = uri.path().trim_start_matches('/');
|
||||||
|
match Asset::get(path) {
|
||||||
|
Some(content) => {
|
||||||
|
let mime = mime_guess::MimeGuess::from_path(path).first_or_octet_stream();
|
||||||
|
([(CONTENT_TYPE, mime.as_ref())], content.data).into_response()
|
||||||
|
}
|
||||||
|
None => StatusCode::NOT_FOUND.into_response(),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
files_dir: Arc<String>,
|
files_dir: Arc<PathBuf>,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
application_name: Arc<RwLock<String>>,
|
application_name: Arc<RwLock<String>>,
|
||||||
}
|
}
|
||||||
|
|
@ -60,7 +81,10 @@ pub async fn create_user(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Router, SameyError> {
|
pub async fn get_router(
|
||||||
|
db: DatabaseConnection,
|
||||||
|
files_dir: impl AsRef<Path>,
|
||||||
|
) -> Result<Router, SameyError> {
|
||||||
let application_name = match SameyConfig::find()
|
let application_name = match SameyConfig::find()
|
||||||
.filter(samey_config::Column::Key.eq(APPLICATION_NAME_KEY))
|
.filter(samey_config::Column::Key.eq(APPLICATION_NAME_KEY))
|
||||||
.one(&db)
|
.one(&db)
|
||||||
|
|
@ -70,11 +94,11 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
|
||||||
None => "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.as_ref().to_owned()),
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
application_name: Arc::new(RwLock::new(application_name)),
|
application_name: Arc::new(RwLock::new(application_name)),
|
||||||
};
|
};
|
||||||
fs::create_dir_all(files_dir).await?;
|
fs::create_dir_all(files_dir.as_ref()).await?;
|
||||||
|
|
||||||
let session_store = SessionStorage::new(db.clone());
|
let session_store = SessionStorage::new(db.clone());
|
||||||
let session_layer = SessionManagerLayer::new(session_store);
|
let session_layer = SessionManagerLayer::new(session_store);
|
||||||
|
|
@ -82,43 +106,47 @@ pub async fn get_router(db: DatabaseConnection, files_dir: &str) -> Result<Route
|
||||||
|
|
||||||
Ok(Router::new()
|
Ok(Router::new()
|
||||||
// Auth routes
|
// Auth routes
|
||||||
.route("/login", post(login))
|
.route_with_tsr("/login", get(login_page).post(login))
|
||||||
.route("/logout", get(logout))
|
.route_with_tsr("/logout", get(logout))
|
||||||
// Tags routes
|
// Tags routes
|
||||||
.route("/search_tags", post(search_tags))
|
.route_with_tsr("/search_tags", post(search_tags))
|
||||||
.route("/select_tag", post(select_tag))
|
.route_with_tsr("/select_tag", post(select_tag))
|
||||||
// Post routes
|
// Post routes
|
||||||
.route(
|
.route_with_tsr(
|
||||||
"/upload",
|
"/upload",
|
||||||
post(upload).layer(DefaultBodyLimit::max(100_000_000)),
|
get(upload_page)
|
||||||
|
.post(upload)
|
||||||
|
.layer(DefaultBodyLimit::max(100_000_000)),
|
||||||
)
|
)
|
||||||
.route("/post/{post_id}", get(view_post).delete(delete_post))
|
.route_with_tsr("/post/{post_id}", get(view_post_page).delete(delete_post))
|
||||||
.route("/post_details/{post_id}/edit", get(edit_post_details))
|
.route_with_tsr("/post_details/{post_id}/edit", get(edit_post_details))
|
||||||
.route(
|
.route_with_tsr(
|
||||||
"/post_details/{post_id}",
|
"/post_details/{post_id}",
|
||||||
get(post_details).put(submit_post_details),
|
get(post_details).put(submit_post_details),
|
||||||
)
|
)
|
||||||
.route("/post_source", post(add_post_source))
|
.route_with_tsr("/post_source", post(add_post_source))
|
||||||
.route("/media/{post_id}/full", get(get_full_media))
|
.route_with_tsr("/media/{post_id}/full", get(get_full_media))
|
||||||
.route("/media/{post_id}", get(get_media))
|
.route_with_tsr("/media/{post_id}", get(get_media))
|
||||||
// Pool routes
|
// Pool routes
|
||||||
.route("/pools", get(get_pools))
|
.route_with_tsr("/create_pool", get(create_pool_page))
|
||||||
.route("/pools/{page}", get(get_pools_page))
|
.route_with_tsr("/pools", get(get_pools))
|
||||||
.route("/pool", post(create_pool))
|
.route_with_tsr("/pools/{page}", get(get_pools_page))
|
||||||
.route("/pool/{pool_id}", get(view_pool))
|
.route_with_tsr("/pool", post(create_pool))
|
||||||
.route("/pool/{pool_id}/public", put(change_pool_visibility))
|
.route_with_tsr("/pool/{pool_id}", get(view_pool))
|
||||||
.route("/pool/{pool_id}/post", post(add_post_to_pool))
|
.route_with_tsr("/pool/{pool_id}/public", put(change_pool_visibility))
|
||||||
.route("/pool/{pool_id}/sort", put(sort_pool))
|
.route_with_tsr("/pool/{pool_id}/post", post(add_post_to_pool))
|
||||||
.route("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
.route_with_tsr("/pool/{pool_id}/sort", put(sort_pool))
|
||||||
|
.route_with_tsr("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
||||||
// Settings routes
|
// Settings routes
|
||||||
.route("/settings", get(settings).put(update_settings))
|
.route_with_tsr("/settings", get(settings).put(update_settings))
|
||||||
// Search routes
|
// Search routes
|
||||||
.route("/posts", get(posts))
|
.route_with_tsr("/posts", get(posts))
|
||||||
.route("/posts/{page}", get(posts_page))
|
.route_with_tsr("/posts/{page}", get(posts_page))
|
||||||
// Other routes
|
// Other routes
|
||||||
.route("/remove", delete(remove_field))
|
.route_with_tsr("/remove", delete(remove_field))
|
||||||
.route("/", get(index))
|
.route("/", get(index))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
.nest_service("/files", ServeDir::new(files_dir))
|
.nest_service("/files", ServeDir::new(files_dir))
|
||||||
|
.nest("/static", assets_router())
|
||||||
.layer(auth_layer))
|
.layer(auth_layer))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
13
src/main.rs
13
src/main.rs
|
|
@ -13,12 +13,6 @@ struct Config {
|
||||||
enum Commands {
|
enum Commands {
|
||||||
Run,
|
Run,
|
||||||
Migrate,
|
Migrate,
|
||||||
AddUser {
|
|
||||||
#[arg(short, long)]
|
|
||||||
username: String,
|
|
||||||
#[arg(short, long)]
|
|
||||||
password: String,
|
|
||||||
},
|
|
||||||
AddAdminUser {
|
AddAdminUser {
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
username: String,
|
username: String,
|
||||||
|
|
@ -39,15 +33,10 @@ async fn main() {
|
||||||
.await
|
.await
|
||||||
.expect("Unable to apply migrations");
|
.expect("Unable to apply migrations");
|
||||||
}
|
}
|
||||||
Some(Commands::AddUser { username, password }) => {
|
|
||||||
create_user(db, username, password, false)
|
|
||||||
.await
|
|
||||||
.expect("Unable to add user");
|
|
||||||
}
|
|
||||||
Some(Commands::AddAdminUser { username, password }) => {
|
Some(Commands::AddAdminUser { username, password }) => {
|
||||||
create_user(db, username, password, true)
|
create_user(db, username, password, true)
|
||||||
.await
|
.await
|
||||||
.expect("Unable to add admin");
|
.expect("Unable to add admin user");
|
||||||
}
|
}
|
||||||
Some(Commands::Run) | None => {
|
Some(Commands::Run) | None => {
|
||||||
Migrator::up(&db, None)
|
Migrator::up(&db, None)
|
||||||
|
|
|
||||||
21
src/query.rs
21
src/query.rs
|
|
@ -19,7 +19,7 @@ use crate::{
|
||||||
pub(crate) struct PostOverview {
|
pub(crate) struct PostOverview {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
pub(crate) thumbnail: String,
|
pub(crate) thumbnail: String,
|
||||||
pub(crate) tags: String,
|
pub(crate) tags: Option<String>,
|
||||||
pub(crate) media_type: String,
|
pub(crate) media_type: String,
|
||||||
pub(crate) rating: String,
|
pub(crate) rating: String,
|
||||||
}
|
}
|
||||||
|
|
@ -60,9 +60,9 @@ pub(crate) fn search_posts(
|
||||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
.inner_join(SameyTagPost)
|
.left_join(SameyTagPost)
|
||||||
.join(
|
.join(
|
||||||
sea_orm::JoinType::InnerJoin,
|
sea_orm::JoinType::LeftJoin,
|
||||||
samey_tag_post::Relation::SameyTag.def(),
|
samey_tag_post::Relation::SameyTag.def(),
|
||||||
);
|
);
|
||||||
if !include_ratings.is_empty() {
|
if !include_ratings.is_empty() {
|
||||||
|
|
@ -83,9 +83,9 @@ pub(crate) fn search_posts(
|
||||||
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
Expr::cust("GROUP_CONCAT(\"samey_tag\".\"name\", ' ')"),
|
||||||
"tags",
|
"tags",
|
||||||
)
|
)
|
||||||
.inner_join(SameyTagPost)
|
.left_join(SameyTagPost)
|
||||||
.join(
|
.join(
|
||||||
sea_orm::JoinType::InnerJoin,
|
sea_orm::JoinType::LeftJoin,
|
||||||
samey_tag_post::Relation::SameyTag.def(),
|
samey_tag_post::Relation::SameyTag.def(),
|
||||||
);
|
);
|
||||||
if !include_tags.is_empty() {
|
if !include_tags.is_empty() {
|
||||||
|
|
@ -136,7 +136,7 @@ pub(crate) fn search_posts(
|
||||||
query
|
query
|
||||||
};
|
};
|
||||||
|
|
||||||
filter_by_user(query, user)
|
filter_posts_by_user(query, user)
|
||||||
.group_by(samey_post::Column::Id)
|
.group_by(samey_post::Column::Id)
|
||||||
.order_by_desc(samey_post::Column::Id)
|
.order_by_desc(samey_post::Column::Id)
|
||||||
.into_model::<PostOverview>()
|
.into_model::<PostOverview>()
|
||||||
|
|
@ -154,6 +154,7 @@ pub(crate) struct PoolPost {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
pub(crate) thumbnail: String,
|
pub(crate) thumbnail: String,
|
||||||
pub(crate) rating: String,
|
pub(crate) rating: String,
|
||||||
|
pub(crate) media_type: String,
|
||||||
pub(crate) pool_post_id: i32,
|
pub(crate) pool_post_id: i32,
|
||||||
pub(crate) position: f32,
|
pub(crate) position: f32,
|
||||||
pub(crate) tags: String,
|
pub(crate) tags: String,
|
||||||
|
|
@ -163,11 +164,12 @@ pub(crate) fn get_posts_in_pool(
|
||||||
pool_id: i32,
|
pool_id: i32,
|
||||||
user: Option<&User>,
|
user: Option<&User>,
|
||||||
) -> Selector<SelectModel<PoolPost>> {
|
) -> Selector<SelectModel<PoolPost>> {
|
||||||
filter_by_user(
|
filter_posts_by_user(
|
||||||
SameyPost::find()
|
SameyPost::find()
|
||||||
.column(samey_post::Column::Id)
|
.column(samey_post::Column::Id)
|
||||||
.column(samey_post::Column::Thumbnail)
|
.column(samey_post::Column::Thumbnail)
|
||||||
.column(samey_post::Column::Rating)
|
.column(samey_post::Column::Rating)
|
||||||
|
.column(samey_post::Column::MediaType)
|
||||||
.column_as(samey_pool_post::Column::Id, "pool_post_id")
|
.column_as(samey_pool_post::Column::Id, "pool_post_id")
|
||||||
.column(samey_pool_post::Column::Position)
|
.column(samey_pool_post::Column::Position)
|
||||||
.column_as(
|
.column_as(
|
||||||
|
|
@ -188,7 +190,10 @@ pub(crate) fn get_posts_in_pool(
|
||||||
.into_model::<PoolPost>()
|
.into_model::<PoolPost>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn filter_by_user(query: Select<SameyPost>, user: Option<&User>) -> Select<SameyPost> {
|
pub(crate) fn filter_posts_by_user(
|
||||||
|
query: Select<SameyPost>,
|
||||||
|
user: Option<&User>,
|
||||||
|
) -> Select<SameyPost> {
|
||||||
match user {
|
match user {
|
||||||
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
|
None => query.filter(samey_post::Column::IsPublic.into_simple_expr()),
|
||||||
Some(user) if user.is_admin => query,
|
Some(user) if user.is_admin => query,
|
||||||
|
|
|
||||||
209
src/views.rs
209
src/views.rs
|
|
@ -39,17 +39,32 @@ use crate::{
|
||||||
},
|
},
|
||||||
error::SameyError,
|
error::SameyError,
|
||||||
query::{
|
query::{
|
||||||
PoolPost, PostOverview, filter_by_user, get_posts_in_pool, get_tags_for_post, search_posts,
|
PoolPost, PostOverview, filter_posts_by_user, get_posts_in_pool, get_tags_for_post,
|
||||||
|
search_posts,
|
||||||
},
|
},
|
||||||
video::{generate_thumbnail, get_dimensions_for_video},
|
video::{generate_thumbnail, get_dimensions_for_video},
|
||||||
};
|
};
|
||||||
|
|
||||||
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
const MAX_THUMBNAIL_DIMENSION: u32 = 192;
|
||||||
|
|
||||||
|
// Filters
|
||||||
|
|
||||||
|
mod filters {
|
||||||
|
pub(crate) fn markdown(
|
||||||
|
s: impl std::fmt::Display,
|
||||||
|
) -> askama::Result<askama::filters::Safe<String>> {
|
||||||
|
let s = s.to_string();
|
||||||
|
let parser = pulldown_cmark::Parser::new(&s);
|
||||||
|
let mut output = String::new();
|
||||||
|
pulldown_cmark::html::push_html(&mut output, parser);
|
||||||
|
Ok(askama::filters::Safe(output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Index view
|
// Index view
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "pages/index.html")]
|
||||||
struct IndexTemplate {
|
struct IndexTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
user: Option<User>,
|
user: Option<User>,
|
||||||
|
|
@ -73,6 +88,27 @@ pub(crate) async fn index(
|
||||||
|
|
||||||
// Auth views
|
// Auth views
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/login.html")]
|
||||||
|
struct LoginPageTemplate {
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn login_page(
|
||||||
|
State(AppState {
|
||||||
|
application_name, ..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
if auth_session.user.is_some() {
|
||||||
|
return Ok(Redirect::to("/").into_response());
|
||||||
|
}
|
||||||
|
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
|
|
||||||
|
Ok(Html(LoginPageTemplate { application_name }.render()?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn login(
|
pub(crate) async fn login(
|
||||||
mut auth_session: AuthSession,
|
mut auth_session: AuthSession,
|
||||||
Form(credentials): Form<Credentials>,
|
Form(credentials): Form<Credentials>,
|
||||||
|
|
@ -98,7 +134,28 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoRes
|
||||||
Ok(Redirect::to("/"))
|
Ok(Redirect::to("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post upload view
|
// Post upload views
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/upload.html")]
|
||||||
|
struct UploadPageTemplate {
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn upload_page(
|
||||||
|
State(AppState {
|
||||||
|
application_name, ..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
if auth_session.user.is_none() {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
|
|
||||||
|
Ok(Html(UploadPageTemplate { application_name }.render()?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
enum Format {
|
enum Format {
|
||||||
Video(&'static str),
|
Video(&'static str),
|
||||||
|
|
@ -151,16 +208,28 @@ pub(crate) async fn upload(
|
||||||
let mut thumbnail_file: Option<String> = None;
|
let mut thumbnail_file: Option<String> = None;
|
||||||
let mut thumbnail_width: Option<NonZero<i32>> = None;
|
let mut thumbnail_width: Option<NonZero<i32>> = None;
|
||||||
let mut thumbnail_height: Option<NonZero<i32>> = None;
|
let mut thumbnail_height: Option<NonZero<i32>> = None;
|
||||||
let base_path = std::path::Path::new(files_dir.as_ref());
|
let base_path = files_dir.as_ref();
|
||||||
|
|
||||||
// Read multipart form data
|
// Read multipart form data
|
||||||
while let Some(mut field) = multipart.next_field().await.unwrap() {
|
while let Some(mut field) = multipart.next_field().await.unwrap() {
|
||||||
match field.name().unwrap() {
|
match field.name().unwrap() {
|
||||||
"tags" => {
|
"tags" => {
|
||||||
if let Ok(tags) = field.text().await {
|
if let Ok(tags) = field.text().await {
|
||||||
let tags: HashSet<String> = tags.split_whitespace().map(String::from).collect();
|
let tags: HashSet<String> = tags
|
||||||
|
.split_whitespace()
|
||||||
|
.filter_map(|tag| {
|
||||||
|
if tag.starts_with(NEGATIVE_PREFIX) || tag.starts_with(RATING_PREFIX) {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(String::from(tag))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let normalized_tags: HashSet<String> =
|
let normalized_tags: HashSet<String> =
|
||||||
tags.iter().map(|tag| tag.to_lowercase()).collect();
|
tags.iter().map(|tag| tag.to_lowercase()).collect();
|
||||||
|
if tags.is_empty() {
|
||||||
|
upload_tags = Some(vec![]);
|
||||||
|
} else {
|
||||||
SameyTag::insert_many(tags.into_iter().map(|tag| samey_tag::ActiveModel {
|
SameyTag::insert_many(tags.into_iter().map(|tag| samey_tag::ActiveModel {
|
||||||
normalized_name: Set(tag.to_lowercase()),
|
normalized_name: Set(tag.to_lowercase()),
|
||||||
name: Set(tag),
|
name: Set(tag),
|
||||||
|
|
@ -181,6 +250,7 @@ pub(crate) async fn upload(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
"media-file" => {
|
"media-file" => {
|
||||||
let content_type = field
|
let content_type = field
|
||||||
|
|
@ -337,17 +407,17 @@ pub(crate) async fn upload(
|
||||||
.last_insert_id;
|
.last_insert_id;
|
||||||
|
|
||||||
// Add tags to post
|
// Add tags to post
|
||||||
SameyTagPost::insert_many(
|
if !upload_tags.is_empty() {
|
||||||
upload_tags
|
SameyTagPost::insert_many(upload_tags.into_iter().map(|tag| {
|
||||||
.into_iter()
|
samey_tag_post::ActiveModel {
|
||||||
.map(|tag| samey_tag_post::ActiveModel {
|
|
||||||
post_id: Set(uploaded_post),
|
post_id: Set(uploaded_post),
|
||||||
tag_id: Set(tag.id),
|
tag_id: Set(tag.id),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}
|
||||||
)
|
}))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
Ok(Redirect::to(&format!("/post/{}", uploaded_post)))
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -363,7 +433,7 @@ struct SearchTag {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "search_tags.html")]
|
#[template(path = "fragments/search_tags.html")]
|
||||||
struct SearchTagsTemplate {
|
struct SearchTagsTemplate {
|
||||||
tags: Vec<SearchTag>,
|
tags: Vec<SearchTag>,
|
||||||
selection_end: usize,
|
selection_end: usize,
|
||||||
|
|
@ -458,9 +528,9 @@ pub(crate) async fn search_tags(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "select_tag.html")]
|
#[template(path = "fragments/select_tag.html")]
|
||||||
struct SelectTagTemplate {
|
struct SelectTagTemplate {
|
||||||
tags: String,
|
tags_value: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
|
|
@ -475,29 +545,31 @@ pub(crate) async fn select_tag(
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let mut tags = String::new();
|
let mut tags = String::new();
|
||||||
for (tag, _) in body.tags[..body.selection_end].split(' ').tuple_windows() {
|
for (tag, _) in body.tags[..body.selection_end].split(' ').tuple_windows() {
|
||||||
|
if !tag.is_empty() {
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
tags.push(' ');
|
tags.push(' ');
|
||||||
}
|
}
|
||||||
tags.push_str(tag);
|
tags.push_str(tag);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if !tags.is_empty() {
|
if !tags.is_empty() {
|
||||||
tags.push(' ');
|
tags.push(' ');
|
||||||
}
|
}
|
||||||
tags.push_str(&body.new_tag);
|
tags.push_str(&body.new_tag);
|
||||||
for tag in body.tags[body.selection_end..].split(' ') {
|
for tag in body.tags[body.selection_end..].split(' ') {
|
||||||
if !tags.is_empty() {
|
if !tag.is_empty() {
|
||||||
tags.push(' ');
|
tags.push(' ');
|
||||||
}
|
|
||||||
tags.push_str(tag);
|
tags.push_str(tag);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
tags.push(' ');
|
tags.push(' ');
|
||||||
Ok(Html(SelectTagTemplate { tags }.render()?))
|
Ok(Html(SelectTagTemplate { tags_value: tags }.render()?))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Post list views
|
// Post list views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "posts.html")]
|
#[template(path = "pages/posts.html")]
|
||||||
struct PostsTemplate<'a> {
|
struct PostsTemplate<'a> {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
tags: Option<Vec<&'a str>>,
|
tags: Option<Vec<&'a str>>,
|
||||||
|
|
@ -541,12 +613,12 @@ pub(crate) async fn posts_page(
|
||||||
let posts = posts
|
let posts = posts
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|post| {
|
.map(|post| {
|
||||||
let mut tags_vec: Vec<_> = post.tags.split_ascii_whitespace().collect();
|
let tags: Option<String> = post.tags.map(|tags| {
|
||||||
|
let mut tags_vec = tags.split_ascii_whitespace().collect::<Vec<&str>>();
|
||||||
tags_vec.sort();
|
tags_vec.sort();
|
||||||
PostOverview {
|
tags_vec.into_iter().join(" ")
|
||||||
tags: tags_vec.into_iter().join(" "),
|
});
|
||||||
..post
|
PostOverview { tags, ..post }
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
|
|
@ -565,6 +637,27 @@ pub(crate) async fn posts_page(
|
||||||
|
|
||||||
// Pool views
|
// Pool views
|
||||||
|
|
||||||
|
#[derive(Template)]
|
||||||
|
#[template(path = "pages/create_pool.html")]
|
||||||
|
struct CreatePoolPageTemplate {
|
||||||
|
application_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn create_pool_page(
|
||||||
|
State(AppState {
|
||||||
|
application_name, ..
|
||||||
|
}): State<AppState>,
|
||||||
|
auth_session: AuthSession,
|
||||||
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
if auth_session.user.is_none() {
|
||||||
|
return Err(SameyError::Forbidden);
|
||||||
|
}
|
||||||
|
|
||||||
|
let application_name = application_name.read().await.clone();
|
||||||
|
|
||||||
|
Ok(Html(CreatePoolPageTemplate { application_name }.render()?).into_response())
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_pools(
|
pub(crate) async fn get_pools(
|
||||||
state: State<AppState>,
|
state: State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
|
|
@ -573,7 +666,7 @@ pub(crate) async fn get_pools(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pools.html")]
|
#[template(path = "pages/pools.html")]
|
||||||
struct GetPoolsTemplate {
|
struct GetPoolsTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
pools: Vec<samey_pool::Model>,
|
pools: Vec<samey_pool::Model>,
|
||||||
|
|
@ -645,7 +738,7 @@ pub(crate) async fn create_pool(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pool.html")]
|
#[template(path = "pages/pool.html")]
|
||||||
struct ViewPoolTemplate {
|
struct ViewPoolTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
pool: samey_pool::Model,
|
pool: samey_pool::Model,
|
||||||
|
|
@ -734,14 +827,14 @@ pub(crate) struct AddPostToPoolForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
pub(crate) struct PoolWithMaxPosition {
|
struct PoolWithMaxPosition {
|
||||||
id: i32,
|
id: i32,
|
||||||
uploader_id: i32,
|
uploader_id: i32,
|
||||||
max_position: Option<f32>,
|
max_position: Option<f32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "add_post_to_pool.html")]
|
#[template(path = "fragments/add_post_to_pool.html")]
|
||||||
struct AddPostToPoolTemplate {
|
struct AddPostToPoolTemplate {
|
||||||
pool: PoolWithMaxPosition,
|
pool: PoolWithMaxPosition,
|
||||||
posts: Vec<PoolPost>,
|
posts: Vec<PoolPost>,
|
||||||
|
|
@ -775,7 +868,7 @@ pub(crate) async fn add_post_to_pool(
|
||||||
return Err(SameyError::Forbidden);
|
return Err(SameyError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let post = filter_by_user(
|
let post = filter_posts_by_user(
|
||||||
SameyPost::find_by_id(body.post_id),
|
SameyPost::find_by_id(body.post_id),
|
||||||
auth_session.user.as_ref(),
|
auth_session.user.as_ref(),
|
||||||
)
|
)
|
||||||
|
|
@ -841,7 +934,7 @@ pub(crate) struct SortPoolForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "pool_posts.html")]
|
#[template(path = "fragments/pool_posts.html")]
|
||||||
struct PoolPostsTemplate {
|
struct PoolPostsTemplate {
|
||||||
pool: samey_pool::Model,
|
pool: samey_pool::Model,
|
||||||
posts: Vec<PoolPost>,
|
posts: Vec<PoolPost>,
|
||||||
|
|
@ -916,7 +1009,7 @@ pub(crate) async fn sort_pool(
|
||||||
// Settings views
|
// Settings views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "settings.html")]
|
#[template(path = "pages/settings.html")]
|
||||||
struct SettingsTemplate {
|
struct SettingsTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
}
|
}
|
||||||
|
|
@ -997,32 +1090,34 @@ pub(crate) async fn update_settings(
|
||||||
// Single post views
|
// Single post views
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "view_post.html")]
|
#[template(path = "pages/view_post.html")]
|
||||||
struct ViewPostTemplate {
|
struct ViewPostPageTemplate {
|
||||||
application_name: String,
|
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: Option<String>,
|
||||||
|
tags_post: String,
|
||||||
sources: Vec<samey_post_source::Model>,
|
sources: Vec<samey_post_source::Model>,
|
||||||
can_edit: bool,
|
can_edit: bool,
|
||||||
parent_post: Option<PostOverview>,
|
parent_post: Option<PostOverview>,
|
||||||
children_posts: Vec<PostOverview>,
|
children_posts: Vec<PostOverview>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn view_post(
|
pub(crate) async fn view_post_page(
|
||||||
State(AppState {
|
State(AppState {
|
||||||
db,
|
db,
|
||||||
application_name,
|
application_name,
|
||||||
..
|
..
|
||||||
}): State<AppState>,
|
}): State<AppState>,
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
|
Query(query): Query<PostsQuery>,
|
||||||
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 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_post = tags.iter().map(|tag| &tag.name).join(" ");
|
||||||
|
|
||||||
let sources = SameyPostSource::find()
|
let sources = SameyPostSource::find()
|
||||||
.filter(samey_post_source::Column::PostId.eq(post_id))
|
.filter(samey_post_source::Column::PostId.eq(post_id))
|
||||||
|
|
@ -1035,19 +1130,21 @@ pub(crate) async fn view_post(
|
||||||
.ok_or(SameyError::NotFound)?;
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
let parent_post = if let Some(parent_id) = post.parent_id {
|
let parent_post = if let Some(parent_id) = post.parent_id {
|
||||||
match filter_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
match filter_posts_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(parent_post) => Some(PostOverview {
|
Some(parent_post) => Some(PostOverview {
|
||||||
id: parent_id,
|
id: parent_id,
|
||||||
thumbnail: parent_post.thumbnail,
|
thumbnail: parent_post.thumbnail,
|
||||||
tags: get_tags_for_post(post_id)
|
tags: Some(
|
||||||
|
get_tags_for_post(post_id)
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
|
),
|
||||||
rating: parent_post.rating,
|
rating: parent_post.rating,
|
||||||
media_type: parent_post.media_type,
|
media_type: parent_post.media_type,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1057,7 +1154,7 @@ pub(crate) async fn view_post(
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let children_posts_models = filter_by_user(
|
let children_posts_models = filter_posts_by_user(
|
||||||
SameyPost::find().filter(samey_post::Column::ParentId.eq(post_id)),
|
SameyPost::find().filter(samey_post::Column::ParentId.eq(post_id)),
|
||||||
auth_session.user.as_ref(),
|
auth_session.user.as_ref(),
|
||||||
)
|
)
|
||||||
|
|
@ -1069,12 +1166,14 @@ pub(crate) async fn view_post(
|
||||||
children_posts.push(PostOverview {
|
children_posts.push(PostOverview {
|
||||||
id: child_post.id,
|
id: child_post.id,
|
||||||
thumbnail: child_post.thumbnail,
|
thumbnail: child_post.thumbnail,
|
||||||
tags: get_tags_for_post(child_post.id)
|
tags: Some(
|
||||||
|
get_tags_for_post(child_post.id)
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
|
),
|
||||||
rating: child_post.rating,
|
rating: child_post.rating,
|
||||||
media_type: child_post.media_type,
|
media_type: child_post.media_type,
|
||||||
});
|
});
|
||||||
|
|
@ -1090,11 +1189,12 @@ pub(crate) async fn view_post(
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ViewPostTemplate {
|
ViewPostPageTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
post,
|
post,
|
||||||
tags,
|
tags,
|
||||||
tags_text,
|
tags_text: query.tags,
|
||||||
|
tags_post,
|
||||||
sources,
|
sources,
|
||||||
can_edit,
|
can_edit,
|
||||||
parent_post,
|
parent_post,
|
||||||
|
|
@ -1105,7 +1205,7 @@ pub(crate) async fn view_post(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "post_details.html")]
|
#[template(path = "fragments/post_details.html")]
|
||||||
struct PostDetailsTemplate {
|
struct PostDetailsTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
sources: Vec<samey_post_source::Model>,
|
sources: Vec<samey_post_source::Model>,
|
||||||
|
|
@ -1159,7 +1259,7 @@ pub(crate) struct SubmitPostDetailsForm {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "submit_post_details.html")]
|
#[template(path = "fragments/submit_post_details.html")]
|
||||||
struct SubmitPostDetailsTemplate {
|
struct SubmitPostDetailsTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
parent_post: Option<PostOverview>,
|
parent_post: Option<PostOverview>,
|
||||||
|
|
@ -1197,19 +1297,21 @@ pub(crate) async fn submit_post_details(
|
||||||
description => Some(description.to_owned()),
|
description => Some(description.to_owned()),
|
||||||
};
|
};
|
||||||
let parent_post = if let Some(parent_id) = body.parent_post.trim().parse().ok() {
|
let parent_post = if let Some(parent_id) = body.parent_post.trim().parse().ok() {
|
||||||
match filter_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
match filter_posts_by_user(SameyPost::find_by_id(parent_id), auth_session.user.as_ref())
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
{
|
{
|
||||||
Some(parent_post) => Some(PostOverview {
|
Some(parent_post) => Some(PostOverview {
|
||||||
id: parent_id,
|
id: parent_id,
|
||||||
thumbnail: parent_post.thumbnail,
|
thumbnail: parent_post.thumbnail,
|
||||||
tags: get_tags_for_post(post_id)
|
tags: Some(
|
||||||
|
get_tags_for_post(post_id)
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?
|
.await?
|
||||||
.iter()
|
.iter()
|
||||||
.map(|tag| &tag.name)
|
.map(|tag| &tag.name)
|
||||||
.join(" "),
|
.join(" "),
|
||||||
|
),
|
||||||
rating: parent_post.rating,
|
rating: parent_post.rating,
|
||||||
media_type: parent_post.media_type,
|
media_type: parent_post.media_type,
|
||||||
}),
|
}),
|
||||||
|
|
@ -1259,6 +1361,9 @@ pub(crate) async fn submit_post_details(
|
||||||
.filter(samey_tag_post::Column::PostId.eq(post_id))
|
.filter(samey_tag_post::Column::PostId.eq(post_id))
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
let tags = if tags.is_empty() {
|
||||||
|
vec![]
|
||||||
|
} else {
|
||||||
// TODO: Improve this to not recreate existing tag-post entries (see above)
|
// TODO: Improve this to not recreate existing tag-post entries (see above)
|
||||||
SameyTag::insert_many(tags.into_iter().map(|tag| samey_tag::ActiveModel {
|
SameyTag::insert_many(tags.into_iter().map(|tag| samey_tag::ActiveModel {
|
||||||
normalized_name: Set(tag.to_lowercase()),
|
normalized_name: Set(tag.to_lowercase()),
|
||||||
|
|
@ -1284,6 +1389,8 @@ pub(crate) async fn submit_post_details(
|
||||||
.exec(&db)
|
.exec(&db)
|
||||||
.await?;
|
.await?;
|
||||||
upload_tags.sort_by(|a, b| a.name.cmp(&b.name));
|
upload_tags.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
upload_tags
|
||||||
|
};
|
||||||
|
|
||||||
let sources = SameyPostSource::find()
|
let sources = SameyPostSource::find()
|
||||||
.filter(samey_post_source::Column::PostId.eq(post_id))
|
.filter(samey_post_source::Column::PostId.eq(post_id))
|
||||||
|
|
@ -1294,7 +1401,7 @@ pub(crate) async fn submit_post_details(
|
||||||
SubmitPostDetailsTemplate {
|
SubmitPostDetailsTemplate {
|
||||||
post,
|
post,
|
||||||
sources,
|
sources,
|
||||||
tags: upload_tags,
|
tags,
|
||||||
parent_post,
|
parent_post,
|
||||||
can_edit: true,
|
can_edit: true,
|
||||||
}
|
}
|
||||||
|
|
@ -1307,7 +1414,7 @@ struct EditPostSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "edit_post_details.html")]
|
#[template(path = "fragments/edit_post_details.html")]
|
||||||
struct EditDetailsTemplate {
|
struct EditDetailsTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
sources: Vec<EditPostSource>,
|
sources: Vec<EditPostSource>,
|
||||||
|
|
@ -1362,7 +1469,7 @@ pub(crate) async fn edit_post_details(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "post_source.html")]
|
#[template(path = "fragments/post_source.html")]
|
||||||
struct AddPostSourceTemplate {
|
struct AddPostSourceTemplate {
|
||||||
source: EditPostSource,
|
source: EditPostSource,
|
||||||
}
|
}
|
||||||
|
|
@ -1381,7 +1488,7 @@ pub(crate) async fn remove_field() -> impl IntoResponse {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "get_image_media.html")]
|
#[template(path = "fragments/get_image_media.html")]
|
||||||
struct GetMediaTemplate {
|
struct GetMediaTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
}
|
}
|
||||||
|
|
@ -1409,7 +1516,7 @@ pub(crate) async fn get_media(
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "get_full_image_media.html")]
|
#[template(path = "fragments/get_full_image_media.html")]
|
||||||
struct GetFullMediaTemplate {
|
struct GetFullMediaTemplate {
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
}
|
}
|
||||||
|
|
@ -1458,7 +1565,7 @@ pub(crate) async fn delete_post(
|
||||||
SameyPost::delete_by_id(post.id).exec(&db).await?;
|
SameyPost::delete_by_id(post.id).exec(&db).await?;
|
||||||
|
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
let base_path = std::path::Path::new(files_dir.as_ref());
|
let base_path = files_dir.as_ref();
|
||||||
let _ = std::fs::remove_file(base_path.join(post.media));
|
let _ = std::fs::remove_file(base_path.join(post.media));
|
||||||
let _ = std::fs::remove_file(base_path.join(post.thumbnail));
|
let _ = std::fs::remove_file(base_path.join(post.thumbnail));
|
||||||
});
|
});
|
||||||
|
|
|
||||||
1
static/htmx.js
Normal file
1
static/htmx.js
Normal file
File diff suppressed because one or more lines are too long
0
static/samey.css
Normal file
0
static/samey.css
Normal file
2
static/sortable.js
Normal file
2
static/sortable.js
Normal file
File diff suppressed because one or more lines are too long
1
static/water.css
Normal file
1
static/water.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1,4 +1,4 @@
|
||||||
{% include "pool_posts.html" %}
|
{% include "fragments/pool_posts.html" %}
|
||||||
<input
|
<input
|
||||||
id="add-post-input"
|
id="add-post-input"
|
||||||
name="post_id"
|
name="post_id"
|
||||||
6
templates/fragments/common_headers.html
Normal file
6
templates/fragments/common_headers.html
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script src="/static/htmx.js"></script>
|
||||||
|
<link rel="stylesheet" href="/static/water.css" />
|
||||||
|
<link rel="stylesheet" href="/static/samey.css" />
|
||||||
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
|
|
@ -1,28 +1,16 @@
|
||||||
<form hx-put="/post_details/{{ post.id }}" hx-target="this" hx-swap="outerHTML">
|
<form hx-put="/post_details/{{ post.id }}" hx-target="this" hx-swap="outerHTML">
|
||||||
<div>
|
<div>
|
||||||
<label>Tags</label>
|
<label>Tags</label>
|
||||||
<input
|
{% let tags_value = tags %} {% include "fragments/tags_input.html" %}
|
||||||
class="tags"
|
|
||||||
type="text"
|
|
||||||
id="search-tags"
|
|
||||||
name="tags"
|
|
||||||
hx-post="/search_tags"
|
|
||||||
hx-trigger="input changed delay:400ms"
|
|
||||||
hx-target="next .tags-autocomplete"
|
|
||||||
hx-swap="innerHTML"
|
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
|
||||||
value="{{ tags }}"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Title</label>
|
<label>Title</label>
|
||||||
<input name="title" type="text" maxlength="100" value="{% if let Some(title) = post.title %}{{ title }}{% endif %}" />
|
<input name="title" type="text" maxlength="100" placeholder="Title" value="{% if let Some(title) = post.title %}{{ title }}{% endif %}" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
<textarea name="description">{% if let Some(description) = post.description %}{{ description }}{% endif %}</textarea>
|
<textarea name="description" placeholder="Description in Markdown">{% if let Some(description) = post.description %}{{ description }}{% endif %}</textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Is public post?</label>
|
<label>Is public post?</label>
|
||||||
|
|
@ -41,7 +29,7 @@
|
||||||
<label>Source(s)</label>
|
<label>Source(s)</label>
|
||||||
<ul id="sources">
|
<ul id="sources">
|
||||||
{% for source in sources %}
|
{% for source in sources %}
|
||||||
{% include "post_source.html" %}
|
{% include "fragments/post_source.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
<button hx-post="/post_source" hx-target="#sources" hx-swap="beforeend">+</button>
|
<button hx-post="/post_source" hx-target="#sources" hx-swap="beforeend">+</button>
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
<a href="/post/{{ post.id }}" title="{{ post.tags }}">
|
<a href="/post/{{ post.id }}" title="{{ post.tags }}">
|
||||||
<img src="/files/{{ post.thumbnail }}" />
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
<div>{{ post.rating | upper }}</div>
|
<div>{{ post.rating | upper }}</div>
|
||||||
|
<div>{{ post.media_type }}</div>
|
||||||
</a>
|
</a>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<button
|
<button
|
||||||
|
|
@ -6,8 +6,10 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Description</label>
|
<label>Description</label>
|
||||||
{% if let Some(description) = post.description %}{{ description }}{% else
|
{% if let Some(description) = post.description %}{{ description | markdown
|
||||||
%}<em>None</em>{% endif %}
|
}}{% else %}
|
||||||
|
<p><em>None</em></p>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>Is public?</label>
|
<label>Is public?</label>
|
||||||
2
templates/fragments/select_tag.html
Normal file
2
templates/fragments/select_tag.html
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
{% include "fragments/tags_input.html" %}
|
||||||
|
<ul class="tags-autocomplete" hx-swap-oob="outerHTML:.tags-autocomplete"></ul>
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
{% include "post_details.html" %} {% if let Some(parent_post) = parent_post %}
|
{% include "fragments/post_details.html" %} {% if let Some(parent_post) =
|
||||||
|
parent_post %}
|
||||||
<article id="parent-post" hx-swap-oob="outerHTML">
|
<article id="parent-post" hx-swap-oob="outerHTML">
|
||||||
<h2>Parent</h2>
|
<h2>Parent</h2>
|
||||||
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
<a
|
||||||
|
href="/post/{{ parent_post.id }}"
|
||||||
|
title="{% if let Some(tags) = parent_post.tags %}{{ tags }}{% endif %}"
|
||||||
|
>
|
||||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||||
<div>{{ parent_post.rating }}</div>
|
<div>{{ parent_post.rating }}</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
@ -3,12 +3,13 @@
|
||||||
type="text"
|
type="text"
|
||||||
id="search-tags"
|
id="search-tags"
|
||||||
name="tags"
|
name="tags"
|
||||||
|
placeholder="Tags"
|
||||||
hx-post="/search_tags"
|
hx-post="/search_tags"
|
||||||
hx-trigger="input changed"
|
hx-trigger="input changed"
|
||||||
hx-target="next .tags-autocomplete"
|
hx-target="next .tags-autocomplete"
|
||||||
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
||||||
value="{{ tags }}"
|
value="{{ tags_value }}"
|
||||||
autofocus
|
aria-autocomplete="list"
|
||||||
|
aria-controls="search-autocomplete"
|
||||||
/>
|
/>
|
||||||
<ul class="tags-autocomplete" hx-swap-oob="outerHTML:.tags-autocomplete"></ul>
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<!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>{{ application_name }}</title>
|
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>{{ application_name }}</h1>
|
|
||||||
<article>
|
|
||||||
<h2>Search</h2>
|
|
||||||
<form method="get" action="/posts/1">
|
|
||||||
<input
|
|
||||||
class="tags"
|
|
||||||
type="text"
|
|
||||||
id="search-tags"
|
|
||||||
name="tags"
|
|
||||||
hx-post="/search_tags"
|
|
||||||
hx-trigger="input changed"
|
|
||||||
hx-target="next .tags-autocomplete"
|
|
||||||
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
|
||||||
<button type="submit">Search</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<a href="/posts/1">Posts</a>
|
|
||||||
<a href="/pools/1">Pools</a>
|
|
||||||
</article>
|
|
||||||
{% if let Some(user) = user %}
|
|
||||||
<article>
|
|
||||||
<h2>Upload media</h2>
|
|
||||||
<form method="post" action="/upload" enctype="multipart/form-data">
|
|
||||||
<input
|
|
||||||
class="tags"
|
|
||||||
type="text"
|
|
||||||
id="upload-tags"
|
|
||||||
name="tags"
|
|
||||||
hx-post="/search_tags"
|
|
||||||
hx-trigger="input changed"
|
|
||||||
hx-target="next .tags-autocomplete"
|
|
||||||
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
|
||||||
/>
|
|
||||||
<ul class="tags-autocomplete" id="upload-autocomplete"></ul>
|
|
||||||
<input
|
|
||||||
type="file"
|
|
||||||
id="media-file"
|
|
||||||
name="media-file"
|
|
||||||
accept=".jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mkv, .mov"
|
|
||||||
/>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h2>Create pool</h2>
|
|
||||||
<form method="post" action="/pool">
|
|
||||||
<input
|
|
||||||
class="pool"
|
|
||||||
type="text"
|
|
||||||
id="pool"
|
|
||||||
name="pool"
|
|
||||||
maxlength="100"
|
|
||||||
/>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<a href="/logout">Log out</a>
|
|
||||||
</article>
|
|
||||||
{% else %}
|
|
||||||
<article>
|
|
||||||
<h2>Log in</h2>
|
|
||||||
<form method="post" action="/login">
|
|
||||||
<div>
|
|
||||||
<label>Username</label>
|
|
||||||
<input id="username" type="text" name="username" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label>Password</label>
|
|
||||||
<input id="password" type="password" name="password" />
|
|
||||||
</div>
|
|
||||||
<button type="submit">Submit</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
{% endif %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
25
templates/pages/create_pool.html
Normal file
25
templates/pages/create_pool.html
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Create pool - {{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<main>
|
||||||
|
<h1>Create pool</h1>
|
||||||
|
<form method="post" action="/pool">
|
||||||
|
<input
|
||||||
|
class="pool"
|
||||||
|
type="text"
|
||||||
|
id="pool"
|
||||||
|
name="pool"
|
||||||
|
maxlength="100"
|
||||||
|
placeholder="Name"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<button type="submit">Create</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
50
templates/pages/index.html
Normal file
50
templates/pages/index.html
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>{{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>{{ application_name }}</h1>
|
||||||
|
<article>
|
||||||
|
<h2>Search</h2>
|
||||||
|
<form method="get" action="/posts/1">
|
||||||
|
{% let tags_value = "" %} {% include "fragments/tags_input.html" %}
|
||||||
|
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
<nav>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="/posts/1">Posts</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/pools/1">Pools</a>
|
||||||
|
</li>
|
||||||
|
{% if let Some(user) = user %}
|
||||||
|
<li>
|
||||||
|
<a href="/upload">Upload media</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="/create_pool">Create pool</a>
|
||||||
|
</li>
|
||||||
|
{% if user.is_admin %}
|
||||||
|
<li>
|
||||||
|
<a href="/settings">Settings</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
<li>
|
||||||
|
<a href="/logout">Log out ({{ user.username }})</a>
|
||||||
|
</li>
|
||||||
|
{% else %}
|
||||||
|
<li>
|
||||||
|
<a href="/login">Login</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
24
templates/pages/login.html
Normal file
24
templates/pages/login.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Login - {{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<main>
|
||||||
|
<h1>Login</h1>
|
||||||
|
<form method="post" action="/login">
|
||||||
|
<div>
|
||||||
|
<label>Username</label>
|
||||||
|
<input id="username" type="text" name="username" autofocus />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Password</label>
|
||||||
|
<input id="password" type="password" name="password" />
|
||||||
|
</div>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
14
templates/pages/not_found.html
Normal file
14
templates/pages/not_found.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Not found</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<main>
|
||||||
|
<h1>Not found</h1>
|
||||||
|
<p>The requested resource could not be found.</p>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,12 +1,9 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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>
|
|
||||||
<script src=" https://cdn.jsdelivr.net/npm/sortablejs@1.15.6/Sortable.min.js "></script>
|
|
||||||
<title>Pool - {{ pool.name }} - {{ application_name }}</title>
|
<title>Pool - {{ pool.name }} - {{ application_name }}</title>
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
{% include "fragments/common_headers.html" %}
|
||||||
|
<script src="/static/sortable.js"></script>
|
||||||
<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 }}" />
|
||||||
|
|
@ -50,12 +47,13 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Pool - {{ pool.name }}</h1>
|
<h1>Pool - {{ pool.name }}</h1>
|
||||||
</main>
|
</main>
|
||||||
<article>
|
<article>
|
||||||
<h2>Posts</h2>
|
<h2>Posts</h2>
|
||||||
{% include "pool_posts.html" %}
|
{% include "fragments/pool_posts.html" %}
|
||||||
</article>
|
</article>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<article>
|
<article>
|
||||||
|
|
@ -68,8 +66,9 @@
|
||||||
<label>Add post</label>
|
<label>Add post</label>
|
||||||
<input
|
<input
|
||||||
id="add-post-input"
|
id="add-post-input"
|
||||||
name="post_id"
|
|
||||||
type="text"
|
type="text"
|
||||||
|
name="post_id"
|
||||||
|
placeholder="Post ID"
|
||||||
pattern="[0-9]*"
|
pattern="[0-9]*"
|
||||||
/>
|
/>
|
||||||
<button>Add</button>
|
<button>Add</button>
|
||||||
37
templates/pages/pools.html
Normal file
37
templates/pages/pools.html
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Pools - {{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<main>
|
||||||
|
<h1>Pools</h1>
|
||||||
|
{% if pools.is_empty() %}
|
||||||
|
<div>No pools found!</div>
|
||||||
|
{% else %}
|
||||||
|
<ul>
|
||||||
|
{% for pool in pools %}
|
||||||
|
<li>
|
||||||
|
<a href="/pool/{{ pool.id }}"> {{ pool.name }} </a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{% for i in 1..=page_count %}
|
||||||
|
<li>
|
||||||
|
{% if i == page as u64 %}
|
||||||
|
<b>{{ i }}</b>
|
||||||
|
{% else %}
|
||||||
|
<a href="/pools/{{ i }}">{{ i }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
79
templates/pages/posts.html
Normal file
79
templates/pages/posts.html
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Posts - {{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<article>
|
||||||
|
<h2>Search</h2>
|
||||||
|
<form method="get" action="/posts">
|
||||||
|
<input
|
||||||
|
class="tags"
|
||||||
|
type="text"
|
||||||
|
id="search-tags"
|
||||||
|
name="tags"
|
||||||
|
placeholder="Tags"
|
||||||
|
hx-post="/search_tags"
|
||||||
|
hx-trigger="input changed"
|
||||||
|
hx-target="next .tags-autocomplete"
|
||||||
|
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
||||||
|
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
||||||
|
value="{% if let Some(tags_text) = tags_text %}{{ tags_text }}{% endif %}"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
{% if let Some(tags) = tags %}
|
||||||
|
{% if !tags.is_empty() %}
|
||||||
|
<article>
|
||||||
|
<h2>Tags</h2>
|
||||||
|
<ul>
|
||||||
|
{% for tag in tags %}
|
||||||
|
<li><a href="/posts?tags={{ tag }}">{{ tag }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</article>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
<main>
|
||||||
|
<h1>Posts</h1>
|
||||||
|
{% if posts.is_empty() %}
|
||||||
|
<div>No posts found!</div>
|
||||||
|
{% else %}
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{% for post in posts %}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="{% if let Some(tags_text) = tags_text %}/post/{{ post.id }}?tags={{ tags_text.replace(' ', "+") }}{% else %}/post/{{ post.id }}{% endif %}"
|
||||||
|
title="{% if let Some(tags) = post.tags %}{{ tags }}{% endif %}"
|
||||||
|
>
|
||||||
|
<img src="/files/{{ post.thumbnail }}" />
|
||||||
|
<div>{{ post.rating | upper }}</div>
|
||||||
|
<div>{{ post.media_type }}</div>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<ul>
|
||||||
|
{% for i in 1..=page_count %}
|
||||||
|
<li>
|
||||||
|
{% if i == page as u64 %}
|
||||||
|
<b>{{ i }}</b>
|
||||||
|
{% else %}
|
||||||
|
<a href="{% if let Some(tags_text) = tags_text %}/posts/{{ i }}?tags={{ tags_text.replace(' ', "+") }}{% else %}/posts/{{ i }}{% endif %}">{{ i }}</a>
|
||||||
|
{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,13 +1,11 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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>
|
<title>Settings - {{ application_name }}</title>
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<form hx-put="/settings" hx-swap="none">
|
<form hx-put="/settings" hx-swap="none">
|
||||||
24
templates/pages/upload.html
Normal file
24
templates/pages/upload.html
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>Upload media - {{ application_name }}</title>
|
||||||
|
{% include "fragments/common_headers.html" %}
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div><a href="/">< To home</a></div>
|
||||||
|
<main>
|
||||||
|
<h1>Upload media</h1>
|
||||||
|
<form method="post" action="/upload" enctype="multipart/form-data">
|
||||||
|
{% let tags_value = "" %} {% include "fragments/tags_input.html" %}
|
||||||
|
<ul class="tags-autocomplete" id="upload-autocomplete"></ul>
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="media-file"
|
||||||
|
name="media-file"
|
||||||
|
accept=".jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mkv, .mov"
|
||||||
|
/>
|
||||||
|
<button type="submit">Submit</button>
|
||||||
|
</form>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<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>Post #{{ post.id }} - {{ application_name }}</title>
|
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
{% include "fragments/common_headers.html" %}
|
||||||
<meta property="og:site_name" content="{{ application_name }}" />
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
<meta
|
<meta
|
||||||
property="og:title"
|
property="og:title"
|
||||||
|
|
@ -24,13 +21,13 @@
|
||||||
<meta property="og:image" content="/files/{{ post.media }}" />
|
<meta property="og:image" content="/files/{{ post.media }}" />
|
||||||
<meta property="og:image:width" content="{{ post.width }}" />
|
<meta property="og:image:width" content="{{ post.width }}" />
|
||||||
<meta property="og:image:height" content="{{ post.height }}" />
|
<meta property="og:image:height" content="{{ post.height }}" />
|
||||||
<meta property="og:image:alt" content="{{ tags_text }}" />
|
<meta property="og:image:alt" content="{{ tags_post }}" />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
{% when "video" %}
|
{% when "video" %}
|
||||||
<meta property="og:video" content="/files/{{ post.media }}" />
|
<meta property="og:video" content="/files/{{ post.media }}" />
|
||||||
<meta property="og:video:width" content="{{ post.width }}" />
|
<meta property="og:video:width" content="{{ post.width }}" />
|
||||||
<meta property="og:video:height" content="{{ post.height }}" />
|
<meta property="og:video:height" content="{{ post.height }}" />
|
||||||
<meta property="og:video:alt" content="{{ tags_text }}" />
|
<meta property="og:video:alt" content="{{ tags_post }}" />
|
||||||
<meta property="og:video:type" content="video/mp4" />
|
<meta property="og:video:type" content="video/mp4" />
|
||||||
<meta property="twitter:card" content="player" />
|
<meta property="twitter:card" content="player" />
|
||||||
<meta name="twitter:player" content="/files/{{ post.media }}" />
|
<meta name="twitter:player" content="/files/{{ post.media }}" />
|
||||||
|
|
@ -40,22 +37,57 @@
|
||||||
{% else %} {% endmatch %}
|
{% else %} {% endmatch %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div><a href="{% if let Some(tags_text) = tags_text %}/posts/1?tags={{ tags_text.replace(' ', "+") }}{% else %}/posts/1{% endif %}">< To posts</a></div>
|
||||||
|
<article>
|
||||||
|
<h2>Search</h2>
|
||||||
|
<form method="get" action="/posts">
|
||||||
|
<input
|
||||||
|
class="tags"
|
||||||
|
type="text"
|
||||||
|
id="search-tags"
|
||||||
|
name="tags"
|
||||||
|
hx-post="/search_tags"
|
||||||
|
hx-trigger="input changed"
|
||||||
|
hx-target="next .tags-autocomplete"
|
||||||
|
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
||||||
|
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
||||||
|
value="{% if let Some(tags_text) = tags_text %}{{ tags_text }}{% endif %}"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
||||||
|
<button type="submit">Search</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h2>Tags</h2>
|
||||||
|
{% if tags.is_empty() %}
|
||||||
|
<p>No tags in post. Consider adding some!</p>
|
||||||
|
{% else %}
|
||||||
|
<ul id="tags-list">
|
||||||
|
{% for tag in tags %}
|
||||||
|
<li>
|
||||||
|
<a href="/posts?tags={{ tag.name }}">{{ tag.name }}</a>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</article>
|
||||||
<main>
|
<main>
|
||||||
<h1>View post #{{ post.id }}</h1>
|
<h1>View post #{{ post.id }}</h1>
|
||||||
<div>
|
<div>
|
||||||
{% match post.media_type.as_ref() %}{% when "image" %}{% include
|
{% match post.media_type.as_ref() %}{% when "image" %}{% include
|
||||||
"get_image_media.html" %}{% when "video" %}{% include
|
"fragments/get_image_media.html" %}{% when "video" %}{% include
|
||||||
"get_video_media.html" %}{% else %}{% endmatch %}
|
"fragments/get_video_media.html" %}{% else %}{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<article>
|
<article>
|
||||||
<h2>Details</h2>
|
<h2>Details</h2>
|
||||||
{% include "post_details.html" %}
|
{% include "fragments/post_details.html" %}
|
||||||
</article>
|
</article>
|
||||||
{% if let Some(parent_post) = parent_post %}
|
{% if let Some(parent_post) = parent_post %}
|
||||||
<article id="parent-post">
|
<article id="parent-post">
|
||||||
<h2>Parent</h2>
|
<h2>Parent</h2>
|
||||||
<a href="/post/{{ parent_post.id }}" title="{{ parent_post.tags }}">
|
<a href="/post/{{ parent_post.id }}" title="{% if let Some(tags) = parent_post.tags %}{{ tags }}{% endif %}">
|
||||||
<img src="/files/{{ parent_post.thumbnail }}" />
|
<img src="/files/{{ parent_post.thumbnail }}" />
|
||||||
<div>{{ parent_post.rating }}</div>
|
<div>{{ parent_post.rating }}</div>
|
||||||
<div>{{ parent_post.media_type }}</div>
|
<div>{{ parent_post.media_type }}</div>
|
||||||
|
|
@ -69,7 +101,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
{% for child_post in children_posts %}
|
{% for child_post in children_posts %}
|
||||||
<li>
|
<li>
|
||||||
<a href="/post/{{ child_post.id }}" title="{{ child_post.tags }}">
|
<a href="/post/{{ child_post.id }}" title="{% if let Some(tags) = child_post.tags %}{{ tags }}{% endif %}">
|
||||||
<img src="/files/{{ child_post.thumbnail }}" />
|
<img src="/files/{{ child_post.thumbnail }}" />
|
||||||
<div>{{ child_post.rating | upper }}</div>
|
<div>{{ child_post.rating | upper }}</div>
|
||||||
<div>{{ child_post.media_type }}</div>
|
<div>{{ child_post.media_type }}</div>
|
||||||
|
|
@ -79,15 +111,5 @@
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<article>
|
|
||||||
<h2>Tags</h2>
|
|
||||||
<ul id="tags-list">
|
|
||||||
{% for tag in tags %}
|
|
||||||
<li>
|
|
||||||
<a href="/posts?tags={{ tag.name }}">{{ tag.name }}</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
<!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>Pools - {{ application_name }}</title>
|
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main>
|
|
||||||
<h1>Viewing pools</h1>
|
|
||||||
{% if pools.is_empty() %}
|
|
||||||
<div>No pools found!</div>
|
|
||||||
{% else %}
|
|
||||||
<ul>
|
|
||||||
{% for pool in pools %}
|
|
||||||
<li>
|
|
||||||
<a href="/pool/{{ pool.id }}"> {{ pool.name }} </a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<div>Page {{ page }} of {{ page_count }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<!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>Posts - {{ application_name }}</title>
|
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<article>
|
|
||||||
<h2>Search</h2>
|
|
||||||
<form method="get" action="/posts">
|
|
||||||
<input
|
|
||||||
class="tags"
|
|
||||||
type="text"
|
|
||||||
id="search-tags"
|
|
||||||
name="tags"
|
|
||||||
hx-post="/search_tags"
|
|
||||||
hx-trigger="input changed"
|
|
||||||
hx-target="next .tags-autocomplete"
|
|
||||||
hx-vals="js:{selection_end: event.target.selectionEnd}"
|
|
||||||
hx-on::after-settle="this.focus(); this.setSelectionRange(-1, -1);"
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
<ul class="tags-autocomplete" id="search-autocomplete"></ul>
|
|
||||||
<button type="submit">Search</button>
|
|
||||||
</form>
|
|
||||||
</article>
|
|
||||||
<article>
|
|
||||||
<h2>Tags</h2>
|
|
||||||
<ul>
|
|
||||||
{% if let Some(tags) = tags %} {% for tag in tags %}
|
|
||||||
<li><a href="/posts?tags={{ tag }}">{{ tag }}</a></li>
|
|
||||||
{% endfor %} {% endif %}
|
|
||||||
</ul>
|
|
||||||
</article>
|
|
||||||
<main>
|
|
||||||
<h1>Viewing posts</h1>
|
|
||||||
{% if posts.is_empty() %}
|
|
||||||
<div>No posts found!</div>
|
|
||||||
{% else %}
|
|
||||||
<ul>
|
|
||||||
{% for post in posts %}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="{% if let Some(tags_text) = tags_text %}/post/{{ post.id }}?tags={{ tags_text.replace(' ', "+") }}{% else %}/post/{{ post.id }}{% endif %}"
|
|
||||||
title="{{ post.tags }}"
|
|
||||||
>
|
|
||||||
<img src="/files/{{ post.thumbnail }}" />
|
|
||||||
<div>{{ post.rating | upper }}</div>
|
|
||||||
<div>{{ post.media_type }}</div>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
<div>Page {{ page }} of {{ page_count }}</div>
|
|
||||||
{% endif %}
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue