add series support
This commit is contained in:
parent
6a79f0c1ed
commit
a91fe9a0bb
@ -70,7 +70,7 @@ impl Calibre {
|
|||||||
&self,
|
&self,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
sort_order: SortOrder,
|
sort_order: &SortOrder,
|
||||||
) -> Result<Vec<Series>, DataStoreError> {
|
) -> Result<Vec<Series>, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Series::multiple(&conn, limit, cursor, sort_order)
|
Series::multiple(&conn, limit, cursor, sort_order)
|
||||||
@ -81,6 +81,11 @@ impl Calibre {
|
|||||||
Series::book_series(&conn, id)
|
Series::book_series(&conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn series_books(&self, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
Book::series_books(&conn, id)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn has_previous_authors(&self, author_sort: &str) -> Result<bool, DataStoreError> {
|
pub fn has_previous_authors(&self, author_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::has_previous_authors(&conn, author_sort)
|
Author::has_previous_authors(&conn, author_sort)
|
||||||
@ -101,10 +106,25 @@ impl Calibre {
|
|||||||
Book::has_more_books(&conn, book_sort)
|
Book::has_more_books(&conn, book_sort)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_previous_series(&self, series_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
Series::has_previous_series(&conn, series_sort)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_more_series(&self, series_sort: &str) -> Result<bool, DataStoreError> {
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
Series::has_more_series(&conn, series_sort)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn scalar_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
pub fn scalar_author(&self, id: u64) -> Result<Author, DataStoreError> {
|
||||||
let conn = self.pool.get()?;
|
let conn = self.pool.get()?;
|
||||||
Author::scalar_author(&conn, id)
|
Author::scalar_author(&conn, id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scalar_series(&self, id: u64) -> Result<Series, DataStoreError> {
|
||||||
|
let conn = self.pool.get()?;
|
||||||
|
Series::scalar_series(&conn, id)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
@ -57,20 +57,10 @@ impl Author {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
sort_name: &str,
|
sort_name: &str,
|
||||||
) -> Result<bool, DataStoreError> {
|
) -> Result<bool, DataStoreError> {
|
||||||
let mut stmt = conn
|
Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::DESC)
|
||||||
.prepare("SELECT Count(1) FROM authors WHERE sort < (:sort_name) ORDER BY sort DESC")?;
|
|
||||||
let params = named_params! { ":sort_name": sort_name };
|
|
||||||
let count: u64 = stmt.query_row(params, |x| x.get(0))?;
|
|
||||||
|
|
||||||
Ok(count > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
pub fn has_more_authors(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
let mut stmt = conn
|
Pagination::has_prev_or_more(conn, "authors", sort_name, &SortOrder::ASC)
|
||||||
.prepare("SELECT Count(1) FROM authors WHERE sort > (:sort_name) ORDER BY sort ASC")?;
|
|
||||||
let params = named_params! { ":sort_name": sort_name };
|
|
||||||
let count: u64 = stmt.query_row(params, |x| x.get(0))?;
|
|
||||||
|
|
||||||
Ok(count > 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,6 +57,19 @@ impl Book {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn series_books(conn: &Connection, id: u64) -> Result<Vec<Book>, DataStoreError> {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
"SELECT books.id, books.title, books.sort, books.path FROM series \
|
||||||
|
INNER JOIN books_series_link ON series.id = books_series_link.series \
|
||||||
|
INNER JOIN books ON books.id = books_series_link.book \
|
||||||
|
WHERE books_series_link.series = (:id) \
|
||||||
|
ORDER BY books.series_index",
|
||||||
|
)?;
|
||||||
|
let params = named_params! { ":id": id };
|
||||||
|
let iter = stmt.query_map(params, Self::from_row)?;
|
||||||
|
Ok(iter.filter_map(Result::ok).collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
pub fn recents(conn: &Connection, limit: u64) -> Result<Vec<Self>, DataStoreError> {
|
||||||
let mut stmt = conn.prepare(
|
let mut stmt = conn.prepare(
|
||||||
"SELECT id, title, sort, path FROM books ORDER BY timestamp DESC LIMIT (:limit)",
|
"SELECT id, title, sort, path FROM books ORDER BY timestamp DESC LIMIT (:limit)",
|
||||||
@ -73,20 +86,10 @@ impl Book {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
pub fn has_previous_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
||||||
let mut stmt = conn
|
Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::DESC)
|
||||||
.prepare("SELECT Count(1) FROM books WHERE sort < (:sort_title) ORDER BY sort DESC")?;
|
|
||||||
let params = named_params! { ":sort_title": sort_title};
|
|
||||||
let count: u64 = stmt.query_row(params, |x| x.get(0))?;
|
|
||||||
|
|
||||||
Ok(count > 0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
pub fn has_more_books(conn: &Connection, sort_title: &str) -> Result<bool, DataStoreError> {
|
||||||
let mut stmt = conn
|
Pagination::has_prev_or_more(conn, "books", sort_title, &SortOrder::ASC)
|
||||||
.prepare("SELECT Count(1) FROM books WHERE sort > (:sort_title) ORDER BY sort ASC")?;
|
|
||||||
let params = named_params! { ":sort_title": sort_title};
|
|
||||||
let count: u64 = stmt.query_row(params, |x| x.get(0))?;
|
|
||||||
|
|
||||||
Ok(count > 0)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{Connection, Row, ToSql};
|
use rusqlite::{named_params, Connection, Row, ToSql};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::error::DataStoreError;
|
use super::error::DataStoreError;
|
||||||
@ -31,6 +31,32 @@ impl<'a> Pagination<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn sort_order_to_sql(sort_order: &SortOrder) -> String {
|
||||||
|
if *sort_order == SortOrder::ASC {
|
||||||
|
">"
|
||||||
|
} else {
|
||||||
|
"<"
|
||||||
|
}
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_prev_or_more(
|
||||||
|
conn: &Connection,
|
||||||
|
table: &str,
|
||||||
|
sort: &str,
|
||||||
|
sort_order: &SortOrder,
|
||||||
|
) -> Result<bool, DataStoreError> {
|
||||||
|
let comparison = Pagination::sort_order_to_sql(sort_order);
|
||||||
|
|
||||||
|
let mut stmt = conn.prepare(&format!(
|
||||||
|
"SELECT Count(1) FROM {table} WHERE sort {comparison} (:sort) ORDER BY sort {sort_order:?}"
|
||||||
|
))?;
|
||||||
|
let params = named_params! { ":sort": sort};
|
||||||
|
let count: u64 = stmt.query_row(params, |x| x.get(0))?;
|
||||||
|
|
||||||
|
Ok(count > 0)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn paginate<T, F>(
|
pub fn paginate<T, F>(
|
||||||
&self,
|
&self,
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
@ -42,11 +68,8 @@ impl<'a> Pagination<'a> {
|
|||||||
F: FnMut(&Row<'_>) -> Result<T, rusqlite::Error>,
|
F: FnMut(&Row<'_>) -> Result<T, rusqlite::Error>,
|
||||||
{
|
{
|
||||||
let cursor = self.cursor.unwrap_or("");
|
let cursor = self.cursor.unwrap_or("");
|
||||||
let comparison = if self.sort_order == SortOrder::ASC {
|
let comparison = Pagination::sort_order_to_sql(&self.sort_order);
|
||||||
">"
|
|
||||||
} else {
|
|
||||||
"<"
|
|
||||||
};
|
|
||||||
let where_sql = if statement.ends_with("AND") {
|
let where_sql = if statement.ends_with("AND") {
|
||||||
""
|
""
|
||||||
} else {
|
} else {
|
||||||
|
@ -26,17 +26,23 @@ impl Series {
|
|||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
limit: u64,
|
limit: u64,
|
||||||
cursor: Option<&str>,
|
cursor: Option<&str>,
|
||||||
sort_order: SortOrder,
|
sort_order: &SortOrder,
|
||||||
) -> Result<Vec<Self>, DataStoreError> {
|
) -> Result<Vec<Self>, DataStoreError> {
|
||||||
let pagination = Pagination::new("sort", cursor, limit, sort_order);
|
let pagination = Pagination::new("sort", cursor, limit, *sort_order);
|
||||||
pagination.paginate(
|
pagination.paginate(
|
||||||
conn,
|
conn,
|
||||||
"SELECT id, title, sort, path FROM series",
|
"SELECT id, name, sort FROM series",
|
||||||
&[],
|
&[],
|
||||||
Self::from_row,
|
Self::from_row,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn scalar_series(conn: &Connection, id: u64) -> Result<Self, DataStoreError> {
|
||||||
|
let mut stmt = conn.prepare("SELECT id, name, sort FROM series WHERE id = (:id)")?;
|
||||||
|
let params = named_params! { ":id": id };
|
||||||
|
Ok(stmt.query_row(params, Self::from_row)?)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn book_series(
|
pub fn book_series(
|
||||||
conn: &Connection,
|
conn: &Connection,
|
||||||
book_id: u64,
|
book_id: u64,
|
||||||
@ -61,4 +67,12 @@ impl Series {
|
|||||||
Err(e) => Err(DataStoreError::SqliteError(e)),
|
Err(e) => Err(DataStoreError::SqliteError(e)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_previous_series(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
|
Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::DESC)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_more_series(conn: &Connection, sort_name: &str) -> Result<bool, DataStoreError> {
|
||||||
|
Pagination::has_prev_or_more(conn, "series", sort_name, &SortOrder::ASC)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,11 +31,10 @@ fn books(
|
|||||||
paginated::render(
|
paginated::render(
|
||||||
"books",
|
"books",
|
||||||
|| {
|
|| {
|
||||||
state.calibre.books(25, cursor, sort_order).map(|x| {
|
state
|
||||||
x.iter()
|
.calibre
|
||||||
.filter_map(|y| Book::full_book(y, &state))
|
.books(25, cursor, sort_order)
|
||||||
.collect()
|
.map(|x| x.iter().filter_map(|y| Book::full_book(y, state)).collect())
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|book| book.sort.clone(),
|
|book| book.sort.clone(),
|
||||||
|cursor| state.calibre.has_previous_books(cursor),
|
|cursor| state.calibre.has_previous_books(cursor),
|
||||||
|
@ -1,10 +1,38 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use poem::{handler, web::Data};
|
use calibre_db::data::pagination::SortOrder;
|
||||||
|
use poem::{
|
||||||
|
handler,
|
||||||
|
web::{Data, Html, Path},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::app_state::AppState;
|
use crate::app_state::AppState;
|
||||||
|
|
||||||
|
use super::paginated;
|
||||||
|
|
||||||
#[handler]
|
#[handler]
|
||||||
pub async fn handler(state: Data<&Arc<AppState>>) -> Result<String, poem::Error> {
|
pub async fn handler_init(state: Data<&Arc<AppState>>) -> Result<Html<String>, poem::Error> {
|
||||||
Ok("series".to_string())
|
series(&state, None, &SortOrder::ASC)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn handler(
|
||||||
|
Path((cursor, sort_order)): Path<(String, SortOrder)>,
|
||||||
|
state: Data<&Arc<AppState>>,
|
||||||
|
) -> Result<Html<String>, poem::Error> {
|
||||||
|
series(&state, Some(&cursor), &sort_order)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn series(
|
||||||
|
state: &Arc<AppState>,
|
||||||
|
cursor: Option<&str>,
|
||||||
|
sort_order: &SortOrder,
|
||||||
|
) -> Result<Html<String>, poem::Error> {
|
||||||
|
paginated::render(
|
||||||
|
"series",
|
||||||
|
|| state.calibre.series(25, cursor, sort_order),
|
||||||
|
|series| series.sort.clone(),
|
||||||
|
|cursor| state.calibre.has_previous_series(cursor),
|
||||||
|
|cursor| state.calibre.has_more_series(cursor),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
35
rusty-library/src/handlers/series_single.rs
Normal file
35
rusty-library/src/handlers/series_single.rs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use poem::{
|
||||||
|
error::InternalServerError,
|
||||||
|
handler,
|
||||||
|
web::{Data, Html, Path},
|
||||||
|
};
|
||||||
|
use tera::Context;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
app_state::AppState, data::book::Book, handlers::error::SqliteError, templates::TEMPLATES,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[handler]
|
||||||
|
pub async fn handler(
|
||||||
|
id: Path<u64>,
|
||||||
|
state: Data<&Arc<AppState>>,
|
||||||
|
) -> Result<Html<String>, poem::Error> {
|
||||||
|
let series = state.calibre.scalar_series(*id).map_err(SqliteError)?;
|
||||||
|
let books = state.calibre.series_books(*id).map_err(SqliteError)?;
|
||||||
|
let books = books
|
||||||
|
.iter()
|
||||||
|
.filter_map(|x| Book::full_book(x, &state))
|
||||||
|
.collect::<Vec<Book>>();
|
||||||
|
|
||||||
|
let mut context = Context::new();
|
||||||
|
context.insert("title", &series.name);
|
||||||
|
context.insert("nav", &series.name);
|
||||||
|
context.insert("books", &books);
|
||||||
|
|
||||||
|
TEMPLATES
|
||||||
|
.render("book_list", &context)
|
||||||
|
.map_err(InternalServerError)
|
||||||
|
.map(Html)
|
||||||
|
}
|
@ -27,6 +27,7 @@ mod handlers {
|
|||||||
pub mod paginated;
|
pub mod paginated;
|
||||||
pub mod recents;
|
pub mod recents;
|
||||||
pub mod series;
|
pub mod series;
|
||||||
|
pub mod series_single;
|
||||||
}
|
}
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
@ -51,13 +52,18 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.at("/", get(handlers::recents::handler))
|
.at("/", get(handlers::recents::handler))
|
||||||
.at("/books", get(handlers::books::handler_init))
|
.at("/books", get(handlers::books::handler_init))
|
||||||
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
.at("/books/:cursor/:sort_order", get(handlers::books::handler))
|
||||||
|
.at("/series", get(handlers::series::handler_init))
|
||||||
|
.at(
|
||||||
|
"/series/:cursor/:sort_order",
|
||||||
|
get(handlers::series::handler),
|
||||||
|
)
|
||||||
|
.at("/series/:id", get(handlers::series_single::handler))
|
||||||
.at("/authors", get(handlers::authors::handler_init))
|
.at("/authors", get(handlers::authors::handler_init))
|
||||||
.at("/authors/:id", get(handlers::author::handler))
|
.at("/authors/:id", get(handlers::author::handler))
|
||||||
.at(
|
.at(
|
||||||
"/authors/:cursor/:sort_order",
|
"/authors/:cursor/:sort_order",
|
||||||
get(handlers::authors::handler),
|
get(handlers::authors::handler),
|
||||||
)
|
)
|
||||||
.at("/series", get(handlers::series::handler))
|
|
||||||
.at("/cover/:id", get(handlers::cover::handler))
|
.at("/cover/:id", get(handlers::cover::handler))
|
||||||
.at("/book/:id/:format", get(handlers::download::handler))
|
.at("/book/:id/:format", get(handlers::download::handler))
|
||||||
.nest("/static", EmbeddedFilesEndpoint::<Files>::new())
|
.nest("/static", EmbeddedFilesEndpoint::<Files>::new())
|
||||||
|
@ -9,6 +9,7 @@ pub static TEMPLATES: Lazy<Tera> = Lazy::new(|| {
|
|||||||
("authors", include_str!("../templates/authors.html")),
|
("authors", include_str!("../templates/authors.html")),
|
||||||
("book_list", include_str!("../templates/book_list.html")),
|
("book_list", include_str!("../templates/book_list.html")),
|
||||||
("books", include_str!("../templates/books.html")),
|
("books", include_str!("../templates/books.html")),
|
||||||
|
("series", include_str!("../templates/series.html")),
|
||||||
])
|
])
|
||||||
.expect("failed to parse tera templates");
|
.expect("failed to parse tera templates");
|
||||||
|
|
||||||
|
21
rusty-library/templates/series.html
Normal file
21
rusty-library/templates/series.html
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{% extends "base" %}
|
||||||
|
{% block title %}
|
||||||
|
{% if has_previous %}
|
||||||
|
<a class="secondary" href="/series/{{ backward_cursor }}/DESC">← back</a>
|
||||||
|
{% endif %}
|
||||||
|
{% if has_previous and has_more %}|{% endif%}
|
||||||
|
|
||||||
|
{% if has_more %}
|
||||||
|
<a class="secondary" href="/series/{{ forward_cursor }}/ASC">more →</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock title %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="grid-container">
|
||||||
|
{% for s in series %}
|
||||||
|
<a class="contrast" href="/series/{{ s.id }}">
|
||||||
|
<article>{{ s.name }}</article>
|
||||||
|
</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
Loading…
Reference in New Issue
Block a user