Working pagination and download of book files

This commit is contained in:
2025-08-04 02:12:41 -04:00
parent 4e0997df06
commit 794abb7d28
11 changed files with 2065 additions and 224 deletions

View File

@@ -244,3 +244,16 @@ class Storage:
_result = session.execute(select(Collection).join(Book)).all()
session.close()
return _result
def get_collection(self, name):
"""Get collection from database.
Returns
-------
_result : ScalarResult Object
"""
session = Session(self.engine)
breakpoint()
_result = session.execute(select(Collection).where(Collection.name == name).join(Book)).all()
session.close()
return _result

2
src/frontend/compile.sh vendored Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/sh
exec npx tsc static/script/pyshelf.ts

View File

@@ -8,7 +8,7 @@ import datetime
from json import dumps
from base64 import b64encode
from fastapi import FastAPI, Request
from fastapi.responses import HTMLResponse, JSONResponse
from fastapi.responses import HTMLResponse, JSONResponse, FileResponse
from fastapi.routing import APIRoute
from fastapi.staticfiles import StaticFiles
from fastapi.templating import Jinja2Templates
@@ -85,7 +85,7 @@ def books_tojson(obj) -> dumps:
def book_tojson(book) -> dumps:
"""Convert a book object to a json."""
return dumps({
"book_id": book[0].book_id,
"book_id": book[0].id,
"title": book[0].title,
"author": book[0].author,
"categories": book[0].categories,
@@ -147,7 +147,12 @@ class FastAPIServer():
_pyShelf_src = sass.compile(
filename='src/frontend/static/styles/pyShelf.sass',
source_map_filename='src/frontend/static/styles/pyShelf.sass',
output_style='compressed')
output_style='compressed',
include_paths=[
'node_modules',
'src/frontend/static/styles'
]
)
with open('src/frontend/static/styles/pyShelf.css', 'w') as _pyShelf:
_pyShelf.write(_pyShelf_src[0])
@@ -161,12 +166,12 @@ class FastAPIServer():
route.operation_id = route.name
@app.get("/", response_class=HTMLResponse)
async def index(request: Request, skip: int = 0, limit: int = 10):
async def index(request: Request, skip: int = 0, limit: int = 30):
storage = Storage(Config(os.path.abspath(os.getcwd())))
books = storage.get_books(collection=None, skip=skip, limit=limit)
books = storage.get_books(collection=None, skip=skip*limit, limit=limit)
collections = storage.get_collections()
"""Home page responder."""
context = {"request": request, "books": books, "collections": collections}
context = {"request": request, "books": books, "collections": collections, "page": skip, "limit": limit}
return templates.TemplateResponse("index.html", context)
@app.get("/api/books", response_class=JSONResponse)
@@ -184,6 +189,16 @@ class FastAPIServer():
book = storage.get_book(book_id)
"""Home page responder."""
return JSONResponse(content=book_tojson(book))
@app.get("/api/get_book/{book_id}", response_class=FileResponse)
async def book(request: Request, book_id: int):
storage = Storage(Config(os.path.abspath(os.getcwd())))
book = storage.get_book(book_id)
file_path = book[0].file_name
if not os.path.exists(file_path):
return JSONResponse(status_code=404, content={"error": "File not found"})
"""Book file responder."""
return FileResponse(path=file_path, filename=os.path.basename(file_path), media_type="application/octet-stream")
@app.get("/api/collections", response_class=JSONResponse)
async def collections(request: Request):
@@ -192,6 +207,12 @@ class FastAPIServer():
"""Home page responder."""
return JSONResponse(content=collections_tojson(collections))
@app.get("/api/collection/{collection_id}", response_class=JSONResponse)
async def collection(request: Request, collection_name: str):
storage = Storage(Config(os.path.abspath(os.getcwd())))
collection = storage.get_collection(collection_name)
"""Collection file responder."""
return JSONResponse(content=collections_tojson(collection))
async def run(self):
"""Front end server entrypoint."""

View File

@@ -24,72 +24,8 @@ $navbar-item-color: $white
$navbar-item-hover-background-color: $ps-color-secondary
$navbar-dropdown-background-color: $ps-color-primary-trans
$navbar-dropdown-item-hover-color: $ps-color-background !important
$footer-background-color: $ps-color-primary-trans
$footer-padding: 0.5rem 0.5rem
//$navbar-dropdown-item-hover-background-color: $background !default
//$navbar-dropdown-item-active-color: $link !default
//$navbar-dropdown-item-active-background-color: $background !default
@import "../../node_modules/bulma/sass/utilities/_all.sass";
@import "../../node_modules/bulma/sass/utilities/initial-variables.sass";
@import "../../node_modules/bulma/sass/utilities/functions.sass";
@import "../../node_modules/bulma/sass/utilities/derived-variables.sass";
@import "../../node_modules/bulma/sass/utilities/mixins.sass";
@import "../../node_modules/bulma/sass/utilities/controls.sass";
@import "../../node_modules/bulma/sass/utilities/extends.sass";
@import "../../node_modules/bulma/sass/base/_all.sass";
@import "../../node_modules/bulma/sass/base/minireset.sass";
@import "../../node_modules/bulma/sass/base/generic.sass";
@import "../../node_modules/bulma/sass/base/animations.sass";
@import "../../node_modules/bulma/sass/elements/_all.sass";
@import "../../node_modules/bulma/sass/elements/box.sass";
@import "../../node_modules/bulma/sass/elements/button.sass";
@import "../../node_modules/bulma/sass/elements/container.sass";
@import "../../node_modules/bulma/sass/elements/content.sass";
@import "../../node_modules/bulma/sass/elements/icon.sass";
@import "../../node_modules/bulma/sass/elements/image.sass";
@import "../../node_modules/bulma/sass/elements/notification.sass";
@import "../../node_modules/bulma/sass/elements/progress.sass";
@import "../../node_modules/bulma/sass/elements/table.sass";
@import "../../node_modules/bulma/sass/elements/tag.sass";
@import "../../node_modules/bulma/sass/elements/title.sass";
@import "../../node_modules/bulma/sass/elements/other.sass";
@import "../../node_modules/bulma/sass/form/_all.sass";
@import "../../node_modules/bulma/sass/form/shared.sass";
@import "../../node_modules/bulma/sass/form/input-textarea.sass";
@import "../../node_modules/bulma/sass/form/checkbox-radio.sass";
@import "../../node_modules/bulma/sass/form/select.sass";
@import "../../node_modules/bulma/sass/form/file.sass";
@import "../../node_modules/bulma/sass/form/tools.sass";
@import "../../node_modules/bulma/sass/components/_all.sass";
@import "../../node_modules/bulma/sass/components/breadcrumb.sass";
@import "../../node_modules/bulma/sass/components/card.sass";
@import "../../node_modules/bulma/sass/components/dropdown.sass";
@import "../../node_modules/bulma/sass/components/level.sass";
@import "../../node_modules/bulma/sass/components/media.sass";
@import "../../node_modules/bulma/sass/components/menu.sass";
@import "../../node_modules/bulma/sass/components/message.sass";
@import "../../node_modules/bulma/sass/components/modal.sass";
@import "../../node_modules/bulma/sass/components/navbar.sass";
@import "../../node_modules/bulma/sass/components/pagination.sass";
@import "../../node_modules/bulma/sass/components/panel.sass";
@import "../../node_modules/bulma/sass/components/tabs.sass";
@import "../../node_modules/bulma/sass/grid/_all.sass";
@import "../../node_modules/bulma/sass/grid/columns.sass";
@import "../../node_modules/bulma/sass/grid/tiles.sass";
@import "../../node_modules/bulma/sass/helpers/_all.sass";
@import "../../node_modules/bulma/sass/helpers/color.sass";
@import "../../node_modules/bulma/sass/helpers/flexbox.sass";
@import "../../node_modules/bulma/sass/helpers/float.sass";
@import "../../node_modules/bulma/sass/helpers/other.sass";
@import "../../node_modules/bulma/sass/helpers/overflow.sass";
@import "../../node_modules/bulma/sass/helpers/position.sass";
@import "../../node_modules/bulma/sass/helpers/spacing.sass";
@import "../../node_modules/bulma/sass/helpers/typography.sass";
@import "../../node_modules/bulma/sass/helpers/visibility.sass";
@import "../../node_modules/bulma/sass/layout/_all.sass";
@import "../../node_modules/bulma/sass/layout/hero.sass";
@import "../../node_modules/bulma/sass/layout/section.sass";
@import "../../node_modules/bulma/sass/layout/footer.sass"
$footer-background-color: $ps-color-primary-trans !important
// $footer-padding: 0.5rem 0.5rem
.center-all
align-items: center;
@@ -110,12 +46,60 @@ $footer-padding: 0.5rem 0.5rem
#book-shelf
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)) // <== add this
gap: 1rem // space between rows and columns
justify-content: space-evenly;
align-items: center;
// align-items: center;
align-content: center;
padding: 1rem;
margin: 1rem;
// background-color: $ps-color-background;
.book-thumbnail
z-index: 1;
position: relative;
.no-image-title
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: $white;
font-size: 1.2rem;
text-align: center;
background-color: rgba(0,0,0,0.7);
padding: 0.5rem;
border-radius: 5px;
.no-image-author
position: absolute;
top: 60%;
left: 50%;
transform: translate(-50%, -50%);
color: $white;
font-size: 1rem;
text-align: center;
background-color: rgba(0,0,0,0.7);
padding: 0.5rem;
border-radius: 5px;
.display-alt::after
content: attr(alt);
display: block !important;
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: rgba(0,0,0,0.7);
color: white;
font-size: 0.8rem;
padding: 0.2rem;
.title, .subtitle
z-index: 2;
position: relative;
top: -155px;
.collection
display: flex;
flex-direction: column;
@@ -123,3 +107,9 @@ $footer-padding: 0.5rem 0.5rem
align-items: center;
align-content: center;
background-color: $ps-color-background;
.footer
display: flex;
padding: 1rem !important;
@import "../../node_modules/bulma/bulma.scss"

View File

@@ -1,18 +1,18 @@
<footer class="footer is-dark" id="footer-main">
<a href="https://python.org">
<img
src="{{ url_for('static', path='images/python-logo-transparent.svg') }}"
alt="Powered by Python"
width="128"
height="24">
</a>
<a href="https://bulma.io">
<img
src="https://bulma.io/images/made-with-bulma--white.png"
alt="Made with Bulma"
width="128"
height="24">
</a>
<a href="https://python.org">
<img
src="{{ url_for('static', path='images/python-logo-transparent.svg') }}"
alt="Powered by Python"
width="128"
height="24">
</a>
<a href="https://bulma.io">
<img
src="https://bulma.io/assets/images/made-with-bulma--dark.png"
alt="Made with Bulma"
width="128"
height="24">
</a>
</footer>
</body>
</body>
</html>

View File

@@ -1,40 +1,53 @@
<!doctype html>
<!DOCTYPE html>
{% block javascript %}
<script type="text/javascript" src=static/script/pako.min.js>
<script type="text/javascript">
const books = {{ books|books_tojson }};
let inflatedJSON = {};
const pako = require('pako');
inflatedJSON = JSON.parse(pako.inflate(books, { to: 'string'}));
</script>
{% endblock %}
{% include 'header.html' %}
{% include 'navigation.html' %}
<section id="master-container">
<div id="book-shelf" class="container is-dark">
{% for book in books %}
{% set cover = book[0].cover|b64decode %}
{% if cover != 'None' %}
<div class="is-dark book" id="{{book[0].id}}">
<div class="image book-thumbnail">
<figure class="image is-4by3">
<img src="data:;base64,{{ book[0].cover|b64decode }}" alt="{{ book[0].title }}">
</figure>
</div>
</div>
{% else %}
<div class="is-dark book" id="{{book[0].id}}">
<div class="image book-thumbnail">
<figure class="image is-4by3">
<img src="static/images/no-cover.jpg" alt="{{ book[0].title }}">
</figure>
</div>
<h3 class="title is-3">{{ book[0].title }}</h3>
<h4 class="subtitle is-4">{{ book[0].author }}</h4>
<p class="content">{{ book[0].description|summarize }}</p>
</div>
{% endif %}
{% endfor %}
</div>
</section>
{% include 'footer.html' %}
<script type="text/javascript">
const books = {{ books|books_tojson }};
let inflatedJSON = {};
const pako = require('pako');
inflatedJSON = JSON.parse(pako.inflate(books, { to: 'string'}));
</script>
{% endblock %}
{% include 'header.html' %}
{% include 'navigation.html' %}
<section id="master-container">
<p>Total books: {{ books|length }}</p>
<!-- <div id="book-shelf" class="container is-dark"> -->
<div id="book-shelf" class="is-dark">
{% for book in books %}
{% set cover = book[0].cover|b64decode %}
{% if cover != 'None' %}
<div class="is-dark book" id="{{book[0].id}}" onclick="window.location.href='/api/get_book/{{ book[0].id }}'">
<div class="image book-thumbnail">
<figure class="image is-4by3">
<img src="data:;base64,{{ book[0].cover|b64decode }}" alt="{{ book[0].title }}">
</figure>
</div>
</div>
{% else %}
<div class="is-dark book" id="{{book[0].id}}" onclick="window.location.href='/api/get_book/{{ book[0].id }}'">
<div class="image book-thumbnail"
style="
background-image: url('static/images/no-cover.jpg');
background-size: cover;
background-position: center;
background-repeat: no-repeat;
">
<figure class="image is-4by3">
<div class="no-image-title">{{ book[0].title }}</div>
<!-- alt="{{ book[0].title }}" -->
</figure>
</div>
<!-- <p class="content">{{ book[0].description|summarize }}</p> -->
</div>
{% endif %}
{% endfor %}
</div>
<div id="pagination" class="container is-dark">
<nav class="pagination is-centered" role="navigation" aria-label="pagination">
<a class="pagination-previous" href="/?skip={{ page-1 }}" id="prev-page">Previous</a>
<a class="pagination-next" href="/?skip={{ page+1 }}" id="next-page">Next</a>
</nav>
</div>
</section>
{% include 'footer.html' %}

View File

@@ -49,7 +49,11 @@
<div class="select is-small is-rounded is-link" id="collection_dropdown">
<select id="collection_select">
{% for collection in collections %}
<option value={{collection[0].id}} class="collection_selection">{{collection[0].collection}}</option>
<option
value={{collection[0].id}}
class="collection_selection"
onclick="window.location.href='/api/collection/{{ collection[0].collection }}'"
>{{collection[0].collection}}</option>
{% endfor %}
</select>
</div>