Nová domovní stránka

Tento commit je obsažen v:
sneedmaster 2024-01-02 12:58:47 +01:00
rodič 6f4403e376
revize 11bba18d93
33 změnil soubory, kde provedl 312 přidání a 178 odebrání

Zobrazit soubor

@ -2,6 +2,7 @@ CREATE TABLE news (
id SERIAL NOT NULL PRIMARY KEY, id SERIAL NOT NULL PRIMARY KEY,
title VARCHAR(256) NOT NULL, title VARCHAR(256) NOT NULL,
content TEXT NOT NULL, content TEXT NOT NULL,
content_nomarkup TEXT NOT NULL,
author VARCHAR(32) NOT NULL REFERENCES accounts(username), author VARCHAR(32) NOT NULL REFERENCES accounts(username),
created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
); );

Zobrazit soubor

@ -0,0 +1,2 @@
ALTER TABLE bans DROP CONSTRAINT bans_issued_by_fkey;
ALTER TABLE news DROP CONSTRAINT news_author_fkey;

Zobrazit soubor

@ -26,9 +26,9 @@ impl Board {
query(&format!( query(&format!(
r#"CREATE TABLE posts_{} ( r#"CREATE TABLE posts_{} (
id SERIAL NOT NULL PRIMARY KEY, id BIGSERIAL NOT NULL PRIMARY KEY,
board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id), board VARCHAR(16) NOT NULL DEFAULT '{}' REFERENCES boards(id),
thread INT DEFAULT NULL REFERENCES posts_{}(id), thread BIGINT DEFAULT NULL REFERENCES posts_{}(id),
name VARCHAR(32) NOT NULL, name VARCHAR(32) NOT NULL,
user_id VARCHAR(6) NOT NULL DEFAULT '000000', user_id VARCHAR(6) NOT NULL DEFAULT '000000',
tripcode VARCHAR(12) DEFAULT NULL, tripcode VARCHAR(12) DEFAULT NULL,
@ -114,6 +114,14 @@ impl Board {
Ok(boards) Ok(boards)
} }
pub async fn read_post_count(&self, ctx: &Ctx) -> Result<i64, NekrochanError> {
let (count,) = query_as(&format!("SELECT last_value FROM posts_{}_id_seq", self.id))
.fetch_one(ctx.db())
.await?;
Ok(count)
}
pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> { pub async fn update_name(&self, ctx: &Ctx, name: String) -> Result<(), NekrochanError> {
query("UPDATE boards SET name = $1 WHERE id = $2") query("UPDATE boards SET name = $1 WHERE id = $2")
.bind(&name) .bind(&name)

30
src/db/local_stats.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,30 @@
use sqlx::query_as;
use crate::{error::NekrochanError, ctx::Ctx};
use super::models::LocalStats;
impl LocalStats {
pub async fn read(ctx: &Ctx) -> Result<Self, NekrochanError> {
let (post_count,) = query_as(
"SELECT coalesce(sum(last_value)::bigint, 0) FROM pg_sequences WHERE sequencename LIKE 'posts_%_id_seq'",
)
.fetch_one(ctx.db())
.await?;
let (file_count, file_size) = query_as(
r#"SELECT count(files), coalesce(sum((files->>'size')::bigint)::bigint, 0) FROM (
SELECT jsonb_array_elements(files) AS files FROM overboard
) flatten"#,
)
.fetch_one(ctx.db())
.await?;
let stats = Self {
post_count,
file_count,
file_size,
};
Ok(stats)
}
}

Zobrazit soubor

@ -5,5 +5,6 @@ mod account;
mod ban; mod ban;
mod banner; mod banner;
mod board; mod board;
mod local_stats;
mod newspost; mod newspost;
mod post; mod post;

Zobrazit soubor

@ -45,18 +45,11 @@ pub struct Report {
pub reporter_ip: IpAddr, pub reporter_ip: IpAddr,
} }
#[derive(FromRow, Serialize, Deserialize)]
pub struct LogRecord {
pub id: i32,
pub message: String,
pub created: DateTime<Utc>,
}
#[derive(FromRow, Serialize, Deserialize)] #[derive(FromRow, Serialize, Deserialize)]
pub struct Post { pub struct Post {
pub id: i32, pub id: i64,
pub board: String, pub board: String,
pub thread: Option<i32>, pub thread: Option<i64>,
pub name: String, pub name: String,
pub user_id: String, pub user_id: String,
pub tripcode: Option<String>, pub tripcode: Option<String>,
@ -89,6 +82,7 @@ pub struct NewsPost {
pub id: i32, pub id: i32,
pub title: String, pub title: String,
pub content: String, pub content: String,
pub content_nomarkup: String,
pub author: String, pub author: String,
pub created: DateTime<Utc>, pub created: DateTime<Utc>,
} }
@ -104,3 +98,9 @@ pub struct File {
pub timestamp: i64, pub timestamp: i64,
pub size: usize, pub size: usize,
} }
pub struct LocalStats {
pub post_count: i64,
pub file_count: i64,
pub file_size: i64,
}

Zobrazit soubor

@ -30,11 +30,21 @@ impl NewsPost {
} }
pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> { pub async fn read_all(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let newsposts = query_as("SELECT * FROM news").fetch_all(ctx.db()).await?; let newsposts = query_as("SELECT * FROM news ORDER BY created DESC")
.fetch_all(ctx.db())
.await?;
Ok(newsposts) Ok(newsposts)
} }
pub async fn read_latest(ctx: &Ctx) -> Result<Option<Self>, NekrochanError> {
let newspost = query_as("SELECT * FROM news ORDER BY created DESC LIMIT 1")
.fetch_optional(ctx.db())
.await?;
Ok(newspost)
}
pub async fn update( pub async fn update(
&self, &self,
ctx: &Ctx, ctx: &Ctx,

Zobrazit soubor

@ -12,7 +12,7 @@ impl Post {
pub async fn create( pub async fn create(
ctx: &Ctx, ctx: &Ctx,
board: &Board, board: &Board,
thread: Option<i32>, thread: Option<i64>,
name: String, name: String,
tripcode: Option<String>, tripcode: Option<String>,
capcode: Option<String>, capcode: Option<String>,
@ -110,7 +110,7 @@ impl Post {
Ok(()) Ok(())
} }
pub async fn read(ctx: &Ctx, board: String, id: i32) -> Result<Option<Self>, NekrochanError> { pub async fn read(ctx: &Ctx, board: String, id: i64) -> Result<Option<Self>, NekrochanError> {
let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2") let post = query_as("SELECT * FROM overboard WHERE board = $1 AND id = $2")
.bind(board) .bind(board)
.bind(id) .bind(id)
@ -181,19 +181,6 @@ impl Post {
Ok(posts) Ok(posts)
} }
pub async fn read_latest(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let posts = query_as(
r#"SELECT * FROM overboard
ORDER BY created DESC
LIMIT $1"#,
)
.bind(15)
.fetch_all(ctx.db())
.await?;
Ok(posts)
}
pub async fn read_reports(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> { pub async fn read_reports(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let posts = query_as( let posts = query_as(
r#"SELECT * FROM overboard r#"SELECT * FROM overboard
@ -206,20 +193,6 @@ impl Post {
Ok(posts) Ok(posts)
} }
pub async fn read_files(ctx: &Ctx) -> Result<Vec<Post>, NekrochanError> {
let posts = query_as(
r#"SELECT *
FROM overboard
WHERE files != '[]'::jsonb
ORDER BY created DESC
LIMIT 3"#,
)
.fetch_all(ctx.db())
.await?;
Ok(posts)
}
pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> { pub async fn read_replies(&self, ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let replies = query_as(&format!( let replies = query_as(&format!(
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC", "SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC",
@ -240,6 +213,14 @@ impl Post {
Ok(posts) Ok(posts)
} }
pub async fn read_all_overboard(ctx: &Ctx) -> Result<Vec<Self>, NekrochanError> {
let posts = query_as("SELECT * FROM overboard")
.fetch_all(ctx.db())
.await?;
Ok(posts)
}
pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> { pub async fn update_user_id(&self, ctx: &Ctx, user_id: String) -> Result<(), NekrochanError> {
query(&format!( query(&format!(
"UPDATE posts_{} SET user_id = $1 WHERE id = $2", "UPDATE posts_{} SET user_id = $1 WHERE id = $2",

Zobrazit soubor

@ -34,6 +34,8 @@ pub enum NekrochanError {
FloodError, FloodError,
#[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)] #[error("Reverzní proxy nevrátilo vyžadovanou hlavičku '{}'.", .0)]
HeaderError(&'static str), HeaderError(&'static str),
#[error("Domovní stránka vznikne po vytvoření nástěnky.")]
HomePageError,
#[error("ID musí mít 1-16 znaků.")] #[error("ID musí mít 1-16 znaků.")]
IdFormatError, IdFormatError,
#[error("Nesprávné řešení CAPTCHA.")] #[error("Nesprávné řešení CAPTCHA.")]
@ -41,7 +43,7 @@ pub enum NekrochanError {
#[error("Nesprávné přihlašovací údaje.")] #[error("Nesprávné přihlašovací údaje.")]
IncorrectCredentialError, IncorrectCredentialError,
#[error("Nesprávné heslo pro příspěvek #{}.", .0)] #[error("Nesprávné heslo pro příspěvek #{}.", .0)]
IncorrectPasswordError(i32), IncorrectPasswordError(i64),
#[error("Nedostatečná oprávnění.")] #[error("Nedostatečná oprávnění.")]
InsufficientPermissionError, InsufficientPermissionError,
#[error("Server se připojil k 41 procentům.")] #[error("Server se připojil k 41 procentům.")]
@ -67,7 +69,7 @@ pub enum NekrochanError {
#[error("Jméno nesmí mít více než 32 znaků.")] #[error("Jméno nesmí mít více než 32 znaků.")]
PostNameFormatError, PostNameFormatError,
#[error("Příspěvek /{}/{} neexistuje.", .0, .1)] #[error("Příspěvek /{}/{} neexistuje.", .0, .1)]
PostNotFound(String, i32), PostNotFound(String, i64),
#[error("Vlákno dosáhlo limitu odpovědí.")] #[error("Vlákno dosáhlo limitu odpovědí.")]
ReplyLimitError, ReplyLimitError,
#[error("Nelze vytvořit odpověď na odpověď.")] #[error("Nelze vytvořit odpověď na odpověď.")]
@ -215,6 +217,7 @@ impl ResponseError for NekrochanError {
NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST, NekrochanError::FileLimitError(_) => StatusCode::BAD_REQUEST,
NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS, NekrochanError::FloodError => StatusCode::TOO_MANY_REQUESTS,
NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY, NekrochanError::HeaderError(_) => StatusCode::BAD_GATEWAY,
NekrochanError::HomePageError => StatusCode::NOT_FOUND,
NekrochanError::IdFormatError => StatusCode::BAD_REQUEST, NekrochanError::IdFormatError => StatusCode::BAD_REQUEST,
NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED, NekrochanError::IncorrectCaptchaError => StatusCode::UNAUTHORIZED,
NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED, NekrochanError::IncorrectCredentialError => StatusCode::UNAUTHORIZED,

Zobrazit soubor

@ -1,19 +1,13 @@
use std::process::Command;
use actix_multipart::form::tempfile::TempFile; use actix_multipart::form::tempfile::TempFile;
use anyhow::Error;
use chrono::Utc; use chrono::Utc;
use glob::glob;
use std::{collections::HashSet, process::Command};
use tokio::{ use tokio::{
fs::{remove_file, rename}, fs::{remove_file, rename},
task::spawn_blocking, task::spawn_blocking,
}; };
use crate::{ use crate::{cfg::Cfg, db::models::File, error::NekrochanError};
cfg::Cfg,
ctx::Ctx,
db::models::{Banner, Board, File, Post},
error::NekrochanError,
};
impl File { impl File {
pub async fn new( pub async fn new(
@ -306,61 +300,3 @@ async fn process_video(
Ok((width, height)) Ok((width, height))
} }
pub async fn cleanup_files(ctx: &Ctx) -> Result<(), Error> {
let mut keep = HashSet::new();
let mut keep_thumbs = HashSet::new();
let banners = Banner::read_all(ctx).await?;
for banner in banners {
keep.insert(format!(
"{}.{}",
banner.banner.timestamp, banner.banner.format
));
}
let boards = Board::read_all(ctx).await?;
for board in boards {
let posts = Post::read_all(ctx, board.id.clone()).await?;
for post in posts {
for file in post.files.0 {
keep.insert(format!("{}.{}", file.timestamp, file.format));
if let Some(thumb_format) = file.thumb_format {
keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format));
}
}
}
}
for file in glob("./uploads/*.*")? {
let file = file?;
let file_name = file.file_name();
if let Some(file_name) = file_name {
let check = file_name.to_string_lossy().to_string();
if !keep.contains(&check) {
remove_file(file).await?;
}
}
}
for file in glob("./uploads/thumb/*.*")? {
let file = file?;
let file_name = file.file_name();
if let Some(file_name) = file_name {
let check = file_name.to_string_lossy().to_string();
if !keep_thumbs.contains(&check) {
remove_file(file).await?;
}
}
}
Ok(())
}

Zobrazit soubor

@ -68,7 +68,7 @@ pub fn czech_humantime(time: &DateTime<Utc>) -> askama::Result<String> {
pub fn czech_datetime(time: &DateTime<Utc>) -> askama::Result<String> { pub fn czech_datetime(time: &DateTime<Utc>) -> askama::Result<String> {
let time = time let time = time
.format_localized("%d.%m.%Y (%a) %H:%M:%S UTC", Locale::cs_CZ) .format_localized("%d.%m.%Y (%a) %H:%M:%S", Locale::cs_CZ)
.to_string(); .to_string();
Ok(time) Ok(time)

Zobrazit soubor

@ -13,6 +13,7 @@ pub mod ctx;
pub mod db; pub mod db;
pub mod error; pub mod error;
pub mod files; pub mod files;
pub mod schedule;
pub mod filters; pub mod filters;
pub mod markup; pub mod markup;
pub mod perms; pub mod perms;

Zobrazit soubor

@ -16,7 +16,7 @@ use nekrochan::{
ctx::Ctx, ctx::Ctx,
db::{cache::init_cache, models::Banner}, db::{cache::init_cache, models::Banner},
error::NekrochanError, error::NekrochanError,
files::cleanup_files, schedule::s_cleanup_files,
web::{self, template_response}, web::{self, template_response},
}; };
use sqlx::migrate; use sqlx::migrate;
@ -46,7 +46,7 @@ async fn run() -> Result<(), Error> {
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
match cleanup_files(&ctx_).await { match s_cleanup_files(&ctx_).await {
Ok(()) => info!("Routine file cleanup successful."), Ok(()) => info!("Routine file cleanup successful."),
Err(err) => error!("Routine file cleanup failed: {err:?}"), Err(err) => error!("Routine file cleanup failed: {err:?}"),
}; };

Zobrazit soubor

@ -107,7 +107,7 @@ fn capcode_fallback(owner: bool) -> String {
pub async fn markup( pub async fn markup(
ctx: &Ctx, ctx: &Ctx,
board: &String, board: &String,
op: Option<i32>, op: Option<i64>,
text: &str, text: &str,
) -> Result<String, NekrochanError> { ) -> Result<String, NekrochanError> {
let text = escape_html(&text); let text = escape_html(&text);
@ -177,8 +177,8 @@ async fn get_quoted_posts(
ctx: &Ctx, ctx: &Ctx,
board: &String, board: &String,
text: &str, text: &str,
) -> Result<HashMap<i32, Post>, NekrochanError> { ) -> Result<HashMap<i64, Post>, NekrochanError> {
let mut quoted_ids: Vec<i32> = Vec::new(); let mut quoted_ids: Vec<i64> = Vec::new();
for quote in QUOTE_REGEX.captures_iter(text) { for quote in QUOTE_REGEX.captures_iter(text) {
let id_raw = &quote[1]; let id_raw = &quote[1];

Zobrazit soubor

@ -12,6 +12,7 @@ pub enum Permissions {
Bans, Bans,
BoardBanners, BoardBanners,
BoardConfig, BoardConfig,
News,
BypassBans, BypassBans,
BypassBoardLock, BypassBoardLock,
BypassThreadLock, BypassThreadLock,
@ -67,6 +68,10 @@ impl PermissionWrapper {
self.0.contains(Permissions::BoardConfig) self.0.contains(Permissions::BoardConfig)
} }
pub fn news(&self) -> bool {
self.0.contains(Permissions::News)
}
pub fn bypass_bans(&self) -> bool { pub fn bypass_bans(&self) -> bool {
self.0.contains(Permissions::BypassBans) self.0.contains(Permissions::BypassBans)
} }

67
src/schedule.rs Normální soubor
Zobrazit soubor

@ -0,0 +1,67 @@
use anyhow::Error;
use glob::glob;
use std::collections::HashSet;
use tokio::fs::remove_file;
use crate::{
ctx::Ctx,
db::models::{Banner, Board, Post},
};
pub async fn s_cleanup_files(ctx: &Ctx) -> Result<(), Error> {
let mut keep = HashSet::new();
let mut keep_thumbs = HashSet::new();
let banners = Banner::read_all(ctx).await?;
for banner in banners {
keep.insert(format!(
"{}.{}",
banner.banner.timestamp, banner.banner.format
));
}
let boards = Board::read_all(ctx).await?;
for board in boards {
let posts = Post::read_all(ctx, board.id.clone()).await?;
for post in posts {
for file in post.files.0 {
keep.insert(format!("{}.{}", file.timestamp, file.format));
if let Some(thumb_format) = file.thumb_format {
keep_thumbs.insert(format!("{}.{}", file.timestamp, thumb_format));
}
}
}
}
for file in glob("./uploads/*.*")? {
let file = file?;
let file_name = file.file_name();
if let Some(file_name) = file_name {
let check = file_name.to_string_lossy().to_string();
if !keep.contains(&check) {
remove_file(file).await?;
}
}
}
for file in glob("./uploads/thumb/*.*")? {
let file = file?;
let file_name = file.file_name();
if let Some(file_name) = file_name {
let check = file_name.to_string_lossy().to_string();
if !keep_thumbs.contains(&check) {
remove_file(file).await?;
}
}
}
Ok(())
}

Zobrazit soubor

@ -25,7 +25,7 @@ use crate::{
#[derive(MultipartForm)] #[derive(MultipartForm)]
pub struct PostForm { pub struct PostForm {
pub board: Text<String>, pub board: Text<String>,
pub thread: Option<Text<i32>>, pub thread: Option<Text<i64>>,
pub name: Text<String>, pub name: Text<String>,
pub email: Text<String>, pub email: Text<String>,
pub content: Text<String>, pub content: Text<String>,

Zobrazit soubor

@ -31,7 +31,7 @@ pub async fn get_posts_from_ids(ctx: &Ctx, ids: Vec<String>) -> Vec<Post> {
posts posts
} }
fn parse_id(id: &str) -> Option<(String, i32)> { fn parse_id(id: &str) -> Option<(String, i64)> {
let (board, id) = id.split_once('/')?; let (board, id) = id.split_once('/')?;
let board = board.to_owned(); let board = board.to_owned();
let id = id.parse().ok()?; let id = id.parse().ok()?;

Zobrazit soubor

@ -2,21 +2,37 @@ use actix_web::{get, web::Data, HttpRequest, HttpResponse};
use askama::Template; use askama::Template;
use super::tcx::TemplateCtx; use super::tcx::TemplateCtx;
use crate::{ctx::Ctx, db::models::NewsPost, error::NekrochanError, web::template_response}; use crate::{
ctx::Ctx,
db::models::{Board, LocalStats, NewsPost},
error::NekrochanError,
filters,
web::template_response,
};
#[derive(Template)] #[derive(Template)]
#[template(path = "index.html")] #[template(path = "index.html")]
struct IndexTemplate { struct IndexTemplate {
tcx: TemplateCtx, tcx: TemplateCtx,
_news: Vec<NewsPost>, news: Option<NewsPost>,
boards: Vec<Board>,
stats: LocalStats,
} }
#[get("/")] #[get("/")]
pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> { pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
let tcx = TemplateCtx::new(&ctx, &req).await?; let tcx = TemplateCtx::new(&ctx, &req).await?;
let _news = NewsPost::read_all(&ctx).await?;
let template = IndexTemplate { tcx, _news }; if tcx.boards.is_empty() {
return Err(NekrochanError::HomePageError);
}
let news = NewsPost::read_latest(&ctx).await?;
let boards = Board::read_all(&ctx).await?;
let stats = LocalStats::read(&ctx).await?;
let template = IndexTemplate { tcx, boards, stats, news };
template_response(&template) template_response(&template)
} }

Zobrazit soubor

@ -48,7 +48,7 @@ impl TemplateCtx {
}; };
let (ip, _) = ip_from_req(req)?; let (ip, _) = ip_from_req(req)?;
let yous = ctx.cache().zrange(format!("yous:{ip}"), 0, -1).await?; let yous = ctx.cache().zrange(format!("by_ip:{ip}"), 0, -1).await?;
let tcx = Self { let tcx = Self {
cfg, cfg,
@ -93,15 +93,12 @@ pub async fn account_from_auth_opt(
} }
pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> { pub fn ip_from_req(req: &HttpRequest) -> Result<(IpAddr, String), NekrochanError> {
let ip = IpAddr::V4(Ipv4Addr::UNSPECIFIED); let ip = req
.connection_info()
// let ip = req .realip_remote_addr()
// .headers() .map_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED), |ip| {
// .get("X-Real-IP") ip.parse().unwrap_or(IpAddr::V4(Ipv4Addr::UNSPECIFIED))
// .ok_or(NekrochanError::HeaderError("X-Real-IP"))? });
// .to_str()
// .map_err(|_| NekrochanError::HeaderError("X-Real-IP"))?
// .parse::<IpAddr>()?;
let country = req.headers().get("X-Country-Code").map_or_else( let country = req.headers().get("X-Country-Code").map_or_else(
|| "xx".into(), || "xx".into(),

Zobrazit soubor

@ -27,7 +27,7 @@ struct ThreadTemplate {
pub async fn thread( pub async fn thread(
ctx: Data<Ctx>, ctx: Data<Ctx>,
req: HttpRequest, req: HttpRequest,
path: Path<(String, i32)>, path: Path<(String, i64)>,
) -> Result<HttpResponse, NekrochanError> { ) -> Result<HttpResponse, NekrochanError> {
let tcx = TemplateCtx::new(&ctx, &req).await?; let tcx = TemplateCtx::new(&ctx, &req).await?;

binární
static/favicon.ico

Binární soubor nebyl zobrazen.

Před

Šířka:  |  Výška:  |  Velikost: 4.2 KiB

Za

Šířka:  |  Výška:  |  Velikost: 766 B

binární
static/spoiler.png

Binární soubor nebyl zobrazen.

Před

Šířka:  |  Výška:  |  Velikost: 2.6 KiB

Za

Šířka:  |  Výška:  |  Velikost: 1.3 KiB

Zobrazit soubor

@ -1,9 +1,12 @@
:root {
font-size: 10pt;
font-family: var(--font);
color: var(--text);
}
body { body {
min-height: 100vh; min-height: 100vh;
font-family: var(--font);
font-size: 10pt;
background: var(--bg); background: var(--bg);
color: var(--text);
margin: 0; margin: 0;
} }
@ -107,14 +110,14 @@ summary {
.table-wrap { .table-wrap {
overflow-x: auto; overflow-x: auto;
margin: 8px 0;
} }
.data-table { .data-table {
width: 100%; width: 100%;
background-color: var(--table-primary); background-color: var(--table-primary);
border-spacing: 0; border-spacing: 0;
border: 1px solid var(--table-border); border-collapse: collapse;
margin: 8px 0;
} }
.data-table tr:nth-child(2n + 1) { .data-table tr:nth-child(2n + 1) {
@ -127,8 +130,8 @@ summary {
.data-table td, .data-table td,
.data-table th { .data-table th {
border: 1px solid var(--table-border);
padding: 4px; padding: 4px;
text-align: center;
} }
.data-table .banner { .data-table .banner {
@ -158,6 +161,7 @@ summary {
background-color: var(--table-head); background-color: var(--table-head);
font-weight: bold; font-weight: bold;
padding: 4px; padding: 4px;
border-bottom: 1px solid var(--table-border);
} }
.infobox-content { .infobox-content {
@ -206,15 +210,14 @@ summary {
margin: 0; margin: 0;
} }
.small {
font-size: 0.8rem;
font-weight: normal;
}
.big { .big {
font-size: 1.2rem; font-size: 1.2rem;
} }
.small {
font-size: 0.8rem;
}
.center { .center {
text-align: center; text-align: center;
} }
@ -223,6 +226,14 @@ summary {
display: inline-block; display: inline-block;
} }
.float-r {
float: right;
}
.fixed-table {
table-layout: fixed;
}
.banner { .banner {
display: block; display: block;
width: 100%; width: 100%;
@ -231,9 +242,19 @@ summary {
border: 1px solid var(--box-border); border: 1px solid var(--box-border);
} }
.headline {
font-size: 1rem;
margin: 0;
}
.headline::after {
content: "";
display: block;
clear: both;
}
.board-links { .board-links {
color: var(--board-links-color); color: var(--board-links-color);
padding: 2px;
} }
.link-separator::after { .link-separator::after {
@ -248,7 +269,11 @@ summary {
content: " ] "; content: " ] ";
} }
.board-links::after { .header {
padding: 2px;
}
.header::after {
content: ""; content: "";
display: block; display: block;
clear: both; clear: both;
@ -257,6 +282,7 @@ summary {
.footer { .footer {
text-align: center; text-align: center;
font-size: 8pt; font-size: 8pt;
margin-top: 8px;
} }
.post { .post {
@ -274,8 +300,8 @@ summary {
} }
.post::after { .post::after {
display: block;
content: ""; content: "";
display: block;
clear: both; clear: both;
} }
@ -359,6 +385,10 @@ summary {
font-family: inherit; font-family: inherit;
white-space: pre-wrap; white-space: pre-wrap;
word-break: break-word; word-break: break-word;
margin: 0;
}
.post .post-content {
margin: 1rem 2rem; margin: 1rem 2rem;
} }
@ -370,10 +400,6 @@ summary {
color: var(--post-link-hover); color: var(--post-link-hover);
} }
.quote {
text-decoration: underline;
}
.dead-quote { .dead-quote {
color: var(--dead-quote-color); color: var(--dead-quote-color);
text-decoration: line-through; text-decoration: line-through;

Zobrazit soubor

@ -13,11 +13,11 @@
<script>0</script> <script>0</script>
</head> </head>
<body> <body>
<div class="board-links"> <div class="board-links header">
<span class="link-group"><a href="/">domov</a></span> <span class="link-group"><a href="/">domov</a></span>
{% call board_links::board_links() %} {% call board_links::board_links() %}
<span class="link-group"><a href="/overboard">nadnástěnka</a></span> <span class="link-group"><a href="/overboard">nadnástěnka</a></span>
<span style="float: right;"> <span class="float-r">
{% if tcx.logged_in %} {% if tcx.logged_in %}
<span class="link-group"><a href="/logout">odhlásit se</a></span> <span class="link-group"><a href="/logout">odhlásit se</a></span>
<span class="link-group"><a href="/staff/account">účet</a></span> <span class="link-group"><a href="/staff/account">účet</a></span>
@ -28,12 +28,13 @@
</div> </div>
<div class="main"> <div class="main">
{% block content %}{% endblock %} {% block content %}{% endblock %}
<hr>
<div class="footer"> <div class="footer">
<div class="box inline-block">
<a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a> <a href="https://git.nekrofilie.com/sneedmaster/nekrochan">nekrochan</a> - Projekt <a href="https://nekrofilie.com/">Nekrofilie</a>
<br> <br>
<span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span> <span>Všechny příspěvky na této stránce byly vytvořeny náhodnými uživateli.</span>
</div> </div>
</div> </div>
</div>
</body> </body>
</html> </html>

Zobrazit soubor

@ -12,12 +12,12 @@
<div class="container"> <div class="container">
<h1 class="title">Je konec...</h1> <h1 class="title">Je konec...</h1>
<div class="infobox"> <div class="infobox center">
<div class="infobox-head center"> <div class="infobox-head">
Chyba {{ error_code }} Chyba {{ error_code }}
</div> </div>
{% if !error_message.is_empty() %} {% if !error_message.is_empty() %}
<div class="infobox-content center"> <div class="infobox-content">
{{ error_message }} {{ error_message }}
</div> </div>
{% endif %} {% endif %}

Zobrazit soubor

@ -5,8 +5,57 @@
{% block title %}{{ tcx.cfg.site.name }}{% endblock %} {% block title %}{{ tcx.cfg.site.name }}{% endblock %}
{% block content %} {% block content %}
<div class="container">
<div class="center"> <div class="center">
<h1 class="title">{{ tcx.cfg.site.name }}</h1> <h1 class="title">{{ tcx.cfg.site.name }}</h1>
<p class="description">{{ tcx.cfg.site.description }}</p> <p class="description">{{ tcx.cfg.site.description }}</p>
<p class="board-links big">{% call board_links::board_links() %}</p>
</div>
{% if let Some(news) = news %}
<table class="data-table">
<tr>
<th>Novinky</th>
</tr>
<tr>
<td>
<h2 class="headline">
<span>{{ news.title }}</span>
<span class="float-r">{{ news.author }} - {{ news.created|czech_datetime }}</span>
</h2>
<hr>
<pre class="post-content">{{ news.content|safe }}</pre>
</td>
</tr>
<tr>
<td>
<a href="/news">Zobrazit všechny novinky...</a>
</td>
</tr>
</table>
{% endif %}
<table class="data-table fixed-table center">
<tr>
<th>Nástěnky</th>
<th>Statistika</th>
</tr>
<tr>
<td>
<ul class="infobox-list">
{% for board in boards %}
<li><a href="/boards/{{ board.id }}">/{{ board.id }}/ - {{ board.name }}</a></li>
{% endfor %}
</ul>
</td>
<td>
{% let board_count = tcx.boards.len() %}
Celkem {{ "byl vytvořen|byly vytvořeny|bylo vytvořeno"|czech_plural(stats.post_count) }}&#32;
<b>{{ stats.post_count }}</b> {{ "příspěvek|příspěvky|příspěvků"|czech_plural(stats.post_count) }}&#32;
na <b>{{ board_count }}</b> {{ "nástěnce|nástěnkách|nástěnkách"|czech_plural(board_count) }}.
<br>
Aktuálně {{ "je nahrán|jsou nahrány|je nahráno"|czech_plural(stats.file_count) }} <b>{{ stats.file_count }}</b>&#32;
{{ "soubor|soubory|souborů"|czech_plural(stats.file_count) }}, celkem <b>{{ stats.file_size|filesizeformat }}</b>.
</td>
</tr>
</table>
</div> </div>
{% endblock %} {% endblock %}

Zobrazit soubor

@ -11,7 +11,7 @@
<h2>Účty</h2> <h2>Účty</h2>
<form method="post" action="/staff/actions/remove-accounts"> <form method="post" action="/staff/actions/remove-accounts">
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <table class="data-table center">
<tr> <tr>
<th></th> <th></th>
<th>Jméno</th> <th>Jméno</th>

Zobrazit soubor

@ -11,7 +11,7 @@
<h2>Bannery</h2> <h2>Bannery</h2>
<form method="post" action="/staff/actions/remove-banners"> <form method="post" action="/staff/actions/remove-banners">
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <table class="data-table center">
<tr> <tr>
<th></th> <th></th>
<th>Banner</th> <th>Banner</th>

Zobrazit soubor

@ -11,7 +11,7 @@
<h2>Bany</h2> <h2>Bany</h2>
<form method="post" action="/staff/actions/remove-bans"> <form method="post" action="/staff/actions/remove-bans">
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <table class="data-table center">
<tr> <tr>
<th></th> <th></th>
<th>IP</th> <th>IP</th>

Zobrazit soubor

@ -11,7 +11,7 @@
<h2>Nástěnky</h2> <h2>Nástěnky</h2>
<form method="post"> <form method="post">
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <table class="data-table center">
<tr> <tr>
<th></th> <th></th>
<th>ID</th> <th>ID</th>

Zobrazit soubor

@ -11,7 +11,7 @@
<hr> <hr>
<h2>Záznamy</h2> <h2>Záznamy</h2>
<div class="table-wrap"> <div class="table-wrap">
<table class="data-table"> <table class="data-table center">
<tr> <tr>
<th>Zpráva</th> <th>Zpráva</th>
<th>Datum</th> <th>Datum</th>