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,
title VARCHAR(256) NOT NULL,
content TEXT NOT NULL,
content_nomarkup TEXT NOT NULL,
author VARCHAR(32) NOT NULL REFERENCES accounts(username),
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!(
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),
thread INT DEFAULT NULL REFERENCES posts_{}(id),
thread BIGINT DEFAULT NULL REFERENCES posts_{}(id),
name VARCHAR(32) NOT NULL,
user_id VARCHAR(6) NOT NULL DEFAULT '000000',
tripcode VARCHAR(12) DEFAULT NULL,
@ -114,6 +114,14 @@ impl Board {
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> {
query("UPDATE boards SET name = $1 WHERE id = $2")
.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 banner;
mod board;
mod local_stats;
mod newspost;
mod post;

Zobrazit soubor

@ -45,18 +45,11 @@ pub struct Report {
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)]
pub struct Post {
pub id: i32,
pub id: i64,
pub board: String,
pub thread: Option<i32>,
pub thread: Option<i64>,
pub name: String,
pub user_id: String,
pub tripcode: Option<String>,
@ -89,6 +82,7 @@ pub struct NewsPost {
pub id: i32,
pub title: String,
pub content: String,
pub content_nomarkup: String,
pub author: String,
pub created: DateTime<Utc>,
}
@ -104,3 +98,9 @@ pub struct File {
pub timestamp: i64,
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> {
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)
}
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(
&self,
ctx: &Ctx,

Zobrazit soubor

@ -12,7 +12,7 @@ impl Post {
pub async fn create(
ctx: &Ctx,
board: &Board,
thread: Option<i32>,
thread: Option<i64>,
name: String,
tripcode: Option<String>,
capcode: Option<String>,
@ -110,7 +110,7 @@ impl Post {
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")
.bind(board)
.bind(id)
@ -181,19 +181,6 @@ impl Post {
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> {
let posts = query_as(
r#"SELECT * FROM overboard
@ -206,20 +193,6 @@ impl Post {
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> {
let replies = query_as(&format!(
"SELECT * FROM posts_{} WHERE thread = $1 ORDER BY created ASC",
@ -240,6 +213,14 @@ impl Post {
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> {
query(&format!(
"UPDATE posts_{} SET user_id = $1 WHERE id = $2",

Zobrazit soubor

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

Zobrazit soubor

@ -1,19 +1,13 @@
use std::process::Command;
use actix_multipart::form::tempfile::TempFile;
use anyhow::Error;
use chrono::Utc;
use glob::glob;
use std::{collections::HashSet, process::Command};
use tokio::{
fs::{remove_file, rename},
task::spawn_blocking,
};
use crate::{
cfg::Cfg,
ctx::Ctx,
db::models::{Banner, Board, File, Post},
error::NekrochanError,
};
use crate::{cfg::Cfg, db::models::File, error::NekrochanError};
impl File {
pub async fn new(
@ -306,61 +300,3 @@ async fn process_video(
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> {
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();
Ok(time)

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -12,6 +12,7 @@ pub enum Permissions {
Bans,
BoardBanners,
BoardConfig,
News,
BypassBans,
BypassBoardLock,
BypassThreadLock,
@ -67,6 +68,10 @@ impl PermissionWrapper {
self.0.contains(Permissions::BoardConfig)
}
pub fn news(&self) -> bool {
self.0.contains(Permissions::News)
}
pub fn bypass_bans(&self) -> bool {
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)]
pub struct PostForm {
pub board: Text<String>,
pub thread: Option<Text<i32>>,
pub thread: Option<Text<i64>>,
pub name: Text<String>,
pub email: 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
}
fn parse_id(id: &str) -> Option<(String, i32)> {
fn parse_id(id: &str) -> Option<(String, i64)> {
let (board, id) = id.split_once('/')?;
let board = board.to_owned();
let id = id.parse().ok()?;

Zobrazit soubor

@ -2,21 +2,37 @@ use actix_web::{get, web::Data, HttpRequest, HttpResponse};
use askama::Template;
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)]
#[template(path = "index.html")]
struct IndexTemplate {
tcx: TemplateCtx,
_news: Vec<NewsPost>,
news: Option<NewsPost>,
boards: Vec<Board>,
stats: LocalStats,
}
#[get("/")]
pub async fn index(ctx: Data<Ctx>, req: HttpRequest) -> Result<HttpResponse, NekrochanError> {
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)
}

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

@ -5,8 +5,57 @@
{% block title %}{{ tcx.cfg.site.name }}{% endblock %}
{% block content %}
<div class="container">
<div class="center">
<h1 class="title">{{ tcx.cfg.site.name }}</h1>
<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>
{% endblock %}

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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

Zobrazit soubor

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