Compare commits
3
Commits
4c0aa053ac
...
a51578cbf1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a51578cbf1 | ||
|
|
e4fe39ecc5 | ||
|
|
0c3e5cec9f |
@@ -1,6 +1,6 @@
|
|||||||
# eBook Library (Go + templ)
|
# eBook Library (Go + templ)
|
||||||
|
|
||||||
Minimalistic, no-JS eBook library web app for reading files (EPUB/PDF) on a
|
Minimalistic, no-JS eBook library web app for reading EPUB files on a
|
||||||
Tolino e-reader browser. UI text/comments are in German.
|
Tolino e-reader browser. UI text/comments are in German.
|
||||||
|
|
||||||
## Build & run
|
## Build & run
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ Minimalistische eBook-Bibliothek für den Tolino-Webbrowser.
|
|||||||
|
|
||||||
## Features (MVP)
|
## Features (MVP)
|
||||||
|
|
||||||
- Listet EPUB/PDF-Dateien aus `books/`
|
- Listet EPUB-Dateien aus `books/`
|
||||||
- Detailseite pro Buch
|
- Detailseite pro Buch
|
||||||
- Download-Link pro Buch (für Tolino)
|
- Download-Link pro Buch (für Tolino)
|
||||||
- Schlichtes, kontrastreiches UI ohne JavaScript-Abhängigkeit
|
- Schlichtes, kontrastreiches UI ohne JavaScript-Abhängigkeit
|
||||||
@@ -56,10 +56,7 @@ go run ./cmd/server
|
|||||||
|
|
||||||
## Bücher hinzufügen
|
## Bücher hinzufügen
|
||||||
|
|
||||||
Lege deine Dateien in den Ordner `books/`:
|
Lege deine EPUB-Dateien in den Ordner `books/`.
|
||||||
|
|
||||||
- `.epub` (bevorzugt)
|
|
||||||
- `.pdf` (optional)
|
|
||||||
|
|
||||||
Alternativ können Benutzer mit Upload-Recht Bücher direkt im Browser hochladen.
|
Alternativ können Benutzer mit Upload-Recht Bücher direkt im Browser hochladen.
|
||||||
|
|
||||||
@@ -74,6 +71,68 @@ go run ./cmd/admin user delete <name>
|
|||||||
go run ./cmd/admin user set-role <name> <reader|uploader|admin>
|
go run ./cmd/admin user set-role <name> <reader|uploader|admin>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Docker / docker-compose Deployment
|
||||||
|
|
||||||
|
Beispiel `docker-compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
networks:
|
||||||
|
web:
|
||||||
|
external: true
|
||||||
|
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
image: git.arnef.de/arnef/ebooks:latest
|
||||||
|
restart: always
|
||||||
|
networks:
|
||||||
|
- web
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
labels:
|
||||||
|
traefik.enable: 'true'
|
||||||
|
traefik.http.routers.ebooks.rule: Host(`books.arnef.de`)
|
||||||
|
traefik.http.routers.ebooks.entrypoints: websecure
|
||||||
|
traefik.http.routers.ebooks.tls.certresolver: letsencrypt
|
||||||
|
traefik.http.services.ebooks.loadbalancer.server.port: 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Der Container läuft als nicht-root Benutzer `ebooks` (UID `100`, GID `101`).
|
||||||
|
`BOOKS_DIR` (`/data/books`) und `USERS_DB` (`/data/users.db`) liegen im
|
||||||
|
Volume `./data`. Der Server legt `BOOKS_DIR` beim Start automatisch an,
|
||||||
|
falls es fehlt — gehört das Host-Verzeichnis `./data` aber nicht dem
|
||||||
|
passenden UID/GID, schlägt das Anlegen fehl bzw. `users.db` kann nicht
|
||||||
|
geöffnet werden (`unable to open database file`) und Uploads schlagen mit
|
||||||
|
"Datei konnte nicht gespeichert werden" fehl.
|
||||||
|
|
||||||
|
1. Datenverzeichnis anlegen und Rechte setzen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p data
|
||||||
|
sudo chown -R 100:101 data
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Container starten:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ersten Benutzer über die im Image enthaltene Admin-CLI anlegen (der
|
||||||
|
Server muss dafür nicht gestoppt werden, `--role admin` empfohlen für
|
||||||
|
den ersten Account):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec app /app/admin user add <name> --role admin
|
||||||
|
docker compose exec app /app/admin user list
|
||||||
|
```
|
||||||
|
|
||||||
|
`USERS_DB` ist im Image bereits auf `/data/users.db` gesetzt, daher ist
|
||||||
|
kein zusätzlicher Parameter nötig.
|
||||||
|
|
||||||
## Konfiguration
|
## Konfiguration
|
||||||
|
|
||||||
Umgebungsvariablen:
|
Umgebungsvariablen:
|
||||||
|
|||||||
+4
-1
@@ -21,7 +21,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer store.Close()
|
defer store.Close()
|
||||||
|
|
||||||
lib := library.New(booksDir)
|
lib, err := library.New(booksDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("books dir: %v", err)
|
||||||
|
}
|
||||||
h := web.NewHandler(lib, store)
|
h := web.NewHandler(lib, store)
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
|
|||||||
+14
-13
@@ -19,8 +19,11 @@ type Service struct {
|
|||||||
booksDir string
|
booksDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(booksDir string) *Service {
|
func New(booksDir string) (*Service, error) {
|
||||||
return &Service{booksDir: booksDir}
|
if err := os.MkdirAll(booksDir, 0o755); err != nil {
|
||||||
|
return nil, fmt.Errorf("books dir: %w", err)
|
||||||
|
}
|
||||||
|
return &Service{booksDir: booksDir}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) BooksDir() string { return s.booksDir }
|
func (s *Service) BooksDir() string { return s.booksDir }
|
||||||
@@ -41,7 +44,7 @@ func (s *Service) ListBooks() ([]Book, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(e.Name()))
|
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||||
if ext != ".epub" && ext != ".pdf" {
|
if ext != ".epub" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,17 +55,15 @@ func (s *Service) ListBooks() ([]Book, error) {
|
|||||||
hasCover := false
|
hasCover := false
|
||||||
title := fallbackTitle
|
title := fallbackTitle
|
||||||
|
|
||||||
if ext == ".epub" {
|
meta, err := readEPUBMetadata(fullPath)
|
||||||
meta, err := readEPUBMetadata(fullPath)
|
if err == nil {
|
||||||
if err == nil {
|
if strings.TrimSpace(meta.Title) != "" {
|
||||||
if strings.TrimSpace(meta.Title) != "" {
|
title = strings.TrimSpace(meta.Title)
|
||||||
title = strings.TrimSpace(meta.Title)
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(meta.Author) != "" {
|
|
||||||
author = strings.TrimSpace(meta.Author)
|
|
||||||
}
|
|
||||||
hasCover = meta.HasCover
|
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(meta.Author) != "" {
|
||||||
|
author = strings.TrimSpace(meta.Author)
|
||||||
|
}
|
||||||
|
hasCover = meta.HasCover
|
||||||
}
|
}
|
||||||
|
|
||||||
books = append(books, Book{
|
books = append(books, Book{
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -209,8 +210,8 @@ func (h *Handler) uploadSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
defer file.Close()
|
defer file.Close()
|
||||||
|
|
||||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||||
if ext != ".epub" && ext != ".pdf" {
|
if ext != ".epub" {
|
||||||
render(w, r.Context(), views.UploadPage("Nur EPUB- und PDF-Dateien erlaubt."))
|
render(w, r.Context(), views.UploadPage("Nur EPUB-Dateien erlaubt."))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -232,12 +233,14 @@ func (h *Handler) uploadSubmit(w http.ResponseWriter, r *http.Request) {
|
|||||||
render(w, r.Context(), views.UploadPage(fmt.Sprintf("Datei '%s' existiert bereits.", safeFilename)))
|
render(w, r.Context(), views.UploadPage(fmt.Sprintf("Datei '%s' existiert bereits.", safeFilename)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Printf("upload: create %q failed: %v", destPath, err)
|
||||||
http.Error(w, "Datei konnte nicht gespeichert werden", http.StatusInternalServerError)
|
http.Error(w, "Datei konnte nicht gespeichert werden", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer out.Close()
|
defer out.Close()
|
||||||
|
|
||||||
if _, err := io.Copy(out, file); err != nil {
|
if _, err := io.Copy(out, file); err != nil {
|
||||||
|
log.Printf("upload: write %q failed: %v", destPath, err)
|
||||||
_ = os.Remove(destPath)
|
_ = os.Remove(destPath)
|
||||||
http.Error(w, "Upload fehlgeschlagen", http.StatusInternalServerError)
|
http.Error(w, "Upload fehlgeschlagen", http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|||||||
+17
-8
@@ -73,26 +73,28 @@ h2 {
|
|||||||
|
|
||||||
.book-list {
|
.book-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
margin: 0 -0.25rem;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-item {
|
.book-item {
|
||||||
display: inline-block;
|
|
||||||
width: 33.3333%;
|
|
||||||
vertical-align: top;
|
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 0.25rem;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-card {
|
.book-card {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
border: 1px solid #000;
|
border: 1px solid #000;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-card-cover {
|
.book-card-cover {
|
||||||
@@ -141,18 +143,25 @@ h2 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-title {
|
.book-card .book-title {
|
||||||
display: block;
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
line-height: 1.25;
|
line-height: 1.25;
|
||||||
margin-bottom: 0.25rem;
|
margin-bottom: 0.25rem;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-author,
|
.book-author,
|
||||||
.book-author-detail {
|
.book-author-detail {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 0.2rem;
|
margin-top: 0.2rem;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-break: break-word;
|
||||||
}
|
}
|
||||||
|
|
||||||
.book-author {
|
.book-author {
|
||||||
|
|||||||
+16
-5
@@ -77,8 +77,8 @@ templ UploadPage(errMsg string) {
|
|||||||
<p class="error">{ errMsg }</p>
|
<p class="error">{ errMsg }</p>
|
||||||
}
|
}
|
||||||
<form method="post" action="/upload" enctype="multipart/form-data" class="upload-form">
|
<form method="post" action="/upload" enctype="multipart/form-data" class="upload-form">
|
||||||
<label for="book">EPUB oder PDF auswählen</label>
|
<label for="book">EPUB auswählen</label>
|
||||||
<input id="book" type="file" name="book" accept=".epub,.pdf" required />
|
<input id="book" type="file" name="book" accept=".epub" required />
|
||||||
<button type="submit" class="btn">Hochladen</button>
|
<button type="submit" class="btn">Hochladen</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
@@ -104,7 +104,7 @@ templ IndexPage(books []library.Book, canUpload bool) {
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
if len(books) == 0 {
|
if len(books) == 0 {
|
||||||
<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>
|
<p>Keine eBooks gefunden. Lege EPUB-Dateien im Ordner <code>books/</code> ab.</p>
|
||||||
} else {
|
} else {
|
||||||
<ul class="book-list">
|
<ul class="book-list">
|
||||||
for _, b := range books {
|
for _, b := range books {
|
||||||
@@ -114,8 +114,8 @@ templ IndexPage(books []library.Book, canUpload bool) {
|
|||||||
@BookCover(b, false)
|
@BookCover(b, false)
|
||||||
</div>
|
</div>
|
||||||
<div class="book-card-body">
|
<div class="book-card-body">
|
||||||
<strong class="book-title">{ b.Title }</strong>
|
<strong class="book-title" title={ b.Title }>{ truncateTitle(b.Title, 60) }</strong>
|
||||||
<span class="book-author">{ b.Author }</span>
|
<span class="book-author">{ truncateTitle(b.Author, 40) }</span>
|
||||||
<span class="format">{ b.Format }</span>
|
<span class="format">{ b.Format }</span>
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
@@ -164,3 +164,14 @@ func placeholderClass(detail bool) string {
|
|||||||
}
|
}
|
||||||
return "book-cover-placeholder"
|
return "book-cover-placeholder"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// truncateTitle kürzt lange Titel für die Kartenansicht, damit lange,
|
||||||
|
// nicht umbrechbare Titel das Grid-Layout nicht sprengen. Der volle Titel
|
||||||
|
// bleibt im Detail-Seiten-Titel und im alt-Text des Covers erhalten.
|
||||||
|
func truncateTitle(title string, maxLen int) string {
|
||||||
|
r := []rune(title)
|
||||||
|
if len(r) <= maxLen {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return string(r[:maxLen]) + "…"
|
||||||
|
}
|
||||||
|
|||||||
+71
-47
@@ -262,7 +262,7 @@ func UploadPage(errMsg string) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " <form method=\"post\" action=\"/upload\" enctype=\"multipart/form-data\" class=\"upload-form\"><label for=\"book\">EPUB oder PDF auswählen</label> <input id=\"book\" type=\"file\" name=\"book\" accept=\".epub,.pdf\" required> <button type=\"submit\" class=\"btn\">Hochladen</button></form>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " <form method=\"post\" action=\"/upload\" enctype=\"multipart/form-data\" class=\"upload-form\"><label for=\"book\">EPUB auswählen</label> <input id=\"book\" type=\"file\" name=\"book\" accept=\".epub\" required> <button type=\"submit\" class=\"btn\">Hochladen</button></form>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -435,7 +435,7 @@ func IndexPage(books []library.Book, canUpload bool) templ.Component {
|
|||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
if len(books) == 0 {
|
if len(books) == 0 {
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p>Keine eBooks gefunden. Lege EPUB- oder PDF-Dateien im Ordner <code>books/</code> ab.</p>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<p>Keine eBooks gefunden. Lege EPUB-Dateien im Ordner <code>books/</code> ab.</p>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -466,51 +466,64 @@ func IndexPage(books []library.Book, canUpload bool) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><div class=\"book-card-body\"><strong class=\"book-title\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div><div class=\"book-card-body\"><strong class=\"book-title\" title=\"")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var23 string
|
var templ_7745c5c3_Var23 string
|
||||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(b.Title)
|
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(b.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 117, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 117, Col: 58}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</strong> <span class=\"book-author\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var24 string
|
var templ_7745c5c3_Var24 string
|
||||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(b.Author)
|
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(truncateTitle(b.Title, 60))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 118, Col: 52}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 117, Col: 89}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</span> <span class=\"format\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</strong> <span class=\"book-author\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var25 string
|
var templ_7745c5c3_Var25 string
|
||||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format)
|
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(truncateTitle(b.Author, 40))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 119, Col: 47}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 118, Col: 71}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span></div></a></li>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span> <span class=\"format\">")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var26 string
|
||||||
|
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinStringErrs(b.Format)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 119, Col: 47}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span></div></a></li>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</ul>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</ul>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -541,12 +554,12 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||||
if templ_7745c5c3_Var26 == nil {
|
if templ_7745c5c3_Var27 == nil {
|
||||||
templ_7745c5c3_Var26 = templ.NopComponent
|
templ_7745c5c3_Var27 = templ.NopComponent
|
||||||
}
|
}
|
||||||
ctx = templ.ClearChildren(ctx)
|
ctx = templ.ClearChildren(ctx)
|
||||||
templ_7745c5c3_Var27 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
templ_7745c5c3_Var28 := templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||||
if !templ_7745c5c3_IsBuffer {
|
if !templ_7745c5c3_IsBuffer {
|
||||||
@@ -558,7 +571,7 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
ctx = templ.InitializeContext(ctx)
|
ctx = templ.InitializeContext(ctx)
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "<article class=\"book-detail\"><div class=\"book-detail-cover\">")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<article class=\"book-detail\"><div class=\"book-detail-cover\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -566,78 +579,78 @@ func BookPage(book library.Book) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div><div class=\"book-detail-body\"><h2>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div><div class=\"book-detail-body\"><h2>")
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
var templ_7745c5c3_Var28 string
|
|
||||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 136, Col: 24}
|
|
||||||
}
|
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
|
||||||
if templ_7745c5c3_Err != nil {
|
|
||||||
return templ_7745c5c3_Err
|
|
||||||
}
|
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</h2><p class=\"book-author-detail\">")
|
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var29 string
|
var templ_7745c5c3_Var29 string
|
||||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 137, Col: 51}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 136, Col: 24}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</p><dl class=\"book-meta\"><div><dt>Datei</dt><dd>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "</h2><p class=\"book-author-detail\">")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var30 string
|
var templ_7745c5c3_Var30 string
|
||||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename)
|
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 141, Col: 31}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 137, Col: 51}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</dd></div><div><dt>Format</dt><dd>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "</p><dl class=\"book-meta\"><div><dt>Datei</dt><dd>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var31 string
|
var templ_7745c5c3_Var31 string
|
||||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format)
|
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.Filename)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 145, Col: 29}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 141, Col: 31}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</dd></div></dl><p><a class=\"btn\" href=\"")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "</dd></div><div><dt>Format</dt><dd>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
var templ_7745c5c3_Var32 templ.SafeURL
|
var templ_7745c5c3_Var32 string
|
||||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + book.ID)
|
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 148, Col: 55}
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 145, Col: 29}
|
||||||
}
|
}
|
||||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "\">Auf Tolino herunterladen</a></p></div></article>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "</dd></div></dl><p><a class=\"btn\" href=\"")
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
var templ_7745c5c3_Var33 templ.SafeURL
|
||||||
|
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + book.ID)
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages.templ`, Line: 148, Col: 55}
|
||||||
|
}
|
||||||
|
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||||
|
if templ_7745c5c3_Err != nil {
|
||||||
|
return templ_7745c5c3_Err
|
||||||
|
}
|
||||||
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">Auf Tolino herunterladen</a></p></div></article>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var27), templ_7745c5c3_Buffer)
|
templ_7745c5c3_Err = Layout(book.Title, "/").Render(templ.WithChildren(ctx, templ_7745c5c3_Var28), templ_7745c5c3_Buffer)
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
@@ -659,4 +672,15 @@ func placeholderClass(detail bool) string {
|
|||||||
return "book-cover-placeholder"
|
return "book-cover-placeholder"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// truncateTitle kürzt lange Titel für die Kartenansicht, damit lange,
|
||||||
|
// nicht umbrechbare Titel das Grid-Layout nicht sprengen. Der volle Titel
|
||||||
|
// bleibt im Detail-Seiten-Titel und im alt-Text des Covers erhalten.
|
||||||
|
func truncateTitle(title string, maxLen int) string {
|
||||||
|
r := []rune(title)
|
||||||
|
if len(r) <= maxLen {
|
||||||
|
return title
|
||||||
|
}
|
||||||
|
return string(r[:maxLen]) + "…"
|
||||||
|
}
|
||||||
|
|
||||||
var _ = templruntime.GeneratedTemplate
|
var _ = templruntime.GeneratedTemplate
|
||||||
|
|||||||
Reference in New Issue
Block a user