Compare commits
3
Commits
4c0aa053ac
...
a51578cbf1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a51578cbf1 | ||
|
|
e4fe39ecc5 | ||
|
|
0c3e5cec9f |
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
## Build & run
|
||||
|
||||
@@ -4,7 +4,7 @@ Minimalistische eBook-Bibliothek für den Tolino-Webbrowser.
|
||||
|
||||
## Features (MVP)
|
||||
|
||||
- Listet EPUB/PDF-Dateien aus `books/`
|
||||
- Listet EPUB-Dateien aus `books/`
|
||||
- Detailseite pro Buch
|
||||
- Download-Link pro Buch (für Tolino)
|
||||
- Schlichtes, kontrastreiches UI ohne JavaScript-Abhängigkeit
|
||||
@@ -56,10 +56,7 @@ go run ./cmd/server
|
||||
|
||||
## Bücher hinzufügen
|
||||
|
||||
Lege deine Dateien in den Ordner `books/`:
|
||||
|
||||
- `.epub` (bevorzugt)
|
||||
- `.pdf` (optional)
|
||||
Lege deine EPUB-Dateien in den Ordner `books/`.
|
||||
|
||||
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>
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
Umgebungsvariablen:
|
||||
|
||||
+4
-1
@@ -21,7 +21,10 @@ func main() {
|
||||
}
|
||||
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)
|
||||
|
||||
mux := http.NewServeMux()
|
||||
|
||||
+14
-13
@@ -19,8 +19,11 @@ type Service struct {
|
||||
booksDir string
|
||||
}
|
||||
|
||||
func New(booksDir string) *Service {
|
||||
return &Service{booksDir: booksDir}
|
||||
func New(booksDir string) (*Service, error) {
|
||||
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 }
|
||||
@@ -41,7 +44,7 @@ func (s *Service) ListBooks() ([]Book, error) {
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(e.Name()))
|
||||
if ext != ".epub" && ext != ".pdf" {
|
||||
if ext != ".epub" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -52,17 +55,15 @@ func (s *Service) ListBooks() ([]Book, error) {
|
||||
hasCover := false
|
||||
title := fallbackTitle
|
||||
|
||||
if ext == ".epub" {
|
||||
meta, err := readEPUBMetadata(fullPath)
|
||||
if err == nil {
|
||||
if strings.TrimSpace(meta.Title) != "" {
|
||||
title = strings.TrimSpace(meta.Title)
|
||||
}
|
||||
if strings.TrimSpace(meta.Author) != "" {
|
||||
author = strings.TrimSpace(meta.Author)
|
||||
}
|
||||
hasCover = meta.HasCover
|
||||
meta, err := readEPUBMetadata(fullPath)
|
||||
if err == nil {
|
||||
if strings.TrimSpace(meta.Title) != "" {
|
||||
title = strings.TrimSpace(meta.Title)
|
||||
}
|
||||
if strings.TrimSpace(meta.Author) != "" {
|
||||
author = strings.TrimSpace(meta.Author)
|
||||
}
|
||||
hasCover = meta.HasCover
|
||||
}
|
||||
|
||||
books = append(books, Book{
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -209,8 +210,8 @@ func (h *Handler) uploadSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
defer file.Close()
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(header.Filename))
|
||||
if ext != ".epub" && ext != ".pdf" {
|
||||
render(w, r.Context(), views.UploadPage("Nur EPUB- und PDF-Dateien erlaubt."))
|
||||
if ext != ".epub" {
|
||||
render(w, r.Context(), views.UploadPage("Nur EPUB-Dateien erlaubt."))
|
||||
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)))
|
||||
return
|
||||
}
|
||||
log.Printf("upload: create %q failed: %v", destPath, err)
|
||||
http.Error(w, "Datei konnte nicht gespeichert werden", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
if _, err := io.Copy(out, file); err != nil {
|
||||
log.Printf("upload: write %q failed: %v", destPath, err)
|
||||
_ = os.Remove(destPath)
|
||||
http.Error(w, "Upload fehlgeschlagen", http.StatusInternalServerError)
|
||||
return
|
||||
|
||||
+17
-8
@@ -73,26 +73,28 @@ h2 {
|
||||
|
||||
.book-list {
|
||||
list-style: none;
|
||||
margin: 0 -0.25rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.5rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.book-item {
|
||||
display: inline-block;
|
||||
width: 33.3333%;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
padding: 0.25rem;
|
||||
margin: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.book-card {
|
||||
display: block;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border: 1px solid #000;
|
||||
padding: 0.5rem;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.book-card-cover {
|
||||
@@ -141,18 +143,25 @@ h2 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.book-title {
|
||||
display: block;
|
||||
.book-card .book-title {
|
||||
font-weight: bold;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.25;
|
||||
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-detail {
|
||||
display: block;
|
||||
margin-top: 0.2rem;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.book-author {
|
||||
|
||||
+16
-5
@@ -77,8 +77,8 @@ templ UploadPage(errMsg string) {
|
||||
<p class="error">{ errMsg }</p>
|
||||
}
|
||||
<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 />
|
||||
<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>
|
||||
}
|
||||
@@ -104,7 +104,7 @@ templ IndexPage(books []library.Book, canUpload bool) {
|
||||
}
|
||||
</section>
|
||||
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 {
|
||||
<ul class="book-list">
|
||||
for _, b := range books {
|
||||
@@ -114,8 +114,8 @@ templ IndexPage(books []library.Book, canUpload bool) {
|
||||
@BookCover(b, false)
|
||||
</div>
|
||||
<div class="book-card-body">
|
||||
<strong class="book-title">{ b.Title }</strong>
|
||||
<span class="book-author">{ b.Author }</span>
|
||||
<strong class="book-title" title={ b.Title }>{ truncateTitle(b.Title, 60) }</strong>
|
||||
<span class="book-author">{ truncateTitle(b.Author, 40) }</span>
|
||||
<span class="format">{ b.Format }</span>
|
||||
</div>
|
||||
</a>
|
||||
@@ -164,3 +164,14 @@ func placeholderClass(detail bool) string {
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -435,7 +435,7 @@ func IndexPage(books []library.Book, canUpload bool) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -466,51 +466,64 @@ func IndexPage(books []library.Book, canUpload bool) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -541,12 +554,12 @@ func BookPage(book library.Book) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var26 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var26 == nil {
|
||||
templ_7745c5c3_Var26 = templ.NopComponent
|
||||
templ_7745c5c3_Var27 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var27 == nil {
|
||||
templ_7745c5c3_Var27 = templ.NopComponent
|
||||
}
|
||||
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_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
@@ -558,7 +571,7 @@ func BookPage(book library.Book) templ.Component {
|
||||
}()
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -566,78 +579,78 @@ func BookPage(book library.Book) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</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\">")
|
||||
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_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 {
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var32 templ.SafeURL
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinURLErrs("/download/" + book.ID)
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Format)
|
||||
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))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -659,4 +672,15 @@ func placeholderClass(detail bool) string {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user