Finishing up the MVP
This commit is contained in:
parent
3619063e68
commit
7f533cc583
25 changed files with 535 additions and 291 deletions
28
Dockerfile
Normal file
28
Dockerfile
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
FROM --platform=$BUILDPLATFORM rust:1.86.0-alpine3.21 AS builder
|
||||||
|
ENV PKGCONFIG_SYSROOTDIR=/
|
||||||
|
RUN apk add --no-cache musl-dev perl build-base zig
|
||||||
|
RUN cargo install --locked cargo-zigbuild
|
||||||
|
RUN rustup target add x86_64-unknown-linux-musl aarch64-unknown-linux-musl
|
||||||
|
WORKDIR /app
|
||||||
|
COPY Cargo.toml Cargo.lock .
|
||||||
|
COPY migration ./migration
|
||||||
|
RUN mkdir src \
|
||||||
|
&& echo "fn main() {}" > src/main.rs \
|
||||||
|
&& cargo fetch \
|
||||||
|
&& cargo zigbuild --release --locked --target x86_64-unknown-linux-musl --target aarch64-unknown-linux-musl \
|
||||||
|
&& rm src/main.rs
|
||||||
|
COPY static ./static
|
||||||
|
COPY templates ./templates
|
||||||
|
COPY src ./src
|
||||||
|
RUN cargo zigbuild --release --locked --target x86_64-unknown-linux-musl --target aarch64-unknown-linux-musl
|
||||||
|
|
||||||
|
FROM --platform=$BUILDPLATFORM scratch AS binary
|
||||||
|
COPY --from=builder /app/target/x86_64-unknown-linux-musl/release/samey /samey-linux-amd64
|
||||||
|
COPY --from=builder /app/target/aarch64-unknown-linux-musl/release/samey /samey-linux-arm64
|
||||||
|
|
||||||
|
FROM alpine:3.21 AS runner
|
||||||
|
ARG TARGETOS
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN apk add --no-cache ffmpeg
|
||||||
|
COPY --from=binary /samey-${TARGETOS}-${TARGETARCH} /usr/bin/samey
|
||||||
|
ENTRYPOINT [ "samey" ]
|
||||||
43
README.md
43
README.md
|
|
@ -1,27 +1,40 @@
|
||||||
# Samey
|
# Samey
|
||||||
|
|
||||||
Sam's small image board. Currently a WIP.
|
Sam's small image board.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Still very much an early WIP.
|
||||||
|
|
||||||
|
### Roadmap
|
||||||
|
|
||||||
|
- [ ] Logging
|
||||||
|
- [ ] Improved error handling
|
||||||
|
- [ ] Caching
|
||||||
|
- [ ] Lossless compression
|
||||||
|
- [ ] Bulk edit tags/Fix tag capitalization
|
||||||
|
- [ ] User management
|
||||||
|
- [ ] Cleanup/fixup background tasks
|
||||||
|
- [ ] Text media
|
||||||
|
- [ ] Improve CSS
|
||||||
|
- [ ] Migrate to Cot...?
|
||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- `ffmpeg` and `ffprobe`
|
- `ffmpeg` and `ffprobe`
|
||||||
|
|
||||||
## TODO
|
## Running
|
||||||
|
|
||||||
- [ ] Show pool(s) in post
|
### Development
|
||||||
- [ ] CSS
|
|
||||||
- [ ] 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
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bacon serve
|
bacon serve
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sqlite3 db.sqlite3 "VACUUM;"
|
||||||
|
docker compose up -d
|
||||||
|
docker compose run --rm samey add-admin-user -u admin -p "superSecretPassword"
|
||||||
|
```
|
||||||
|
|
|
||||||
12
docker-compose.yml
Normal file
12
docker-compose.yml
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
services:
|
||||||
|
samey:
|
||||||
|
image: badmanners/samey:latest
|
||||||
|
container_name: samey
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8080:3000
|
||||||
|
volumes:
|
||||||
|
- ./files:/files:rw
|
||||||
|
- type: bind
|
||||||
|
source: ./db.sqlite3
|
||||||
|
target: /db.sqlite3
|
||||||
|
|
@ -1 +1,40 @@
|
||||||
|
use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
SameyError,
|
||||||
|
entities::{prelude::SameyConfig, samey_config},
|
||||||
|
};
|
||||||
|
|
||||||
pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME";
|
pub(crate) const APPLICATION_NAME_KEY: &str = "APPLICATION_NAME";
|
||||||
|
pub(crate) const AGE_CONFIRMATION_KEY: &str = "AGE_CONFIRMATION";
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct AppConfig {
|
||||||
|
pub(crate) application_name: String,
|
||||||
|
pub(crate) age_confirmation: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AppConfig {
|
||||||
|
pub(crate) async fn new(db: &DatabaseConnection) -> Result<Self, 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 age_confirmation = match SameyConfig::find()
|
||||||
|
.filter(samey_config::Column::Key.eq(AGE_CONFIRMATION_KEY))
|
||||||
|
.one(db)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
|
Some(row) => row.data.as_bool().unwrap_or(false),
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
Ok(Self {
|
||||||
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
17
src/error.rs
17
src/error.rs
|
|
@ -1,8 +1,13 @@
|
||||||
|
use askama::Template;
|
||||||
use axum::{
|
use axum::{
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::{IntoResponse, Response},
|
response::{Html, IntoResponse, Response},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(askama::Template)]
|
||||||
|
#[template(path = "pages/not_found.html")]
|
||||||
|
struct NotFoundTemplate;
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SameyError {
|
pub enum SameyError {
|
||||||
#[error("Integer conversion error: {0}")]
|
#[error("Integer conversion error: {0}")]
|
||||||
|
|
@ -47,7 +52,15 @@ impl IntoResponse for SameyError {
|
||||||
SameyError::Multipart(_) | SameyError::BadRequest(_) => {
|
SameyError::Multipart(_) | SameyError::BadRequest(_) => {
|
||||||
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
(StatusCode::BAD_REQUEST, "Invalid request").into_response()
|
||||||
}
|
}
|
||||||
SameyError::NotFound => (StatusCode::NOT_FOUND, "Resource not found").into_response(),
|
SameyError::NotFound => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Html(
|
||||||
|
NotFoundTemplate {}
|
||||||
|
.render()
|
||||||
|
.expect("shouldn't fail to render NotFoundTemplate"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
SameyError::Authentication(_) => {
|
SameyError::Authentication(_) => {
|
||||||
(StatusCode::UNAUTHORIZED, "Not authorized").into_response()
|
(StatusCode::UNAUTHORIZED, "Not authorized").into_response()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
21
src/lib.rs
21
src/lib.rs
|
|
@ -21,15 +21,14 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_extra::routing::RouterExt;
|
use axum_extra::routing::RouterExt;
|
||||||
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, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter};
|
use sea_orm::{ActiveValue::Set, DatabaseConnection, EntityTrait};
|
||||||
use tokio::{fs, sync::RwLock};
|
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::config::AppConfig;
|
||||||
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::*;
|
||||||
|
|
@ -61,7 +60,7 @@ fn assets_router() -> Router {
|
||||||
pub(crate) struct AppState {
|
pub(crate) struct AppState {
|
||||||
files_dir: Arc<PathBuf>,
|
files_dir: Arc<PathBuf>,
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
application_name: Arc<RwLock<String>>,
|
app_config: Arc<RwLock<AppConfig>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn create_user(
|
pub async fn create_user(
|
||||||
|
|
@ -85,18 +84,10 @@ pub async fn get_router(
|
||||||
db: DatabaseConnection,
|
db: DatabaseConnection,
|
||||||
files_dir: impl AsRef<Path>,
|
files_dir: impl AsRef<Path>,
|
||||||
) -> Result<Router, SameyError> {
|
) -> 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.as_ref().to_owned()),
|
files_dir: Arc::new(files_dir.as_ref().to_owned()),
|
||||||
db: db.clone(),
|
db: db.clone(),
|
||||||
application_name: Arc::new(RwLock::new(application_name)),
|
app_config: Arc::new(RwLock::new(AppConfig::new(&db).await?)),
|
||||||
};
|
};
|
||||||
fs::create_dir_all(files_dir.as_ref()).await?;
|
fs::create_dir_all(files_dir.as_ref()).await?;
|
||||||
|
|
||||||
|
|
@ -125,8 +116,6 @@ pub async fn get_router(
|
||||||
get(post_details).put(submit_post_details),
|
get(post_details).put(submit_post_details),
|
||||||
)
|
)
|
||||||
.route_with_tsr("/post_source", post(add_post_source))
|
.route_with_tsr("/post_source", post(add_post_source))
|
||||||
.route_with_tsr("/media/{post_id}/full", get(get_full_media))
|
|
||||||
.route_with_tsr("/media/{post_id}", get(get_media))
|
|
||||||
// Pool routes
|
// Pool routes
|
||||||
.route_with_tsr("/create_pool", get(create_pool_page))
|
.route_with_tsr("/create_pool", get(create_pool_page))
|
||||||
.route_with_tsr("/pools", get(get_pools))
|
.route_with_tsr("/pools", get(get_pools))
|
||||||
|
|
@ -139,7 +128,7 @@ pub async fn get_router(
|
||||||
.route_with_tsr("/pool/{pool_id}/sort", put(sort_pool))
|
.route_with_tsr("/pool/{pool_id}/sort", put(sort_pool))
|
||||||
.route_with_tsr("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
.route_with_tsr("/pool_post/{pool_post_id}", delete(remove_pool_post))
|
||||||
// Settings routes
|
// Settings routes
|
||||||
.route_with_tsr("/settings", get(settings).put(update_settings))
|
.route_with_tsr("/settings", get(settings).post(update_settings))
|
||||||
// Search routes
|
// Search routes
|
||||||
.route_with_tsr("/posts", get(posts))
|
.route_with_tsr("/posts", get(posts))
|
||||||
.route_with_tsr("/posts/{page}", get(posts_page))
|
.route_with_tsr("/posts/{page}", get(posts_page))
|
||||||
|
|
|
||||||
69
src/query.rs
69
src/query.rs
|
|
@ -2,16 +2,17 @@ use std::collections::HashSet;
|
||||||
|
|
||||||
use migration::{Expr, Query};
|
use migration::{Expr, Query};
|
||||||
use sea_orm::{
|
use sea_orm::{
|
||||||
ColumnTrait, Condition, EntityTrait, FromQueryResult, IntoSimpleExpr, QueryFilter, QueryOrder,
|
ColumnTrait, Condition, DatabaseConnection, EntityTrait, FromQueryResult, IntoSimpleExpr,
|
||||||
QuerySelect, RelationTrait, Select, SelectModel, Selector,
|
QueryFilter, QueryOrder, QuerySelect, RelationTrait, Select, SelectColumns, SelectModel,
|
||||||
|
Selector,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
NEGATIVE_PREFIX, RATING_PREFIX,
|
NEGATIVE_PREFIX, RATING_PREFIX, SameyError,
|
||||||
auth::User,
|
auth::User,
|
||||||
entities::{
|
entities::{
|
||||||
prelude::{SameyPoolPost, SameyPost, SameyTag, SameyTagPost},
|
prelude::{SameyPool, SameyPoolPost, SameyPost, SameyTag, SameyTagPost},
|
||||||
samey_pool_post, samey_post, samey_tag, samey_tag_post,
|
samey_pool, samey_pool_post, samey_post, samey_tag, samey_tag_post,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -149,6 +150,64 @@ pub(crate) fn get_tags_for_post(post_id: i32) -> Select<SameyTag> {
|
||||||
.order_by_asc(samey_tag::Column::Name)
|
.order_by_asc(samey_tag::Column::Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub(crate) struct PostPoolData {
|
||||||
|
pub(crate) id: i32,
|
||||||
|
pub(crate) name: String,
|
||||||
|
pub(crate) previous_post_id: Option<i32>,
|
||||||
|
pub(crate) next_post_id: Option<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, FromQueryResult)]
|
||||||
|
struct PostInPool {
|
||||||
|
id: i32,
|
||||||
|
name: String,
|
||||||
|
position: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) async fn get_pool_data_for_post(
|
||||||
|
db: &DatabaseConnection,
|
||||||
|
post_id: i32,
|
||||||
|
user: Option<&User>,
|
||||||
|
) -> Result<Vec<PostPoolData>, SameyError> {
|
||||||
|
let mut query = SameyPool::find()
|
||||||
|
.inner_join(SameyPoolPost)
|
||||||
|
.select_column(samey_pool_post::Column::Position)
|
||||||
|
.filter(samey_pool_post::Column::PostId.eq(post_id));
|
||||||
|
query = match user {
|
||||||
|
None => query.filter(samey_pool::Column::IsPublic.into_simple_expr()),
|
||||||
|
Some(user) if user.is_admin => query,
|
||||||
|
Some(user) => query.filter(
|
||||||
|
Condition::any()
|
||||||
|
.add(samey_pool::Column::IsPublic.into_simple_expr())
|
||||||
|
.add(samey_pool::Column::UploaderId.eq(user.id)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
let pools = query.into_model::<PostInPool>().all(db).await?;
|
||||||
|
|
||||||
|
let mut post_pool_datas = Vec::with_capacity(pools.len());
|
||||||
|
for pool in pools.into_iter() {
|
||||||
|
let posts_in_pool = get_posts_in_pool(pool.id, user).all(db).await?;
|
||||||
|
if let Ok(index) = posts_in_pool.binary_search_by(|post| {
|
||||||
|
post.position
|
||||||
|
.partial_cmp(&pool.position)
|
||||||
|
.expect("position should never be NaN")
|
||||||
|
}) {
|
||||||
|
post_pool_datas.push(PostPoolData {
|
||||||
|
id: pool.id,
|
||||||
|
name: pool.name,
|
||||||
|
previous_post_id: index
|
||||||
|
.checked_sub(1)
|
||||||
|
.and_then(|idx| posts_in_pool.get(idx))
|
||||||
|
.map(|post| post.id),
|
||||||
|
next_post_id: posts_in_pool.get(index + 1).map(|post| post.id),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(post_pool_datas)
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, FromQueryResult)]
|
#[derive(Debug, FromQueryResult)]
|
||||||
pub(crate) struct PoolPost {
|
pub(crate) struct PoolPost {
|
||||||
pub(crate) id: i32,
|
pub(crate) id: i32,
|
||||||
|
|
|
||||||
275
src/views.rs
275
src/views.rs
|
|
@ -3,6 +3,7 @@ use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
fs::OpenOptions,
|
fs::OpenOptions,
|
||||||
io::{BufReader, Seek, Write},
|
io::{BufReader, Seek, Write},
|
||||||
|
mem,
|
||||||
num::NonZero,
|
num::NonZero,
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
@ -28,7 +29,7 @@ use tokio::{task::spawn_blocking, try_join};
|
||||||
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,
|
config::{AGE_CONFIRMATION_KEY, APPLICATION_NAME_KEY},
|
||||||
entities::{
|
entities::{
|
||||||
prelude::{
|
prelude::{
|
||||||
SameyConfig, SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag,
|
SameyConfig, SameyPool, SameyPoolPost, SameyPost, SameyPostSource, SameyTag,
|
||||||
|
|
@ -39,8 +40,8 @@ use crate::{
|
||||||
},
|
},
|
||||||
error::SameyError,
|
error::SameyError,
|
||||||
query::{
|
query::{
|
||||||
PoolPost, PostOverview, filter_posts_by_user, get_posts_in_pool, get_tags_for_post,
|
PoolPost, PostOverview, PostPoolData, filter_posts_by_user, get_pool_data_for_post,
|
||||||
search_posts,
|
get_posts_in_pool, get_tags_for_post, search_posts,
|
||||||
},
|
},
|
||||||
video::{generate_thumbnail, get_dimensions_for_video},
|
video::{generate_thumbnail, get_dimensions_for_video},
|
||||||
};
|
};
|
||||||
|
|
@ -67,19 +68,22 @@ mod filters {
|
||||||
#[template(path = "pages/index.html")]
|
#[template(path = "pages/index.html")]
|
||||||
struct IndexTemplate {
|
struct IndexTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
user: Option<User>,
|
user: Option<User>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn index(
|
pub(crate) async fn index(
|
||||||
State(AppState {
|
State(AppState { app_config, .. }): State<AppState>,
|
||||||
application_name, ..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
let application_name = application_name.read().await.clone();
|
let app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
IndexTemplate {
|
IndexTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
user: auth_session.user,
|
user: auth_session.user,
|
||||||
}
|
}
|
||||||
.render()?,
|
.render()?,
|
||||||
|
|
@ -92,21 +96,30 @@ pub(crate) async fn index(
|
||||||
#[template(path = "pages/login.html")]
|
#[template(path = "pages/login.html")]
|
||||||
struct LoginPageTemplate {
|
struct LoginPageTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn login_page(
|
pub(crate) async fn login_page(
|
||||||
State(AppState {
|
State(AppState { app_config, .. }): State<AppState>,
|
||||||
application_name, ..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
if auth_session.user.is_some() {
|
if auth_session.user.is_some() {
|
||||||
return Ok(Redirect::to("/").into_response());
|
return Ok(Redirect::to("/").into_response());
|
||||||
}
|
}
|
||||||
|
|
||||||
let application_name = application_name.read().await.clone();
|
let app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
|
|
||||||
Ok(Html(LoginPageTemplate { application_name }.render()?).into_response())
|
Ok(Html(
|
||||||
|
LoginPageTemplate {
|
||||||
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn login(
|
pub(crate) async fn login(
|
||||||
|
|
@ -140,21 +153,30 @@ pub(crate) async fn logout(mut auth_session: AuthSession) -> Result<impl IntoRes
|
||||||
#[template(path = "pages/upload.html")]
|
#[template(path = "pages/upload.html")]
|
||||||
struct UploadPageTemplate {
|
struct UploadPageTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn upload_page(
|
pub(crate) async fn upload_page(
|
||||||
State(AppState {
|
State(AppState { app_config, .. }): State<AppState>,
|
||||||
application_name, ..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
if auth_session.user.is_none() {
|
if auth_session.user.is_none() {
|
||||||
return Err(SameyError::Forbidden);
|
return Err(SameyError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let application_name = application_name.read().await.clone();
|
let app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
|
|
||||||
Ok(Html(UploadPageTemplate { application_name }.render()?).into_response())
|
Ok(Html(
|
||||||
|
UploadPageTemplate {
|
||||||
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Format {
|
enum Format {
|
||||||
|
|
@ -572,6 +594,7 @@ pub(crate) async fn select_tag(
|
||||||
#[template(path = "pages/posts.html")]
|
#[template(path = "pages/posts.html")]
|
||||||
struct PostsTemplate<'a> {
|
struct PostsTemplate<'a> {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
tags: Option<Vec<&'a str>>,
|
tags: Option<Vec<&'a str>>,
|
||||||
tags_text: Option<String>,
|
tags_text: Option<String>,
|
||||||
posts: Vec<PostOverview>,
|
posts: Vec<PostOverview>,
|
||||||
|
|
@ -593,16 +616,15 @@ pub(crate) async fn posts(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn posts_page(
|
pub(crate) async fn posts_page(
|
||||||
State(AppState {
|
State(AppState { db, app_config, .. }): 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 app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
let tags = query
|
let tags = query
|
||||||
.tags
|
.tags
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -625,6 +647,7 @@ pub(crate) async fn posts_page(
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
PostsTemplate {
|
PostsTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
tags_text: tags.as_ref().map(|tags| tags.iter().join(" ")),
|
tags_text: tags.as_ref().map(|tags| tags.iter().join(" ")),
|
||||||
tags,
|
tags,
|
||||||
posts,
|
posts,
|
||||||
|
|
@ -641,21 +664,30 @@ pub(crate) async fn posts_page(
|
||||||
#[template(path = "pages/create_pool.html")]
|
#[template(path = "pages/create_pool.html")]
|
||||||
struct CreatePoolPageTemplate {
|
struct CreatePoolPageTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn create_pool_page(
|
pub(crate) async fn create_pool_page(
|
||||||
State(AppState {
|
State(AppState { app_config, .. }): State<AppState>,
|
||||||
application_name, ..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
if auth_session.user.is_none() {
|
if auth_session.user.is_none() {
|
||||||
return Err(SameyError::Forbidden);
|
return Err(SameyError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let application_name = application_name.read().await.clone();
|
let app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
|
|
||||||
Ok(Html(CreatePoolPageTemplate { application_name }.render()?).into_response())
|
Ok(Html(
|
||||||
|
CreatePoolPageTemplate {
|
||||||
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
|
}
|
||||||
|
.render()?,
|
||||||
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn get_pools(
|
pub(crate) async fn get_pools(
|
||||||
|
|
@ -669,21 +701,21 @@ pub(crate) async fn get_pools(
|
||||||
#[template(path = "pages/pools.html")]
|
#[template(path = "pages/pools.html")]
|
||||||
struct GetPoolsTemplate {
|
struct GetPoolsTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
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 {
|
State(AppState { db, app_config, .. }): 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 app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
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(),
|
||||||
|
|
@ -702,6 +734,7 @@ pub(crate) async fn get_pools_page(
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
GetPoolsTemplate {
|
GetPoolsTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
pools,
|
pools,
|
||||||
page,
|
page,
|
||||||
page_count,
|
page_count,
|
||||||
|
|
@ -741,21 +774,21 @@ pub(crate) async fn create_pool(
|
||||||
#[template(path = "pages/pool.html")]
|
#[template(path = "pages/pool.html")]
|
||||||
struct ViewPoolTemplate {
|
struct ViewPoolTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
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 {
|
State(AppState { db, app_config, .. }): 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 app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
let pool = SameyPool::find_by_id(pool_id)
|
let pool = SameyPool::find_by_id(pool_id)
|
||||||
.one(&db)
|
.one(&db)
|
||||||
.await?
|
.await?
|
||||||
|
|
@ -777,6 +810,7 @@ pub(crate) async fn view_pool(
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ViewPoolTemplate {
|
ViewPoolTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
pool,
|
pool,
|
||||||
can_edit,
|
can_edit,
|
||||||
posts,
|
posts,
|
||||||
|
|
@ -1063,21 +1097,21 @@ pub(crate) async fn sort_pool(
|
||||||
#[template(path = "pages/settings.html")]
|
#[template(path = "pages/settings.html")]
|
||||||
struct SettingsTemplate {
|
struct SettingsTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn settings(
|
pub(crate) async fn settings(
|
||||||
State(AppState {
|
State(AppState { db, app_config, .. }): State<AppState>,
|
||||||
db,
|
|
||||||
application_name,
|
|
||||||
..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
if auth_session.user.is_none_or(|user| !user.is_admin) {
|
if auth_session.user.is_none_or(|user| !user.is_admin) {
|
||||||
return Err(SameyError::Forbidden);
|
return Err(SameyError::Forbidden);
|
||||||
}
|
}
|
||||||
|
|
||||||
let application_name = application_name.read().await.clone();
|
let app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
|
|
||||||
let config = SameyConfig::find().all(&db).await?;
|
let config = SameyConfig::find().all(&db).await?;
|
||||||
|
|
||||||
|
|
@ -1093,21 +1127,22 @@ pub(crate) async fn settings(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
SettingsTemplate { application_name }.render_with_values(&values)?,
|
SettingsTemplate {
|
||||||
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
|
}
|
||||||
|
.render_with_values(&values)?,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub(crate) struct UpdateSettingsForm {
|
pub(crate) struct UpdateSettingsForm {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn update_settings(
|
pub(crate) async fn update_settings(
|
||||||
State(AppState {
|
State(AppState { db, app_config, .. }): State<AppState>,
|
||||||
db,
|
|
||||||
application_name,
|
|
||||||
..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Form(body): Form<UpdateSettingsForm>,
|
Form(body): Form<UpdateSettingsForm>,
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
) -> Result<impl IntoResponse, SameyError> {
|
||||||
|
|
@ -1118,7 +1153,10 @@ pub(crate) async fn update_settings(
|
||||||
let mut configs = vec![];
|
let mut configs = vec![];
|
||||||
|
|
||||||
if !body.application_name.is_empty() {
|
if !body.application_name.is_empty() {
|
||||||
*application_name.write().await = body.application_name.clone();
|
let _ = mem::replace(
|
||||||
|
&mut app_config.write().await.application_name,
|
||||||
|
body.application_name.clone(),
|
||||||
|
);
|
||||||
configs.push(samey_config::ActiveModel {
|
configs.push(samey_config::ActiveModel {
|
||||||
key: Set(APPLICATION_NAME_KEY.into()),
|
key: Set(APPLICATION_NAME_KEY.into()),
|
||||||
data: Set(body.application_name.into()),
|
data: Set(body.application_name.into()),
|
||||||
|
|
@ -1126,16 +1164,29 @@ pub(crate) async fn update_settings(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
SameyConfig::insert_many(configs)
|
let age_confirmation = body.age_confirmation.is_some();
|
||||||
.on_conflict(
|
let _ = mem::replace(
|
||||||
OnConflict::column(samey_config::Column::Key)
|
&mut app_config.write().await.age_confirmation,
|
||||||
.update_column(samey_config::Column::Data)
|
age_confirmation,
|
||||||
.to_owned(),
|
);
|
||||||
)
|
configs.push(samey_config::ActiveModel {
|
||||||
.exec(&db)
|
key: Set(AGE_CONFIRMATION_KEY.into()),
|
||||||
.await?;
|
data: Set(age_confirmation.into()),
|
||||||
|
..Default::default()
|
||||||
|
});
|
||||||
|
|
||||||
Ok("")
|
if !configs.is_empty() {
|
||||||
|
SameyConfig::insert_many(configs)
|
||||||
|
.on_conflict(
|
||||||
|
OnConflict::column(samey_config::Column::Key)
|
||||||
|
.update_column(samey_config::Column::Data)
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
.exec(&db)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Redirect::to("/"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single post views
|
// Single post views
|
||||||
|
|
@ -1144,7 +1195,9 @@ pub(crate) async fn update_settings(
|
||||||
#[template(path = "pages/view_post.html")]
|
#[template(path = "pages/view_post.html")]
|
||||||
struct ViewPostPageTemplate {
|
struct ViewPostPageTemplate {
|
||||||
application_name: String,
|
application_name: String,
|
||||||
|
age_confirmation: bool,
|
||||||
post: samey_post::Model,
|
post: samey_post::Model,
|
||||||
|
pool_data: Vec<PostPoolData>,
|
||||||
tags: Vec<samey_tag::Model>,
|
tags: Vec<samey_tag::Model>,
|
||||||
tags_text: Option<String>,
|
tags_text: Option<String>,
|
||||||
tags_post: String,
|
tags_post: String,
|
||||||
|
|
@ -1155,18 +1208,30 @@ struct ViewPostPageTemplate {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn view_post_page(
|
pub(crate) async fn view_post_page(
|
||||||
State(AppState {
|
State(AppState { db, app_config, .. }): State<AppState>,
|
||||||
db,
|
|
||||||
application_name,
|
|
||||||
..
|
|
||||||
}): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
auth_session: AuthSession,
|
||||||
Query(query): Query<PostsQuery>,
|
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 app_config = app_config.read().await;
|
||||||
|
let application_name = app_config.application_name.clone();
|
||||||
|
let age_confirmation = app_config.age_confirmation;
|
||||||
|
drop(app_config);
|
||||||
|
|
||||||
|
let post = SameyPost::find_by_id(post_id)
|
||||||
|
.one(&db)
|
||||||
|
.await?
|
||||||
|
.ok_or(SameyError::NotFound)?;
|
||||||
|
|
||||||
|
let can_edit = match auth_session.user.as_ref() {
|
||||||
|
None => false,
|
||||||
|
Some(user) => user.is_admin || post.uploader_id == user.id,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !post.is_public && !can_edit {
|
||||||
|
return Err(SameyError::NotFound);
|
||||||
|
}
|
||||||
|
|
||||||
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_post = tags.iter().map(|tag| &tag.name).join(" ");
|
let tags_post = tags.iter().map(|tag| &tag.name).join(" ");
|
||||||
|
|
||||||
|
|
@ -1175,11 +1240,6 @@ pub(crate) async fn view_post_page(
|
||||||
.all(&db)
|
.all(&db)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let post = SameyPost::find_by_id(post_id)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
.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_posts_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)
|
||||||
|
|
@ -1230,19 +1290,14 @@ pub(crate) async fn view_post_page(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let can_edit = match auth_session.user {
|
let pool_data = get_pool_data_for_post(&db, post_id, auth_session.user.as_ref()).await?;
|
||||||
None => false,
|
|
||||||
Some(user) => user.is_admin || post.uploader_id == user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !post.is_public && !can_edit {
|
|
||||||
return Err(SameyError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Html(
|
Ok(Html(
|
||||||
ViewPostPageTemplate {
|
ViewPostPageTemplate {
|
||||||
application_name,
|
application_name,
|
||||||
|
age_confirmation,
|
||||||
post,
|
post,
|
||||||
|
pool_data,
|
||||||
tags,
|
tags,
|
||||||
tags_text: query.tags,
|
tags_text: query.tags,
|
||||||
tags_post,
|
tags_post,
|
||||||
|
|
@ -1538,62 +1593,6 @@ pub(crate) async fn remove_field() -> impl IntoResponse {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "fragments/get_image_media.html")]
|
|
||||||
struct GetMediaTemplate {
|
|
||||||
post: samey_post::Model,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_media(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
|
||||||
Path(post_id): Path<i32>,
|
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
|
||||||
let post = SameyPost::find_by_id(post_id)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
.ok_or(SameyError::NotFound)?;
|
|
||||||
|
|
||||||
let can_edit = match auth_session.user {
|
|
||||||
None => false,
|
|
||||||
Some(user) => user.is_admin || post.uploader_id == user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !post.is_public && !can_edit {
|
|
||||||
return Err(SameyError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Html(GetMediaTemplate { post }.render()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Template)]
|
|
||||||
#[template(path = "fragments/get_full_image_media.html")]
|
|
||||||
struct GetFullMediaTemplate {
|
|
||||||
post: samey_post::Model,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) async fn get_full_media(
|
|
||||||
State(AppState { db, .. }): State<AppState>,
|
|
||||||
auth_session: AuthSession,
|
|
||||||
Path(post_id): Path<i32>,
|
|
||||||
) -> Result<impl IntoResponse, SameyError> {
|
|
||||||
let post = SameyPost::find_by_id(post_id)
|
|
||||||
.one(&db)
|
|
||||||
.await?
|
|
||||||
.ok_or(SameyError::NotFound)?;
|
|
||||||
|
|
||||||
let can_edit = match auth_session.user {
|
|
||||||
None => false,
|
|
||||||
Some(user) => user.is_admin || post.uploader_id == user.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
if !post.is_public && !can_edit {
|
|
||||||
return Err(SameyError::NotFound);
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Html(GetFullMediaTemplate { post }.render()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
||||||
|
|
|
||||||
5
static/alpine.js
Normal file
5
static/alpine.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -2,6 +2,10 @@ img {
|
||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dialog::backdrop {
|
||||||
|
backdrop-filter: blur(32px);
|
||||||
|
}
|
||||||
|
|
||||||
div.center-item {
|
div.center-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
20
templates/fragments/age_restricted_check.html
Normal file
20
templates/fragments/age_restricted_check.html
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
<dialog
|
||||||
|
x-init="localStorage.ageVerified !== 'true' && $el.showModal()"
|
||||||
|
@cancel.prevent
|
||||||
|
@close-modal="$el.close()"
|
||||||
|
>
|
||||||
|
<header>Age restricted website</header>
|
||||||
|
<p>You must be 18+ to access this page.</p>
|
||||||
|
<p>
|
||||||
|
By confirming that you are at least 18 years old, your selection will be
|
||||||
|
saved to your browser to prevent this screen from appearing in the future.
|
||||||
|
</p>
|
||||||
|
<menu>
|
||||||
|
<button
|
||||||
|
@click="localStorage.ageVerified = 'true'; $event.target.dispatchEvent(new CustomEvent('close-modal', { bubbles: true }))"
|
||||||
|
>
|
||||||
|
I agree and am 18+ years old
|
||||||
|
</button>
|
||||||
|
<button @click="location.href = 'about:blank'">Take me back!</button>
|
||||||
|
</menu>
|
||||||
|
</dialog>
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<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="/static/htmx.js"></script>
|
<script src="/static/htmx.js"></script>
|
||||||
|
<script defer src="/static/alpine.js"></script>
|
||||||
<link rel="stylesheet" href="/static/water.css" />
|
<link rel="stylesheet" href="/static/water.css" />
|
||||||
<link rel="stylesheet" href="/static/samey.css" />
|
<link rel="stylesheet" href="/static/samey.css" />
|
||||||
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
<meta name="generator" content="Samey {{ env!("CARGO_PKG_VERSION") }}" />
|
||||||
<meta property="og:site_name" content="{{ application_name }}" />
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<img
|
|
||||||
hx-get="/media/{{ post.id }}"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
id="media"
|
|
||||||
src="/files/{{ post.media }}"
|
|
||||||
style="width: {{ post.width }}px; height: {{ post.height }}px; aspect-ratio: {{ post.width }} / {{ post.height }}; cursor: zoom-out"
|
|
||||||
/>
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
<img
|
<img
|
||||||
hx-get="/media/{{ post.id }}/full"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
id="media"
|
id="media"
|
||||||
|
@click="maximized = !maximized"
|
||||||
|
@keyup.enter="maximized = !maximized"
|
||||||
|
tabindex="0"
|
||||||
src="/files/{{ post.media }}"
|
src="/files/{{ post.media }}"
|
||||||
style="width: 100%; height: 100%; max-width: {{ post.width }}px; max-height: {{ post.height }}px; aspect-ratio: {{ post.width }} / {{ post.height }}; cursor: zoom-in"
|
:style="maximized ? { width: width + 'px', height: height + 'px', 'aspect-ratio': width + ' / ' + height, cursor: 'zoom-out' } : { width: '100%', height: '100%', 'max-width': width + 'px', 'max-height': height + 'px', 'aspect-ratio': width + ' / ' + height, cursor: 'zoom-in' }"
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,5 @@
|
||||||
id="media"
|
id="media"
|
||||||
src="/files/{{ post.media }}"
|
src="/files/{{ post.media }}"
|
||||||
controls="controls"
|
controls="controls"
|
||||||
style="width: 100%; height: 100%; max-width: {{ post.width }}px; max-height: {{ post.height }}px; aspect-ratio: {{ post.width }} / {{ post.height }}"
|
:style="{ width: '100%', height: '100%', 'max-width': width + 'px', 'max-height': height + 'px', 'aspect-ratio': width + ' / ' + height }"
|
||||||
/>
|
></video>
|
||||||
|
|
|
||||||
|
|
@ -1,55 +1,58 @@
|
||||||
<div id="post-details" hx-target="this" hx-swap="outerHTML">
|
<article id="post-details" hx-target="this" hx-swap="outerHTML">
|
||||||
<div>
|
<h2>
|
||||||
<label>Title</label>
|
{% if let Some(title) = post.title %}{{ title }}{% else %}Details{%
|
||||||
{% if let Some(title) = post.title %}{{ title }}{% else %}<em>None</em>{%
|
|
||||||
endif %}
|
endif %}
|
||||||
</div>
|
</h2>
|
||||||
<div>
|
{% if let Some(description) = post.description %}
|
||||||
<label>Description</label>
|
<div id="description">{{ description | markdown }}</div>
|
||||||
{% if let Some(description) = post.description %}{{ description | markdown
|
{% endif %}
|
||||||
}}{% else %}
|
<table>
|
||||||
<p><em>None</em></p>
|
<tr>
|
||||||
{% endif %}
|
<th>Is public post?</th>
|
||||||
</div>
|
<td>
|
||||||
<div>
|
{% if post.is_public %}Yes{% else %}No{% endif %}
|
||||||
<label>Is public?</label>
|
</td>
|
||||||
{% if post.is_public %}Yes{% else %}No{% endif %}
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
<div>
|
<th>Rating</th>
|
||||||
<label>Rating</label>
|
<td>
|
||||||
{% match post.rating.as_ref() %} {% when "u" %} Unrated {% when "s" %} Safe
|
{% match post.rating.as_ref() %} {% when "u" %} Unrated {% when "s" %} Safe
|
||||||
{% when "q" %} Questionable {% when "e" %} Explicit {% else %} Unknown {%
|
{% when "q" %} Questionable {% when "e" %} Explicit {% else %} Unknown {%
|
||||||
endmatch %}
|
endmatch %}
|
||||||
</div>
|
</td>
|
||||||
<div>
|
</tr>
|
||||||
<label>Source(s)</label>
|
<tr>
|
||||||
{% if sources.is_empty() %}
|
<th>Source(s)</th>
|
||||||
<em>None</em>{% else %}
|
<td>
|
||||||
<ul>
|
{% if sources.is_empty() %}
|
||||||
{% for source in sources %}
|
<em>None</em>{% else %}
|
||||||
<li id="source-{{ source.id }}">
|
<ul class="reset">
|
||||||
<a href="{{ source.url }}">{{ source.url }}</a>
|
{% for source in sources %}
|
||||||
</li>
|
<li id="source-{{ source.id }}">
|
||||||
{% endfor %}
|
<a href="{{ source.url }}">{{ source.url }}</a>
|
||||||
</ul>
|
</li>
|
||||||
{% endif %}
|
{% endfor %}
|
||||||
</div>
|
</ul>
|
||||||
<div>
|
{% endif %}
|
||||||
<label>Type</label>
|
</td>
|
||||||
{{ post.media_type | capitalize }}
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
<div>
|
<th>Type</th>
|
||||||
<label>Width</label>
|
<td>{{ post.media_type | capitalize }}</td>
|
||||||
{{ post.width }}px
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
<div>
|
<th>Width</th>
|
||||||
<label>Height</label>
|
<td>{{ post.width }}px</td>
|
||||||
{{ post.height }}px
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
<div>
|
<th>Height</th>
|
||||||
<label>Upload date</label>
|
<td>{{ post.height }}px</td>
|
||||||
{{ post.uploaded_at }}
|
</tr>
|
||||||
</div>
|
<tr>
|
||||||
|
<th>Upload date</th>
|
||||||
|
<td>{{ post.uploaded_at }}</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
<button hx-get="/post_details/{{ post.id }}/edit">Edit post</button>
|
<button hx-get="/post_details/{{ post.id }}/edit">Edit post</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Create pool - {{ application_name }}</title>
|
<title>Create pool - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Create pool</h1>
|
<h1>Create pool</h1>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>{{ application_name }}</title>
|
<title>{{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<main>
|
<main>
|
||||||
<h1>{{ application_name }}</h1>
|
<h1>{{ application_name }}</h1>
|
||||||
<article>
|
<article>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Login - {{ application_name }}</title>
|
<title>Login - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Login</h1>
|
<h1>Login</h1>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Pool - {{ pool.name }} - {{ application_name }}</title>
|
<title>Pool - {{ pool.name }} - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
<script src="/static/sortable.js"></script>
|
<script src="/static/sortable.js"></script>
|
||||||
<meta property="og:title" content="{{ pool.name }}" />
|
<meta property="og:title" content="{{ pool.name }}" />
|
||||||
|
|
@ -47,16 +48,37 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1 id="pool-title">Pool - {{ pool.name }}</h1>
|
<h1 id="pool-title">Pool - {{ pool.name }}</h1>
|
||||||
|
<article>
|
||||||
|
<h2>Posts</h2>
|
||||||
|
{% include "fragments/pool_posts.html" %}
|
||||||
|
</article>
|
||||||
</main>
|
</main>
|
||||||
<article>
|
|
||||||
<h2>Posts</h2>
|
|
||||||
{% include "fragments/pool_posts.html" %}
|
|
||||||
</article>
|
|
||||||
{% if can_edit %}
|
{% if can_edit %}
|
||||||
|
<hr />
|
||||||
<article>
|
<article>
|
||||||
|
<h2>Add post to pool</h2>
|
||||||
|
<form
|
||||||
|
hx-post="/pool/{{ pool.id }}/post"
|
||||||
|
hx-target="#pool-posts"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="add-post-input"
|
||||||
|
type="text"
|
||||||
|
name="post_id"
|
||||||
|
placeholder="Post ID"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
/>
|
||||||
|
<button>Add post</button>
|
||||||
|
</form>
|
||||||
|
</article>
|
||||||
|
<article>
|
||||||
|
<h2>Pool settings</h2>
|
||||||
<div>
|
<div>
|
||||||
<label>Rename pool</label>
|
<label>Rename pool</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -66,7 +88,6 @@
|
||||||
hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
|
hx-trigger="input changed delay:500ms, keyup[key=='Enter']"
|
||||||
hx-swap="none"
|
hx-swap="none"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
pattern="[0-9]*"
|
|
||||||
value="{{ pool.name }}"
|
value="{{ pool.name }}"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -85,23 +106,6 @@
|
||||||
value="true"
|
value="true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<form
|
|
||||||
hx-post="/pool/{{ pool.id }}/post"
|
|
||||||
hx-target="#pool-posts"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<label>Add post</label>
|
|
||||||
<input
|
|
||||||
id="add-post-input"
|
|
||||||
type="text"
|
|
||||||
name="post_id"
|
|
||||||
placeholder="Post ID"
|
|
||||||
pattern="[0-9]*"
|
|
||||||
/>
|
|
||||||
<button>Add post</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Pools - {{ application_name }}</title>
|
<title>Pools - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Pools</h1>
|
<h1>Pools</h1>
|
||||||
|
|
@ -18,8 +21,10 @@
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
|
<hr />
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<div class="flex"><span>Pages</span></div>
|
||||||
|
<ul class="reset flex">
|
||||||
{% for i in 1..=page_count %}
|
{% for i in 1..=page_count %}
|
||||||
<li>
|
<li>
|
||||||
{% if i == page as u64 %}
|
{% if i == page as u64 %}
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Posts - {{ application_name }}</title>
|
<title>Posts - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<article>
|
<article>
|
||||||
<h2>Search</h2>
|
<h2>Search</h2>
|
||||||
|
|
@ -49,8 +52,10 @@
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<hr>
|
||||||
<div>
|
<div>
|
||||||
<ul>
|
<div class="flex"><span>Pages</span></div>
|
||||||
|
<ul class="reset flex">
|
||||||
{% for i in 1..=page_count %}
|
{% for i in 1..=page_count %}
|
||||||
<li>
|
<li>
|
||||||
{% if i == page as u64 %}
|
{% if i == page as u64 %}
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,38 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Settings - {{ application_name }}</title>
|
<title>Settings - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Settings</h1>
|
<h1>Settings</h1>
|
||||||
<form hx-put="/settings" hx-swap="none">
|
<form method="post" action="/settings">
|
||||||
<label>Application name</label>
|
<div>
|
||||||
<input
|
<label>Application name</label>
|
||||||
name="application_name"
|
<input
|
||||||
type="text"
|
name="application_name"
|
||||||
value="{{ application_name }}"
|
type="text"
|
||||||
/>
|
value="{{ application_name }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>Ask for age confirmation?</label>
|
||||||
|
<input
|
||||||
|
name="age_confirmation"
|
||||||
|
type="checkbox"
|
||||||
|
{%
|
||||||
|
if
|
||||||
|
age_confirmation
|
||||||
|
%}checked{%
|
||||||
|
endif
|
||||||
|
%}
|
||||||
|
value="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<button>Save changes</button>
|
<button>Save changes</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
</main>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,12 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Upload media - {{ application_name }}</title>
|
<title>Upload media - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="/">< To home</a></div>
|
<div><a href="/">< To home</a></div>
|
||||||
<main>
|
<main>
|
||||||
<h1>Upload media</h1>
|
<h1>Upload media</h1>
|
||||||
|
|
@ -15,7 +18,7 @@
|
||||||
type="file"
|
type="file"
|
||||||
id="media-file"
|
id="media-file"
|
||||||
name="media-file"
|
name="media-file"
|
||||||
accept=".jpg, .jpeg, .png, .webp, .gif, .mp4, .webm, .mkv, .mov"
|
accept=".jpg, .jpeg, .png, .webp, .gif, .bmp, .tiff, .mp4, .webm, .mkv, .mov"
|
||||||
/>
|
/>
|
||||||
<button type="submit">Create post</button>
|
<button type="submit">Create post</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
<title>Post #{{ post.id }} - {{ application_name }}</title>
|
||||||
|
<meta property="og:site_name" content="{{ application_name }}" />
|
||||||
{% include "fragments/common_headers.html" %}
|
{% include "fragments/common_headers.html" %}
|
||||||
{% if let Some(title) = post.title %}<meta property="og:title" content="{{ title }}"/>{% else %}<meta property="og:title" content="{{ tags_post }}" />{% endif %}
|
{% if let Some(title) = post.title %}<meta property="og:title" content="{{ title }}"/>{% else %}<meta property="og:title" content="{{ tags_post }}" />{% endif %}
|
||||||
<meta property="og:url" content="/post/{{ post.id }}" />
|
<meta property="og:url" content="/post/{{ post.id }}" />
|
||||||
|
|
@ -12,11 +13,11 @@
|
||||||
<meta property="og:image:height" content="{{ post.height }}" />
|
<meta property="og:image:height" content="{{ post.height }}" />
|
||||||
<meta property="og:image:alt" content="{{ tags_post }}" />
|
<meta property="og:image:alt" content="{{ tags_post }}" />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
{% if let Some(title) = post.title %}<meta property="twitter:title" content="{{ title }}"/>{% else %}<meta property="twitter:title" content="{{ tags_post }}"/>{% endif %}
|
{% if let Some(title) = post.title %}<meta property="twitter:title" content="{{ title }}"/>{% else %}<meta property="twitter:title" content="{{ tags_post }}" />{% endif %}
|
||||||
{% if let Some(description) = post.description %}<meta property="twitter:description" content="{{ description }}" />{% endif %}
|
{% if let Some(description) = post.description %}<meta property="twitter:description" content="{{ description }}" />{% endif %}
|
||||||
<meta property="twitter:image" content="/files/{{ post.media }}" />
|
<meta property="twitter:image" content="/files/{{ post.media }}" />
|
||||||
{% when "video" %}
|
{% when "video" %}
|
||||||
<meta property="og:type" content="video.other">
|
<meta property="og:type" content="video.other" />
|
||||||
<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 }}" />
|
||||||
|
|
@ -25,6 +26,8 @@
|
||||||
{% else %} {% endmatch %}
|
{% else %} {% endmatch %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
{% if age_confirmation %}{% include "fragments/age_restricted_check.html"
|
||||||
|
%}{% endif %}
|
||||||
<div><a href="{% if let Some(tags_text) = tags_text %}/posts/1?tags={{ tags_text.replace(' ', "+") }}{% else %}/posts/1{% endif %}">< To posts</a></div>
|
<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>
|
<article>
|
||||||
<h2>Search</h2>
|
<h2>Search</h2>
|
||||||
|
|
@ -34,44 +37,48 @@
|
||||||
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="{% if let Some(tags_text) = tags_text %}{{ tags_text }}{% endif %}"
|
value="{% if let Some(tags_text) = tags_text %}{{ tags_text }}{% endif %}"
|
||||||
autofocus
|
|
||||||
/>
|
/>
|
||||||
<ul class="reset tags-autocomplete" id="search-autocomplete"></ul>
|
<ul class="reset tags-autocomplete" id="search-autocomplete"></ul>
|
||||||
<button type="submit">Search</button>
|
<button type="submit">Search</button>
|
||||||
</form>
|
</form>
|
||||||
</article>
|
</article>
|
||||||
<article>
|
<article>
|
||||||
<h2>Tags</h2>
|
<table>
|
||||||
{% if tags.is_empty() %}
|
{% for item in pool_data %}
|
||||||
<p>No tags in post. Consider adding some!</p>
|
<tr>
|
||||||
{% else %}
|
<td>
|
||||||
<ul id="tags-list">
|
{% if let Some(previous_post_id) = item.previous_post_id %}
|
||||||
{% for tag in tags %}
|
<a href="/post/{{ previous_post_id }}">< Previous</a>
|
||||||
<li>
|
{% endif %}
|
||||||
<a href="/posts?tags={{ tag.name }}">{{ tag.name }}</a>
|
</td>
|
||||||
</li>
|
<th>
|
||||||
|
<a href="/pool/{{ item.id }}">Pool: {{ item.name }}</a>
|
||||||
|
</th>
|
||||||
|
<td>
|
||||||
|
{% if let Some(next_post_id) = item.next_post_id %}
|
||||||
|
<a href="/post/{{ next_post_id }}">Next ></a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</table>
|
||||||
{% endif %}
|
|
||||||
</article>
|
</article>
|
||||||
<main>
|
<main>
|
||||||
<h1>View post #{{ post.id }}</h1>
|
<h1>View post #{{ post.id }}</h1>
|
||||||
<div class="center-item">
|
<div class="center-item" x-data="{ maximized: false, width: {{ post.width }}, height: {{ post.height }} }">
|
||||||
{% match post.media_type.as_ref() %}{% when "image" %}{% include
|
{% match post.media_type.as_ref() %}{% when "image" %}{% include
|
||||||
"fragments/get_image_media.html" %}{% when "video" %}{% include
|
"fragments/get_image_media.html" %}{% when "video" %}{% include
|
||||||
"fragments/get_video_media.html" %}{% else %}{% endmatch %}
|
"fragments/get_video_media.html" %}{% else %}{% endmatch %}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<article>
|
{% include "fragments/post_details.html" %}
|
||||||
<h2>Details</h2>
|
|
||||||
{% include "fragments/post_details.html" %}
|
|
||||||
</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 post</h2>
|
<h2>Parent post</h2>
|
||||||
|
|
@ -105,5 +112,19 @@
|
||||||
</ul>
|
</ul>
|
||||||
</article>
|
</article>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<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>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue