Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aea6a4e2c | ||
|
|
b10bf3e8c7 | ||
|
|
8c3273a0fc | ||
|
|
aec226af1a | ||
|
|
1c1f5cf1bb | ||
|
|
9e4cc4efd0 | ||
|
|
7b0b2793a6 | ||
|
|
077afaca56 | ||
|
|
388b62b277 | ||
|
|
5da97b2e5d | ||
|
|
45e3a256e2 | ||
|
|
64ef691d64 | ||
|
|
a4dbe79d0b | ||
|
|
1002cbca93 | ||
|
|
65f6bdf075 | ||
|
|
4faa217822 | ||
|
|
5cdfe5a8dd | ||
|
|
1cd8557b58 | ||
|
|
b4c956aed4 | ||
|
|
7caa46c2da | ||
|
|
5329bf35a9 | ||
|
|
80e5d5b0d1 | ||
|
|
b3a429d7e1 | ||
|
|
d8179bb066 | ||
|
|
960305dfa9 | ||
|
|
16bbeec5a3 | ||
|
|
39f9c39360 | ||
|
|
011b6d14e4 | ||
|
|
d4661e7f04 | ||
|
|
27a93ba2ea | ||
|
|
7afad5dcde | ||
|
|
929a862e74 | ||
|
|
0e55dafb1e | ||
|
|
cc8084a713 |
@@ -20,6 +20,9 @@ logs/
|
||||
|
||||
# Dependencies (install fresh in container)
|
||||
node_modules/
|
||||
# Local-only lockfile: a stale one pins old foliate-js commits into builds
|
||||
# (npm reuses name@version without re-resolving the git pin)
|
||||
package-lock.json
|
||||
|
||||
# Environment
|
||||
.env
|
||||
|
||||
@@ -56,6 +56,9 @@ jobs:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
# Bake the version into the binary (Admin → Settings → About).
|
||||
build-args: |
|
||||
APP_VERSION=${{ env.TAG }}
|
||||
# Publishes both the exact version (e.g. v0.2.0) and the movable "latest" tag.
|
||||
# Deployments default to "latest" via ${IMAGE_TAG:-latest} in docker-compose.yml;
|
||||
# pin or roll back by setting IMAGE_TAG in .env.
|
||||
|
||||
+21
-2
@@ -15,7 +15,10 @@ RUN --mount=type=cache,target=/root/go/pkg/mod \
|
||||
|
||||
# Copy package files and install npm dependencies (cached unless package.json changes)
|
||||
COPY package*.json ./
|
||||
RUN --mount=type=cache,target=/root/.npm \
|
||||
# npm cache mount renamed from /root/.npm: the old volume held a stale
|
||||
# foliate-js tarball (name@version reuse) that --no-cache and builder prune
|
||||
# could not evict; a new mount id guarantees a clean fetch.
|
||||
RUN --mount=type=cache,target=/root/.npm-cache2 \
|
||||
npm install
|
||||
|
||||
# Copy Go mod files (cached unless go.mod changes)
|
||||
@@ -34,10 +37,23 @@ RUN cd templates && templ generate
|
||||
RUN npm run build:css:prod
|
||||
RUN npm run build:ts
|
||||
|
||||
# App version baked into the binary (shown in Admin → Settings → About).
|
||||
# Declared after `COPY . .` so a version bump only invalidates this layer.
|
||||
# The release workflow passes the git tag, e.g. APP_VERSION=v1.5.
|
||||
ARG APP_VERSION=dev
|
||||
|
||||
# Build Go binary (cached unless Go files or generated code changes)
|
||||
RUN --mount=type=cache,target=/root/go/pkg/mod \
|
||||
--mount=type=cache,target=/root/.cache/go-build \
|
||||
CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo -o main ./cmd/server
|
||||
CGO_ENABLED=0 GOOS=linux go build -installsuffix cgo \
|
||||
-ldflags "-X bookhoard/internal/version.Version=${APP_VERSION}" \
|
||||
-o main ./cmd/server
|
||||
|
||||
# Materialize the Go module cache into an image layer so the test-runner
|
||||
# stage can restore it. GOMODCACHE is /go/pkg/mod; cache mounts elsewhere are
|
||||
# ignored by the go tool, and without this copy `go test` in the tests
|
||||
# container re-downloads every dependency on each invocation.
|
||||
RUN cp -r /go/pkg/mod /app/go-module-cache
|
||||
|
||||
# Test runner stage - includes Go runtime and test dependencies
|
||||
# This stage is ONLY used for running tests, never deployed to production
|
||||
@@ -50,6 +66,9 @@ WORKDIR /app
|
||||
# Copy everything from builder (includes compiled binary, source code, and dependencies)
|
||||
COPY --from=builder /app ./
|
||||
|
||||
# Restore the materialized Go module cache so `go test` works offline.
|
||||
RUN mkdir -p /go/pkg/mod && cp -r /app/go-module-cache/. /go/pkg/mod/ && rm -rf /app/go-module-cache
|
||||
|
||||
# Install kepubify for conversion tests
|
||||
RUN wget -O /usr/bin/kepubify https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-linux-64bit \
|
||||
&& chmod +x /usr/bin/kepubify
|
||||
|
||||
@@ -121,6 +121,9 @@ func main() {
|
||||
|
||||
// Create library service
|
||||
libraryService := services.NewLibraryService(queries)
|
||||
// The progress service verifies submitted anchors against the book
|
||||
// itself — it needs to resolve library-relative file paths.
|
||||
progressService.SetMediaPathResolver(libraryService)
|
||||
|
||||
// Sync Go AllowedExtensions into DB so API clients see correct extensions
|
||||
libraryService.SyncAllowedExtensions(context.Background())
|
||||
|
||||
@@ -0,0 +1,639 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/services"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// Cross-library SHA move detection + archived-twin merge integration tests.
|
||||
// These cover the scenario where identical book content moves between
|
||||
// libraries of the same type: the row must be repointed (history follows)
|
||||
// instead of duplicated, deliberate copies stay independent, type rules are
|
||||
// enforced, and archived twins can be merged into their active copy.
|
||||
|
||||
const (
|
||||
moveTestAdminEmail = "testuser@tests.bookhoard.internal"
|
||||
moveTestFileName = "Test Book.epub"
|
||||
)
|
||||
|
||||
// createEbooksTestLibrary creates a library of the given type via the API and
|
||||
// swaps its default folder for a fresh temp directory the test controls.
|
||||
func createTypedTestLibrary(t *testing.T, setup *TestServerSetup, name, libType string) (pgtype.UUID, string) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
libBody, _ := json.Marshal(map[string]interface{}{"name": name, "type": libType})
|
||||
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/libraries", bytes.NewBuffer(libBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
require.Equal(t, http.StatusCreated, resp.StatusCode, "library creation should succeed")
|
||||
|
||||
var libResp map[string]interface{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&libResp))
|
||||
libID, err := uuid.Parse(libResp["id"].(string))
|
||||
require.NoError(t, err)
|
||||
pgLib := pgtype.UUID{Bytes: libID, Valid: true}
|
||||
|
||||
folders, err := setup.DB.GetLibraryFolders(ctx, pgLib)
|
||||
require.NoError(t, err)
|
||||
for _, f := range folders {
|
||||
_, err = setup.DB.DeleteLibraryFolder(ctx, database.DeleteLibraryFolderParams{
|
||||
LibraryID: pgLib,
|
||||
FolderPath: f.FolderPath,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
tmpDir := t.TempDir()
|
||||
_, err = setup.DB.AddLibraryFolder(ctx, database.AddLibraryFolderParams{
|
||||
LibraryID: pgLib,
|
||||
FolderPath: tmpDir,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
return pgLib, tmpDir
|
||||
}
|
||||
|
||||
// deleteLibraries removes libraries (cascading to media items, progress,
|
||||
// annotations, and processing issues) so reruns never see leftovers from a
|
||||
// previous run — stale rows with identical content hashes would otherwise be
|
||||
// picked up as move candidates. Call via defer AFTER the setup.Close defer so
|
||||
// it executes while the pool is still open (defers run LIFO).
|
||||
func deleteLibraries(t *testing.T, setup *TestServerSetup, libs ...pgtype.UUID) {
|
||||
t.Helper()
|
||||
for _, lib := range libs {
|
||||
_ = setup.DB.DeleteLibrary(context.Background(), lib)
|
||||
}
|
||||
}
|
||||
|
||||
// scanTestFolder runs a synchronous scan over one folder.
|
||||
func scanTestFolder(t *testing.T, setup *TestServerSetup, adminID pgtype.UUID, folder string) {
|
||||
t.Helper()
|
||||
scanner := services.NewMediaScanner(setup.DB)
|
||||
defer func() { _ = scanner.Close() }()
|
||||
scanner.SetAdminID(adminID)
|
||||
require.NoError(t, scanner.SetFolders([]string{folder}, false))
|
||||
require.NoError(t, scanner.ScanFolders(context.Background()))
|
||||
}
|
||||
|
||||
// waitForCond polls cond until true or the timeout elapses.
|
||||
func waitForCond(t *testing.T, timeout time.Duration, msg string, cond func() bool) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if cond() {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("condition not met within %v: %s", timeout, msg)
|
||||
}
|
||||
|
||||
func moveTestAdminID(t *testing.T, setup *TestServerSetup) (pgtype.UUID, pgtype.UUID) {
|
||||
t.Helper()
|
||||
admin, err := setup.DB.GetUserByEmail(context.Background(), moveTestAdminEmail)
|
||||
require.NoError(t, err)
|
||||
pgAdmin := pgtype.UUID{Bytes: admin.ID.Bytes, Valid: true}
|
||||
return pgAdmin, pgAdmin
|
||||
}
|
||||
|
||||
// writeTestEbook places an identical dummy epub under folder/Author/.
|
||||
func writeTestEbook(t *testing.T, folder string, content []byte) string {
|
||||
t.Helper()
|
||||
authorDir := filepath.Join(folder, "Author")
|
||||
require.NoError(t, os.MkdirAll(authorDir, 0755))
|
||||
bookPath := filepath.Join(authorDir, moveTestFileName)
|
||||
require.NoError(t, os.WriteFile(bookPath, content, 0644))
|
||||
return bookPath
|
||||
}
|
||||
|
||||
// TestCrossLibraryMovePreservesHistory is the core scenario: a book moves
|
||||
// from library A to library B (same type). The existing row must be repointed
|
||||
// to library B keeping its ID so progress and annotations survive, instead of
|
||||
// creating a duplicate while the old row goes missing.
|
||||
func TestCrossLibraryMovePreservesHistory(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, adminPgID := moveTestAdminID(t, setup)
|
||||
libA, folderA := createTypedTestLibrary(t, setup, "Move Src Library", "ebooks")
|
||||
libB, folderB := createTypedTestLibrary(t, setup, "Move Dst Library", "ebooks")
|
||||
defer deleteLibraries(t, setup, libA, libB)
|
||||
|
||||
content := []byte("cross library move test epub content")
|
||||
writeTestEbook(t, folderA, content)
|
||||
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
|
||||
var original database.ListMediaItemsByLibraryRow
|
||||
waitForCond(t, 5*time.Second, "book should be imported into library A", func() bool {
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
if err != nil || len(books) != 1 {
|
||||
return false
|
||||
}
|
||||
original = books[0]
|
||||
return true
|
||||
})
|
||||
|
||||
// Give the book real user data at its original location.
|
||||
_, err := setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
|
||||
MediaItemID: original.ID,
|
||||
UserID: adminPgID,
|
||||
CurrentPage: pgtype.Int4{Int32: 12, Valid: true},
|
||||
TotalPages: pgtype.Int4{Int32: 300, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
||||
MediaItemID: original.ID,
|
||||
UserID: adminPgID,
|
||||
SelectionText: "a line worth keeping",
|
||||
Color: pgtype.Text{String: "#ffff00", Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Move the file: same bytes, new library folder, old copy deleted.
|
||||
writeTestEbook(t, folderB, content)
|
||||
require.NoError(t, os.Remove(filepath.Join(folderA, "Author", moveTestFileName)))
|
||||
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
|
||||
var moved database.ListMediaItemsByLibraryRow
|
||||
waitForCond(t, 5*time.Second, "book should be repointed into library B", func() bool {
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libB)
|
||||
if err != nil || len(books) != 1 {
|
||||
return false
|
||||
}
|
||||
moved = books[0]
|
||||
return true
|
||||
})
|
||||
|
||||
assert.Equal(t, original.ID, moved.ID, "row ID must be preserved so history follows the book")
|
||||
assert.Equal(t, libB, moved.LibraryID, "row must now belong to library B")
|
||||
assert.Equal(t, filepath.ToSlash(filepath.Join("Author", moveTestFileName)), moved.FilePath, "path must be repointed relative to library B")
|
||||
assert.False(t, moved.ArchivedAt.Valid, "moved row must not be archived")
|
||||
assert.Equal(t, int32(0), moved.MissingScanCount, "moved row must not be marked missing")
|
||||
assert.True(t, moved.LibraryTypeName.Valid && moved.LibraryTypeName.String == "ebooks", "library_type_name must stay truthful after library change")
|
||||
|
||||
// History followed the row.
|
||||
progress, err := setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: original.ID, UserID: adminPgID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(12), progress.CurrentPage.Int32, "reading progress must survive the move")
|
||||
|
||||
highlights, err := setup.DB.GetMediaHighlights(ctx, database.GetMediaHighlightsParams{MediaItemID: original.ID, UserID: adminPgID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, highlights, 1, "annotation must survive the move")
|
||||
|
||||
// Library A no longer holds the row: no duplicate, no archived ghost.
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, booksA, "library A must not keep a duplicate row after the move")
|
||||
}
|
||||
|
||||
// TestCrossLibraryCopyStaysIndependent verifies that placing identical
|
||||
// content in two libraries on purpose creates two independent rows: the
|
||||
// second import is NOT blocked, and neither row is repointed or archived.
|
||||
func TestCrossLibraryCopyStaysIndependent(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, adminPgID := moveTestAdminID(t, setup)
|
||||
libA, folderA := createTypedTestLibrary(t, setup, "Copy Lib A", "ebooks")
|
||||
libB, folderB := createTypedTestLibrary(t, setup, "Copy Lib B", "ebooks")
|
||||
defer deleteLibraries(t, setup, libA, libB)
|
||||
|
||||
content := []byte("deliberate duplicate content")
|
||||
writeTestEbook(t, folderA, content)
|
||||
writeTestEbook(t, folderB, content)
|
||||
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
|
||||
var itemA, itemB database.ListMediaItemsByLibraryRow
|
||||
waitForCond(t, 5*time.Second, "both libraries should hold their own copy", func() bool {
|
||||
booksA, errA := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
booksB, errB := setup.DB.ListMediaItemsByLibrary(ctx, libB)
|
||||
if errA != nil || errB != nil || len(booksA) != 1 || len(booksB) != 1 {
|
||||
return false
|
||||
}
|
||||
itemA, itemB = booksA[0], booksB[0]
|
||||
return true
|
||||
})
|
||||
|
||||
assert.NotEqual(t, itemA.ID, itemB.ID, "deliberate copies must stay independent rows")
|
||||
assert.False(t, itemA.ArchivedAt.Valid && itemB.ArchivedAt.Valid, "no copy should be archived")
|
||||
|
||||
// History on one copy does not leak into the other.
|
||||
_, err := setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
|
||||
MediaItemID: itemA.ID,
|
||||
UserID: adminPgID,
|
||||
CurrentPage: pgtype.Int4{Int32: 7, Valid: true},
|
||||
TotalPages: pgtype.Int4{Int32: 100, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
_, err = setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: itemB.ID, UserID: adminPgID})
|
||||
assert.Error(t, err, "copy B must not inherit copy A's progress")
|
||||
}
|
||||
|
||||
// TestCrossLibraryMoveRejectedForTypeMismatch verifies the type guard: the
|
||||
// cross-library lookup only matches same-type libraries, so a reflowable EPUB
|
||||
// registered in a manga library whose file moves to ANOTHER manga library is
|
||||
// detected as a move but must be rejected by the type rules (manga libraries
|
||||
// only accept fixed-layout/image content). The move is rejected, a processing
|
||||
// issue is recorded, and the file stays untracked rather than being
|
||||
// force-imported.
|
||||
func TestCrossLibraryMoveRejectedForTypeMismatch(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, _ := moveTestAdminID(t, setup)
|
||||
libA, folderA := createTypedTestLibrary(t, setup, "Type Src Manga", "manga")
|
||||
libM, folderM := createTypedTestLibrary(t, setup, "Type Dst Manga", "manga")
|
||||
defer deleteLibraries(t, setup, libA, libM)
|
||||
|
||||
content := []byte("reflowable epub that manga libraries must not adopt via move")
|
||||
writeTestEbook(t, folderA, content)
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
|
||||
var original database.ListMediaItemsByLibraryRow
|
||||
waitForCond(t, 5*time.Second, "book should be imported into the first manga library", func() bool {
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
if err != nil || len(books) != 1 {
|
||||
return false
|
||||
}
|
||||
original = books[0]
|
||||
return true
|
||||
})
|
||||
require.Equal(t, "reflowable", original.FormatGroup, "dummy epub must scan as reflowable for this test")
|
||||
|
||||
// Move into the second manga library: same hash, same type, but the
|
||||
// reflowable format violates manga library rules.
|
||||
writeTestEbook(t, folderM, content)
|
||||
require.NoError(t, os.Remove(filepath.Join(folderA, "Author", moveTestFileName)))
|
||||
|
||||
scanTestFolder(t, setup, adminID, folderM)
|
||||
|
||||
// The second manga library must not adopt the item.
|
||||
booksM, err := setup.DB.ListMediaItemsByLibrary(ctx, libM)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, booksM, "reflowable epub must not be repointed between manga libraries")
|
||||
|
||||
// The original row is untouched in the first manga library.
|
||||
after, err := setup.DB.GetMediaItem(ctx, original.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, libA, after.LibraryID, "original row must stay in its library")
|
||||
|
||||
// A processing issue explains the rejection.
|
||||
waitForCond(t, 5*time.Second, "processing issue should be recorded", func() bool {
|
||||
issues, err := setup.DB.ListProcessingIssuesByLibrary(ctx, libM)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, issue := range issues {
|
||||
if issue.IssueType == "library_type_mismatch" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
// TestListHiddenMediaItemsMatchesActiveTwin verifies the archived-page query:
|
||||
// an archived row with an active same-hash twin reports it in match_* columns;
|
||||
// an archived row without a twin reports no match.
|
||||
func TestListHiddenMediaItemsMatchesActiveTwin(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, _ := moveTestAdminID(t, setup)
|
||||
libA, folderA := createTypedTestLibrary(t, setup, "Twin Lib A", "ebooks")
|
||||
libB, folderB := createTypedTestLibrary(t, setup, "Twin Lib B", "ebooks")
|
||||
libC, folderC := createTypedTestLibrary(t, setup, "Twin Lib C", "ebooks")
|
||||
defer deleteLibraries(t, setup, libA, libB, libC)
|
||||
|
||||
twinContent := []byte("twin content shared by two libraries")
|
||||
writeTestEbook(t, folderA, twinContent)
|
||||
writeTestEbook(t, folderB, twinContent)
|
||||
lonelyContent := []byte("unique content with no twin anywhere")
|
||||
writeTestEbook(t, folderC, lonelyContent)
|
||||
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
scanTestFolder(t, setup, adminID, folderC)
|
||||
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
twin := booksA[0]
|
||||
|
||||
booksC, err := setup.DB.ListMediaItemsByLibrary(ctx, libC)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksC, 1)
|
||||
lonely := booksC[0]
|
||||
|
||||
// Archive both: the A row now has an active twin in B, the C row has none.
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, twin.ID))
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, lonely.ID))
|
||||
|
||||
rows, err := setup.DB.ListHiddenMediaItems(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
foundTwin, foundLonely := false, false
|
||||
for _, row := range rows {
|
||||
if row.ID == twin.ID {
|
||||
foundTwin = true
|
||||
assert.True(t, row.MatchID.Valid, "twin row must report its active match")
|
||||
if row.MatchID.Valid {
|
||||
assert.Equal(t, findTwinTargetID(t, setup, libB), uuid.UUID(row.MatchID.Bytes).String())
|
||||
}
|
||||
assert.Equal(t, "Test Book", row.MatchTitle)
|
||||
}
|
||||
if row.ID == lonely.ID {
|
||||
foundLonely = true
|
||||
assert.False(t, row.MatchID.Valid, "row without a twin must report no match")
|
||||
}
|
||||
}
|
||||
require.True(t, foundTwin, "twin row should be listed")
|
||||
require.True(t, foundLonely, "lonely row should be listed")
|
||||
}
|
||||
|
||||
// findTwinTargetID returns the single active item in the given library.
|
||||
func findTwinTargetID(t *testing.T, setup *TestServerSetup, libID pgtype.UUID) string {
|
||||
t.Helper()
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(context.Background(), libID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, books, 1)
|
||||
return uuid.UUID(books[0].ID.Bytes).String()
|
||||
}
|
||||
|
||||
// mergeRequest issues POST /api/media-items/{source}/merge.
|
||||
func mergeRequest(t *testing.T, setup *TestServerSetup, token, sourceID string, body map[string]interface{}) *http.Response {
|
||||
t.Helper()
|
||||
payload, _ := json.Marshal(body)
|
||||
req, _ := http.NewRequest("POST", setup.Server.URL+"/api/media-items/"+sourceID+"/merge", bytes.NewBuffer(payload))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
return resp
|
||||
}
|
||||
|
||||
// archiveAndIsolate imports one book into the library and archives it,
|
||||
// simulating a file that was copied elsewhere then deleted from here.
|
||||
func archiveAndIsolate(t *testing.T, setup *TestServerSetup, adminID, adminPgID pgtype.UUID, libID pgtype.UUID, folder, content string) database.ListMediaItemsByLibraryRow {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
writeTestEbook(t, folder, []byte(content))
|
||||
scanTestFolder(t, setup, adminID, folder)
|
||||
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, books, 1)
|
||||
item := books[0]
|
||||
|
||||
// Reading history that must survive the merge.
|
||||
_, err = setup.DB.UpdateReadingProgress(ctx, database.UpdateReadingProgressParams{
|
||||
MediaItemID: item.ID,
|
||||
UserID: adminPgID,
|
||||
CurrentPage: pgtype.Int4{Int32: 42, Valid: true},
|
||||
TotalPages: pgtype.Int4{Int32: 200, Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
_, err = setup.DB.CreateMediaHighlight(ctx, database.CreateMediaHighlightParams{
|
||||
MediaItemID: item.ID,
|
||||
UserID: adminPgID,
|
||||
SelectionText: "annotation on the old copy",
|
||||
Color: pgtype.Text{String: "#00ff00", Valid: true},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, item.ID))
|
||||
return item
|
||||
}
|
||||
|
||||
// TestMergeArchivedItemIntoActiveTwin covers the copy-then-delete-later
|
||||
// workflow: the active copy exists in another library, the archived twin
|
||||
// holds the real reading history. Merging moves progress + annotations onto
|
||||
// the active row and removes the archived row.
|
||||
func TestMergeArchivedItemIntoActiveTwin(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, adminPgID := moveTestAdminID(t, setup)
|
||||
libOld, folderOld := createTypedTestLibrary(t, setup, "Merge Old Lib", "ebooks")
|
||||
libNew, folderNew := createTypedTestLibrary(t, setup, "Merge New Lib", "ebooks")
|
||||
defer deleteLibraries(t, setup, libOld, libNew)
|
||||
|
||||
content := "merge workflow epub content"
|
||||
|
||||
// Old library: book + history, then archived (file "deleted").
|
||||
oldItem := archiveAndIsolate(t, setup, adminID, adminPgID, libOld, folderOld, content)
|
||||
|
||||
// New library: the surviving active copy, no history of its own.
|
||||
writeTestEbook(t, folderNew, []byte(content))
|
||||
scanTestFolder(t, setup, adminID, folderNew)
|
||||
booksNew, err := setup.DB.ListMediaItemsByLibrary(ctx, libNew)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksNew, 1)
|
||||
active := booksNew[0]
|
||||
|
||||
oldID := uuid.UUID(oldItem.ID.Bytes).String()
|
||||
activeID := uuid.UUID(active.ID.Bytes).String()
|
||||
resp := mergeRequest(t, setup, setup.Token, oldID, map[string]interface{}{"target_id": activeID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
require.Equal(t, http.StatusOK, resp.StatusCode, "merge should succeed")
|
||||
|
||||
var mergeResp map[string]interface{}
|
||||
require.NoError(t, json.NewDecoder(resp.Body).Decode(&mergeResp))
|
||||
|
||||
// Archived row is gone.
|
||||
_, err = setup.DB.GetMediaItem(ctx, oldItem.ID)
|
||||
assert.Error(t, err, "archived row must be removed after merge")
|
||||
|
||||
// History landed on the active row.
|
||||
progress, err := setup.DB.GetReadingProgress(ctx, database.GetReadingProgressParams{MediaItemID: active.ID, UserID: adminPgID})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int32(42), progress.CurrentPage.Int32, "progress must merge into the active copy")
|
||||
|
||||
highlights, err := setup.DB.GetMediaHighlights(ctx, database.GetMediaHighlightsParams{MediaItemID: active.ID, UserID: adminPgID})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, highlights, 1, "annotations must merge into the active copy")
|
||||
|
||||
booksOld, err := setup.DB.ListMediaItemsByLibrary(ctx, libOld)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, booksOld, "old library must be empty after merge")
|
||||
}
|
||||
|
||||
func TestMergeArchivedItemRejections(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, adminPgID := moveTestAdminID(t, setup)
|
||||
libA, folderA := createTypedTestLibrary(t, setup, "Reject Lib A", "ebooks")
|
||||
libB, folderB := createTypedTestLibrary(t, setup, "Reject Lib B", "ebooks")
|
||||
defer deleteLibraries(t, setup, libA, libB)
|
||||
|
||||
t.Run("requires admin", func(t *testing.T) {
|
||||
writeTestEbook(t, folderA, []byte("admin check content"))
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
itemID := uuid.UUID(booksA[0].ID.Bytes).String()
|
||||
|
||||
resp := mergeRequest(t, setup, setup.RegularToken, itemID, map[string]interface{}{"target_id": itemID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects active source", func(t *testing.T) {
|
||||
writeTestEbook(t, folderA, []byte("active source content"))
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
|
||||
|
||||
writeTestEbook(t, folderB, []byte("active source content"))
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
targetID := findTwinTargetID(t, setup, libB)
|
||||
|
||||
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "source is not archived/missing")
|
||||
})
|
||||
|
||||
t.Run("rejects hash mismatch", func(t *testing.T) {
|
||||
writeTestEbook(t, folderA, []byte("hash mismatch content A"))
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
source := booksA[0]
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, source.ID))
|
||||
sourceID := uuid.UUID(source.ID.Bytes).String()
|
||||
|
||||
writeTestEbook(t, folderB, []byte("hash mismatch content B - different bytes"))
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
targetID := findTwinTargetID(t, setup, libB)
|
||||
|
||||
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "different content must not merge")
|
||||
|
||||
// Both rows still exist.
|
||||
_, err = setup.DB.GetMediaItem(ctx, source.ID)
|
||||
assert.NoError(t, err, "source must survive a rejected merge")
|
||||
})
|
||||
|
||||
t.Run("requires target_id", func(t *testing.T) {
|
||||
writeTestEbook(t, folderA, []byte("missing target content"))
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
|
||||
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
|
||||
|
||||
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects self merge", func(t *testing.T) {
|
||||
writeTestEbook(t, folderA, []byte("self merge content"))
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
|
||||
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
|
||||
|
||||
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": sourceID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
||||
})
|
||||
|
||||
t.Run("rejects archived target", func(t *testing.T) {
|
||||
content := []byte("archived target content")
|
||||
writeTestEbook(t, folderA, content)
|
||||
scanTestFolder(t, setup, adminID, folderA)
|
||||
booksA, err := setup.DB.ListMediaItemsByLibrary(ctx, libA)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksA, 1)
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksA[0].ID))
|
||||
sourceID := uuid.UUID(booksA[0].ID.Bytes).String()
|
||||
|
||||
writeTestEbook(t, folderB, content)
|
||||
scanTestFolder(t, setup, adminID, folderB)
|
||||
booksB, err := setup.DB.ListMediaItemsByLibrary(ctx, libB)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, booksB, 1)
|
||||
require.NoError(t, setup.DB.ArchiveMediaItem(ctx, booksB[0].ID))
|
||||
targetID := uuid.UUID(booksB[0].ID.Bytes).String()
|
||||
|
||||
resp := mergeRequest(t, setup, setup.Token, sourceID, map[string]interface{}{"target_id": targetID})
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
assert.Equal(t, http.StatusBadRequest, resp.StatusCode, "target must be active")
|
||||
})
|
||||
|
||||
_ = adminPgID
|
||||
}
|
||||
|
||||
// TestLibraryTypeNameTriggerOnLibraryChange proves the schema trigger keeps
|
||||
// media_items.library_type_name in sync when a row is repointed to a library
|
||||
// of a different type (the INSERT-only trigger previously left it stale).
|
||||
func TestLibraryTypeNameTriggerOnLibraryChange(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
adminID, _ := moveTestAdminID(t, setup)
|
||||
libE, folderE := createTypedTestLibrary(t, setup, "Trigger Ebooks", "ebooks")
|
||||
libC, _ := createTypedTestLibrary(t, setup, "Trigger Comics", "comics")
|
||||
defer deleteLibraries(t, setup, libE, libC)
|
||||
|
||||
writeTestEbook(t, folderE, []byte("trigger check content"))
|
||||
scanTestFolder(t, setup, adminID, folderE)
|
||||
|
||||
books, err := setup.DB.ListMediaItemsByLibrary(ctx, libE)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, books, 1)
|
||||
item := books[0]
|
||||
require.True(t, item.LibraryTypeName.Valid && item.LibraryTypeName.String == "ebooks")
|
||||
|
||||
require.NoError(t, setup.DB.MoveMediaItemToLibrary(ctx, database.MoveMediaItemToLibraryParams{
|
||||
ID: item.ID,
|
||||
LibraryID: libC,
|
||||
FilePath: "Author/Test Book.epub",
|
||||
FileSize: pgtype.Int8{Int64: 27, Valid: true},
|
||||
}))
|
||||
|
||||
after, err := setup.DB.GetMediaItem(ctx, item.ID)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, libC, after.LibraryID)
|
||||
assert.True(t, after.LibraryTypeName.Valid && after.LibraryTypeName.String == "comics",
|
||||
"trigger must refresh library_type_name when library_id changes")
|
||||
}
|
||||
@@ -252,6 +252,88 @@ func TestListPendingRegistrations(t *testing.T) {
|
||||
assert.NotNil(t, pending, "Pending registrations should not be nil")
|
||||
}
|
||||
|
||||
// Regression test for the "approve returns 500 after app reinstall" bug:
|
||||
// an app reinstall that preserves data (e.g. Android Studio installDebug
|
||||
// over an existing install) re-registers with the SAME device_identifier,
|
||||
// and the blind INSERT in ApproveDevice hit the UNIQUE(device_identifier)
|
||||
// constraint. Re-approval must be idempotent: same row (id unchanged),
|
||||
// rotated token, old token invalidated.
|
||||
func TestApproveDeviceReapprovalRotatesToken(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
userID := getTestUserID(t, setup.DB)
|
||||
|
||||
identifier := fmt.Sprintf("repro-reinstall-%s", uuid.New().String())
|
||||
|
||||
registerAndApprove := func() string {
|
||||
regRequest := map[string]interface{}{
|
||||
"device_name": "Reinstall Device",
|
||||
"device_type": "mobile",
|
||||
"device_identifier": identifier,
|
||||
}
|
||||
regBody, _ := json.Marshal(regRequest)
|
||||
|
||||
req := httptest.NewRequest("POST", "/api/devices/register", bytes.NewReader(regBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec := httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusCreated, rec.Code, "Should initiate registration")
|
||||
|
||||
var regResponse map[string]interface{}
|
||||
json.Unmarshal(rec.Body.Bytes(), ®Response)
|
||||
registrationID, ok := regResponse["registration_id"].(string)
|
||||
require.True(t, ok, "Should have registration_id")
|
||||
|
||||
req = httptest.NewRequest("GET", fmt.Sprintf("/api/devices/approve/%s", registrationID), nil)
|
||||
req.Header.Set("Authorization", "Bearer "+setup.Token)
|
||||
rec = httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code,
|
||||
"Approve must succeed even when the identifier already exists (was the reinstall 500)")
|
||||
|
||||
// The status response is single-use and carries the credentials.
|
||||
statusBody, _ := json.Marshal(map[string]string{"registration_id": registrationID})
|
||||
req = httptest.NewRequest("POST", "/api/devices/register/status", bytes.NewReader(statusBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
rec = httptest.NewRecorder()
|
||||
setup.Server.Config.Handler.ServeHTTP(rec, req)
|
||||
require.Equal(t, http.StatusOK, rec.Code)
|
||||
|
||||
var statusResponse map[string]interface{}
|
||||
json.Unmarshal(rec.Body.Bytes(), &statusResponse)
|
||||
require.Equal(t, "approved", statusResponse["status"])
|
||||
token, ok := statusResponse["auth_token"].(string)
|
||||
require.True(t, ok, "Should have auth_token")
|
||||
require.NotEmpty(t, token)
|
||||
return token
|
||||
}
|
||||
|
||||
token1 := registerAndApprove()
|
||||
token2 := registerAndApprove()
|
||||
|
||||
assert.NotEqual(t, token1, token2, "Re-approval must rotate the auth token (fresh install = fresh credentials)")
|
||||
|
||||
devices, err := setup.DB.ListDevicesByUser(context.Background(),
|
||||
pgtype.UUID{Bytes: [16]byte(userID), Valid: true})
|
||||
require.NoError(t, err)
|
||||
rows := 0
|
||||
for _, d := range devices {
|
||||
if d.DeviceIdentifier == identifier {
|
||||
rows++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 1, rows, "Re-approval must reuse the existing devices row, not duplicate it")
|
||||
|
||||
// The rotated-in token must be the live one.
|
||||
var live *database.Devices
|
||||
for i := range devices {
|
||||
if devices[i].DeviceIdentifier == identifier {
|
||||
live = &devices[i]
|
||||
}
|
||||
}
|
||||
require.NotNil(t, live)
|
||||
assert.Equal(t, token2, live.AuthToken, "The devices row must carry the newly rotated token")
|
||||
}
|
||||
|
||||
func TestApproveDeviceRegistration(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
@@ -435,3 +436,115 @@ func TestOPDSSearchAcrossLibraries(t *testing.T) {
|
||||
t.Logf("❌ FAILED - OPDS returned %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestOPDSLibraryFolders covers the library-folder navigation feed: the root
|
||||
// catalog is a navigation feed with an "All Books" entry plus one folder per
|
||||
// visible library, the scoped catalog only lists that library's books, and
|
||||
// the library-scoped search parameter restricts results to that library.
|
||||
// TestOPDSLibraryFolders covers the library-folder navigation feed: the root
|
||||
// catalog is a navigation feed with an "All Books" entry plus one folder per
|
||||
// visible library, the scoped catalog only lists that library's books, and
|
||||
// the library-scoped search parameter restricts results to that library.
|
||||
// NOTE: library names deliberately avoid the word "test" — setupTestServer's
|
||||
// setup-time cleanup deletes every library whose name contains it (and
|
||||
// setupDeviceTest re-runs that cleanup mid-test).
|
||||
func TestOPDSLibraryFolders(t *testing.T) {
|
||||
setup := setupTestServer(t)
|
||||
client := &http.Client{}
|
||||
|
||||
// Two libraries, one distinct book each.
|
||||
lib1Resp := createLibrary(t, client, setup, "OPDS Folder Lib Alpha")
|
||||
lib2Resp := createLibrary(t, client, setup, "OPDS Folder Lib Beta")
|
||||
lib1ID := lib1Resp["id"].(string)
|
||||
lib2ID := lib2Resp["id"].(string)
|
||||
// Real directories: the folder API validates that the path exists on the
|
||||
// host running the server (container path /app/uploads won't exist when
|
||||
// tests run on the host).
|
||||
addFolderToLibrary(t, setup, lib1ID, t.TempDir())
|
||||
addFolderToLibrary(t, setup, lib2ID, t.TempDir())
|
||||
createTestMediaItemIDInLibrary(t, client, setup, lib1ID, "OPDS Folder Book 1")
|
||||
createTestMediaItemIDInLibrary(t, client, setup, lib2ID, "OPDS Folder Book 2")
|
||||
|
||||
deviceSetup := setupDeviceTest(t)
|
||||
device := deviceSetup.CreateDevice(t, "OPDS Folders Device", "koreader", "opds-folders-"+uuid.NewString())
|
||||
base := setup.Server.URL + "/opds/devices/" + device.ID.String()
|
||||
|
||||
// Explicit cleanup: these names don't contain "test", so the harness
|
||||
// cleanup skips them; remove them ourselves. Registered as a defer AFTER
|
||||
// the setup.Close defer so LIFO ordering runs it while the pool is open.
|
||||
defer deleteLibraries(t, setup,
|
||||
pgtype.UUID{Bytes: uuid.MustParse(lib1ID), Valid: true},
|
||||
pgtype.UUID{Bytes: uuid.MustParse(lib2ID), Valid: true})
|
||||
|
||||
getFeed := func(t *testing.T, url string) (int, string, string) {
|
||||
t.Helper()
|
||||
req, _ := http.NewRequest("GET", url, nil)
|
||||
req.Header.Set("Authorization", "Bearer "+device.AuthToken)
|
||||
resp, err := client.Do(req)
|
||||
require.NoError(t, err)
|
||||
defer func(Body io.ReadCloser) {
|
||||
_ = Body.Close()
|
||||
}(resp.Body)
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
return resp.StatusCode, contentType, string(bodyBytes)
|
||||
}
|
||||
|
||||
t.Run("root catalog is a navigation feed with All Books and library folders", func(t *testing.T) {
|
||||
status, contentType, body := getFeed(t, base+"/catalog")
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
assert.Contains(t, contentType, "kind=navigation")
|
||||
assert.Contains(t, body, "All Books")
|
||||
assert.Contains(t, body, "OPDS Folder Lib Alpha")
|
||||
assert.Contains(t, body, "OPDS Folder Lib Beta")
|
||||
assert.Contains(t, body, "/library/"+lib1ID+"/catalog")
|
||||
assert.Contains(t, body, "/library/"+lib2ID+"/catalog")
|
||||
// The nav feed itself must not inline book entries.
|
||||
assert.NotContains(t, body, "OPDS Folder Book 1")
|
||||
})
|
||||
|
||||
t.Run("all=1 keeps the flat cross-library catalog", func(t *testing.T) {
|
||||
status, contentType, body := getFeed(t, base+"/catalog?all=1")
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
assert.Contains(t, contentType, "kind=acquisition")
|
||||
assert.Contains(t, body, "OPDS Folder Book 1")
|
||||
assert.Contains(t, body, "OPDS Folder Book 2")
|
||||
})
|
||||
|
||||
t.Run("scoped catalog lists only its own library's books", func(t *testing.T) {
|
||||
status, contentType, body := getFeed(t, base+"/library/"+lib1ID+"/catalog")
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
assert.Contains(t, contentType, "kind=acquisition")
|
||||
assert.Contains(t, body, "OPDS Folder Book 1")
|
||||
assert.NotContains(t, body, "OPDS Folder Book 2")
|
||||
// Scoped search discovery: template pins the library.
|
||||
assert.Contains(t, body, "library_id="+lib1ID)
|
||||
})
|
||||
|
||||
t.Run("scoped catalog rejects libraries outside device visibility", func(t *testing.T) {
|
||||
status, _, _ := getFeed(t, base+"/library/11111111-2222-3333-4444-555555555555/catalog")
|
||||
assert.Equal(t, http.StatusForbidden, status)
|
||||
})
|
||||
|
||||
t.Run("scoped search restricts results to the library", func(t *testing.T) {
|
||||
status, _, body := getFeed(t, base+"/search?q=OPDS+Folder&library_id="+lib2ID)
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
assert.Contains(t, body, "OPDS Folder Book 2")
|
||||
assert.NotContains(t, body, "OPDS Folder Book 1")
|
||||
})
|
||||
|
||||
t.Run("scoped search rejects inaccessible library", func(t *testing.T) {
|
||||
status, _, _ := getFeed(t, base+"/search?q=OPDS&library_id=11111111-2222-3333-4444-555555555555")
|
||||
assert.Equal(t, http.StatusForbidden, status)
|
||||
})
|
||||
|
||||
t.Run("global search labels entries with their library", func(t *testing.T) {
|
||||
status, _, body := getFeed(t, base+"/search?q=OPDS+Folder")
|
||||
require.Equal(t, http.StatusOK, status)
|
||||
assert.Contains(t, body, "OPDS Folder Book 1")
|
||||
assert.Contains(t, body, "OPDS Folder Book 2")
|
||||
assert.Contains(t, body, "OPDS Folder Lib Alpha")
|
||||
assert.Contains(t, body, "OPDS Folder Lib Beta")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"bookhoard/internal/config"
|
||||
"bookhoard/internal/database"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestSchemaInitializationIsIdempotent replays the application's production
|
||||
// schema initializer twice against the same database. schema.sql runs on
|
||||
// EVERY app startup, so it must apply cleanly to a database where a previous
|
||||
// startup already applied it. A regression here crash-loops the production
|
||||
// container on its second boot (e.g. SQLSTATE 2BP01: DROP FUNCTION refused
|
||||
// while a trigger created by the previous run still references it).
|
||||
//
|
||||
// Uses a dedicated pool: production Initialize holds the advisory lock on one
|
||||
// connection while executing the schema on another, but the shared test pool
|
||||
// is capped at MaxConns=1 and would deadlock.
|
||||
func TestSchemaInitializationIsIdempotent(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping schema re-initialization in -short mode")
|
||||
}
|
||||
|
||||
// Bring up the standard test environment (seeds users, starts server).
|
||||
setup := setupTestServer(t)
|
||||
defer func() { _ = setup.Close() }()
|
||||
|
||||
// Dedicated pool with default connection limits for the initializer.
|
||||
poolCfg, err := pgxpool.ParseConfig(config.LoadConfig().DatabaseURL())
|
||||
require.NoError(t, err, "failed to parse database URL")
|
||||
pool, err := pgxpool.NewWithConfig(context.Background(), poolCfg)
|
||||
require.NoError(t, err, "failed to connect for schema initialization test")
|
||||
defer func() { pool.Close() }()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// First boot: applies the schema to the (possibly fresh) database.
|
||||
if err := database.Initialize(ctx, pool); err != nil {
|
||||
t.Fatalf("first schema initialization failed: %v", err)
|
||||
}
|
||||
|
||||
// Second boot against the now-initialized database: this is exactly
|
||||
// where non-idempotent statements (bare CREATE TRIGGER, DROP FUNCTION
|
||||
// with dependent objects, etc.) blow up.
|
||||
if err := database.Initialize(ctx, pool); err != nil {
|
||||
t.Fatalf("second schema initialization failed: %v — schema.sql must stay idempotent across restarts", err)
|
||||
}
|
||||
}
|
||||
@@ -544,6 +544,10 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
AnnotationService: annotationService,
|
||||
DeviceAuthMiddleware: deviceAuthMiddleware,
|
||||
LoginTracker: loginAttemptTracker,
|
||||
// RegisterRoutes dereferences Settings for the auth rate limit;
|
||||
// the main binary wires database.NewSettingsRegistry(queries) —
|
||||
// the test harness must too or route registration nil-panics.
|
||||
Settings: database.NewSettingsRegistry(queries),
|
||||
}
|
||||
|
||||
router.RegisterRoutes(routerConfig)
|
||||
@@ -588,21 +592,41 @@ func setupTestServer(t *testing.T) *TestServerSetup {
|
||||
}
|
||||
}
|
||||
|
||||
// Create fresh admin test user
|
||||
// The admin test user doubles as the shared dev admin and is
|
||||
// deliberately PRESERVED by the cleanup above — reuse it when it
|
||||
// already exists instead of re-creating (a blind INSERT hits
|
||||
// users_email_key on every run after the first).
|
||||
passwordHash := "$2a$10$JjAtK7PPa1WexQC3AUGe8OXLeuseZ/haN1Mz7emMo6CfOvMiTVXWq"
|
||||
adminUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@tests.bookhoard.internal",
|
||||
Username: "testuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "admin",
|
||||
})
|
||||
require.NoError(t, err, "Failed to create admin test user")
|
||||
|
||||
adminUUID, err := uuid.FromBytes(adminUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||
var adminUUID uuid.UUID
|
||||
if existing, gerr := queries.GetUserByEmail(ctx, "testuser@tests.bookhoard.internal"); gerr == nil {
|
||||
adminUUID, err = uuid.FromBytes(existing.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||
// Reset the password to the known test constant: other tests (e.g.
|
||||
// password-change tests) mutate the shared admin's password, which
|
||||
// would otherwise lock every later test out with login 401s until
|
||||
// the database is wiped.
|
||||
if updErr := queries.UpdatePassword(ctx, database.UpdatePasswordParams{
|
||||
ID: existing.ID,
|
||||
PasswordHash: passwordHash,
|
||||
}); updErr != nil {
|
||||
require.NoError(t, updErr, "Failed to reset shared admin test password")
|
||||
}
|
||||
} else {
|
||||
adminUser, cerr := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
Email: "testuser@tests.bookhoard.internal",
|
||||
Username: "testuser",
|
||||
PasswordHash: passwordHash,
|
||||
FirstName: pgtype.Text{String: "Test", Valid: true},
|
||||
LastName: pgtype.Text{String: "User", Valid: true},
|
||||
Role: "admin",
|
||||
})
|
||||
require.NoError(t, cerr, "Failed to create admin test user")
|
||||
adminUUID, err = uuid.FromBytes(adminUser.ID.Bytes[:])
|
||||
require.NoError(t, err, "Failed to parse admin user UUID")
|
||||
// Default collections are created exactly once per user; the
|
||||
// preserved dev admin already has its set from the first run.
|
||||
createDefaultCollectionsForUser(t, queries, pgtype.UUID{Bytes: adminUUID, Valid: true})
|
||||
}
|
||||
|
||||
// Create fresh regular test user
|
||||
regularUser, err := queries.CreateUser(ctx, database.CreateUserParams{
|
||||
|
||||
@@ -259,8 +259,11 @@ CREATE INDEX IF NOT EXISTS idx_media_items_archived ON media_items (archived_at)
|
||||
CREATE INDEX IF NOT EXISTS idx_media_items_tags_search ON media_items USING GIN (tags_search);
|
||||
CREATE INDEX IF NOT EXISTS idx_media_items_contributors_search ON media_items USING GIN (contributors_search);
|
||||
|
||||
-- Drop existing trigger if exists
|
||||
-- Drop existing triggers if exists. Both must be dropped BEFORE the function:
|
||||
-- dropping a function that triggers still reference fails (SQLSTATE 2BP01),
|
||||
-- which would abort the schema transaction on every startup after the first.
|
||||
DROP TRIGGER IF EXISTS set_library_type_name_on_insert ON media_items;
|
||||
DROP TRIGGER IF EXISTS set_library_type_name_on_library_change ON media_items;
|
||||
-- Drop existing function if exists
|
||||
DROP FUNCTION IF EXISTS set_library_type_name();
|
||||
|
||||
@@ -279,6 +282,14 @@ CREATE TRIGGER set_library_type_name_on_insert
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_library_type_name();
|
||||
|
||||
-- Keep library_type_name truthful when an item is repointed to another
|
||||
-- library (cross-library move detection reuses the same function; the row
|
||||
-- keeps its ID while its library changes).
|
||||
CREATE TRIGGER set_library_type_name_on_library_change
|
||||
BEFORE UPDATE OF library_id ON media_items
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION set_library_type_name();
|
||||
|
||||
-- Add GIN indexes for pg_trgm fuzzy search performance
|
||||
CREATE INDEX IF NOT EXISTS idx_media_items_author_trgm
|
||||
ON media_items USING GIN (author gin_trgm_ops);
|
||||
|
||||
+40
-108
@@ -394,6 +394,11 @@ Authorization: Bearer <token>
|
||||
|
||||
## Reading Progress
|
||||
|
||||
Full field reference: [api/progress/](api/progress/) — and read the
|
||||
[Position Contract](api/progress/position-contract.md) (verification,
|
||||
healing, the OPF spine numbering hazard, offset currencies) before
|
||||
writing a client.
|
||||
|
||||
### Get Reading Progress
|
||||
|
||||
```http
|
||||
@@ -405,57 +410,56 @@ Authorization: Bearer <token>
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"media_item_id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"current_page": 45,
|
||||
"total_pages": 200,
|
||||
"percentage": 0.225,
|
||||
"character_offset": 15432,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"chapter": 3,
|
||||
"chapter_progress": 0.5,
|
||||
"last_read_at": "2026-01-31T10:00:00Z",
|
||||
"percentage": 0.045,
|
||||
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
|
||||
"context_text": "from day to day, but there was none in which ...",
|
||||
"character_offset": 16375,
|
||||
"current_page": null,
|
||||
"total_pages": null,
|
||||
"chapter": null,
|
||||
"chapter_progress": null,
|
||||
"format_group": "reflowable",
|
||||
"viewport_y": 0.12,
|
||||
"zoom_level": 1.0
|
||||
"total_characters": 592216,
|
||||
"chapter_count": 1,
|
||||
"last_read_at": "2026-09-26T14:52:34Z",
|
||||
"last_sync_source": "koreader",
|
||||
"css_selector": "body>div:nth-child(4)>p:nth-child(29)",
|
||||
"anchor_href": "1984.xhtml",
|
||||
"char_offset": 598
|
||||
}
|
||||
```
|
||||
|
||||
`css_selector`, `anchor_href` and `char_offset` are server-derived
|
||||
restore handles, served for convertible reflowable books with a
|
||||
resolvable anchor. `char_offset` is UTF-16 code units within the anchor
|
||||
block's text; `character_offset` is a book-wide rune count. Resolve the
|
||||
document by `anchor_href`, never by the CFI's spine step (OPF numbering
|
||||
includes `linear="no"` items).
|
||||
|
||||
### Update Reading Progress
|
||||
|
||||
```http
|
||||
PUT /api/media-items/{media_id}/progress
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"source": "web",
|
||||
"location": {
|
||||
"percentage": 0.45678,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"character": 15432,
|
||||
"chapter": 3,
|
||||
"page": 89,
|
||||
"total_pages": 200
|
||||
},
|
||||
"device_metadata": {
|
||||
"device_type": "web",
|
||||
"user_agent": "Mozilla/5.0..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Response** (200):
|
||||
|
||||
```json
|
||||
{
|
||||
"sync_status": "success",
|
||||
"progress_updated": true,
|
||||
"devices_notified": ["device-1", "device-2"],
|
||||
"broadcast": true
|
||||
"percentage": 0.0415,
|
||||
"context_text": "was at war with one of these Powers it was generally a",
|
||||
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
|
||||
}
|
||||
```
|
||||
|
||||
All fields are optional. The server verifies the submission against the
|
||||
book and heals it on mismatch; the response is the stored row after
|
||||
verification. A `percentage` below 0.005 is ignored while stored
|
||||
progress exceeds 0.01 (first-page anti-clobber).
|
||||
|
||||
### Delete Reading Progress
|
||||
|
||||
```http
|
||||
@@ -1297,82 +1301,10 @@ Authorization: Bearer <device_token>
|
||||
|
||||
## Universal Progress
|
||||
|
||||
### Get Universal Progress
|
||||
|
||||
```http
|
||||
GET /api/progress/{book_uuid}
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**Response** (200):
|
||||
|
||||
```json
|
||||
{
|
||||
"book_id": "book-uuid",
|
||||
"format_group": "reflowable",
|
||||
"universal_progress": 0.45678,
|
||||
"location_references": {
|
||||
"percentage": 0.45678,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"character": 15432,
|
||||
"chapter": 3,
|
||||
"chapter_progress": 0.234,
|
||||
"viewport_y": 0.12
|
||||
},
|
||||
"device_progress": {
|
||||
"koreader": {
|
||||
"percentage": 0.45678,
|
||||
"last_sync": "2026-01-30T20:00:00Z"
|
||||
},
|
||||
"kobo": {
|
||||
"percentage": 45.6,
|
||||
"last_sync": "2026-01-30T19:55:00Z"
|
||||
},
|
||||
"web": {
|
||||
"display_page": 89,
|
||||
"total_pages": 200,
|
||||
"last_sync": "2026-01-30T20:05:00Z"
|
||||
}
|
||||
},
|
||||
"annotations": {
|
||||
"highlights": [...],
|
||||
"notes": [...],
|
||||
"bookmarks": [...]
|
||||
},
|
||||
"conflicts": [
|
||||
{
|
||||
"id": "conflict-uuid",
|
||||
"type": "progress",
|
||||
"resolved": false,
|
||||
"sources": ["koreader", "kobo"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Update Universal Progress
|
||||
|
||||
```http
|
||||
POST /api/progress/{book_uuid}
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"source": "web|koreader|kobo|mobile",
|
||||
"location": {
|
||||
"percentage": 0.45678,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"character": 15432,
|
||||
"chapter": 3,
|
||||
"page": 89,
|
||||
"total_pages": 200
|
||||
},
|
||||
"device_metadata": {
|
||||
"device_type": "web",
|
||||
"user_agent": "..."
|
||||
}
|
||||
}
|
||||
```
|
||||
Universal progress is the media-item progress — one row per (user,
|
||||
media item) shared by every device. See [Reading Progress](#reading-progress)
|
||||
and the [Position Contract](api/progress/position-contract.md). There
|
||||
are no separate `/api/progress/:id` GET/POST endpoints.
|
||||
|
||||
## Conflicts
|
||||
|
||||
|
||||
@@ -117,10 +117,14 @@ See [Media Item Operations](media-items/)
|
||||
|
||||
## Reading Progress
|
||||
|
||||
See [Progress Tracking](progress/)
|
||||
See [Progress Tracking](progress/) — in particular the
|
||||
[Position Contract](progress/position-contract.md) (verification,
|
||||
healing, the OPF spine numbering hazard, and offset currencies) before
|
||||
writing a client.
|
||||
|
||||
- GET /api/progress/:id - Get universal progress
|
||||
- POST /api/progress/:id - Update universal progress
|
||||
- GET /api/media-items/:id/progress - Get reading progress + restore handles
|
||||
- PUT /api/media-items/:id/progress - Submit reading progress (verified/healed server-side)
|
||||
- DELETE /api/media-items/:id/progress - Delete reading progress
|
||||
- GET /api/progress/:id/history - Get progress history
|
||||
|
||||
## Notes & Highlights
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
# Get Metadata
|
||||
|
||||
Get metadata for a book from KOReader device.
|
||||
Get a book's stored progress and annotations for a KOReader device —
|
||||
the pull half of the device sync. The reference client calls this after
|
||||
linking a book via [Resolve Book](resolve_book.md) and navigates to the
|
||||
returned position.
|
||||
|
||||
**Endpoint**: `GET /api/sync/koreader/metadata/:uuid`
|
||||
**Auth**: Required (Device authentication)
|
||||
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
|
||||
|
||||
## Path Parameters
|
||||
|
||||
@@ -11,41 +14,55 @@ Get metadata for a book from KOReader device.
|
||||
| --------- | ------------- | -------- | ----------- |
|
||||
| uuid | string (UUID) | Yes | Book UUID |
|
||||
|
||||
## Device Authentication
|
||||
|
||||
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
|
||||
|
||||
## Request Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
| ------------ | ------ | -------- | ------------------------- |
|
||||
| X-Device-ID | string | Yes | Device UUID |
|
||||
| X-Device-Key | string | Yes | Device authentication key |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /api/sync/koreader/metadata/550e8400-e29b-41d4-a716-446655440000
|
||||
X-Device-ID: 550e8400-e29b-41d4-a716-446655440000
|
||||
X-Device-Key: device-auth-key
|
||||
GET /api/sync/koreader/metadata/774641f9-317b-4087-8e04-53bb4392ae56
|
||||
Authorization: Bearer {device_token}
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "uuid",
|
||||
"title": "Book Title",
|
||||
"authors": ["Author Name"],
|
||||
"path": "/path/to/book.epub",
|
||||
"file_size": 1234567,
|
||||
"modified_at": "2026-02-08T10:00:00Z"
|
||||
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||
"title": "1984",
|
||||
"author": "George Orwell",
|
||||
"progress": {
|
||||
"percentage": 0.045,
|
||||
"koreader_xpointer": "/body/DocFragment[1]/body/p[29]/text().598",
|
||||
"chapter": null,
|
||||
"chapter_progress": null,
|
||||
"page": null,
|
||||
"total_pages": null
|
||||
},
|
||||
"annotations": {
|
||||
"highlights": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Progress Object
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `percentage` | float | Stored position as a book fraction. |
|
||||
| `koreader_xpointer` | string | The stored canonical position converted back to a CRE xpointer (UTF-16 `text().N` offset). **The device should navigate to this.** Reflowable books only. |
|
||||
| `epubcfi` | string | The stored canonical CFI, when the conversion to a CRE xpointer is unavailable. Fallback after `koreader_xpointer`. |
|
||||
| `character` | int | Book-wide rune offset (internal currency). |
|
||||
| `chapter`, `chapter_progress` | int, float | Chapter position when known. |
|
||||
| `page`, `total_pages` | int | Fixed-layout page position — the canonical locator for image-based books (CFI/xpointer are omitted for them). |
|
||||
|
||||
`progress` is `null` when the book has no stored progress.
|
||||
|
||||
The `annotations` object carries device-format highlights/bookmarks/notes
|
||||
synced from other clients; its presence depends on annotation sync being
|
||||
enabled.
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ---------------------------- |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Invalid book UUID |
|
||||
| 401 | Device authentication failed |
|
||||
| 404 | Book or device not found |
|
||||
| 404 | Book not found |
|
||||
|
||||
@@ -1,63 +1,87 @@
|
||||
# Sync Progress
|
||||
|
||||
Sync reading progress from KOReader device.
|
||||
Push reading progress from a KOReader device.
|
||||
|
||||
**Endpoint**: `POST /api/sync/koreader/progress`
|
||||
**Auth**: Required (Device authentication)
|
||||
**Auth**: Required (Device authentication — `Authorization: Bearer {device_token}`)
|
||||
|
||||
## Device Authentication
|
||||
|
||||
This endpoint requires device authentication (not user JWT). Devices authenticate using their device credentials.
|
||||
This is the device-native tier of the [Position
|
||||
Contract](../progress/position-contract.md): the KOReader payload carries
|
||||
a CRE xpointer and the server converts it to the canonical standard CFI,
|
||||
verifies it against the submitted `context_text`, and heals it on
|
||||
mismatch — exactly like every other client.
|
||||
|
||||
## Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | ------------------------- |
|
||||
| device_id | string (UUID) | Yes | Device UUID |
|
||||
| progress | array | Yes | Array of progress objects |
|
||||
| Field | Type | Required | Description |
|
||||
| --------- | ------ | -------- | ----------- |
|
||||
| `books` | array | Yes | One book object (the reference client sends a single-element array). |
|
||||
| `sync_mode`| string | No | `immediate` (default) or `manual`. |
|
||||
|
||||
### Progress Object
|
||||
### Book Object
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ----------- | ------- | -------- | ---------------------------------- |
|
||||
| book | string | Yes | Book identifier (filename or UUID) |
|
||||
| percent | float | Yes | Progress percentage (0-100) |
|
||||
| page | integer | No | Current page number |
|
||||
| total_pages | integer | No | Total pages in document |
|
||||
| date_read | string | No | ISO 8601 timestamp of last read |
|
||||
| updated_at | string | Yes | ISO 8601 timestamp |
|
||||
| Field | Type | Required | Description |
|
||||
| ----- | ---- | -------- | ----------- |
|
||||
| `uuid` | string (UUID) | No | Bookhoard UUID, once the device has linked the book via [Resolve Book](resolve_book.md). |
|
||||
| `sha256` | string | Yes | File content hash (64 hex chars) — the primary book identity. |
|
||||
| `title` | string | No | Document title. |
|
||||
| `authors` | array | No | Author names. |
|
||||
| `percentage` | float | Yes | Position as a fraction of the book (0..1). |
|
||||
| `context_text` | string | No | Up to 100 whitespace-normalized chars from the current position — enables the server's verification/healing. Strongly recommended. |
|
||||
| `page` | int | No | Current page (fixed-layout books). |
|
||||
| `total_pages` | int | No | Page count (fixed-layout books). |
|
||||
| `epubcfi` | string | Reflowable only | **A CRE xpointer** (`/body/DocFragment[N]/body/...`), not a CFI — the field name is historical. Fixed-layout books must omit it and carry their position in `page`/`total_pages`. |
|
||||
| `file_path` | string | No | Device-local file path (informational). |
|
||||
| `device_info` | object | No | `{ koreader_version, device_model }`. |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
POST /api/sync/koreader/progress
|
||||
Authorization: Bearer {device_token}
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"device_id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"progress": [
|
||||
"books": [
|
||||
{
|
||||
"book": "book.epub",
|
||||
"percent": 75.5,
|
||||
"page": 150,
|
||||
"total_pages": 200,
|
||||
"date_read": "2026-02-08T10:00:00Z",
|
||||
"updated_at": "2026-02-08T10:00:00Z"
|
||||
"uuid": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||
"sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||
"title": "1984",
|
||||
"authors": ["George Orwell"],
|
||||
"percentage": 0.045,
|
||||
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim",
|
||||
"page": 14,
|
||||
"total_pages": 311,
|
||||
"epubcfi": "/body/DocFragment[1]/body/p[29]/text().598",
|
||||
"device_info": {
|
||||
"koreader_version": "v2026.07.1",
|
||||
"device_model": "emulator"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"sync_mode": "immediate"
|
||||
}
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
## Response (202 Accepted)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "Progress synced successfully",
|
||||
"synced_count": 1
|
||||
"sync_status": "ok",
|
||||
"books_synced": 1,
|
||||
"timestamp": "2026-09-26T21:25:09Z"
|
||||
}
|
||||
```
|
||||
|
||||
Per-book results and any detected sync conflicts are carried in
|
||||
`book_results` and `conflicts` when present.
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ---------------------------- |
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Invalid request format |
|
||||
| 401 | Device authentication failed |
|
||||
| 400 | Invalid request data |
|
||||
| 404 | Device not found |
|
||||
| 500 | Database error |
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
# Delete Media Item Progress
|
||||
|
||||
Delete the stored reading progress for a media item.
|
||||
|
||||
**Endpoint**: `DELETE /api/media-items/:id/progress`
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | ---------------- |
|
||||
| id | string (UUID) | Yes | Media item UUID |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
DELETE /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "reading progress deleted"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Invalid media item id |
|
||||
| 401 | Invalid or expired token |
|
||||
| 500 | Database error |
|
||||
@@ -1,36 +0,0 @@
|
||||
# Delete Reading Progress
|
||||
|
||||
Delete reading progress for a media item.
|
||||
|
||||
**Endpoint**: `DELETE /api/media-items/{media_id}/progress`
|
||||
**Auth**: Required
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------ | -------- | --------------- |
|
||||
| media_id | string | Yes | Media item UUID |
|
||||
|
||||
## Request Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | ------------ |
|
||||
| Authorization | string | Yes | Bearer token |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
DELETE /api/media-items/uuid/progress
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## Response (204 No Content)
|
||||
|
||||
Progress deleted successfully.
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------ |
|
||||
| 401 | Invalid or expired token |
|
||||
| 404 | Media item not found |
|
||||
@@ -0,0 +1,98 @@
|
||||
# Get Media Item Progress
|
||||
|
||||
Get the stored reading progress for a media item, plus the
|
||||
server-derived restore handles for reflowable books.
|
||||
|
||||
**Endpoint**: `GET /api/media-items/:id/progress`
|
||||
**Auth**: Required (Bearer token)
|
||||
|
||||
See [Position Contract](position-contract.md) for the semantics of every
|
||||
field — what is verified, what the currencies are, and how clients
|
||||
should restore.
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | --------------- |
|
||||
| id | string (UUID) | Yes | Media item UUID |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
```
|
||||
|
||||
## Response (200 OK) — no progress stored
|
||||
|
||||
When the item has no progress row, an empty fixed-layout-style stub is
|
||||
returned (not 404):
|
||||
|
||||
```json
|
||||
{
|
||||
"current_page": 0,
|
||||
"total_pages": null
|
||||
}
|
||||
```
|
||||
|
||||
## Response (200 OK) — progress stored
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "e8239659-0ed6-42ae-a946-8440d7655b42",
|
||||
"media_item_id": "774641f9-317b-4087-8e04-53bb4392ae56",
|
||||
"user_id": "1b64992e-3408-4d84-9e03-e2dc4950e1dd",
|
||||
"current_page": null,
|
||||
"total_pages": null,
|
||||
"last_read_at": "2026-09-26T14:52:34.806446Z",
|
||||
"percentage": 0.045,
|
||||
"character_offset": 16375,
|
||||
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/58/1:598)",
|
||||
"chapter": null,
|
||||
"chapter_progress": null,
|
||||
"format_group": "reflowable",
|
||||
"total_characters": 592216,
|
||||
"chapter_count": 1,
|
||||
"last_sync_device": "web",
|
||||
"last_sync_source": "koreader",
|
||||
"last_sync_timestamp": "2026-09-26T14:52:34.806446Z",
|
||||
"css_selector": "body>div:nth-child(4)>p:nth-child(29)",
|
||||
"anchor_href": "1984.xhtml",
|
||||
"char_offset": 598,
|
||||
"context_text": "from day to day, but there was none in which Goldstein was not the principal figure. He was the prim"
|
||||
}
|
||||
```
|
||||
|
||||
### Field Reference
|
||||
|
||||
Base fields (always present when a row exists):
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `percentage` | float | Position as a fraction of the whole book (0..1). |
|
||||
| `epubcfi` | string | The stored canonical standard CFI. **Spine steps index the OPF spine as written, including `linear="no"` items — do not resolve them against a readium reading order.** See the [Position Contract](position-contract.md#spine-numbering-hazard--read-this-before-parsing-a-stored-cfi). |
|
||||
| `context_text` | string | The stored verification context (≤100 whitespace-normalized chars from the anchor). |
|
||||
| `character_offset` | int | Book-wide rune offset. Internal currency — consistent with `total_characters`. |
|
||||
| `current_page`, `total_pages` | int | Fixed-layout page position (null for reflowable). |
|
||||
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction, when known. |
|
||||
| `format_group` | string | `reflowable`, `fixed_layout`, `comic_archive`, … Gates the restore handles. |
|
||||
| `total_characters`, `chapter_count` | int | Book metrics, for client-side fraction math. |
|
||||
| `last_sync_device`, `last_sync_source`, `last_sync_timestamp` | — | Which client last wrote the row. |
|
||||
|
||||
Restore handles (conditional — served only for convertible reflowable
|
||||
books whose stored anchor re-resolves at GET time):
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `anchor_href` | string | The spine document containing the anchor (e.g. `1984.xhtml`). **Resolve your resource by this**, never by the CFI's spine step. |
|
||||
| `css_selector` | string | Body-relative chain to the anchor's block element. |
|
||||
| `char_offset` | int | Anchor offset within the block's concatenated text, in UTF-16 code units. |
|
||||
| `epubcfi` | string | Re-served (possibly healed) canonical CFI — present whenever re-verification produced one. |
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Invalid media item id |
|
||||
| 401 | Invalid or expired token |
|
||||
| 500 | Database error |
|
||||
@@ -1,52 +0,0 @@
|
||||
# Get Reading Progress
|
||||
|
||||
Retrieve reading progress for a specific media item.
|
||||
|
||||
**Endpoint**: `GET /api/media-items/{media_id}/progress`
|
||||
**Auth**: Required
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------ | -------- | --------------- |
|
||||
| media_id | string | Yes | Media item UUID |
|
||||
|
||||
## Request Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | ------------ |
|
||||
| Authorization | string | Yes | Bearer token |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /api/media-items/uuid/progress
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"media_item_id": "uuid",
|
||||
"user_id": "uuid",
|
||||
"current_page": 45,
|
||||
"total_pages": 200,
|
||||
"percentage": 0.225,
|
||||
"character_offset": 15432,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"chapter": 3,
|
||||
"chapter_progress": 0.5,
|
||||
"last_read_at": "2026-01-31T10:00:00Z",
|
||||
"format_group": "reflowable",
|
||||
"viewport_y": 0.12,
|
||||
"zoom_level": 1.0
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------ |
|
||||
| 401 | Invalid or expired token |
|
||||
| 404 | Media item not found |
|
||||
@@ -1,50 +0,0 @@
|
||||
# Get Universal Progress
|
||||
|
||||
Get universal (device-agnostic) reading progress for a media item.
|
||||
|
||||
**Endpoint**: `GET /api/progress/:id`
|
||||
**Auth**: Required
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | --------------- |
|
||||
| id | string (UUID) | Yes | Media item UUID |
|
||||
|
||||
## Request Headers
|
||||
|
||||
| Header | Type | Required | Description |
|
||||
| ------------- | ------ | -------- | ------------ |
|
||||
| Authorization | string | Yes | Bearer token |
|
||||
|
||||
### Example Request
|
||||
|
||||
```http
|
||||
GET /api/progress/550e8400-e29b-41d4-a716-446655440000
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"media_item_id": "uuid",
|
||||
"percentage": 75.5,
|
||||
"position": 1234,
|
||||
"page": 150,
|
||||
"total_pages": 200,
|
||||
"finished": false,
|
||||
"updated_at": "2026-02-08T10:00:00Z",
|
||||
"device": {
|
||||
"id": "device-uuid",
|
||||
"name": "My Kobo"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------ |
|
||||
| 401 | Invalid or expired token |
|
||||
| 404 | Media item not found |
|
||||
@@ -0,0 +1,135 @@
|
||||
# Position Contract
|
||||
|
||||
How reading positions are represented, submitted, verified, and restored
|
||||
across clients. This is the contract every client speaks — readium-based
|
||||
apps, web (foliate), KOReader, and Kobo devices alike.
|
||||
|
||||
**The server is the position authority.** Every submission is
|
||||
independently verified against the book itself before it is stored; a
|
||||
client whose locator math is wrong cannot poison the stored position.
|
||||
In exchange, the server hands back handles clients can apply directly,
|
||||
so they never have to parse or trust CFIs themselves.
|
||||
|
||||
## The three-tier submission
|
||||
|
||||
Clients submit what their renderer can reliably observe. All three tiers
|
||||
are accepted on `PUT /api/media-items/:id/progress` (and on the KOReader
|
||||
device endpoints):
|
||||
|
||||
| Tier | Field | Currency | Notes |
|
||||
| ------ | -------------- | ----------------------------------------------------- | ----- |
|
||||
| 1 | `percentage` | Float 0..1 of the whole book | Universal. The only field every client can supply. |
|
||||
| 2 | `context_text` | Up to 100 chars, whitespace-normalized, starting at the anchor | The verification anchor. The server extracts the text at the submitted structural anchor and compares. |
|
||||
| 3 | `epubcfi` | Standard wrapped CFI, terminals in UTF-16 code units | Optional structural anchor. KOReader submits a CRE xpointer and the server converts it. |
|
||||
|
||||
Tier 2 is what makes the system self-correcting: percentages alone
|
||||
cannot distinguish "the reader's locator is right" from "the reader
|
||||
silently reported wherever it is currently scrolled".
|
||||
|
||||
## Verification and healing (ingest side)
|
||||
|
||||
On every progress save for a convertible reflowable book that carries a
|
||||
`context_text`, the server:
|
||||
|
||||
1. Parses the submitted `epubcfi` (if any) and resolves it against the
|
||||
book's own XHTML.
|
||||
2. Extracts the text at the resolved anchor and cross-checks it with the
|
||||
submitted `context_text`.
|
||||
3. On a mismatch — or an unresolvable anchor, or no anchor at all —
|
||||
heals the position by text search, using the submitted `percentage`
|
||||
to disambiguate repeated phrases. The healed CFI and a recomputed
|
||||
percentage are stored in place of the submitted ones.
|
||||
4. Refreshes the book-wide `character_offset` column from the verified
|
||||
anchor on every verified save, so it never goes stale behind the
|
||||
anchor.
|
||||
|
||||
If verification fails outright (book file unreadable, context not found
|
||||
and percentage cannot disambiguate), the error is logged and the
|
||||
submission is stored as-is — verification never rejects a save, it only
|
||||
corrects.
|
||||
|
||||
## The restore handles
|
||||
|
||||
`GET /api/media-items/:id/progress` serves, alongside the raw stored
|
||||
fields, the server-derived handles for reflowable books with a
|
||||
resolvable anchor:
|
||||
|
||||
| Field | Currency | Meaning |
|
||||
| ------------- | ----------------------- | ------- |
|
||||
| `anchor_href` | — | The spine document the anchor lives in (e.g. `1984.xhtml`). **This is how a client finds the right resource.** |
|
||||
| `css_selector`| — | Body-relative `tag:nth-child(k)` chain of the anchor's block element (e.g. `body>div:nth-child(4)>p:nth-child(29)`). |
|
||||
| `char_offset` | UTF-16 code units | Offset of the anchor within the concatenated text of that block. |
|
||||
| `epubcfi` | UTF-16 terminals | The stored canonical CFI — re-served healed if the GET-time re-verification improved it. |
|
||||
| `context_text`| — | The stored verification context (≤100 normalized chars from the anchor). |
|
||||
|
||||
**Recommended restore sequence for a client:**
|
||||
|
||||
1. Open the book and jump to the stored `percentage` (coarse floor).
|
||||
2. Resolve `anchor_href` against your own spine (an ends-with match on
|
||||
document hrefs) and open that document if you are not already there.
|
||||
3. Query `css_selector` in that document, walk its text nodes counting
|
||||
UTF-16 units to `char_offset`, and scroll that position into view.
|
||||
4. If the anchor cannot be measured (renderer-specific laziness), the
|
||||
percentage floor stands.
|
||||
|
||||
## SPINE NUMBERING HAZARD — read this before parsing a stored CFI
|
||||
|
||||
**`epubcfi` spine steps index the OPF spine AS WRITTEN, including
|
||||
`linear="no"` items.** Several rendering engines (notably readium)
|
||||
number their reading order EXCLUDING `linear="no"` items. When a book's
|
||||
cover (or any other item) is `linear="no"`, the two numberings differ by
|
||||
a constant offset from that item onward — a client resolving a stored
|
||||
CFI's spine step against its own numbering lands in the WRONG DOCUMENT.
|
||||
|
||||
This is not hypothetical: "1984" epubs commonly have a `linear="no"`
|
||||
cover, which makes readium spine 0 = OPF spine 1. The server heals such
|
||||
numbering mismatches at ingest (that is what the context check is for),
|
||||
but the durable rule for client authors is:
|
||||
|
||||
> **Never resolve a stored CFI's spine step yourself.** Resolve the
|
||||
> document by `anchor_href`, then land with `css_selector` +
|
||||
> `char_offset`. Treat the CFI as opaque server currency.
|
||||
|
||||
## Offset currencies
|
||||
|
||||
Two counting systems are in play, and they are deliberately kept apart:
|
||||
|
||||
| Quantity | Currency | Why |
|
||||
| -------- | -------- | --- |
|
||||
| CFI terminal offsets (`…/1:456`) | UTF-16 code units | The EPUB CFI spec, and what every client observes (JavaScript `.length`). |
|
||||
| `char_offset` (block-relative handle) | UTF-16 code units | Same reason — clients walk DOM text with JS semantics. |
|
||||
| KOReader CRE `text().N` offsets | UTF-16 code units | crengine is UCS-16 internally. |
|
||||
| `character_offset` (book-wide column) | Unicode runes | Internal, consistent with `total_characters` and the percentage derivations. |
|
||||
|
||||
For all-BMP text the two currencies are identical. They diverge on
|
||||
astral-plane characters (emoji, rare CJK ideographs): one rune, two
|
||||
UTF-16 units. The server converts at every wire boundary; internal
|
||||
arithmetic never crosses.
|
||||
|
||||
## `context_text` rules
|
||||
|
||||
- Starts at the anchor position (it may begin mid-word).
|
||||
- Whitespace-normalized (all runs of whitespace collapse to single
|
||||
spaces).
|
||||
- At most 100 characters.
|
||||
- Comparison is containment-based (client and server suffixes of the
|
||||
same block verify in either direction); contexts shorter than 12
|
||||
chars never match.
|
||||
|
||||
## Engine-specific notes
|
||||
|
||||
- **readium-based clients** (the Android app): submit
|
||||
percentage + `context_text` + a CFI generated from the laid-out
|
||||
WebView. Their locally-generated CFI spine steps use readium
|
||||
numbering — the server heals the difference; nothing to do.
|
||||
- **Web (foliate)**: submit all three tiers; foliate CFIs are the same
|
||||
currency the server stores.
|
||||
- **KOReader**: submits a CRE xpointer in the `epubcfi` field of the
|
||||
device payload (historical field name; it is a CRE xpointer, not a
|
||||
CFI). The server converts CRE → canonical at ingest and canonical →
|
||||
CRE on pull (`koreader_xpointer` in the metadata response).
|
||||
- **Kobo**: submits kepub CFI locators, converted server-side the same
|
||||
way.
|
||||
- **Fixed-layout content** (PDF/CBZ): the page index is the canonical
|
||||
locator; CFI/xpointer are meaningless and neither submitted nor
|
||||
served.
|
||||
@@ -0,0 +1,91 @@
|
||||
# Update Media Item Progress
|
||||
|
||||
Submit reading progress for a media item. This is the position-authority
|
||||
ingest point: the server independently verifies the submission against
|
||||
the book and heals it when the client's projection is wrong.
|
||||
|
||||
**Endpoint**: `PUT /api/media-items/:id/progress`
|
||||
**Auth**: Required (Bearer token)
|
||||
**Content-Type**: `application/json`
|
||||
|
||||
See the [Position Contract](position-contract.md) for the three-tier
|
||||
submission model, the verification/healing semantics, and the offset
|
||||
currencies.
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | ---------------- |
|
||||
| id | string (UUID) | Yes | Media item UUID |
|
||||
|
||||
## Request Body
|
||||
|
||||
All fields are optional; submit what your renderer can observe.
|
||||
|
||||
| Field | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| `percentage` | float | Position as a fraction of the whole book (0..1). Tier 1 — the universal field. |
|
||||
| `context_text` | string | Up to 100 whitespace-normalized chars starting at the anchor. Tier 2 — enables verification and healing. |
|
||||
| `epubcfi` | string | Standard wrapped CFI (UTF-16 terminals). Tier 3 — the structural anchor. Note: the server re-derives the stored canonical CFI; a client's own spine numbering is healed if it disagrees with the OPF spine. |
|
||||
| `character_offset` | int | Book-wide rune offset. Accepted but recomputed server-side from the verified anchor on every verified save. |
|
||||
| `current_page`, `total_pages` | int | Fixed-layout position. For fixed-layout formats the page index is the canonical locator. |
|
||||
| `chapter`, `chapter_progress` | int, float | Chapter index and within-chapter fraction. |
|
||||
| `reading_mode` | string | `paged` or `scrolled` (fixed-layout reader state). |
|
||||
| `zoom_level`, `scroll_position_x`, `scroll_position_y` | float | Fixed-layout viewport state. |
|
||||
|
||||
### Example Request (reflowable, all three tiers)
|
||||
|
||||
```http
|
||||
PUT /api/media-items/774641f9-317b-4087-8e04-53bb4392ae56/progress
|
||||
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"percentage": 0.0415,
|
||||
"context_text": "was at war with one of these Powers it was generally at peace with the other. But what was strange w",
|
||||
"epubcfi": "epubcfi(/6/4!/4/8[_idContainer003]/62/1:456)"
|
||||
}
|
||||
```
|
||||
|
||||
### Example Request (fixed-layout)
|
||||
|
||||
```json
|
||||
{
|
||||
"percentage": 0.15,
|
||||
"current_page": 30,
|
||||
"total_pages": 194,
|
||||
"reading_mode": "paged"
|
||||
}
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
- The submission is verified against the book's XHTML when
|
||||
`context_text` is present and the book is convertible reflowable: the
|
||||
CFI is resolved, the text at the anchor is compared with
|
||||
`context_text`, and mismatches heal by text search (percentage
|
||||
disambiguates repeats). See [Position Contract](position-contract.md).
|
||||
- **Anti-clobber guard**: a submission with `percentage < 0.005` is
|
||||
ignored with `{"status": "ignored"}` when the stored row already holds
|
||||
a percentage above `0.01` — re-opening a book at its first page does
|
||||
not wipe real progress.
|
||||
- The response is the stored row after verification, including the
|
||||
healed `epubcfi` and the refreshed `character_offset` when
|
||||
verification ran.
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
The saved progress row (same shape as
|
||||
[GET](get_media_progress.md), minus the GET-time handles). The
|
||||
`epubcfi` and `percentage` in the response are the server-verified
|
||||
values, which may differ from the submitted ones when healing occurred.
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ----------- |
|
||||
| 400 | Invalid request data |
|
||||
| 401 | Invalid or expired token |
|
||||
| 500 | Database error |
|
||||
@@ -1,68 +0,0 @@
|
||||
# Update Reading Progress
|
||||
|
||||
Update reading progress for a media item. This will sync across all devices via WebSocket.
|
||||
|
||||
**Endpoint**: `PUT /api/media-items/{media_id}/progress`
|
||||
**Auth**: Required
|
||||
**Content-Type**: `application/json`
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------ | -------- | --------------- |
|
||||
| media_id | string | Yes | Media item UUID |
|
||||
|
||||
## Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| --------------------------- | ------- | -------- | ------------------------------------------------- |
|
||||
| source | string | Yes | Progress source (e.g., "web", "koreader", "kobo") |
|
||||
| location | object | Yes | Location information |
|
||||
| location.percentage | float | No | Progress percentage (0-1) |
|
||||
| location.epubcfi | string | No | EPUB CFI location |
|
||||
| location.character | integer | No | Character offset |
|
||||
| location.chapter | integer | No | Chapter number |
|
||||
| location.page | integer | No | Current page |
|
||||
| location.total_pages | integer | No | Total pages |
|
||||
| device_metadata | object | No | Device metadata |
|
||||
| device_metadata.device_type | string | No | Device type |
|
||||
| device_metadata.user_agent | string | No | User agent string |
|
||||
|
||||
### Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"source": "web",
|
||||
"location": {
|
||||
"percentage": 0.45678,
|
||||
"epubcfi": "epubcfi(/6/4/2:15)",
|
||||
"character": 15432,
|
||||
"chapter": 3,
|
||||
"page": 89,
|
||||
"total_pages": 200
|
||||
},
|
||||
"device_metadata": {
|
||||
"device_type": "web",
|
||||
"user_agent": "Mozilla/5.0..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"sync_status": "success",
|
||||
"progress_updated": true,
|
||||
"devices_notified": ["device-1", "device-2"],
|
||||
"broadcast": true
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------ |
|
||||
| 400 | Invalid location data |
|
||||
| 401 | Invalid or expired token |
|
||||
| 404 | Media item not found |
|
||||
@@ -1,56 +0,0 @@
|
||||
# Update Universal Progress
|
||||
|
||||
Update universal reading progress for a media item.
|
||||
|
||||
**Endpoint**: `POST /api/progress/:id`
|
||||
**Auth**: Required
|
||||
**Content-Type**: `application/json`
|
||||
|
||||
## Path Parameters
|
||||
|
||||
| Parameter | Type | Required | Description |
|
||||
| --------- | ------------- | -------- | --------------- |
|
||||
| id | string (UUID) | Yes | Media item UUID |
|
||||
|
||||
## Request Body
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
| ---------- | ------------- | -------- | ------------------------------------------- |
|
||||
| percentage | float | No | Progress percentage (0-100) |
|
||||
| position | integer | No | Current position in bytes |
|
||||
| page | integer | No | Current page number |
|
||||
| finished | boolean | No | Whether the book is finished |
|
||||
| device_id | string (UUID) | No | Device UUID (optional, for tracking source) |
|
||||
|
||||
### Example Request
|
||||
|
||||
```json
|
||||
{
|
||||
"percentage": 75.5,
|
||||
"position": 1234,
|
||||
"page": 150,
|
||||
"finished": false,
|
||||
"device_id": "device-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
## Response (200 OK)
|
||||
|
||||
```json
|
||||
{
|
||||
"media_item_id": "uuid",
|
||||
"percentage": 75.5,
|
||||
"position": 1234,
|
||||
"page": 150,
|
||||
"finished": false,
|
||||
"updated_at": "2026-02-08T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Error Responses
|
||||
|
||||
| Code | Description |
|
||||
| ---- | ------------------------ |
|
||||
| 400 | Invalid request data |
|
||||
| 401 | Invalid or expired token |
|
||||
| 404 | Media item not found |
|
||||
@@ -77,6 +77,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
| **Deploy Bookhoard** | [Operations Portal → Troubleshooting](operations/troubleshooting.md) |
|
||||
| **Contribute code** | [Contributing Portal → Development Guide](contributing/development.md) |
|
||||
| **Understand sync** | [User Portal → Sync Guide](user/sync-guide.md) |
|
||||
| **Read in the browser** | [User Portal → Web Reader Guide](user/reader-guide.md) |
|
||||
|
||||
---
|
||||
|
||||
@@ -90,6 +91,7 @@ Complete guide to Bookhoard documentation. Find what you need quickly.
|
||||
| ...set up KOReader? | [KOReader Setup Guide](user/devices/koreader-setup.md) |
|
||||
| ...use a Kobo? | [Kobo Setup Guide](user/devices/kobo-setup.md) - native sync coming soon; KOReader works today |
|
||||
| ...understand sync? | [Sync Guide](user/sync-guide.md) |
|
||||
| ...read in the browser? | [Web Reader Guide](user/reader-guide.md) - reading, highlights, notes |
|
||||
| ...resolve conflicts? | [Sync Guide](user/sync-guide.md) - Managing Conflicts |
|
||||
| ...troubleshoot deployment? | [Troubleshooting Guide](operations/troubleshooting.md) |
|
||||
| ...use the API? | [API Reference](developer/api-reference.md) |
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
# Web Reader Guide
|
||||
|
||||
Bookhoard's built-in web reader works in any modern browser — desktop or
|
||||
mobile — and keeps your position, highlights, bookmarks, and notes in sync
|
||||
with the Android app and KOReader.
|
||||
|
||||
## Opening a Book
|
||||
|
||||
From your library or book page, click **Read**. The reader opens in an
|
||||
immersive view with the book's cover, title, and chapter in the top bar
|
||||
(EPUB) or page number (PDF/comics).
|
||||
|
||||
## Navigation
|
||||
|
||||
### On a phone or tablet (touch)
|
||||
|
||||
| Gesture | Action |
|
||||
|---|---|
|
||||
| **Swipe** left / right | Turn the page |
|
||||
| **Tap** once | Toggle the reader menu (headers, settings, drawers) |
|
||||
| **Tap** in a corner zone | With the menu open: jump back / forward by chapter |
|
||||
|
||||
Paging is swipe-only by design: taps are reserved for the menu so a page
|
||||
turn can never fire while you're trying to select text.
|
||||
|
||||
### On desktop
|
||||
|
||||
- Scroll wheel, trackpad, arrow keys, or **Page Up / Page Down** to page
|
||||
- Click the progress bar (bottom, with the menu open) to jump
|
||||
- Contents drawer for the table of contents; Pages grid for PDF/comics
|
||||
|
||||
## Text Selection & Highlights
|
||||
|
||||
### Selecting text (touch)
|
||||
|
||||
1. **Press and hold** a word (~half a second) until it highlights
|
||||
2. **Drag** to extend the selection word by word — it stays on the
|
||||
visible page
|
||||
3. **Drag the round handles** (EPUB) to fine-tune the start and end
|
||||
points
|
||||
4. Lift your finger and pause — the highlight menu appears **below** the
|
||||
selection, underneath the browser's own copy menu
|
||||
|
||||
The browser's address bar hides while a selection is active to give the
|
||||
page room; it returns as soon as the selection is cleared.
|
||||
|
||||
### Creating a highlight or note
|
||||
|
||||
With the selection menu open:
|
||||
|
||||
- **Color dots** — tap one to highlight immediately in that color
|
||||
- **Pencil** — opens a note; write it and tap **Highlight with note**
|
||||
to save the highlight and its note together
|
||||
- **Copy** — copies the selected text (works even over plain HTTP on
|
||||
your LAN)
|
||||
- While the note editor is open, you can still change the color — the
|
||||
popover stays put until you save or cancel
|
||||
|
||||
### Editing or removing a highlight
|
||||
|
||||
Tap anywhere on a painted highlight to reopen it in the menu. From there
|
||||
you can change its color, edit the note (pencil → **Save note**), or
|
||||
delete it (trash icon). Tapping anywhere else closes the menu.
|
||||
|
||||
### PDFs and comics
|
||||
|
||||
Text selection works the same way on PDFs. Highlights are stored as
|
||||
page-anchored rectangles and sync to the Android app. Comics/manga
|
||||
libraries disable annotation (bookmarks still work).
|
||||
|
||||
## Notes & Bookmarks
|
||||
|
||||
The **annotations drawer** (right side, from the open menu) lists your
|
||||
highlights, standalone notes, and bookmarks for the book — tap any entry
|
||||
to jump to it.
|
||||
|
||||
## Where Reading Position Is Stored
|
||||
|
||||
Position, highlights, bookmarks, and notes sync automatically through the
|
||||
server (see the [Sync Guide](sync-guide.md)). Close the tab whenever you
|
||||
like — the reader resumes exactly where you left off on any device.
|
||||
@@ -36,6 +36,14 @@ Learn how to configure your e-reader devices to sync with Bookhoard:
|
||||
- Troubleshooting
|
||||
- Best practices
|
||||
|
||||
## 📖 Reading
|
||||
|
||||
- **[Web Reader Guide](reader-guide.md)** - Reading and annotating in the browser
|
||||
- Touch navigation (swipe to page, tap for menu)
|
||||
- Selecting text: long-press, drag, handles
|
||||
- Highlights, notes, copy, and editing
|
||||
- PDFs and comics
|
||||
|
||||
## 🎨 Frontend Guide
|
||||
|
||||
**[Frontend Guide](frontend-guide.md)** - Learn how to use the Bookhoard web interface
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
github.com/ArcadiaLin/go-epub v0.1.1 h1:13roe62tarrZ1Y1QTxE+Bzd/NKlChhRzegLVmU5Hgws=
|
||||
github.com/ArcadiaLin/go-epub v0.1.1/go.mod h1:GY09AG6jnEbsYytkw6VeICLOt25F1GQDGXjYS7cxNhU=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e h1:HjVbSQHy+dnlS6C3XajZ69NYAb5jbGNfHanvm1+iYlo=
|
||||
github.com/a-h/parse v0.0.0-20250122154542-74294addb73e/go.mod h1:3mnrkvGpurZ4ZrTDbYU84xhwXW2TjTKShSwjRi2ihfQ=
|
||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbfjek=
|
||||
@@ -12,8 +14,13 @@ github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU
|
||||
github.com/bodgit/plumbing v1.3.0/go.mod h1:JOTb4XiRu5xfnmdnDJo6GmSbSbtSyufrsyZFByMtKEs=
|
||||
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
|
||||
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
|
||||
github.com/cli/browser v1.3.0 h1:LejqCrpWr+1pRqmEPDGnTZOjsMe7sehifLynZJuqJpo=
|
||||
github.com/cli/browser v1.3.0/go.mod h1:HH8s+fOAxjhQoBUAsKuPCbqUuxZDhQ2/aD+SzsEfBTk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk=
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
|
||||
github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
@@ -22,6 +29,8 @@ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8Yc
|
||||
github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
|
||||
github.com/dlclark/regexp2 v1.12.0 h1:0j4c5qQmnC6XOWNjP3PIXURXN2gWx76rd3KvgdPkCz8=
|
||||
github.com/dlclark/regexp2 v1.12.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
|
||||
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
|
||||
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||
@@ -72,16 +81,23 @@ github.com/labstack/echo/v5 v5.1.0 h1:MvIRydoN+p9cx/zq8Lff6YXqUW2ZaEsOMISzEGSMrB
|
||||
github.com/labstack/echo/v5 v5.1.0/go.mod h1:SyvlSdObGjRXeQfCCXW/sybkZdOOQZBmpKF0bvALaeo=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.23 h1:7ykA0T0jkPpzSvMS5i9uoNn2Xy3R383f9HDx3RybWcw=
|
||||
github.com/mattn/go-runewidth v0.0.23/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA=
|
||||
github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A=
|
||||
github.com/natefinch/atomic v1.0.1/go.mod h1:N/D/ELrljoqDyT3rZrsUmtsuzvHkeB/wWjHV22AZRbM=
|
||||
github.com/nwaples/rardecode v1.1.3 h1:cWCaZwfM5H7nAD6PyEdcVnczzV8i/JtotnyW/dD9lEc=
|
||||
github.com/nwaples/rardecode v1.1.3/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0=
|
||||
github.com/pdfcpu/pdfcpu v0.11.1 h1:htHBSkGH5jMKWC6e0sihBFbcKZ8vG1M67c8/dJxhjas=
|
||||
github.com/pdfcpu/pdfcpu v0.11.1/go.mod h1:pP3aGga7pRvwFWAm9WwFvo+V68DfANi9kxSQYioNYcw=
|
||||
github.com/pierrec/lz4/v4 v4.1.26 h1:GrpZw1gZttORinvzBdXPUXATeqlJjqUG/D87TKMnhjY=
|
||||
github.com/pierrec/lz4/v4 v4.1.26/go.mod h1:EoQMVJgeeEOMsCqCzqFm2O0cJvljX2nGZjcRIPL34O4=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -91,6 +107,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/cors v1.11.0 h1:0B9GE/r9Bc2UxRMMtymBkHTenPkHDv0CW4Y98GBY+po=
|
||||
github.com/rs/cors v1.11.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
@@ -98,6 +116,8 @@ github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
@@ -118,16 +138,22 @@ golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY=
|
||||
golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
||||
@@ -259,6 +259,12 @@ type Querier interface {
|
||||
GetMediaItemFormats(ctx context.Context, mediaItemID pgtype.UUID) ([]MediaItemFormats, error)
|
||||
// Per-item user-data counts, used when choosing which duplicate copy to keep
|
||||
GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtype.UUID) (GetMediaItemUsageCountsRow, error)
|
||||
// Cross-library move detection: identical content registered in a different
|
||||
// library of the same type (ebooks stay with ebooks, comics with comics).
|
||||
// Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||
// hidden/archived rows whose file vanished (move candidates - the scanner
|
||||
// verifies disk state before repointing).
|
||||
GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error)
|
||||
GetMediaNote(ctx context.Context, id pgtype.UUID) (MediaNotes, error)
|
||||
// ============================================
|
||||
// ANNOTATION SYNC QUERIES (notes)
|
||||
@@ -349,7 +355,10 @@ type Querier interface {
|
||||
ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]Devices, error)
|
||||
// Items hidden from libraries: either missing from disk (waiting on the
|
||||
// second scan that archives them) or already archived and pending purge.
|
||||
// Feeds the admin archived-items page.
|
||||
// Feeds the admin archived-items page. match_* columns identify an active
|
||||
// item with identical content (same SHA-256) so the UI can offer merging the
|
||||
// archived row's reading data into its active twin instead of waiting for a
|
||||
// file that will never return.
|
||||
ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error)
|
||||
ListLibraries(ctx context.Context) ([]ListLibrariesRow, error)
|
||||
ListMediaItems(ctx context.Context, arg ListMediaItemsParams) ([]ListMediaItemsRow, error)
|
||||
@@ -374,6 +383,11 @@ type Querier interface {
|
||||
// a move. Repoints the row (and refreshes file-level fields) so reading
|
||||
// history follows the book; archive/missing state is cleared separately.
|
||||
MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFilePathParams) error
|
||||
// Cross-library move: identical content reappeared in a different library of
|
||||
// the same type while the old file vanished. Repoints the row so reading
|
||||
// history, annotations and collections follow the book; archive/missing
|
||||
// state is cleared. library_type_name is refreshed by trigger.
|
||||
MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error
|
||||
// Manual bulk purge from the library admin page.
|
||||
PurgeAllArchivedMediaItems(ctx context.Context) ([]PurgeAllArchivedMediaItemsRow, error)
|
||||
// Retention sweep at library-scan time: hard-delete archived items older
|
||||
|
||||
@@ -2623,6 +2623,8 @@ SELECT ci.id, ci.collection_id, ci.media_item_id, ci.added_at, ci.added_by_user_
|
||||
FROM collection_items ci
|
||||
JOIN media_items mi ON ci.media_item_id = mi.id
|
||||
WHERE ci.collection_id = $1
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY ci.added_at DESC
|
||||
`
|
||||
|
||||
@@ -2676,6 +2678,8 @@ SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.fi
|
||||
INNER JOIN collection_items ci ON ci.media_item_id = mi.id
|
||||
WHERE ci.collection_id = $1
|
||||
AND ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY ci.added_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
@@ -2982,6 +2986,8 @@ INNER JOIN (
|
||||
ORDER BY media_item_id, last_read_at DESC
|
||||
) rp ON rp.media_item_id = mi.id
|
||||
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY rp.last_read_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
@@ -4378,6 +4384,8 @@ func (q *Queries) GetLibraryFolders(ctx context.Context, libraryID pgtype.UUID)
|
||||
const GetLibraryItems = `-- name: GetLibraryItems :many
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
||||
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY mi.created_at DESC
|
||||
`
|
||||
|
||||
@@ -5694,6 +5702,184 @@ func (q *Queries) GetMediaItemUsageCounts(ctx context.Context, mediaItemID pgtyp
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetMediaItemsBySHA256AndSameLibraryType = `-- name: GetMediaItemsBySHA256AndSameLibraryType :many
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, lt.name AS library_type_name
|
||||
FROM media_items mi
|
||||
JOIN libraries l ON mi.library_id = l.id
|
||||
JOIN library_types lt ON l.library_type_id = lt.id
|
||||
JOIN libraries target_lib ON target_lib.id = $1
|
||||
WHERE mi.file_sha256 = $2
|
||||
AND mi.library_id <> $1
|
||||
AND lt.id = target_lib.library_type_id
|
||||
ORDER BY (mi.archived_at IS NOT NULL) DESC,
|
||||
mi.missing_scan_count DESC,
|
||||
mi.created_at ASC
|
||||
`
|
||||
|
||||
type GetMediaItemsBySHA256AndSameLibraryTypeParams struct {
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
||||
}
|
||||
|
||||
type GetMediaItemsBySHA256AndSameLibraryTypeRow struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
Title string `db:"title" json:"title"`
|
||||
Author pgtype.Text `db:"author" json:"author"`
|
||||
Isbn pgtype.Text `db:"isbn" json:"isbn"`
|
||||
Description pgtype.Text `db:"description" json:"description"`
|
||||
FilePath string `db:"file_path" json:"file_path"`
|
||||
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||
MimeType pgtype.Text `db:"mime_type" json:"mime_type"`
|
||||
CoverImagePath pgtype.Text `db:"cover_image_path" json:"cover_image_path"`
|
||||
Series pgtype.Text `db:"series" json:"series"`
|
||||
SeriesNumber pgtype.Int4 `db:"series_number" json:"series_number"`
|
||||
Tags []string `db:"tags" json:"tags"`
|
||||
Asin pgtype.Text `db:"asin" json:"asin"`
|
||||
DatePublished pgtype.Date `db:"date_published" json:"date_published"`
|
||||
Publisher pgtype.Text `db:"publisher" json:"publisher"`
|
||||
Contributors []string `db:"contributors" json:"contributors"`
|
||||
Language pgtype.Text `db:"language" json:"language"`
|
||||
Edition pgtype.Text `db:"edition" json:"edition"`
|
||||
PageCount pgtype.Int4 `db:"page_count" json:"page_count"`
|
||||
Genre pgtype.Text `db:"genre" json:"genre"`
|
||||
CopyrightYear pgtype.Int4 `db:"copyright_year" json:"copyright_year"`
|
||||
GoodreadsID pgtype.Text `db:"goodreads_id" json:"goodreads_id"`
|
||||
OpenlibraryID pgtype.Text `db:"openlibrary_id" json:"openlibrary_id"`
|
||||
GoogleBooksID pgtype.Text `db:"google_books_id" json:"google_books_id"`
|
||||
AddedByAdminID pgtype.UUID `db:"added_by_admin_id" json:"added_by_admin_id"`
|
||||
CreatedAt pgtype.Timestamptz `db:"created_at" json:"created_at"`
|
||||
ImportedAt pgtype.Timestamptz `db:"imported_at" json:"imported_at"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
FormatGroup string `db:"format_group" json:"format_group"`
|
||||
FormatMimetype pgtype.Text `db:"format_mimetype" json:"format_mimetype"`
|
||||
IsReflowable pgtype.Bool `db:"is_reflowable" json:"is_reflowable"`
|
||||
HasFixedLayout pgtype.Bool `db:"has_fixed_layout" json:"has_fixed_layout"`
|
||||
TotalCharacters pgtype.Int8 `db:"total_characters" json:"total_characters"`
|
||||
ChapterCount pgtype.Int4 `db:"chapter_count" json:"chapter_count"`
|
||||
EntitlementID pgtype.Text `db:"entitlement_id" json:"entitlement_id"`
|
||||
RevisionNumber pgtype.Int4 `db:"revision_number" json:"revision_number"`
|
||||
KoboContentID pgtype.Text `db:"kobo_content_id" json:"kobo_content_id"`
|
||||
KoboMetadata []byte `db:"kobo_metadata" json:"kobo_metadata"`
|
||||
MangaType pgtype.Text `db:"manga_type" json:"manga_type"`
|
||||
ReadingDirection pgtype.Text `db:"reading_direction" json:"reading_direction"`
|
||||
SeriesCount pgtype.Int4 `db:"series_count" json:"series_count"`
|
||||
Volume pgtype.Int4 `db:"volume" json:"volume"`
|
||||
Imprint pgtype.Text `db:"imprint" json:"imprint"`
|
||||
AgeRating pgtype.Text `db:"age_rating" json:"age_rating"`
|
||||
WebUrl pgtype.Text `db:"web_url" json:"web_url"`
|
||||
StoryArc pgtype.Text `db:"story_arc" json:"story_arc"`
|
||||
IsBlackAndWhite pgtype.Bool `db:"is_black_and_white" json:"is_black_and_white"`
|
||||
MetadataNotes pgtype.Text `db:"metadata_notes" json:"metadata_notes"`
|
||||
CommunityRating pgtype.Float8 `db:"community_rating" json:"community_rating"`
|
||||
AlternateInfo []byte `db:"alternate_info" json:"alternate_info"`
|
||||
ScanInformation pgtype.Text `db:"scan_information" json:"scan_information"`
|
||||
Summary pgtype.Text `db:"summary" json:"summary"`
|
||||
MetadataOverrides []string `db:"metadata_overrides" json:"metadata_overrides"`
|
||||
MissingScanCount int32 `db:"missing_scan_count" json:"missing_scan_count"`
|
||||
ArchivedAt pgtype.Timestamptz `db:"archived_at" json:"archived_at"`
|
||||
ChapterMetadata []byte `db:"chapter_metadata" json:"chapter_metadata"`
|
||||
LibraryTypeName pgtype.Text `db:"library_type_name" json:"library_type_name"`
|
||||
TagsSearch []string `db:"tags_search" json:"tags_search"`
|
||||
ContributorsSearch []string `db:"contributors_search" json:"contributors_search"`
|
||||
FileSha256 pgtype.Text `db:"file_sha256" json:"file_sha256"`
|
||||
OpfIdentifier pgtype.Text `db:"opf_identifier" json:"opf_identifier"`
|
||||
OpfUuid pgtype.Text `db:"opf_uuid" json:"opf_uuid"`
|
||||
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||
}
|
||||
|
||||
// Cross-library move detection: identical content registered in a different
|
||||
// library of the same type (ebooks stay with ebooks, comics with comics).
|
||||
// Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||
// hidden/archived rows whose file vanished (move candidates - the scanner
|
||||
// verifies disk state before repointing).
|
||||
func (q *Queries) GetMediaItemsBySHA256AndSameLibraryType(ctx context.Context, arg GetMediaItemsBySHA256AndSameLibraryTypeParams) ([]GetMediaItemsBySHA256AndSameLibraryTypeRow, error) {
|
||||
rows, err := q.db.Query(ctx, GetMediaItemsBySHA256AndSameLibraryType, arg.LibraryID, arg.FileSha256)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []GetMediaItemsBySHA256AndSameLibraryTypeRow{}
|
||||
for rows.Next() {
|
||||
var i GetMediaItemsBySHA256AndSameLibraryTypeRow
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.LibraryID,
|
||||
&i.Title,
|
||||
&i.Author,
|
||||
&i.Isbn,
|
||||
&i.Description,
|
||||
&i.FilePath,
|
||||
&i.FileSize,
|
||||
&i.MimeType,
|
||||
&i.CoverImagePath,
|
||||
&i.Series,
|
||||
&i.SeriesNumber,
|
||||
&i.Tags,
|
||||
&i.Asin,
|
||||
&i.DatePublished,
|
||||
&i.Publisher,
|
||||
&i.Contributors,
|
||||
&i.Language,
|
||||
&i.Edition,
|
||||
&i.PageCount,
|
||||
&i.Genre,
|
||||
&i.CopyrightYear,
|
||||
&i.GoodreadsID,
|
||||
&i.OpenlibraryID,
|
||||
&i.GoogleBooksID,
|
||||
&i.AddedByAdminID,
|
||||
&i.CreatedAt,
|
||||
&i.ImportedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.FormatGroup,
|
||||
&i.FormatMimetype,
|
||||
&i.IsReflowable,
|
||||
&i.HasFixedLayout,
|
||||
&i.TotalCharacters,
|
||||
&i.ChapterCount,
|
||||
&i.EntitlementID,
|
||||
&i.RevisionNumber,
|
||||
&i.KoboContentID,
|
||||
&i.KoboMetadata,
|
||||
&i.MangaType,
|
||||
&i.ReadingDirection,
|
||||
&i.SeriesCount,
|
||||
&i.Volume,
|
||||
&i.Imprint,
|
||||
&i.AgeRating,
|
||||
&i.WebUrl,
|
||||
&i.StoryArc,
|
||||
&i.IsBlackAndWhite,
|
||||
&i.MetadataNotes,
|
||||
&i.CommunityRating,
|
||||
&i.AlternateInfo,
|
||||
&i.ScanInformation,
|
||||
&i.Summary,
|
||||
&i.MetadataOverrides,
|
||||
&i.MissingScanCount,
|
||||
&i.ArchivedAt,
|
||||
&i.ChapterMetadata,
|
||||
&i.LibraryTypeName,
|
||||
&i.TagsSearch,
|
||||
&i.ContributorsSearch,
|
||||
&i.FileSha256,
|
||||
&i.OpfIdentifier,
|
||||
&i.OpfUuid,
|
||||
&i.HashConfidence,
|
||||
&i.LibraryTypeName_2,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetMediaNote = `-- name: GetMediaNote :one
|
||||
SELECT id, media_item_id, user_id, content, position, created_at, updated_at, percentage_location, character_start, character_end, epubcfi_location, chapter_reference, paragraph_reference, device_sync_data, dedup_key, last_modified_at, last_modified_source, deleted, deleted_at FROM media_notes WHERE id = $1
|
||||
`
|
||||
@@ -5909,6 +6095,8 @@ func (q *Queries) GetNextRetryTime(ctx context.Context) (interface{}, error) {
|
||||
const GetNotStartedItems = `-- name: GetNotStartedItems :many
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
||||
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM reading_progress rp
|
||||
WHERE rp.media_item_id = mi.id
|
||||
@@ -6312,6 +6500,8 @@ func (q *Queries) GetReadingSpeed(ctx context.Context, arg GetReadingSpeedParams
|
||||
const GetRecentlyAddedItems = `-- name: GetRecentlyAddedItems :many
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence FROM media_items mi
|
||||
WHERE ($1::uuid IS NULL OR mi.library_id = $1::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
@@ -6416,6 +6606,8 @@ INNER JOIN (
|
||||
ORDER BY media_item_id, last_read_at DESC
|
||||
) rp ON rp.media_item_id = mi.id
|
||||
WHERE ($2::uuid IS NULL OR mi.library_id = $2::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY rp.last_read_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
@@ -8419,10 +8611,28 @@ func (q *Queries) ListDevicesByUser(ctx context.Context, userID pgtype.UUID) ([]
|
||||
}
|
||||
|
||||
const ListHiddenMediaItems = `-- name: ListHiddenMediaItems :many
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name
|
||||
SELECT mi.id, mi.library_id, mi.title, mi.author, mi.isbn, mi.description, mi.file_path, mi.file_size, mi.mime_type, mi.cover_image_path, mi.series, mi.series_number, mi.tags, mi.asin, mi.date_published, mi.publisher, mi.contributors, mi.language, mi.edition, mi.page_count, mi.genre, mi.copyright_year, mi.goodreads_id, mi.openlibrary_id, mi.google_books_id, mi.added_by_admin_id, mi.created_at, mi.imported_at, mi.updated_at, mi.format_group, mi.format_mimetype, mi.is_reflowable, mi.has_fixed_layout, mi.total_characters, mi.chapter_count, mi.entitlement_id, mi.revision_number, mi.kobo_content_id, mi.kobo_metadata, mi.manga_type, mi.reading_direction, mi.series_count, mi.volume, mi.imprint, mi.age_rating, mi.web_url, mi.story_arc, mi.is_black_and_white, mi.metadata_notes, mi.community_rating, mi.alternate_info, mi.scan_information, mi.summary, mi.metadata_overrides, mi.missing_scan_count, mi.archived_at, mi.chapter_metadata, mi.library_type_name, mi.tags_search, mi.contributors_search, mi.file_sha256, mi.opf_identifier, mi.opf_uuid, mi.hash_confidence, l.name as library_name, lt.name as library_type_name,
|
||||
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
|
||||
twin_library.name AS match_library_name
|
||||
FROM media_items mi
|
||||
JOIN libraries l ON mi.library_id = l.id
|
||||
JOIN library_types lt ON l.library_type_id = lt.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT t.id, t.title, t.library_id
|
||||
FROM media_items t
|
||||
JOIN libraries tl ON t.library_id = tl.id
|
||||
JOIN library_types tlt ON tl.library_type_id = tlt.id
|
||||
WHERE t.file_sha256 = mi.file_sha256
|
||||
AND t.id <> mi.id
|
||||
AND t.archived_at IS NULL
|
||||
AND t.missing_scan_count = 0
|
||||
ORDER BY (tlt.id = lt.id) DESC,
|
||||
(tl.id = l.id) DESC,
|
||||
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
|
||||
t.created_at ASC
|
||||
LIMIT 1
|
||||
) twin ON true
|
||||
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
|
||||
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC
|
||||
`
|
||||
@@ -8494,11 +8704,17 @@ type ListHiddenMediaItemsRow struct {
|
||||
HashConfidence pgtype.Text `db:"hash_confidence" json:"hash_confidence"`
|
||||
LibraryName string `db:"library_name" json:"library_name"`
|
||||
LibraryTypeName_2 string `db:"library_type_name_2" json:"library_type_name_2"`
|
||||
MatchID pgtype.UUID `db:"match_id" json:"match_id"`
|
||||
MatchTitle string `db:"match_title" json:"match_title"`
|
||||
MatchLibraryName pgtype.Text `db:"match_library_name" json:"match_library_name"`
|
||||
}
|
||||
|
||||
// Items hidden from libraries: either missing from disk (waiting on the
|
||||
// second scan that archives them) or already archived and pending purge.
|
||||
// Feeds the admin archived-items page.
|
||||
// Feeds the admin archived-items page. match_* columns identify an active
|
||||
// item with identical content (same SHA-256) so the UI can offer merging the
|
||||
// archived row's reading data into its active twin instead of waiting for a
|
||||
// file that will never return.
|
||||
func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaItemsRow, error) {
|
||||
rows, err := q.db.Query(ctx, ListHiddenMediaItems)
|
||||
if err != nil {
|
||||
@@ -8575,6 +8791,9 @@ func (q *Queries) ListHiddenMediaItems(ctx context.Context) ([]ListHiddenMediaIt
|
||||
&i.HashConfidence,
|
||||
&i.LibraryName,
|
||||
&i.LibraryTypeName_2,
|
||||
&i.MatchID,
|
||||
&i.MatchTitle,
|
||||
&i.MatchLibraryName,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -9999,6 +10218,38 @@ func (q *Queries) MoveMediaItemFilePath(ctx context.Context, arg MoveMediaItemFi
|
||||
return err
|
||||
}
|
||||
|
||||
const MoveMediaItemToLibrary = `-- name: MoveMediaItemToLibrary :exec
|
||||
UPDATE media_items
|
||||
SET library_id = $2,
|
||||
file_path = $3,
|
||||
file_size = $4,
|
||||
archived_at = NULL,
|
||||
missing_scan_count = 0,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
type MoveMediaItemToLibraryParams struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
LibraryID pgtype.UUID `db:"library_id" json:"library_id"`
|
||||
FilePath string `db:"file_path" json:"file_path"`
|
||||
FileSize pgtype.Int8 `db:"file_size" json:"file_size"`
|
||||
}
|
||||
|
||||
// Cross-library move: identical content reappeared in a different library of
|
||||
// the same type while the old file vanished. Repoints the row so reading
|
||||
// history, annotations and collections follow the book; archive/missing
|
||||
// state is cleared. library_type_name is refreshed by trigger.
|
||||
func (q *Queries) MoveMediaItemToLibrary(ctx context.Context, arg MoveMediaItemToLibraryParams) error {
|
||||
_, err := q.db.Exec(ctx, MoveMediaItemToLibrary,
|
||||
arg.ID,
|
||||
arg.LibraryID,
|
||||
arg.FilePath,
|
||||
arg.FileSize,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
const PurgeAllArchivedMediaItems = `-- name: PurgeAllArchivedMediaItems :many
|
||||
DELETE FROM media_items
|
||||
WHERE archived_at IS NOT NULL
|
||||
|
||||
@@ -328,6 +328,20 @@ WHERE id = $1;
|
||||
UPDATE media_items SET archived_at = NOW(), updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: MoveMediaItemToLibrary :exec
|
||||
-- Cross-library move: identical content reappeared in a different library of
|
||||
-- the same type while the old file vanished. Repoints the row so reading
|
||||
-- history, annotations and collections follow the book; archive/missing
|
||||
-- state is cleared. library_type_name is refreshed by trigger.
|
||||
UPDATE media_items
|
||||
SET library_id = $2,
|
||||
file_path = $3,
|
||||
file_size = $4,
|
||||
archived_at = NULL,
|
||||
missing_scan_count = 0,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
|
||||
-- name: ClearMediaItemArchive :exec
|
||||
-- File is back on disk (by path or content hash): restore visibility and
|
||||
-- reset the missing-scan counter. No-op for items that were never archived.
|
||||
@@ -353,11 +367,32 @@ SELECT COUNT(*) FROM media_items WHERE archived_at IS NOT NULL;
|
||||
-- name: ListHiddenMediaItems :many
|
||||
-- Items hidden from libraries: either missing from disk (waiting on the
|
||||
-- second scan that archives them) or already archived and pending purge.
|
||||
-- Feeds the admin archived-items page.
|
||||
SELECT mi.*, l.name as library_name, lt.name as library_type_name
|
||||
-- Feeds the admin archived-items page. match_* columns identify an active
|
||||
-- item with identical content (same SHA-256) so the UI can offer merging the
|
||||
-- archived row's reading data into its active twin instead of waiting for a
|
||||
-- file that will never return.
|
||||
SELECT mi.*, l.name as library_name, lt.name as library_type_name,
|
||||
twin.id AS match_id, COALESCE(twin.title, '') AS match_title,
|
||||
twin_library.name AS match_library_name
|
||||
FROM media_items mi
|
||||
JOIN libraries l ON mi.library_id = l.id
|
||||
JOIN library_types lt ON l.library_type_id = lt.id
|
||||
LEFT JOIN LATERAL (
|
||||
SELECT t.id, t.title, t.library_id
|
||||
FROM media_items t
|
||||
JOIN libraries tl ON t.library_id = tl.id
|
||||
JOIN library_types tlt ON tl.library_type_id = tlt.id
|
||||
WHERE t.file_sha256 = mi.file_sha256
|
||||
AND t.id <> mi.id
|
||||
AND t.archived_at IS NULL
|
||||
AND t.missing_scan_count = 0
|
||||
ORDER BY (tlt.id = lt.id) DESC,
|
||||
(tl.id = l.id) DESC,
|
||||
(SELECT COUNT(*) FROM reading_progress rp WHERE rp.media_item_id = t.id) DESC,
|
||||
t.created_at ASC
|
||||
LIMIT 1
|
||||
) twin ON true
|
||||
LEFT JOIN libraries twin_library ON twin_library.id = twin.library_id
|
||||
WHERE mi.archived_at IS NOT NULL OR mi.missing_scan_count > 0
|
||||
ORDER BY mi.archived_at NULLS LAST, mi.updated_at DESC;
|
||||
|
||||
@@ -1898,6 +1933,24 @@ SELECT * FROM media_items WHERE file_sha256 = $1;
|
||||
-- name: GetMediaItemBySHA256AndLibrary :one
|
||||
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2;
|
||||
|
||||
-- name: GetMediaItemsBySHA256AndSameLibraryType :many
|
||||
-- Cross-library move detection: identical content registered in a different
|
||||
-- library of the same type (ebooks stay with ebooks, comics with comics).
|
||||
-- Candidates may be active copies (deliberate duplicates - leave alone) or
|
||||
-- hidden/archived rows whose file vanished (move candidates - the scanner
|
||||
-- verifies disk state before repointing).
|
||||
SELECT mi.*, lt.name AS library_type_name
|
||||
FROM media_items mi
|
||||
JOIN libraries l ON mi.library_id = l.id
|
||||
JOIN library_types lt ON l.library_type_id = lt.id
|
||||
JOIN libraries target_lib ON target_lib.id = sqlc.arg('library_id')
|
||||
WHERE mi.file_sha256 = sqlc.arg('file_sha256')
|
||||
AND mi.library_id <> sqlc.arg('library_id')
|
||||
AND lt.id = target_lib.library_type_id
|
||||
ORDER BY (mi.archived_at IS NOT NULL) DESC,
|
||||
mi.missing_scan_count DESC,
|
||||
mi.created_at ASC;
|
||||
|
||||
-- List all media items sharing a SHA-256 hash within a library (hash conflict group)
|
||||
-- name: ListMediaItemsBySHA256AndLibrary :many
|
||||
SELECT * FROM media_items WHERE file_sha256 = $1 AND library_id = $2 ORDER BY file_path;
|
||||
@@ -2143,6 +2196,8 @@ SELECT ci.*, mi.title, mi.author, mi.cover_image_path, mi.library_id
|
||||
FROM collection_items ci
|
||||
JOIN media_items mi ON ci.media_item_id = mi.id
|
||||
WHERE ci.collection_id = $1
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY ci.added_at DESC;
|
||||
|
||||
-- Get collections for book
|
||||
@@ -2449,12 +2504,16 @@ INNER JOIN (
|
||||
ORDER BY media_item_id, last_read_at DESC
|
||||
) rp ON rp.media_item_id = mi.id
|
||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY rp.last_read_at DESC
|
||||
LIMIT sqlc.narg('limit');
|
||||
|
||||
-- name: GetRecentlyAddedItems :many
|
||||
SELECT mi.* FROM media_items mi
|
||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY mi.imported_at DESC NULLS LAST, mi.created_at DESC
|
||||
LIMIT sqlc.narg('limit');
|
||||
|
||||
@@ -2468,12 +2527,16 @@ INNER JOIN (
|
||||
ORDER BY media_item_id, last_read_at DESC
|
||||
) rp ON rp.media_item_id = mi.id
|
||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY rp.last_read_at DESC
|
||||
LIMIT sqlc.narg('limit');
|
||||
|
||||
-- name: GetNotStartedItems :many
|
||||
SELECT mi.* FROM media_items mi
|
||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM reading_progress rp
|
||||
WHERE rp.media_item_id = mi.id
|
||||
@@ -2488,12 +2551,16 @@ SELECT mi.*, ci.excluded FROM media_items mi
|
||||
INNER JOIN collection_items ci ON ci.media_item_id = mi.id
|
||||
WHERE ci.collection_id = sqlc.narg('collection_id')
|
||||
AND (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY ci.added_at DESC
|
||||
LIMIT sqlc.narg('limit');
|
||||
|
||||
-- name: GetLibraryItems :many
|
||||
SELECT mi.* FROM media_items mi
|
||||
WHERE (sqlc.narg('library_id')::uuid IS NULL OR mi.library_id = sqlc.narg('library_id')::uuid)
|
||||
AND mi.archived_at IS NULL
|
||||
AND mi.missing_scan_count = 0
|
||||
ORDER BY mi.created_at DESC;
|
||||
|
||||
-- name: GetDistinctSeries :many
|
||||
|
||||
+123
-20
@@ -6,11 +6,16 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/skip2/go-qrcode"
|
||||
@@ -106,7 +111,14 @@ type PendingRegistration struct {
|
||||
SyncEndpoints map[string]string
|
||||
}
|
||||
|
||||
var pendingRegistrations = make(map[string]*PendingRegistration)
|
||||
// pendingRegistrations holds in-flight (unapproved) device registrations.
|
||||
// HTTP handlers touch it from multiple goroutines — every access must hold
|
||||
// pendingMu (Go maps are not safe for concurrent use; a racing write is a
|
||||
// runtime fatal, not an error).
|
||||
var (
|
||||
pendingRegistrations = make(map[string]*PendingRegistration)
|
||||
pendingMu sync.Mutex
|
||||
)
|
||||
|
||||
func (h *DeviceHandler) InitiateRegistration(c *echo.Context) error {
|
||||
req := DeviceRegistrationRequest{}
|
||||
@@ -130,7 +142,9 @@ func (h *DeviceHandler) InitiateRegistration(c *echo.Context) error {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
pendingMu.Lock()
|
||||
pendingRegistrations[registrationID] = registration
|
||||
pendingMu.Unlock()
|
||||
|
||||
authURL := fmt.Sprintf("%s/devices/approve/%s", h.cfg.BaseURL, registrationID)
|
||||
|
||||
@@ -167,26 +181,34 @@ func (h *DeviceHandler) CheckRegistrationStatus(c *echo.Context) error {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request format"})
|
||||
}
|
||||
|
||||
pendingMu.Lock()
|
||||
registration, exists := pendingRegistrations[req.RegistrationID]
|
||||
if !exists {
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
||||
}
|
||||
|
||||
if time.Now().After(registration.ExpiresAt) {
|
||||
delete(pendingRegistrations, req.RegistrationID)
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
|
||||
}
|
||||
|
||||
if registration.Approved {
|
||||
delete(pendingRegistrations, req.RegistrationID)
|
||||
|
||||
return c.JSON(http.StatusOK, DeviceAuthStatusResponse{
|
||||
// Snapshot under the lock: the approver wrote these fields, and
|
||||
// the row is gone from the map — no other reader/writer remains.
|
||||
resp := DeviceAuthStatusResponse{
|
||||
Status: "approved",
|
||||
AuthToken: registration.AuthToken,
|
||||
DeviceID: registration.DeviceID,
|
||||
SyncEndpoints: registration.SyncEndpoints,
|
||||
})
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, DeviceAuthStatusResponse{
|
||||
Status: "pending",
|
||||
@@ -552,17 +574,21 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
|
||||
registrationID := c.Param("registration_id")
|
||||
|
||||
pendingMu.Lock()
|
||||
registration, exists := pendingRegistrations[registrationID]
|
||||
if !exists {
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
||||
}
|
||||
|
||||
if time.Now().After(registration.ExpiresAt) {
|
||||
delete(pendingRegistrations, registrationID)
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusGone, map[string]string{"error": "registration expired"})
|
||||
}
|
||||
|
||||
if registration.Approved {
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "device already approved",
|
||||
"device_name": registration.DeviceName,
|
||||
@@ -570,6 +596,7 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
"approved": true,
|
||||
})
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
|
||||
authToken, err := generateDeviceToken()
|
||||
if err != nil {
|
||||
@@ -577,23 +604,87 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
}
|
||||
|
||||
pgUserID := pgtype.UUID{Bytes: [16]byte(userUUID), Valid: true}
|
||||
syncEnabled := pgtype.Bool{Bool: true, Valid: true}
|
||||
autoSync := pgtype.Bool{Bool: true, Valid: true}
|
||||
syncFreq := pgtype.Int4{Int32: 5, Valid: true}
|
||||
|
||||
device, err := h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{
|
||||
UserID: pgUserID,
|
||||
DeviceName: registration.DeviceName,
|
||||
DeviceType: registration.DeviceType,
|
||||
DeviceIdentifier: registration.DeviceIdentifier,
|
||||
AuthToken: authToken,
|
||||
SyncEnabled: syncEnabled,
|
||||
AutoSync: autoSync,
|
||||
SyncFrequencyMinutes: syncFreq,
|
||||
DeviceMetadata: []byte("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to create device"})
|
||||
// Re-approval: app reinstalls that preserve data (e.g. Android Studio
|
||||
// installDebug over an existing install) re-register with the SAME
|
||||
// device_identifier, but the devices row from the previous install
|
||||
// still exists — device_identifier is UNIQUE, so a blind INSERT fails
|
||||
// with a unique violation (the historical "approve returns 500 after
|
||||
// reinstall" bug). Look the device up first and rotate its token
|
||||
// instead; a fresh install MUST get fresh credentials, so the old
|
||||
// token is invalidated either way.
|
||||
rotateExisting := func(existing database.Devices) (database.Devices, bool, error) {
|
||||
// The identifier is globally unique; a row owned by another user
|
||||
// means cross-account identifier reuse — refuse it.
|
||||
if !existing.UserID.Valid || existing.UserID.Bytes != pgUserID.Bytes {
|
||||
return existing, false, nil
|
||||
}
|
||||
updated, err := h.db.UpdateDeviceAuthToken(c.Request().Context(),
|
||||
database.UpdateDeviceAuthTokenParams{
|
||||
ID: pgtype.UUID{Bytes: existing.ID.Bytes, Valid: true},
|
||||
AuthToken: authToken,
|
||||
})
|
||||
return updated, true, err
|
||||
}
|
||||
|
||||
existing, err := h.db.GetDeviceByIdentifier(c.Request().Context(),
|
||||
registration.DeviceIdentifier)
|
||||
var device database.Devices
|
||||
switch {
|
||||
case err == nil:
|
||||
// Known device: rotate the token, keep the row (id unchanged —
|
||||
// highlights/bookmarks/progress anchored to it stay valid).
|
||||
var ok bool
|
||||
device, ok, err = rotateExisting(existing)
|
||||
if err != nil {
|
||||
slog.Error("device re-approval failed",
|
||||
"registration_id", registrationID, "error", err)
|
||||
return c.JSON(http.StatusInternalServerError,
|
||||
map[string]string{"error": "failed to approve device"})
|
||||
}
|
||||
if !ok {
|
||||
return c.JSON(http.StatusConflict,
|
||||
map[string]string{"error": "device identifier already registered to another user"})
|
||||
}
|
||||
case errors.Is(err, pgx.ErrNoRows):
|
||||
device, err = h.db.CreateDevice(c.Request().Context(), database.CreateDeviceParams{
|
||||
UserID: pgUserID,
|
||||
DeviceName: registration.DeviceName,
|
||||
DeviceType: registration.DeviceType,
|
||||
DeviceIdentifier: registration.DeviceIdentifier,
|
||||
AuthToken: authToken,
|
||||
SyncEnabled: pgtype.Bool{Bool: true, Valid: true},
|
||||
AutoSync: pgtype.Bool{Bool: true, Valid: true},
|
||||
SyncFrequencyMinutes: pgtype.Int4{Int32: 5, Valid: true},
|
||||
DeviceMetadata: []byte("{}"),
|
||||
})
|
||||
if err != nil {
|
||||
// Concurrent approve racing us onto the unique index: fall
|
||||
// through to the rotate path for the winner's row.
|
||||
var pgErr *pgconn.PgError
|
||||
if asErr := errors.As(err, &pgErr); asErr && pgErr.Code == "23505" {
|
||||
if winner, lerr := h.db.GetDeviceByIdentifier(
|
||||
c.Request().Context(), registration.DeviceIdentifier); lerr == nil {
|
||||
var ok bool
|
||||
device, ok, err = rotateExisting(winner)
|
||||
if err == nil && !ok {
|
||||
return c.JSON(http.StatusConflict,
|
||||
map[string]string{"error": "device identifier already registered to another user"})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
slog.Error("device creation failed",
|
||||
"registration_id", registrationID, "error", err)
|
||||
return c.JSON(http.StatusInternalServerError,
|
||||
map[string]string{"error": "failed to create device"})
|
||||
}
|
||||
default:
|
||||
slog.Error("device lookup failed",
|
||||
"registration_id", registrationID, "error", err)
|
||||
return c.JSON(http.StatusInternalServerError,
|
||||
map[string]string{"error": "failed to look up device"})
|
||||
}
|
||||
|
||||
syncEndpoints := map[string]string{}
|
||||
@@ -607,11 +698,16 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
syncEndpoints["library"] = fmt.Sprintf("%s/api/sync/kobo/library", h.cfg.BaseURL)
|
||||
}
|
||||
|
||||
// Publish the credentials under the map lock: the status poller
|
||||
// snapshots these fields only after Approved flips, so the token can
|
||||
// never be read half-written.
|
||||
pendingMu.Lock()
|
||||
registration.UserID = userUUID
|
||||
registration.Approved = true
|
||||
registration.AuthToken = authToken
|
||||
registration.DeviceID = device.ID.Bytes
|
||||
registration.SyncEndpoints = syncEndpoints
|
||||
pendingMu.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"message": "device approved successfully",
|
||||
@@ -625,12 +721,15 @@ func (h *DeviceHandler) ApproveDevice(c *echo.Context) error {
|
||||
func (h *DeviceHandler) RejectDevice(c *echo.Context) error {
|
||||
registrationID := c.Param("registration_id")
|
||||
|
||||
pendingMu.Lock()
|
||||
_, exists := pendingRegistrations[registrationID]
|
||||
if !exists {
|
||||
pendingMu.Unlock()
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "registration not found"})
|
||||
}
|
||||
|
||||
delete(pendingRegistrations, registrationID)
|
||||
pendingMu.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"message": "device registration rejected",
|
||||
@@ -639,6 +738,7 @@ func (h *DeviceHandler) RejectDevice(c *echo.Context) error {
|
||||
|
||||
func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[string]interface{}, error) {
|
||||
registrations := []map[string]interface{}{}
|
||||
pendingMu.Lock()
|
||||
for _, reg := range pendingRegistrations {
|
||||
if reg.UserID == (uuid.UUID{}) {
|
||||
registrations = append(registrations, map[string]interface{}{
|
||||
@@ -652,6 +752,7 @@ func (h *DeviceHandler) GetPendingRegistrationsData(c *echo.Context) ([]map[stri
|
||||
})
|
||||
}
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
|
||||
return registrations, nil
|
||||
}
|
||||
@@ -664,6 +765,7 @@ func (h *DeviceHandler) ListPendingRegistrations(c *echo.Context) error {
|
||||
}
|
||||
|
||||
registrations := []map[string]interface{}{}
|
||||
pendingMu.Lock()
|
||||
for _, reg := range pendingRegistrations {
|
||||
if reg.UserID == userUUID || reg.UserID == (uuid.UUID{}) {
|
||||
registrations = append(registrations, map[string]interface{}{
|
||||
@@ -677,6 +779,7 @@ func (h *DeviceHandler) ListPendingRegistrations(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
pendingMu.Unlock()
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"registrations": registrations,
|
||||
|
||||
@@ -953,6 +953,37 @@ func (mh *MediaHandler) GetMediaReadingProgress(c *echo.Context) error {
|
||||
"last_sync_timestamp": progress.LastSyncTimestamp,
|
||||
}
|
||||
|
||||
// Position authority: re-verify the stored anchor and derive the
|
||||
// client-facing handles (cssSelector + anchor document href) server-
|
||||
// side, so clients scroll to an address instead of parsing CFIs.
|
||||
if progress.Epubcfi.Valid && wsync.IsConvertibleFormat(progress.FormatGroup) && mh.libraryService != nil {
|
||||
contextText := ""
|
||||
if progress.ContextText.Valid {
|
||||
contextText = progress.ContextText.String
|
||||
}
|
||||
pct := 0.0
|
||||
if progress.Percentage.Valid {
|
||||
pct = progress.Percentage.Float64
|
||||
}
|
||||
if mediaItem, err := mh.db.GetMediaItem(c.Request().Context(), pgtype.UUID{Bytes: mediaUUID, Valid: true}); err == nil {
|
||||
if path, perr := mh.libraryService.ResolveMediaPath(c.Request().Context(), mediaItem.LibraryID, mediaItem.FilePath); perr == nil && path != "" {
|
||||
if finalCFI, sel, href, charOff, _, _, _, verr := wsync.VerifyProgressAnchor(path, progress.Epubcfi.String, contextText, pct); verr == nil && sel != "" {
|
||||
resp["css_selector"] = sel
|
||||
resp["anchor_href"] = href
|
||||
if charOff != nil {
|
||||
resp["char_offset"] = *charOff
|
||||
}
|
||||
if finalCFI != "" {
|
||||
resp["epubcfi"] = finalCFI
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if progress.ContextText.Valid {
|
||||
resp["context_text"] = progress.ContextText.String
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -1354,6 +1385,83 @@ func (mh *MediaHandler) UnarchiveMediaItem(c *echo.Context) error {
|
||||
return c.NoContent(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// MergeArchivedMediaItem handles POST /api/media-items/:id/merge (admin
|
||||
// only). Merges an archived/missing item's reading data (progress,
|
||||
// annotations, highlights, notes, bookmarks, ratings, collections, kobo
|
||||
// shelves) into an active item with identical content (same SHA-256), then
|
||||
// deletes the archived row. Covers the copy-then-delete-later workflow: the
|
||||
// active copy exists, the archived twin holds the real reading history.
|
||||
// Per-user collisions keep the active copy's data, mirroring hash-conflict
|
||||
// resolution.
|
||||
func (mh *MediaHandler) MergeArchivedMediaItem(c *echo.Context) error {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
|
||||
if user.Role != "admin" {
|
||||
return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
|
||||
}
|
||||
|
||||
sourceUUID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid media item id"})
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TargetID string `json:"target_id"`
|
||||
}
|
||||
if err := c.Bind(&req); err != nil || req.TargetID == "" {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "target_id is required"})
|
||||
}
|
||||
targetUUID, err := uuid.Parse(req.TargetID)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid target_id"})
|
||||
}
|
||||
if sourceUUID == targetUUID {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "cannot merge an item into itself"})
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
source, err := mh.db.GetMediaItem(ctx, pgtype.UUID{Bytes: sourceUUID, Valid: true})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "archived item not found"})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if !source.ArchivedAt.Valid && source.MissingScanCount == 0 {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "source item is not archived or missing"})
|
||||
}
|
||||
|
||||
target, err := mh.db.GetMediaItem(ctx, pgtype.UUID{Bytes: targetUUID, Valid: true})
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return c.JSON(http.StatusNotFound, map[string]string{"error": "target item not found"})
|
||||
}
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
if target.ArchivedAt.Valid || target.MissingScanCount > 0 {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "target item is not active"})
|
||||
}
|
||||
|
||||
if !source.FileSha256.Valid || source.FileSha256.String == "" ||
|
||||
source.FileSha256.String != target.FileSha256.String {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "items do not share the same content hash"})
|
||||
}
|
||||
|
||||
if err := mh.db.ReparentMediaItemChildren(ctx, database.ReparentMediaItemChildrenParams{
|
||||
Column1: pgtype.UUID{Bytes: targetUUID, Valid: true},
|
||||
Column2: pgtype.UUID{Bytes: sourceUUID, Valid: true},
|
||||
}); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to merge reading data: " + err.Error()})
|
||||
}
|
||||
if err := mh.db.DeleteMediaItem(ctx, pgtype.UUID{Bytes: sourceUUID, Valid: true}); err != nil {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "failed to remove archived item: " + err.Error()})
|
||||
}
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]string{
|
||||
"message": "Merged reading progress, annotations, and collections into the active item",
|
||||
})
|
||||
}
|
||||
|
||||
// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
|
||||
// (admin only). Hard-deletes every archived item (files missing from disk for
|
||||
// 2+ scans) together with its reading history. The archive retention window
|
||||
|
||||
+288
-11
@@ -90,6 +90,9 @@ func appendToken(url, token string) string {
|
||||
// catalogMediaType is the OPDS media type for an acquisition catalog feed.
|
||||
const catalogMediaType = "application/atom+xml;profile=opds-catalog;kind=acquisition"
|
||||
|
||||
// navFeedMediaType is the OPDS media type for a navigation catalog feed.
|
||||
const navFeedMediaType = "application/atom+xml;profile=opds-catalog;kind=navigation"
|
||||
|
||||
// addCatalogPaginationLinks adds OPDS pagination links (self, start, first,
|
||||
// previous, next, last) and OpenSearch paging metadata (totalResults,
|
||||
// itemsPerPage, startIndex) to a feed based on the current page position.
|
||||
@@ -172,8 +175,109 @@ func formatLabelFromPath(path string) string {
|
||||
}
|
||||
}
|
||||
|
||||
// GetDeviceCatalog returns the OPDS catalog feed for a device
|
||||
// GetDeviceCatalog serves the OPDS root feed for a device.
|
||||
//
|
||||
// By default it returns a navigation feed: an "All Books" entry (the flat
|
||||
// cross-library catalog) followed by one folder entry per visible library.
|
||||
// OPDS clients render navigation entries as folders, so each library's
|
||||
// books — and its library-scoped search — live under their own node instead
|
||||
// of interleaving duplicate copies of the same book in one flat list.
|
||||
// Passing ?all=1 serves the flat cross-library acquisition feed directly,
|
||||
// preserving the pre-folders behavior.
|
||||
func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
if c.QueryParam("all") == "1" {
|
||||
return h.serveAllBooksCatalog(c)
|
||||
}
|
||||
|
||||
deviceID := c.Param("deviceId")
|
||||
|
||||
// Get base URLs
|
||||
_, opdsBaseURL, err := h.getBaseURLs(c)
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
|
||||
}
|
||||
|
||||
// Parse device ID
|
||||
deviceUUID, err := uuid.Parse(deviceID)
|
||||
if err != nil {
|
||||
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
|
||||
}
|
||||
|
||||
// Verify device exists
|
||||
device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
|
||||
}
|
||||
|
||||
// Get user's visible libraries
|
||||
userID := device.UserID.Bytes
|
||||
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
|
||||
}
|
||||
|
||||
token := h.getAuthToken(c)
|
||||
|
||||
feed := opds.NewFeed(
|
||||
fmt.Sprintf("urn:uuid:%s", deviceID),
|
||||
"Bookhoard Library",
|
||||
)
|
||||
|
||||
// OpenSearch discovery: the root search stays global (all libraries).
|
||||
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID), token)
|
||||
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
|
||||
|
||||
updated := time.Now().UTC().Format("2006-01-02T15:04:05Z")
|
||||
|
||||
// "All Books" first: the flat cross-library catalog (?all=1).
|
||||
allBooks := opds.NewEntry(
|
||||
fmt.Sprintf("urn:uuid:%s-all-books", deviceID),
|
||||
"All Books",
|
||||
"",
|
||||
updated,
|
||||
)
|
||||
allBooks.AddLink(appendToken(fmt.Sprintf("%s/devices/%s/catalog?all=1", opdsBaseURL, deviceID), token), catalogMediaType, "subsection")
|
||||
allBooks.SetSummary("Browse every book across all your libraries")
|
||||
feed.AddEntry(allBooks)
|
||||
|
||||
// One folder per visible library, with live book counts.
|
||||
counts := make(map[pgtype.UUID]int64)
|
||||
if countRows, cerr := h.db.GetVisibleLibraryMediaCounts(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true}); cerr == nil {
|
||||
for _, row := range countRows {
|
||||
counts[row.ID] = row.MediaCount
|
||||
}
|
||||
}
|
||||
|
||||
for _, lib := range libraries {
|
||||
libUUID := uuid.UUID(lib.ID.Bytes).String()
|
||||
title := lib.Name
|
||||
if n, ok := counts[lib.ID]; ok {
|
||||
title = fmt.Sprintf("%s (%d)", lib.Name, n)
|
||||
}
|
||||
entry := opds.NewEntry(
|
||||
fmt.Sprintf("urn:uuid:%s-library-%s", deviceID, libUUID),
|
||||
title,
|
||||
"",
|
||||
updated,
|
||||
)
|
||||
entry.AddLink(appendToken(fmt.Sprintf("%s/devices/%s/library/%s/catalog", opdsBaseURL, deviceID, libUUID), token), catalogMediaType, "subsection")
|
||||
entry.SetSummary("Browse books in the " + lib.Name + " library")
|
||||
feed.AddEntry(entry)
|
||||
}
|
||||
|
||||
// Generate XML
|
||||
xmlString, err := feed.GenerateXMLString()
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Content-Type", navFeedMediaType)
|
||||
return c.String(http.StatusOK, xmlString)
|
||||
}
|
||||
|
||||
// serveAllBooksCatalog is the original flat cross-library acquisition feed:
|
||||
// every book from every visible library, paginated as one list.
|
||||
func (h *OPDSHandler) serveAllBooksCatalog(c *echo.Context) error {
|
||||
deviceID := c.Param("deviceId")
|
||||
|
||||
page := c.QueryParam("page")
|
||||
@@ -216,8 +320,6 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
|
||||
// Get user's visible libraries
|
||||
userID := device.UserID.Bytes
|
||||
userUUID := uuid.UUID(userID)
|
||||
|
||||
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
|
||||
@@ -276,7 +378,23 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
|
||||
|
||||
// Add entries
|
||||
for _, item := range allItems {
|
||||
h.addAcquisitionEntries(c.Request().Context(), feed, device, deviceID, allItems, baseURL, opdsBaseURL, token)
|
||||
|
||||
// Generate XML
|
||||
xmlString, err := feed.GenerateXMLString()
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Content-Type", catalogMediaType)
|
||||
return c.String(http.StatusOK, xmlString)
|
||||
}
|
||||
|
||||
// addAcquisitionEntries fills an acquisition feed with book entries for the
|
||||
// given items. Shared body of the all-books and per-library catalogs.
|
||||
func (h *OPDSHandler) addAcquisitionEntries(ctx context.Context, feed *opds.Feed, device database.Devices, deviceID string, items []database.ListMediaItemsByLibraryRow, baseURL, opdsBaseURL, token string) {
|
||||
userUUID := uuid.UUID(device.UserID.Bytes)
|
||||
for _, item := range items {
|
||||
bookUUID := uuid.UUID(item.ID.Bytes).String()
|
||||
title := item.Title
|
||||
author := ""
|
||||
@@ -323,7 +441,7 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
}
|
||||
|
||||
// Add collections as categories
|
||||
collections, err := h.db.GetCollectionsForBook(c.Request().Context(), pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
|
||||
collections, err := h.db.GetCollectionsForBook(ctx, pgtype.UUID{Bytes: item.ID.Bytes, Valid: true})
|
||||
if err == nil {
|
||||
collectionScheme := fmt.Sprintf("%s/collections", baseURL)
|
||||
for _, col := range collections {
|
||||
@@ -335,6 +453,125 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
|
||||
feed.AddEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// GetLibraryCatalog serves an acquisition feed scoped to one of the device
|
||||
// owner's visible libraries. Its search link is library-scoped: OPDS clients
|
||||
// only substitute {searchTerms}, so opening search from inside this folder
|
||||
// searches within this library only.
|
||||
func (h *OPDSHandler) GetLibraryCatalog(c *echo.Context) error {
|
||||
deviceID := c.Param("deviceId")
|
||||
libraryID := c.Param("libraryId")
|
||||
|
||||
page := c.QueryParam("page")
|
||||
perPage := c.QueryParam("per_page")
|
||||
includeFormat := c.QueryParam("include_format")
|
||||
|
||||
// Parse pagination parameters
|
||||
pageNum := 1
|
||||
if page != "" {
|
||||
if num, err := strconv.Atoi(page); err == nil && num > 0 {
|
||||
pageNum = num
|
||||
}
|
||||
}
|
||||
|
||||
perPageNum := h.opdsDefaultPageSize()
|
||||
maxPerPage := h.opdsMaxPageSize()
|
||||
if perPage != "" {
|
||||
if num, err := strconv.Atoi(perPage); err == nil && num > 0 && num <= maxPerPage {
|
||||
perPageNum = num
|
||||
}
|
||||
}
|
||||
|
||||
// Get base URLs
|
||||
baseURL, opdsBaseURL, err := h.getBaseURLs(c)
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get config"))
|
||||
}
|
||||
|
||||
// Parse IDs
|
||||
deviceUUID, err := uuid.Parse(deviceID)
|
||||
if err != nil {
|
||||
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid device ID"))
|
||||
}
|
||||
libUUID, err := uuid.Parse(libraryID)
|
||||
if err != nil {
|
||||
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid library ID"))
|
||||
}
|
||||
|
||||
// Verify device exists
|
||||
device, err := h.db.GetDevice(c.Request().Context(), pgtype.UUID{Bytes: deviceUUID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusNotFound, opds.NewErrorFeed("Device not found"))
|
||||
}
|
||||
|
||||
// The library must be visible to the device owner.
|
||||
userID := device.UserID.Bytes
|
||||
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
|
||||
}
|
||||
var library *database.GetUserVisibleLibrariesRow
|
||||
for i := range libraries {
|
||||
if libraries[i].ID.Bytes == libUUID {
|
||||
library = &libraries[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
if library == nil {
|
||||
return c.XML(http.StatusForbidden, opds.NewErrorFeed("Library not accessible"))
|
||||
}
|
||||
|
||||
// Get media items
|
||||
items, err := h.db.ListMediaItemsByLibrary(c.Request().Context(), library.ID)
|
||||
if err != nil {
|
||||
items = []database.ListMediaItemsByLibraryRow{}
|
||||
}
|
||||
|
||||
// Filter by format if specified
|
||||
if includeFormat != "" {
|
||||
filteredItems := []database.ListMediaItemsByLibraryRow{}
|
||||
for _, item := range items {
|
||||
if strings.EqualFold(item.FormatGroup, includeFormat) {
|
||||
filteredItems = append(filteredItems, item)
|
||||
}
|
||||
}
|
||||
items = filteredItems
|
||||
}
|
||||
|
||||
// Pagination
|
||||
totalItems := len(items)
|
||||
startIdx := (pageNum - 1) * perPageNum
|
||||
endIdx := startIdx + perPageNum
|
||||
|
||||
if startIdx >= totalItems {
|
||||
items = []database.ListMediaItemsByLibraryRow{}
|
||||
} else if endIdx > totalItems {
|
||||
items = items[startIdx:]
|
||||
} else {
|
||||
items = items[startIdx:endIdx]
|
||||
}
|
||||
|
||||
// Create OPDS feed
|
||||
feed := opds.NewFeed(
|
||||
fmt.Sprintf("urn:uuid:%s-library-%s", deviceID, libUUID),
|
||||
library.Name,
|
||||
)
|
||||
|
||||
token := h.getAuthToken(c)
|
||||
catalogBase := fmt.Sprintf("%s/devices/%s/library/%s/catalog", opdsBaseURL, deviceID, libUUID)
|
||||
addCatalogPaginationLinks(feed, catalogBase, pageNum, perPageNum, totalItems, token)
|
||||
|
||||
// Library-scoped search discovery: the template pins this library.
|
||||
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}&library_id=%s", opdsBaseURL, deviceID, libUUID), token)
|
||||
feed.AddLink(searchURL, "application/opensearchdescription+xml", "search")
|
||||
|
||||
// Up-link back to the root navigation feed.
|
||||
rootURL := appendToken(fmt.Sprintf("%s/devices/%s/catalog", opdsBaseURL, deviceID), token)
|
||||
feed.AddLink(rootURL, navFeedMediaType, "up")
|
||||
|
||||
// Add entries
|
||||
h.addAcquisitionEntries(c.Request().Context(), feed, device, deviceID, items, baseURL, opdsBaseURL, token)
|
||||
|
||||
// Generate XML
|
||||
xmlString, err := feed.GenerateXMLString()
|
||||
@@ -342,7 +579,7 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to generate feed"))
|
||||
}
|
||||
|
||||
c.Response().Header().Set("Content-Type", "application/atom+xml;profile=opds-catalog;kind=acquisition")
|
||||
c.Response().Header().Set("Content-Type", catalogMediaType)
|
||||
return c.String(http.StatusOK, xmlString)
|
||||
}
|
||||
|
||||
@@ -377,20 +614,50 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
|
||||
|
||||
// Get user's visible libraries
|
||||
userID := device.UserID.Bytes
|
||||
_, err = h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
|
||||
libraries, err := h.db.GetUserVisibleLibraries(c.Request().Context(), pgtype.UUID{Bytes: userID, Valid: true})
|
||||
if err != nil {
|
||||
return c.XML(http.StatusInternalServerError, opds.NewErrorFeed("Failed to get libraries"))
|
||||
}
|
||||
|
||||
// Optional library scope: library folder feeds advertise their own search
|
||||
// link with this parameter, so opening search from inside a library
|
||||
// folder searches within that library only.
|
||||
var scopedLibraryID pgtype.UUID
|
||||
scopedLibraryName := ""
|
||||
if libParam := c.QueryParam("library_id"); libParam != "" {
|
||||
parsed, perr := uuid.Parse(libParam)
|
||||
if perr != nil {
|
||||
return c.XML(http.StatusBadRequest, opds.NewErrorFeed("Invalid library ID"))
|
||||
}
|
||||
for i := range libraries {
|
||||
if libraries[i].ID.Bytes == parsed {
|
||||
scopedLibraryID = libraries[i].ID
|
||||
scopedLibraryName = libraries[i].Name
|
||||
break
|
||||
}
|
||||
}
|
||||
if !scopedLibraryID.Valid {
|
||||
return c.XML(http.StatusForbidden, opds.NewErrorFeed("Library not accessible"))
|
||||
}
|
||||
}
|
||||
|
||||
token := h.getAuthToken(c)
|
||||
|
||||
// No query: serve the OpenSearch description document so clients can learn
|
||||
// the search template (contains the {searchTerms} placeholder).
|
||||
// the search URL template (contains the {searchTerms} placeholder).
|
||||
if query == "" {
|
||||
searchURL := appendToken(fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID), token)
|
||||
template := fmt.Sprintf("%s/devices/%s/search?q={searchTerms}", opdsBaseURL, deviceID)
|
||||
shortName := "Bookhoard"
|
||||
description := "Search the Bookhoard library"
|
||||
if scopedLibraryID.Valid {
|
||||
template += "&library_id=" + scopedLibraryID.String()
|
||||
shortName = "Bookhoard: " + scopedLibraryName
|
||||
description = "Search the " + scopedLibraryName + " library"
|
||||
}
|
||||
searchURL := appendToken(template, token)
|
||||
desc := opds.NewSearchDescription(
|
||||
"Bookhoard",
|
||||
"Search the Bookhoard library",
|
||||
shortName,
|
||||
description,
|
||||
searchURL,
|
||||
)
|
||||
xmlString, err := desc.GenerateXMLString()
|
||||
@@ -404,6 +671,7 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
|
||||
// Search media items
|
||||
allItems, err := h.db.SearchMediaItems(c.Request().Context(), database.SearchMediaItemsParams{
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
LibraryID: scopedLibraryID,
|
||||
SearchPattern: pgtype.Text{String: "%" + query + "%", Valid: true},
|
||||
Offset: pgtype.Int4{Int32: 0, Valid: true},
|
||||
Limit: pgtype.Int4{Int32: 1000, Valid: true},
|
||||
@@ -451,6 +719,15 @@ func (h *OPDSHandler) SearchDeviceCatalog(c *echo.Context) error {
|
||||
entry.SetSummary(item.Description.String)
|
||||
}
|
||||
|
||||
// Global (unscoped) search: label which library each copy lives in so
|
||||
// duplicate copies of the same book are distinguishable.
|
||||
if !scopedLibraryID.Valid && item.LibraryName != "" {
|
||||
entry.AddCategory(baseURL+"/libraries", item.LibraryName)
|
||||
if !item.Description.Valid || item.Description.String == "" {
|
||||
entry.SetSummary("Library: " + item.LibraryName)
|
||||
}
|
||||
}
|
||||
|
||||
downloadURL := appendToken(fmt.Sprintf("%s/devices/%s/download/%s", opdsBaseURL, deviceID, bookUUID), token)
|
||||
entry.AddAcquisitionLink(downloadURL, resolveMimeType(item.MimeType, item.FormatMimetype))
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
|
||||
|
||||
// Determine if panel detection should be enabled
|
||||
enablePanelDetection := shouldEnablePanelDetection(libraryWithType.TypeName, mediaItem.FormatGroup)
|
||||
treatAsComic := ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup)
|
||||
|
||||
// Get reading progress
|
||||
var progress database.ReadingProgress
|
||||
@@ -133,6 +134,7 @@ func (h *ReaderHandler) ShowReader(c *echo.Context) error {
|
||||
"progress": progress,
|
||||
"bookmarks": bookmarks,
|
||||
"enable_panel_detection": enablePanelDetection,
|
||||
"treat_as_comic": treatAsComic,
|
||||
"format_group": mediaItem.FormatGroup,
|
||||
"manga_type": mediaItem.MangaType,
|
||||
"reading_direction": mediaItem.ReadingDirection,
|
||||
@@ -639,6 +641,25 @@ func (h *ReaderHandler) ParseEbook(c *echo.Context) error {
|
||||
})
|
||||
}
|
||||
|
||||
// ShouldTreatAsComic reports whether a fixed-layout item that lives in a
|
||||
// comics or manga library should get comic reader treatment: bookmarks
|
||||
// only — no text-selection highlights, no annotations, no in-book search.
|
||||
// Comic archives already behave this way; this flag extends it to fixed-
|
||||
// layout PDFs shelved in comic libraries (scanned or official manga PDFs).
|
||||
func ShouldTreatAsComic(libraryType string, formatGroup string) bool {
|
||||
comicLibraries := map[string]bool{
|
||||
"manga": true,
|
||||
"comics": true,
|
||||
}
|
||||
|
||||
fixedFormats := map[string]bool{
|
||||
"fixed_layout": true,
|
||||
"comic_archive": true,
|
||||
}
|
||||
|
||||
return comicLibraries[libraryType] && fixedFormats[formatGroup]
|
||||
}
|
||||
|
||||
// shouldEnablePanelDetection determines if panel detection should be enabled
|
||||
// based on library type and format group
|
||||
func shouldEnablePanelDetection(libraryType string, formatGroup string) bool {
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/internal/services"
|
||||
"bookhoard/internal/utils"
|
||||
"bookhoard/internal/version"
|
||||
"bookhoard/templates"
|
||||
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
@@ -873,6 +874,11 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
FilePath: row.FilePath,
|
||||
MissingScans: row.MissingScanCount,
|
||||
}
|
||||
if row.MatchID.Valid {
|
||||
item.MatchID = uuid.UUID(row.MatchID.Bytes).String()
|
||||
item.MatchTitle = row.MatchTitle
|
||||
item.MatchLibraryName = getText(row.MatchLibraryName)
|
||||
}
|
||||
if row.ArchivedAt.Valid {
|
||||
archived := row.ArchivedAt.Time
|
||||
item.ArchivedAt = &archived
|
||||
@@ -1450,6 +1456,7 @@ func (cfg *Config) GetHealth(c *echo.Context) error {
|
||||
|
||||
return c.JSON(http.StatusOK, map[string]interface{}{
|
||||
"status": "healthy",
|
||||
"version": version.Version,
|
||||
"database": "connected",
|
||||
"scan": map[string]interface{}{
|
||||
"scan_in_progress": cfg.Worker.HasActiveScans(),
|
||||
|
||||
@@ -64,6 +64,7 @@ func registerMediaRoutes(cfg *Config) {
|
||||
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
|
||||
admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
|
||||
admin.POST("/media-items/:id/unarchive", cfg.MediaHandler.UnarchiveMediaItem)
|
||||
admin.POST("/media-items/:id/merge", cfg.MediaHandler.MergeArchivedMediaItem)
|
||||
|
||||
// Shelf management (protected)
|
||||
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
|
||||
|
||||
@@ -19,6 +19,7 @@ func registerOPDSRoutes(cfg *Config) {
|
||||
opds := e.Group("/opds/devices")
|
||||
opds.Use(cfg.DeviceAuthMiddleware.Authenticate)
|
||||
opds.GET("/:deviceId/catalog", cfg.OPDSHandler.GetDeviceCatalog)
|
||||
opds.GET("/:deviceId/library/:libraryId/catalog", cfg.OPDSHandler.GetLibraryCatalog)
|
||||
opds.GET("/:deviceId/search", cfg.OPDSHandler.SearchDeviceCatalog)
|
||||
opds.GET("/:deviceId/nav", cfg.OPDSHandler.GetDeviceNavigation)
|
||||
opds.GET("/:deviceId/download/:bookId", cfg.OPDSHandler.DownloadBook)
|
||||
|
||||
@@ -66,6 +66,13 @@ func registerReaderRoutes(cfg *Config) {
|
||||
if !visible {
|
||||
return renderErrorPage(c, "Access denied", "access_denied")
|
||||
}
|
||||
// Library type decides comic reader treatment: fixed-layout items
|
||||
// (e.g. manga PDFs) shelved in comics/manga libraries are paged
|
||||
// through like comics — bookmarks only, no highlights or search.
|
||||
libraryWithType, err := cfg.Queries.GetLibraryWithType(c.Request().Context(), mediaItem.LibraryID)
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Failed to get library info", "library_error")
|
||||
}
|
||||
// Convert to template types. Reading state is deliberately NOT
|
||||
// fetched or embedded: the reader pulls position, bookmarks, and
|
||||
// annotations from the APIs at open time so the page can never
|
||||
@@ -89,6 +96,7 @@ func registerReaderRoutes(cfg *Config) {
|
||||
FileURL: utils.ResolveMediaURL(mediaItem.LibraryID, pgtype.Text{String: mediaItem.FilePath, Valid: true}),
|
||||
TotalCharacters: mediaItem.TotalCharacters.Int64,
|
||||
EstimatedPages: sync.EstimatedPages(mediaItem.TotalCharacters.Int64),
|
||||
TreatAsComic: handlers.ShouldTreatAsComic(libraryWithType.TypeName, mediaItem.FormatGroup),
|
||||
}
|
||||
// Render template
|
||||
var buf bytes.Buffer
|
||||
|
||||
@@ -848,6 +848,17 @@ func (s *MediaScanner) processMediaFile(ctx context.Context, path string) (bool,
|
||||
}
|
||||
}
|
||||
|
||||
// Cross-library move detection: the file may have been moved into a
|
||||
// different library of the same type (e.g. reorganizing ebooks into a
|
||||
// new library). If identical content is registered elsewhere and the old
|
||||
// file is gone from disk, repoint that row so reading history follows
|
||||
// the book instead of creating a duplicate.
|
||||
if hashInfo.FileSHA256 != "" {
|
||||
if handled := s.detectCrossLibraryMove(ctx, hashInfo.FileSHA256, libraryID, path, info.Size()); handled {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// REMOVED: Comic metadata extraction now handled by mergeMetadata()
|
||||
// This avoids duplicate extraction and ensures smart merging happens
|
||||
|
||||
@@ -1741,6 +1752,136 @@ func (s *MediaScanner) DetectFixedLayoutEPUB(epubPath string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// moveCandidate is the subset of a cross-library hash-match row the scanner
|
||||
// needs to decide whether a file is a move or a deliberate copy.
|
||||
type moveCandidate struct {
|
||||
ID pgtype.UUID
|
||||
LibraryID pgtype.UUID
|
||||
FilePath string
|
||||
Title string
|
||||
Archived bool
|
||||
MissingScans int32
|
||||
}
|
||||
|
||||
// selectMoveCandidate returns the first candidate whose file has vanished
|
||||
// from its library folders. Candidates arrive pre-ordered (archived first,
|
||||
// then most missing scans, then oldest), so the first missing one is the
|
||||
// best repoint target. Candidates whose file still exists are deliberate
|
||||
// copies in another library and are left untouched. Returns ok=false when
|
||||
// every candidate is still on disk.
|
||||
func selectMoveCandidate(candidates []moveCandidate, fileExists func(moveCandidate) bool) (moveCandidate, bool) {
|
||||
for _, candidate := range candidates {
|
||||
if !fileExists(candidate) {
|
||||
return candidate, true
|
||||
}
|
||||
}
|
||||
return moveCandidate{}, false
|
||||
}
|
||||
|
||||
// detectCrossLibraryMove repoints an existing row from another same-type
|
||||
// library when this file is that row's content at a new location and the old
|
||||
// file is gone from disk. Returns true when the file was fully handled as a
|
||||
// cross-library move (or rejected as type-incompatible) and must not be
|
||||
// imported again by the caller.
|
||||
func (s *MediaScanner) detectCrossLibraryMove(ctx context.Context, sha string, libraryID pgtype.UUID, path string, size int64) bool {
|
||||
rows, err := s.db.GetMediaItemsBySHA256AndSameLibraryType(ctx, database.GetMediaItemsBySHA256AndSameLibraryTypeParams{
|
||||
FileSha256: pgtype.Text{String: sha, Valid: true},
|
||||
LibraryID: libraryID,
|
||||
})
|
||||
if err != nil {
|
||||
if !errors.Is(err, pgx.ErrNoRows) {
|
||||
fmt.Printf("Warning: cross-library SHA lookup failed for %s: %v\n", path, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
candidates := make([]moveCandidate, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
candidates = append(candidates, moveCandidate{
|
||||
ID: row.ID,
|
||||
LibraryID: row.LibraryID,
|
||||
FilePath: row.FilePath,
|
||||
Title: row.Title,
|
||||
Archived: row.ArchivedAt.Valid,
|
||||
MissingScans: row.MissingScanCount,
|
||||
})
|
||||
}
|
||||
|
||||
candidate, ok := selectMoveCandidate(candidates, func(c moveCandidate) bool {
|
||||
return s.mediaItemFileExists(ctx, c.LibraryID, c.FilePath)
|
||||
})
|
||||
if !ok {
|
||||
// Every registered copy of this content still lives in its own
|
||||
// library: a deliberate multi-library import, not a move.
|
||||
return false
|
||||
}
|
||||
|
||||
return s.repointCrossLibraryMove(ctx, candidate, libraryID, path, size)
|
||||
}
|
||||
|
||||
// mediaItemFileExists resolves a media item's relative path against its own
|
||||
// library's folders and reports whether the file is present on disk.
|
||||
func (s *MediaScanner) mediaItemFileExists(ctx context.Context, libraryID pgtype.UUID, relativePath string) bool {
|
||||
folders, err := s.db.GetLibraryFolders(ctx, libraryID)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, folder := range folders {
|
||||
if _, statErr := os.Stat(filepath.Join(folder.FolderPath, relativePath)); statErr == nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// repointCrossLibraryMove validates the target library's type rules, then
|
||||
// moves the candidate row into the target library at its new path. Reading
|
||||
// history, annotations and collections follow automatically because the row
|
||||
// keeps its ID. Returns true when the file was handled (moved or rejected)
|
||||
// and must not be imported again.
|
||||
func (s *MediaScanner) repointCrossLibraryMove(ctx context.Context, candidate moveCandidate, libraryID pgtype.UUID, path string, size int64) bool {
|
||||
item, err := s.db.GetMediaItem(ctx, candidate.ID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to load cross-library move candidate %q: %v\n", candidate.Title, err)
|
||||
return false
|
||||
}
|
||||
|
||||
targetLib, err := s.db.GetLibraryWithType(ctx, libraryID)
|
||||
if err != nil {
|
||||
fmt.Printf("Warning: failed to load target library for cross-library move of %q: %v\n", candidate.Title, err)
|
||||
return false
|
||||
}
|
||||
|
||||
// Respect the target library's type rules: a reflowable EPUB must not
|
||||
// silently land in a manga library, mirroring import validation.
|
||||
if issue := s.ValidateMediaItemForLibrary(ctx, item, targetLib); issue != nil {
|
||||
fmt.Printf("[MOVE] Rejected cross-library move of %q: %s\n", candidate.Title, *issue)
|
||||
s.logger.LogDelete(fmt.Sprintf("[MOVE] Rejected moving item '%s' into library %s: %s", candidate.Title, libraryID, *issue))
|
||||
if logErr := s.LogProcessingIssue(ctx, uuid.UUID(candidate.ID.Bytes), uuid.UUID(libraryID.Bytes),
|
||||
"library_type_mismatch", *issue, "warning"); logErr != nil {
|
||||
fmt.Printf("Warning: failed to record processing issue for %q: %v\n", candidate.Title, logErr)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if err := s.db.MoveMediaItemToLibrary(ctx, database.MoveMediaItemToLibraryParams{
|
||||
ID: candidate.ID,
|
||||
LibraryID: libraryID,
|
||||
FilePath: s.getRelativePath(path),
|
||||
FileSize: pgtype.Int8{Int64: size, Valid: true},
|
||||
}); err != nil {
|
||||
fmt.Printf("Warning: failed to repoint cross-library move %q -> %s: %v\n", candidate.FilePath, path, err)
|
||||
return false
|
||||
}
|
||||
|
||||
fmt.Printf("[MOVE] Cross-library move: %q -> %s (history preserved)\n", candidate.FilePath, path)
|
||||
s.logger.LogDelete(fmt.Sprintf("[MOVE] Repointed item '%s' from %q to %s (library changed)", candidate.Title, candidate.FilePath, path))
|
||||
return true
|
||||
}
|
||||
|
||||
// ValidateMediaItemForLibrary checks if item matches library type expectations
|
||||
// and returns issue description if validation fails, nil if valid
|
||||
func (s *MediaScanner) ValidateMediaItemForLibrary(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func mustUUID(t *testing.T, raw string) pgtype.UUID {
|
||||
t.Helper()
|
||||
parsed, err := uuid.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("invalid test uuid %q: %v", raw, err)
|
||||
}
|
||||
return pgtype.UUID{Bytes: parsed, Valid: true}
|
||||
}
|
||||
|
||||
// alwaysExists simulates every candidate's file still being on disk:
|
||||
// deliberate copies across libraries must never be repointed.
|
||||
func TestSelectMoveCandidateAllFilesExist(t *testing.T) {
|
||||
candidates := []moveCandidate{
|
||||
{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "a/book.epub"},
|
||||
{ID: mustUUID(t, "00000000-0000-0000-0000-000000000002"), FilePath: "b/book.epub"},
|
||||
}
|
||||
|
||||
if _, ok := selectMoveCandidate(candidates, func(moveCandidate) bool { return true }); ok {
|
||||
t.Fatal("expected no move candidate when every file still exists")
|
||||
}
|
||||
}
|
||||
|
||||
// Input arrives pre-ordered (archived first, then most missing scans, then
|
||||
// oldest), so the first missing candidate must win.
|
||||
func TestSelectMoveCandidatePicksFirstMissing(t *testing.T) {
|
||||
onDisk := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "a/book.epub"}
|
||||
gone := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000002"), FilePath: "b/book.epub"}
|
||||
candidates := []moveCandidate{onDisk, gone}
|
||||
|
||||
got, ok := selectMoveCandidate(candidates, func(c moveCandidate) bool {
|
||||
return c.ID == onDisk.ID
|
||||
})
|
||||
if !ok {
|
||||
t.Fatal("expected a move candidate")
|
||||
}
|
||||
if got.ID != gone.ID {
|
||||
t.Fatalf("expected candidate %v, got %v", gone.ID, got.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMoveCandidateOnlyMissingQualifies(t *testing.T) {
|
||||
gone := moveCandidate{ID: mustUUID(t, "00000000-0000-0000-0000-000000000001"), FilePath: "old/book.epub"}
|
||||
candidates := []moveCandidate{gone}
|
||||
|
||||
got, ok := selectMoveCandidate(candidates, func(moveCandidate) bool { return false })
|
||||
if !ok {
|
||||
t.Fatal("expected the missing candidate to qualify")
|
||||
}
|
||||
if got.ID != gone.ID {
|
||||
t.Fatalf("expected candidate %v, got %v", gone.ID, got.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSelectMoveCandidateEmptyList(t *testing.T) {
|
||||
if _, ok := selectMoveCandidate(nil, func(moveCandidate) bool { return false }); ok {
|
||||
t.Fatal("expected no candidate for an empty list")
|
||||
}
|
||||
}
|
||||
+115
-24
@@ -431,7 +431,9 @@ func firstTextDescendant(n *html.Node) *html.Node {
|
||||
// textNodeAtRuneOffset walks text nodes under elem in document order and
|
||||
// returns the node containing the rune offset plus the local offset within
|
||||
// that node. Offsets beyond the end clamp to the last node.
|
||||
func textNodeAtRuneOffset(elem *html.Node, offset int) (*html.Node, int) {
|
||||
// textNodeAtUTF16Offset resolves a crengine text().N offset — UTF-16 code
|
||||
// units — to (text node, rune offset) within elem's text.
|
||||
func textNodeAtUTF16Offset(elem *html.Node, offset int) (*html.Node, int) {
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
@@ -441,15 +443,15 @@ func textNodeAtRuneOffset(elem *html.Node, offset int) (*html.Node, int) {
|
||||
var walk func(*html.Node) bool
|
||||
walk = func(node *html.Node) bool {
|
||||
if node.Type == html.TextNode {
|
||||
length := utf8.RuneCountInString(node.Data)
|
||||
length := utf16Len(node.Data)
|
||||
if remaining < length {
|
||||
target = node
|
||||
local = remaining
|
||||
local = utf16ToRuneIndex(node.Data, remaining)
|
||||
return true
|
||||
}
|
||||
remaining -= length
|
||||
target = node
|
||||
local = length
|
||||
local = utf8.RuneCountInString(node.Data)
|
||||
return false
|
||||
}
|
||||
for child := node.FirstChild; child != nil; child = child.NextSibling {
|
||||
@@ -507,7 +509,7 @@ func (c *CFIConverter) convertByStructuralPath(body *html.Node, xp *CREXPointer,
|
||||
var textNode *html.Node
|
||||
var localOffset int
|
||||
if xp.CharOffset > 0 {
|
||||
textNode, localOffset = textNodeAtRuneOffset(elem, xp.CharOffset)
|
||||
textNode, localOffset = textNodeAtUTF16Offset(elem, xp.CharOffset)
|
||||
} else {
|
||||
textNode = firstTextDescendant(elem)
|
||||
localOffset = 0
|
||||
@@ -1058,24 +1060,96 @@ func indexChildNodes(parent *html.Node) []indexedNode {
|
||||
return nodes
|
||||
}
|
||||
|
||||
func findTextChunkIndex(parent *html.Node, textNode *html.Node) (int, int) {
|
||||
// Character-offset currency policy: the EPUB CFI spec and crengine both
|
||||
// count UTF-16 code units (JavaScript `.length` semantics — what foliate,
|
||||
// readium, KOReader and Kobo clients all observe), so every offset that
|
||||
// CROSSES the wire — CFI terminals, CRE text() offsets, the served
|
||||
// char_offset handle — is UTF-16. Internal arithmetic (book-level
|
||||
// character_offset, percentage fractions) stays rune-based, consistent
|
||||
// with TotalCharacters. These helpers convert at the boundaries; for
|
||||
// all-BMP text the two currencies are identical, so ASCII books are
|
||||
// unaffected.
|
||||
|
||||
// utf16Len returns the UTF-16 code-unit length of s.
|
||||
func utf16Len(s string) int {
|
||||
n := 0
|
||||
for _, r := range s {
|
||||
if r >= 0x10000 {
|
||||
n += 2
|
||||
} else {
|
||||
n++
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// utf16ToRuneIndex converts a UTF-16 code-unit offset within s to a rune
|
||||
// index (clamped to len(runes)).
|
||||
func utf16ToRuneIndex(s string, u16 int) int {
|
||||
if u16 <= 0 {
|
||||
return 0
|
||||
}
|
||||
units := 0
|
||||
i := 0
|
||||
for _, r := range s {
|
||||
if units >= u16 {
|
||||
return i
|
||||
}
|
||||
if r >= 0x10000 {
|
||||
units += 2
|
||||
} else {
|
||||
units++
|
||||
}
|
||||
i++
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// runeToUTF16Index converts a rune index within s to a UTF-16 code-unit
|
||||
// offset (clamped to the string's unit length).
|
||||
func runeToUTF16Index(s string, runeIdx int) int {
|
||||
if runeIdx <= 0 {
|
||||
return 0
|
||||
}
|
||||
units := 0
|
||||
i := 0
|
||||
for _, r := range s {
|
||||
if i >= runeIdx {
|
||||
return units
|
||||
}
|
||||
if r >= 0x10000 {
|
||||
units += 2
|
||||
} else {
|
||||
units++
|
||||
}
|
||||
i++
|
||||
}
|
||||
return units
|
||||
}
|
||||
|
||||
// findTextChunk locates the indexed text chunk containing textNode and
|
||||
// returns its chunk index, the rune offset of the node within the chunk,
|
||||
// and the chunk text up to and including the node (for UTF-16 conversion
|
||||
// of chunk-relative offsets).
|
||||
func findTextChunk(parent *html.Node, textNode *html.Node) (int, int, string) {
|
||||
indexed := indexChildNodes(parent)
|
||||
for i, node := range indexed {
|
||||
if node.isTextChunk() {
|
||||
for j, tn := range node.textChunk {
|
||||
var sb strings.Builder
|
||||
chunkOffset := 0
|
||||
for _, tn := range node.textChunk {
|
||||
if tn == textNode {
|
||||
chunkOffset := 0
|
||||
for k := 0; k < j; k++ {
|
||||
chunkOffset += utf8.RuneCountInString(node.textChunk[k].Data)
|
||||
}
|
||||
return i, chunkOffset
|
||||
return i, chunkOffset, sb.String() + textNode.Data
|
||||
}
|
||||
chunkOffset += utf8.RuneCountInString(tn.Data)
|
||||
sb.WriteString(tn.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1, 0
|
||||
return -1, 0, ""
|
||||
}
|
||||
|
||||
|
||||
func findElementCFIIndex(parent *html.Node, element *html.Node) int {
|
||||
indexed := indexChildNodes(parent)
|
||||
for i, node := range indexed {
|
||||
@@ -1110,15 +1184,17 @@ func buildCFI(spineIndex int, textNode *html.Node, charOffset int) (string, erro
|
||||
return "", fmt.Errorf("text node has no parent")
|
||||
}
|
||||
|
||||
chunkIdx, chunkOffset := findTextChunkIndex(parent, textNode)
|
||||
chunkIdx, chunkOffset, chunkText := findTextChunk(parent, textNode)
|
||||
if chunkIdx == -1 {
|
||||
return "", fmt.Errorf("text node not found in parent's indexed children")
|
||||
}
|
||||
|
||||
// The CFI terminal offset is UTF-16 code units (spec currency); the
|
||||
// internal charOffset is runes. Convert over the chunk text.
|
||||
totalOffset := chunkOffset + charOffset
|
||||
|
||||
var parts []string
|
||||
parts = append(parts, fmt.Sprintf("/%d:%d", chunkIdx, totalOffset))
|
||||
parts = append(parts, fmt.Sprintf("/%d:%d", chunkIdx, runeToUTF16Index(chunkText, totalOffset)))
|
||||
|
||||
current := parent
|
||||
for current != nil {
|
||||
@@ -1245,9 +1321,16 @@ func countTextChars(n *html.Node) int {
|
||||
return count
|
||||
}
|
||||
|
||||
// countTextCharsBefore counts the text characters preceding target within
|
||||
// its document: preceding siblings at each ancestor level, strictly
|
||||
// EXCLUDING target's own subtree (callers add the in-node offset
|
||||
// separately). Starting the walk at target itself — or recursing after
|
||||
// counting the current node's subtree — would re-count the accumulated
|
||||
// document once per ancestor level and multiply the result by the node's
|
||||
// depth.
|
||||
func countTextCharsBefore(target *html.Node) int {
|
||||
count := 0
|
||||
for c := target; c != nil; c = c.PrevSibling {
|
||||
for c := target.PrevSibling; c != nil; c = c.PrevSibling {
|
||||
count += countTextChars(c)
|
||||
}
|
||||
if target.Parent != nil {
|
||||
@@ -1519,24 +1602,31 @@ func resolveCFIToNode(doc *html.Node, steps []cfiStep) (*html.Node, int, error)
|
||||
entry := indexed[lastStep.Index]
|
||||
|
||||
if entry.isTextChunk() {
|
||||
textOffset := 0
|
||||
// The CFI terminal offset arrives in UTF-16 code units (spec
|
||||
// currency — foliate/readium/KOReader/Kobo all emit UTF-16).
|
||||
// Walk the chunk in UTF-16 units, then convert the hit position
|
||||
// to the internal rune offset.
|
||||
u16Remaining := 0
|
||||
if lastStep.HasOffset {
|
||||
textOffset = lastStep.Offset
|
||||
u16Remaining = lastStep.Offset
|
||||
}
|
||||
|
||||
var targetNode *html.Node
|
||||
remainingOffset := textOffset
|
||||
runeIntoTarget := 0
|
||||
for _, tn := range entry.textChunk {
|
||||
textLen := utf8.RuneCountInString(tn.Data)
|
||||
if remainingOffset < textLen || (remainingOffset == textLen && targetNode == nil) {
|
||||
units := utf16Len(tn.Data)
|
||||
if u16Remaining < units || (u16Remaining == units && targetNode == nil) {
|
||||
targetNode = tn
|
||||
runeIntoTarget = utf16ToRuneIndex(tn.Data, u16Remaining)
|
||||
break
|
||||
}
|
||||
remainingOffset -= textLen
|
||||
u16Remaining -= units
|
||||
targetNode = tn
|
||||
runeIntoTarget = utf8.RuneCountInString(tn.Data)
|
||||
}
|
||||
if targetNode == nil && len(entry.textChunk) > 0 {
|
||||
targetNode = entry.textChunk[len(entry.textChunk)-1]
|
||||
runeIntoTarget = utf8.RuneCountInString(targetNode.Data)
|
||||
}
|
||||
|
||||
parent := targetNode.Parent
|
||||
@@ -1547,7 +1637,7 @@ func resolveCFIToNode(doc *html.Node, steps []cfiStep) (*html.Node, int, error)
|
||||
}
|
||||
totalOffset += countTextChars(c)
|
||||
}
|
||||
totalOffset += remainingOffset
|
||||
totalOffset += runeIntoTarget
|
||||
|
||||
return targetNode, totalOffset, nil
|
||||
}
|
||||
@@ -1606,7 +1696,8 @@ func buildCREXPointer(spineIndex int, node *html.Node, charOffset int) (string,
|
||||
xpointer := fmt.Sprintf("/body/DocFragment[%d]/body%s", fragIndex, strings.Join(parts, ""))
|
||||
|
||||
if charOffset > 0 || (node.Type == html.TextNode) {
|
||||
xpointer += fmt.Sprintf("/text().%d", charOffset)
|
||||
// crengine counts UTF-16 code units; charOffset is internal runes.
|
||||
xpointer += fmt.Sprintf("/text().%d", runeToUTF16Index(node.Data, charOffset))
|
||||
}
|
||||
|
||||
return xpointer, nil
|
||||
|
||||
@@ -133,7 +133,7 @@ func writeTestEPUB(t *testing.T) string {
|
||||
}
|
||||
docs := []spineDoc{
|
||||
{"doc1.xhtml", "<body><div><p>Chapter one opening page.</p></div></body>"},
|
||||
{"doc2.xhtml", "<body><div><p>The family of Dashwood had long been settled in Sussex.</p><p>Their estate was large, and their residence was at Norland Park.</p></div></body>"},
|
||||
{"doc2.xhtml", "<body><div><p>The family of Dashwood had long been settled in Sussex.</p><p>Their estate was large, and their residence was at Norland Park.</p><p>The family crest shows a globe \U0001F30D and a rocket \U0001F680 flying onward.</p></div></body>"},
|
||||
{"doc3.xhtml", "<body><div><p>Chapter three contents.</p></div></body>"},
|
||||
{"doc4.xhtml", "<body><div><p>Chapter four contents.</p></div></body>"},
|
||||
{"doc5.xhtml", "<body><div><p>Chapter five contents.</p></div></body>"},
|
||||
|
||||
@@ -0,0 +1,349 @@
|
||||
package sync
|
||||
|
||||
// Ingest-side position authority for reading progress.
|
||||
//
|
||||
// Clients submit (percentage, context_text, epubcfi). The epubcfi is a
|
||||
// standard wrapped CFI — epubcfi(/6/N!/…) — resolvable against the same
|
||||
// document this package parses, so every submission is INDEPENDENTLY
|
||||
// verified: the text at the resolved anchor is extracted and compared
|
||||
// with the submitted context. A mismatch (or an unresolvable anchor)
|
||||
// heals the position by text search, with the submitted percentage
|
||||
// disambiguating repeated phrases, instead of trusting a client-side
|
||||
// projection. This keeps buggy clients from poisoning stored positions:
|
||||
// a resolver that silently returns "wherever I'm currently scrolled"
|
||||
// fails the context check and gets healed to the true location.
|
||||
//
|
||||
// The anchor's block element is also derived as a cssSelector plus a
|
||||
// block-relative character offset and served back — the readium-native
|
||||
// handle clients scroll to, so they never have to parse or trust CFIs
|
||||
// themselves.
|
||||
//
|
||||
// Note: readium-based clients number their readingOrder excluding
|
||||
// linear="no" spine items, while this package's spine index follows the
|
||||
// OPF spine as written. The spine index is therefore internal-only;
|
||||
// client-facing responses carry the anchor document's href instead.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/net/html"
|
||||
)
|
||||
|
||||
// IsConvertibleFormat reports whether a format group uses standard CFIs
|
||||
// as its structural locator currency (i.e. whether CFI verification and
|
||||
// cssSelector derivation apply to it).
|
||||
func IsConvertibleFormat(formatGroup string) bool {
|
||||
return isConvertible(string(formatGroup))
|
||||
}
|
||||
|
||||
func truncateRunes(s string, n int) string {
|
||||
r := []rune(s)
|
||||
if len(r) <= n {
|
||||
return s
|
||||
}
|
||||
return string(r[:n])
|
||||
}
|
||||
|
||||
// anchorBlock returns the nearest non-inline (block-level) ancestor of a
|
||||
// resolved text/element node — the server-side equivalent of the
|
||||
// readers' computed-style block walk.
|
||||
func anchorBlock(node *html.Node) *html.Node {
|
||||
if node == nil {
|
||||
return nil
|
||||
}
|
||||
if node.Type == html.ElementNode && !isInlineFormatting(node) {
|
||||
return node
|
||||
}
|
||||
return findBlockParent(node)
|
||||
}
|
||||
|
||||
// blockContextText extracts the normalized text from (node, runeOff) to
|
||||
// the end of the anchor block — the same excerpt rule the readers use for
|
||||
// context_text, ≤100 chars.
|
||||
func blockContextText(block, node *html.Node, runeOff int) string {
|
||||
segments := collectInlineText(block)
|
||||
var sb strings.Builder
|
||||
started := false
|
||||
for _, seg := range segments {
|
||||
if !started && seg.node == node {
|
||||
started = true
|
||||
runes := []rune(string(seg.runes))
|
||||
if runeOff < len(runes) {
|
||||
sb.WriteString(string(runes[runeOff:]))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if started {
|
||||
sb.WriteString(string(seg.runes))
|
||||
}
|
||||
}
|
||||
return truncateRunes(normalizeWhitespace(sb.String()), 100)
|
||||
}
|
||||
|
||||
// contextMatches reports whether a server-extracted context and a client-
|
||||
// submitted context describe the same anchor. Both are suffixes of the
|
||||
// same block text when the anchors share a block, so containment in
|
||||
// either direction verifies; empty or very short contexts never match.
|
||||
func contextMatches(serverCtx, submitted string) bool {
|
||||
s := truncateRunes(normalizeWhitespace(submitted), 100)
|
||||
t := truncateRunes(normalizeWhitespace(serverCtx), 100)
|
||||
if s == "" || t == "" {
|
||||
return false
|
||||
}
|
||||
short, long := s, t
|
||||
if len([]rune(short)) > len([]rune(long)) {
|
||||
short, long = long, short
|
||||
}
|
||||
if len([]rune(short)) < 12 {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(long, short)
|
||||
}
|
||||
|
||||
// cssSelectorFor mirrors the readers' selOf: a body-relative
|
||||
// tag:nth-child(k) chain (k = 1-based position among element siblings).
|
||||
func cssSelectorFor(block *html.Node) string {
|
||||
var segs []string
|
||||
n := block
|
||||
for n != nil && n.Type == html.ElementNode && n.Data != "body" {
|
||||
k := 1
|
||||
sib := n.Parent.FirstChild
|
||||
for sib != nil && sib != n {
|
||||
if sib.Type == html.ElementNode {
|
||||
k++
|
||||
}
|
||||
sib = sib.NextSibling
|
||||
}
|
||||
segs = append([]string{n.Data + ":nth-child(" + fmt.Sprintf("%d", k) + ")"}, segs...)
|
||||
n = n.Parent
|
||||
}
|
||||
return "body>" + strings.Join(segs, ">")
|
||||
}
|
||||
|
||||
// blockCharOffset computes the offset of (node, runeOff) within the
|
||||
// concatenated text of its block, in UTF-16 code units — the client-side
|
||||
// scroll-target currency (JavaScript .length semantics).
|
||||
func blockCharOffset(block, node *html.Node, runeOff int) int {
|
||||
segments := collectInlineText(block)
|
||||
var sb strings.Builder
|
||||
for _, seg := range segments {
|
||||
if seg.node == node {
|
||||
runes := seg.runes
|
||||
if runeOff < len(runes) {
|
||||
runes = runes[:runeOff]
|
||||
}
|
||||
sb.WriteString(string(runes))
|
||||
return runeToUTF16Index(sb.String(), utf8.RuneCountInString(sb.String()))
|
||||
}
|
||||
sb.WriteString(string(seg.runes))
|
||||
}
|
||||
return utf16Len(sb.String())
|
||||
}
|
||||
|
||||
// bookCharOffset computes the book-wide rune offset of (node, runeOff) —
|
||||
// the reading_progress.character_offset column's currency, consistent with
|
||||
// TotalCharacters and the percentage derivations.
|
||||
func bookCharOffset(conv *CFIConverter, spineIndex int, node *html.Node, runeOff int) int {
|
||||
spine, err := conv.loadSpine()
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
before := 0
|
||||
for i := 0; i < spineIndex && i < len(spine.items); i++ {
|
||||
doc, _, derr := conv.getContentDoc(i + 1)
|
||||
if derr != nil {
|
||||
continue
|
||||
}
|
||||
if b := findBody(doc); b != nil {
|
||||
before += countTextChars(b)
|
||||
}
|
||||
}
|
||||
return before + countTextCharsBefore(node) + runeOff
|
||||
}
|
||||
|
||||
// ProgressAnchor is the full server-computed apply handle for a stored
|
||||
// standard CFI: the anchor block's cssSelector, the character offset
|
||||
// within that block's text, and the spine document's href.
|
||||
type ProgressAnchor struct {
|
||||
CSSSelector string
|
||||
CharOffset int
|
||||
Href string
|
||||
HealedCFI string
|
||||
Healed bool
|
||||
HealedPct *float64
|
||||
}
|
||||
|
||||
// VerifyProgressAnchor resolves a client-submitted standard CFI against
|
||||
// the EPUB, cross-checks the submitted context text, and heals the anchor
|
||||
// by text search on any mismatch. charOffset is the anchor's block-
|
||||
// relative UTF-16 offset (the served char_offset handle); bookOffset is
|
||||
// the anchor's book-wide rune offset (the character_offset column's
|
||||
// currency) — callers refresh the column from it on every verified save
|
||||
// so it never goes stale behind the anchor.
|
||||
func VerifyProgressAnchor(epubPath, epubcfi, contextText string, percentage float64) (finalCFI string, cssSelector string, anchorHref string, charOffset *int, bookOffset *int, healedPct *float64, healed bool, err error) {
|
||||
finalCFI = epubcfi
|
||||
anchorHref = ""
|
||||
|
||||
spineIndex, localSteps, err := parseEPUBCFI(epubcfi)
|
||||
if err != nil {
|
||||
cfi, sel, href, off, book, pct, healedFlag, herr := healFromContext(epubPath, contextText, percentage)
|
||||
return cfi, sel, href, off, book, pct, healedFlag, herr
|
||||
}
|
||||
conv := cachedConverter(epubPath)
|
||||
doc, docHref, err := conv.getContentDoc(spineIndex + 1)
|
||||
if err != nil {
|
||||
return healFromContext(epubPath, contextText, percentage)
|
||||
}
|
||||
node, runeOff, rerr := resolveCFIToNode(doc, localSteps)
|
||||
if rerr != nil {
|
||||
return healFromContext(epubPath, contextText, percentage)
|
||||
}
|
||||
anchorHref = docHref
|
||||
|
||||
block := anchorBlock(node)
|
||||
serverCtx := blockContextText(block, node, runeOff)
|
||||
if contextMatches(serverCtx, contextText) {
|
||||
off := blockCharOffset(block, node, runeOff)
|
||||
book := bookCharOffset(conv, spineIndex, node, runeOff)
|
||||
return finalCFI, cssSelectorFor(block), anchorHref, &off, &book, nil, false, nil
|
||||
}
|
||||
|
||||
// Mismatch: heal by text search.
|
||||
hCFI, hPct, hSel, hHref, hBook, herr := healAnchorByText(epubPath, contextText, percentage, spineIndex)
|
||||
if herr != nil {
|
||||
return finalCFI, "", hHref, nil, nil, nil, false, fmt.Errorf("context mismatch (server %q vs client %q) and heal failed: %w",
|
||||
truncateRunes(serverCtx, 40), truncateRunes(contextText, 40), herr)
|
||||
}
|
||||
hOff := blockCharOffsetFor(epubPath, hCFI)
|
||||
return hCFI, hSel, hHref, &hOff, &hBook, &hPct, true, nil
|
||||
}
|
||||
|
||||
func healFromContext(epubPath, contextText string, percentage float64) (string, string, string, *int, *int, *float64, bool, error) {
|
||||
cfi, healedPct, sel, href, book, err := healAnchorByText(epubPath, contextText, percentage, -1)
|
||||
if err != nil {
|
||||
return "", "", "", nil, nil, nil, false, err
|
||||
}
|
||||
off := blockCharOffsetFor(epubPath, cfi)
|
||||
return cfi, sel, href, &off, &book, &healedPct, true, nil
|
||||
}
|
||||
|
||||
func healAnchorByText(epubPath, contextText string, percentage float64, spineIndex int) (string, float64, string, string, int, error) {
|
||||
if epubPath == "" {
|
||||
return "", 0, "", "", 0, fmt.Errorf("no epub available for text anchoring")
|
||||
}
|
||||
conv := cachedConverter(epubPath)
|
||||
spine, err := conv.loadSpine()
|
||||
if err != nil {
|
||||
return "", 0, "", "", 0, err
|
||||
}
|
||||
needle := truncateRunes(normalizeWhitespace(contextText), 40)
|
||||
if len([]rune(needle)) < 12 {
|
||||
return "", 0, "", "", 0, fmt.Errorf("context too short to anchor")
|
||||
}
|
||||
|
||||
type match struct {
|
||||
spine int
|
||||
node *html.Node
|
||||
off int
|
||||
}
|
||||
var matches []match
|
||||
totalChars := 0
|
||||
charsBefore := make([]int, len(spine.items))
|
||||
for i := range spine.items {
|
||||
doc, _, derr := conv.getContentDoc(i + 1)
|
||||
if derr != nil {
|
||||
continue
|
||||
}
|
||||
body := findBody(doc)
|
||||
if body == nil {
|
||||
continue
|
||||
}
|
||||
charsBefore[i] = totalChars
|
||||
totalChars += countTextChars(body)
|
||||
if n, runeOff := findTextInNode(body, needle); n != nil {
|
||||
matches = append(matches, match{spine: i, node: n, off: runeOff})
|
||||
}
|
||||
}
|
||||
if len(matches) == 0 || totalChars <= 0 {
|
||||
return "", 0, "", "", 0, fmt.Errorf("context not found in book")
|
||||
}
|
||||
|
||||
best := matches[0]
|
||||
bestDist := -1.0
|
||||
for _, m := range matches {
|
||||
frac := (float64(charsBefore[m.spine]) + float64(countTextCharsBefore(m.node)+m.off)) / float64(totalChars)
|
||||
d := frac - percentage
|
||||
if d < 0 {
|
||||
d = -d
|
||||
}
|
||||
if bestDist < 0 || d < bestDist {
|
||||
best = m
|
||||
bestDist = d
|
||||
}
|
||||
}
|
||||
|
||||
bookOff := charsBefore[best.spine] + countTextCharsBefore(best.node) + best.off
|
||||
healedPct := float64(bookOff) / float64(totalChars)
|
||||
cfi, err := buildCFI(best.spine, best.node, best.off)
|
||||
if err != nil {
|
||||
return "", 0, "", "", 0, err
|
||||
}
|
||||
sel := ""
|
||||
if block := anchorBlock(best.node); block != nil {
|
||||
sel = cssSelectorFor(block)
|
||||
}
|
||||
return cfi, healedPct, sel, spine.items[best.spine].href, bookOff, nil
|
||||
}
|
||||
|
||||
func blockCharOffsetFor(epubPath, cfi string) int {
|
||||
n, off, _, herr := cfiTextAtAnchorInternal(epubPath, cfi)
|
||||
if herr != nil {
|
||||
return 0
|
||||
}
|
||||
block := anchorBlock(n)
|
||||
if block == nil {
|
||||
return 0
|
||||
}
|
||||
return blockCharOffset(block, n, off)
|
||||
}
|
||||
|
||||
// cfiTextAtAnchorInternal resolves a stored standard CFI to its anchor
|
||||
// text node, rune offset, and containing document.
|
||||
func cfiTextAtAnchorInternal(epubPath, cfi string) (*html.Node, int, *html.Node, error) {
|
||||
spineIndex, localSteps, err := parseEPUBCFI(cfi)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
conv := cachedConverter(epubPath)
|
||||
doc, _, err := conv.getContentDoc(spineIndex + 1)
|
||||
if err != nil {
|
||||
return nil, 0, nil, err
|
||||
}
|
||||
node, off, err := resolveCFIToNode(doc, localSteps)
|
||||
return node, off, doc, err
|
||||
}
|
||||
|
||||
// ProgressCSSSelector derives the anchor block's cssSelector from a stored
|
||||
// standard CFI.
|
||||
func ProgressCSSSelector(epubPath, epubcfi string) (string, error) {
|
||||
spineIndex, localSteps, err := parseEPUBCFI(epubcfi)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
conv := cachedConverter(epubPath)
|
||||
doc, _, err := conv.getContentDoc(spineIndex + 1)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
node, _, err := resolveCFIToNode(doc, localSteps)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
block := anchorBlock(node)
|
||||
if block == nil {
|
||||
return "", fmt.Errorf("no block ancestor for anchor")
|
||||
}
|
||||
return cssSelectorFor(block), nil
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestManualRealBookAnchor(t *testing.T) {
|
||||
path := "/home/nymusicman/Code/bookhoard/uploads/Ebooks/George Orwell/1984 (1269)/1984 - George Orwell.epub"
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
t.Skip("real book not present on this machine")
|
||||
}
|
||||
cfi := "epubcfi(/6/2!/4[x1984]/8[_idContainer003]/62/1:456)"
|
||||
ctx := "was at war with one of these Powers it was generally at peace with the other"
|
||||
finalCFI, sel, href, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, cfi, ctx, 0.0415)
|
||||
if err != nil {
|
||||
t.Fatalf("VerifyProgressAnchor error: %v", err)
|
||||
}
|
||||
t.Logf("final=%q\n healed=%v healedPct=%v\n selector=%q href=%q charOff=%v", finalCFI, healed, healedPct, sel, href, charOff)
|
||||
sel2, err2 := ProgressCSSSelector(path, cfi)
|
||||
t.Logf("ProgressCSSSelector: %q err=%v", sel2, err2)
|
||||
}
|
||||
@@ -0,0 +1,248 @@
|
||||
package sync
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The fixture (writeTestEPUB in cfi_converter_test.go): 6 spine docs.
|
||||
// doc2 = spine index 1 with two paragraphs:
|
||||
// p1: "The family of Dashwood had long been settled in Sussex."
|
||||
// p2: "Their estate was large, and their residence was at Norland Park."
|
||||
// Hand-derived local paths (html→body /4, body→div /4, div→p /4|/6,
|
||||
// p→text chunk /1):
|
||||
const (
|
||||
dashwoodCFI = "epubcfi(/6/4!/4/2/2/1:0)"
|
||||
dashwoodText = "The family of Dashwood had long been settled in Sussex."
|
||||
estateCFI = "epubcfi(/6/4!/4/2/4/1:0)"
|
||||
estateText = "Their estate was large, and their residence was at Norland Park."
|
||||
dashwoodSelect = "body>div:nth-child(1)>p:nth-child(1)"
|
||||
estateSelect = "body>div:nth-child(1)>p:nth-child(2)"
|
||||
)
|
||||
|
||||
func TestVerifyProgressAnchorAcceptsExact(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
finalCFI, sel, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, dashwoodText, 0.3)
|
||||
_ = charOff
|
||||
if err != nil {
|
||||
t.Fatalf("verify error: %v", err)
|
||||
}
|
||||
if healed {
|
||||
t.Errorf("exact anchor should not heal")
|
||||
}
|
||||
if healedPct != nil {
|
||||
t.Errorf("exact anchor should not carry a healed percentage")
|
||||
}
|
||||
if finalCFI != dashwoodCFI {
|
||||
t.Errorf("finalCFI = %q, want unchanged %q", finalCFI, dashwoodCFI)
|
||||
}
|
||||
if sel != dashwoodSelect {
|
||||
t.Errorf("cssSelector = %q, want %q", sel, dashwoodSelect)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProgressAnchorHealsMismatch(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
// Anchored at p1 but the context is p2's text: the classic
|
||||
// client-projection bug — the server must heal to the true location.
|
||||
finalCFI, _, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, dashwoodCFI, estateText, 0.3)
|
||||
_ = charOff
|
||||
if err != nil {
|
||||
t.Fatalf("verify error: %v", err)
|
||||
}
|
||||
if !healed {
|
||||
t.Fatalf("expected healing, got none (cfi=%q)", finalCFI)
|
||||
}
|
||||
if finalCFI != estateCFI {
|
||||
t.Errorf("healed CFI = %q, want %q", finalCFI, estateCFI)
|
||||
}
|
||||
if healedPct == nil || *healedPct <= 0 {
|
||||
t.Errorf("healed percentage not recomputed: %v", healedPct)
|
||||
}
|
||||
|
||||
// Healing must converge: re-verifying the healed anchor is a no-op.
|
||||
finalCFI2, _, _, _, _, healedPct2, healed2, err := VerifyProgressAnchor(path, finalCFI, estateText, 0.3)
|
||||
if err != nil {
|
||||
t.Fatalf("re-verify error: %v", err)
|
||||
}
|
||||
if healed2 {
|
||||
t.Errorf("healed anchor should be stable, got healed again to %q (pct %v)", finalCFI2, healedPct2)
|
||||
}
|
||||
if finalCFI2 != finalCFI {
|
||||
t.Errorf("re-verify CFI = %q, want %q", finalCFI2, finalCFI)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProgressAnchorHealsUnresolvable(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
// Element index 99 is out of range in doc2 — the anchor cannot resolve.
|
||||
bogus := "epubcfi(/6/4!/4/2/99/1:0)"
|
||||
finalCFI, _, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, bogus, dashwoodText, 0.05)
|
||||
_ = charOff
|
||||
if err != nil {
|
||||
t.Fatalf("verify error: %v", err)
|
||||
}
|
||||
if !healed {
|
||||
t.Fatal("unresolvable anchor should heal from context")
|
||||
}
|
||||
if finalCFI != dashwoodCFI {
|
||||
t.Errorf("healed CFI = %q, want the Dashwood anchor %q", finalCFI, dashwoodCFI)
|
||||
}
|
||||
if healedPct == nil {
|
||||
t.Errorf("healed percentage not recomputed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifyProgressAnchorDumbClient(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
// No CFI at all — a client that only knows percentage + context is
|
||||
// fully supported: the server anchors structurally from the text.
|
||||
finalCFI, sel, _, charOff, _, healedPct, healed, err := VerifyProgressAnchor(path, "", estateText, 0.3)
|
||||
_ = charOff
|
||||
if err != nil {
|
||||
t.Fatalf("verify error: %v", err)
|
||||
}
|
||||
if !healed {
|
||||
t.Fatal("context-only submission should count as anchored-by-heal")
|
||||
}
|
||||
if finalCFI != estateCFI {
|
||||
t.Errorf("anchored CFI = %q, want %q", finalCFI, estateCFI)
|
||||
}
|
||||
if sel != estateSelect {
|
||||
t.Errorf("cssSelector = %q, want %q", sel, estateSelect)
|
||||
}
|
||||
if healedPct == nil {
|
||||
t.Errorf("percentage not recomputed for context-only anchor")
|
||||
}
|
||||
}
|
||||
|
||||
func TestContextMatches(t *testing.T) {
|
||||
block := "The family of Dashwood had long been settled in Sussex."
|
||||
cases := []struct {
|
||||
name string
|
||||
serverCtx string
|
||||
submitted string
|
||||
want bool
|
||||
}{
|
||||
{"exact", block, block, true},
|
||||
{"suffix of block", block, "settled in Sussex.", true},
|
||||
{"block is suffix", "settled in Sussex.", block, true},
|
||||
{"mid-block substring", block, "Dashwood had long been", true},
|
||||
{"different text", block, "completely unrelated words here", false},
|
||||
{"too short", block, "the", false},
|
||||
{"empty submitted", block, "", false},
|
||||
{"empty server", "", "some long enough context text", false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := contextMatches(tc.serverCtx, tc.submitted); got != tc.want {
|
||||
t.Errorf("contextMatches(%q, %q) = %v, want %v", tc.serverCtx, tc.submitted, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseStandardCFIRange(t *testing.T) {
|
||||
// Web progress CFIs are range CFIs; parse must resolve to the start arm.
|
||||
spineIndex, localSteps, err := parseEPUBCFI("epubcfi(/6/4!/4/4:0,/4/4:53)")
|
||||
if err != nil {
|
||||
t.Fatalf("parse error: %v", err)
|
||||
}
|
||||
if spineIndex != 1 {
|
||||
t.Errorf("spineIndex = %d, want 1", spineIndex)
|
||||
}
|
||||
if len(localSteps) != 4 {
|
||||
t.Fatalf("steps = %d, want 4 (parent + start arm)", len(localSteps))
|
||||
}
|
||||
last := localSteps[len(localSteps)-1]
|
||||
if last.Index != 4 || !last.HasOffset || last.Offset != 53 {
|
||||
t.Errorf("start-arm step = %+v, want /4:0", last)
|
||||
}
|
||||
}
|
||||
|
||||
// The emoji in fixture doc2's third paragraph are astral plane runes:
|
||||
// one rune, two UTF-16 code units. Every wire offset (CFI terminals,
|
||||
// the served char_offset) must therefore be UTF-16, while the book-wide
|
||||
// character_offset column stays rune-based.
|
||||
//
|
||||
// Case 1: the context starts at rune 31 of the paragraph text ("The
|
||||
// family crest shows a globe " = 31 BMP runes), so its UTF-16 offset is
|
||||
// also 31 — the currencies agree.
|
||||
//
|
||||
// Case 2: the context starts at rune 33 ("and a rocket ..."), with the
|
||||
// astral 🌍 (rune 31, units 31-32) BEFORE the offset — the UTF-16 offset
|
||||
// is 34, one more than the rune offset. That +1 is the whole point of
|
||||
// the boundary conversion.
|
||||
//
|
||||
// Local path: p3 of doc2's div = /4/2/6, text chunk /1.
|
||||
func TestAstralOffsetCurrency(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
cases := []struct {
|
||||
name string
|
||||
context string
|
||||
wantTerm int
|
||||
}{
|
||||
{"before any emoji", "🌍 and a rocket 🚀 flying onward.", 31},
|
||||
{"after one emoji", "and a rocket 🚀 flying onward.", 34},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
wantCFI := fmt.Sprintf("epubcfi(/6/4!/4/2/6/1:%d)", tc.wantTerm)
|
||||
|
||||
finalCFI, sel, _, charOff, bookOff, _, healed, err := VerifyProgressAnchor(path, "", tc.context, 0.3)
|
||||
if err != nil {
|
||||
t.Fatalf("heal error: %v", err)
|
||||
}
|
||||
if !healed {
|
||||
t.Fatalf("expected heal from context-only submission")
|
||||
}
|
||||
if finalCFI != wantCFI {
|
||||
t.Errorf("healed CFI = %q, want %q (UTF-16 terminal)", finalCFI, wantCFI)
|
||||
}
|
||||
if sel != "body>div:nth-child(1)>p:nth-child(3)" {
|
||||
t.Errorf("cssSelector = %q", sel)
|
||||
}
|
||||
if charOff == nil || *charOff != tc.wantTerm {
|
||||
t.Errorf("block char_offset = %v, want %d UTF-16 units", charOff, tc.wantTerm)
|
||||
}
|
||||
if bookOff == nil || *bookOff <= 0 {
|
||||
t.Errorf("book offset = %v, want a positive book-wide rune offset", bookOff)
|
||||
}
|
||||
|
||||
// Round trip: the healed CFI must verify exactly, with the
|
||||
// same UTF-16 block handle and a stable book offset.
|
||||
finalCFI2, _, _, charOff2, bookOff2, healedPct2, healed2, err := VerifyProgressAnchor(path, finalCFI, tc.context, 0.3)
|
||||
if err != nil {
|
||||
t.Fatalf("re-verify error: %v", err)
|
||||
}
|
||||
if healed2 || healedPct2 != nil {
|
||||
t.Errorf("exact round trip should not heal (healed=%v)", healed2)
|
||||
}
|
||||
if finalCFI2 != wantCFI {
|
||||
t.Errorf("re-verified CFI = %q, want %q", finalCFI2, wantCFI)
|
||||
}
|
||||
if charOff2 == nil || *charOff2 != tc.wantTerm {
|
||||
t.Errorf("re-verified block char_offset = %v, want %d", charOff2, tc.wantTerm)
|
||||
}
|
||||
if bookOff2 == nil || bookOff == nil || *bookOff2 != *bookOff {
|
||||
t.Errorf("book offset unstable: %v vs %v", bookOff2, bookOff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// The book-wide offset must order anchors the way the book orders them:
|
||||
// a later paragraph in the same document has a strictly larger
|
||||
// character_offset.
|
||||
func TestBookOffsetOrdering(t *testing.T) {
|
||||
path := writeTestEPUB(t)
|
||||
_, _, _, _, book1, _, _, err1 := VerifyProgressAnchor(path, dashwoodCFI, dashwoodText, 0.3)
|
||||
_, _, _, _, book2, _, _, err2 := VerifyProgressAnchor(path, estateCFI, estateText, 0.3)
|
||||
if err1 != nil || err2 != nil {
|
||||
t.Fatalf("verify errors: %v %v", err1, err2)
|
||||
}
|
||||
if book1 == nil || book2 == nil {
|
||||
t.Fatalf("book offsets missing: %v %v", book1, book2)
|
||||
}
|
||||
if *book2 <= *book1 {
|
||||
t.Errorf("estate offset %d should exceed dashwood offset %d", *book2, *book1)
|
||||
}
|
||||
}
|
||||
@@ -274,8 +274,18 @@ func EstimatedPages(totalCharacters int64) int {
|
||||
type ProgressService struct {
|
||||
db *database.Queries
|
||||
connManager *ConnectionManager
|
||||
mediaPaths MediaPathResolver
|
||||
}
|
||||
|
||||
// MediaPathResolver resolves a media item's library-relative file path to
|
||||
// an absolute path, so the progress service can verify submitted anchors
|
||||
// against the book itself.
|
||||
type MediaPathResolver interface {
|
||||
ResolveMediaPath(ctx context.Context, libraryID pgtype.UUID, relativePath string) (string, error)
|
||||
}
|
||||
|
||||
func (s *ProgressService) SetMediaPathResolver(r MediaPathResolver) { s.mediaPaths = r }
|
||||
|
||||
func NewProgressService(db *database.Queries, connManager *ConnectionManager) *ProgressService {
|
||||
return &ProgressService{db: db, connManager: connManager}
|
||||
}
|
||||
@@ -435,6 +445,43 @@ func (s *ProgressService) SaveProgress(ctx context.Context, req SaveProgressRequ
|
||||
}
|
||||
}
|
||||
|
||||
// Canonical position authority: resolve the submitted CFI against the
|
||||
// book itself, cross-check the submitted context text, and heal the
|
||||
// anchor by text search on any mismatch. Clients submit what their
|
||||
// renderer can reliably observe (percentage + visible text); the
|
||||
// server owns the structural math and keeps buggy clients from
|
||||
// poisoning stored positions.
|
||||
if !isFixed && s.mediaPaths != nil && params.ContextText.Valid &&
|
||||
isConvertible(string(formatGroup)) && formatGroup != "" {
|
||||
if path, perr := s.mediaPaths.ResolveMediaPath(ctx, mediaItem.LibraryID, mediaItem.FilePath); perr == nil && path != "" {
|
||||
submittedCFI := ""
|
||||
if params.Epubcfi.Valid {
|
||||
submittedCFI = params.Epubcfi.String
|
||||
}
|
||||
submittedPct := 0.0
|
||||
if params.Percentage.Valid {
|
||||
submittedPct = params.Percentage.Float64
|
||||
}
|
||||
if finalCFI, _, _, _, bookOff, healedPct, healed, verr := VerifyProgressAnchor(path, submittedCFI, params.ContextText.String, submittedPct); verr != nil {
|
||||
log.Printf("Bookhoard: progress anchor verify failed for %s: %v", req.MediaItemID.String(), verr)
|
||||
} else {
|
||||
if healed || (!params.Epubcfi.Valid && finalCFI != "") {
|
||||
params.Epubcfi = pgtype.Text{String: finalCFI, Valid: true}
|
||||
if healedPct != nil {
|
||||
params.Percentage = pgtype.Float8{Float64: *healedPct, Valid: true}
|
||||
}
|
||||
}
|
||||
// The verified book-wide offset refreshes the column on
|
||||
// EVERY verified save — not only heals — so it never goes
|
||||
// stale behind the anchor (and a block-relative offset is
|
||||
// never written into the book-level column).
|
||||
if bookOff != nil {
|
||||
params.CharacterOffset = pgtype.Int8{Int64: int64(*bookOff), Valid: true}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
conflictDetected := false
|
||||
if hasExisting && existing.LastSyncSource.Valid && existing.LastSyncSource.String != req.Source {
|
||||
if existing.LastSyncTimestamp.Valid {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package version
|
||||
|
||||
// Version is the app version. It defaults to "dev" for local builds and is
|
||||
// overridden at build time in release images via:
|
||||
//
|
||||
// go build -ldflags "-X bookhoard/internal/version.Version=v1.5"
|
||||
//
|
||||
// The release workflow passes the git tag (e.g. v1.5) as the APP_VERSION
|
||||
// Docker build arg, which the Dockerfile forwards to this flag.
|
||||
var Version = "dev"
|
||||
+1
-1
@@ -12,7 +12,7 @@
|
||||
"dev": "npm run build:ts:dev && npm run build:css"
|
||||
},
|
||||
"dependencies": {
|
||||
"@bookhoard/foliate-js": "git+https://github.com/john-okeefe/foliate-js.git#e448d36",
|
||||
"@bookhoard/foliate-js": "git+https://git.linuxhg.com/Bookhoard/foliate-js.git#e16530a",
|
||||
"alpinejs": "^3.15.8",
|
||||
"chart.js": "^4.5.1",
|
||||
"highlight.js": "^11.11.1",
|
||||
|
||||
@@ -71,6 +71,18 @@ templ AdminArchived(user User, items []ArchivedItem, retentionDays int) {
|
||||
<td class="px-4 py-3" style="color: var(--text-primary)">{ item.LibraryName }</td>
|
||||
<td class="px-4 py-3" style="color: var(--text-secondary)">{ item.Status }</td>
|
||||
<td class="px-4 py-3 text-right whitespace-nowrap">
|
||||
if item.MatchID != "" {
|
||||
<button
|
||||
type="button"
|
||||
data-merge-source={ item.ID }
|
||||
data-merge-target={ item.MatchID }
|
||||
class="btn btn-ghost text-xs"
|
||||
title={ "Merge this archived copy's reading data into the active copy \"" + item.MatchTitle + "\" (" + item.MatchLibraryName + ") and remove the archived entry" }
|
||||
>
|
||||
@Icon("copy", "h-3.5 w-3.5")
|
||||
Merge
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
data-unarchive={ item.ID }
|
||||
|
||||
@@ -57,7 +57,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(retentionDays)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 30, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 30, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -113,7 +113,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 65, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 65, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -131,7 +131,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 67, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 67, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -149,7 +149,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 69, Col: 103}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 69, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -162,7 +162,7 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.LibraryName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 71, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 71, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -175,26 +175,83 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 72, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 72, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"px-4 py-3 text-right whitespace-nowrap\"><button type=\"button\" data-unarchive=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><td class=\"px-4 py-3 text-right whitespace-nowrap\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 76, Col: 37}
|
||||
if item.MatchID != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<button type=\"button\" data-merge-source=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 77, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" data-merge-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.MatchID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 78, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-ghost text-xs\" title=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("Merge this archived copy's reading data into the active copy \"" + item.MatchTitle + "\" (" + item.MatchLibraryName + ") and remove the archived entry")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 80, Col: 174}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("copy", "h-3.5 w-3.5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Merge</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<button type=\"button\" data-unarchive=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "\" class=\"btn btn-ghost text-xs\" title=\"Restore to libraries now; if the file is still gone the next scan hides it again\">")
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 88, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" class=\"btn btn-ghost text-xs\" title=\"Restore to libraries now; if the file is still gone the next scan hides it again\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -202,20 +259,20 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "Restore</button> <button type=\"button\" data-delete-archived=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "Restore</button> <button type=\"button\" data-delete-archived=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_archived.templ`, Line: 85, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_archived.templ`, Line: 97, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "\" class=\"btn btn-ghost text-xs\" title=\"Delete this item and its reading history permanently\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "\" class=\"btn btn-ghost text-xs\" title=\"Delete this item and its reading history permanently\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -223,17 +280,17 @@ func AdminArchived(user User, items []ArchivedItem, retentionDays int) templ.Com
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Delete</button></td></tr>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete</button></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "</tbody></table></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</tbody></table></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</div></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("conflict-" + conflict.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 27, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 27, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -80,7 +80,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d copies", len(conflict.Items)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 33, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 33, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -93,7 +93,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.LibraryName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 36, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 36, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -106,7 +106,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.SHAShort)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 38, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 38, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -119,7 +119,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflict.ID + "/resolve")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 46, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 46, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -132,7 +132,7 @@ func HashConflictCard(conflict HashConflictData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflict.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 48, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 48, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -194,7 +194,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 71, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 71, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -212,7 +212,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 73, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 73, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -230,7 +230,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 75, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 75, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -243,7 +243,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f MB", float64(item.FileSize)/(1024*1024)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 77, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 77, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -261,7 +261,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 79, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 79, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -279,7 +279,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.UsageSummary)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 81, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 81, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -297,7 +297,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/admin/hash-conflicts/" + conflictID + "/resolve")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 87, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 87, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -310,7 +310,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf(`{"action": "keep", "keep_uuid": "%s"}`, item.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 88, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 88, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -323,7 +323,7 @@ func HashConflictItemCardWrapper(item HashConflictItemData, conflictID string) t
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#conflict-" + conflictID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 89, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 89, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -383,7 +383,7 @@ func AdminHashConflicts(user User, conflicts []HashConflictData) templ.Component
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var19).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_hash_conflicts.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_hash_conflicts.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -151,10 +151,18 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<p class="mb-6" style="color: var(--text-primary)">
|
||||
<p class="mb-3" style="color: var(--text-primary)">
|
||||
Are you sure you want to delete <strong id="delete-library-name"></strong>?
|
||||
This will remove the library and all its folder mappings. Media files will not be deleted.
|
||||
</p>
|
||||
<ul class="mb-6 space-y-2 text-sm list-disc list-inside" style="color: var(--text-primary)">
|
||||
<li>
|
||||
<strong>Permanently removed, no archive or undo:</strong>
|
||||
the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Not touched:</strong> media files on disk.
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" id="delete-library-confirm" class="btn btn-danger">
|
||||
@@ -201,6 +209,15 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCou
|
||||
htmx.process(content);
|
||||
document.getElementById('folder-browser-modal').classList.remove('hidden');
|
||||
}
|
||||
// Close the delete-library modal after a successful delete: the
|
||||
// htmx swap already removes the deleted row from the libraries
|
||||
// list, but the modal lives outside the swapped container and
|
||||
// would otherwise stay open.
|
||||
document.getElementById('delete-library-confirm').addEventListener('htmx:afterRequest', function(evt) {
|
||||
if (evt.detail.successful) {
|
||||
document.getElementById('delete-library-modal').classList.add('hidden');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -65,7 +65,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 42, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 42, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -140,7 +140,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><p class=\"mb-6\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>? This will remove the library and all its folder mappings. Media files will not be deleted.</p><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</button></div><p class=\"mb-3\" style=\"color: var(--text-primary)\">Are you sure you want to delete <strong id=\"delete-library-name\"></strong>?</p><ul class=\"mb-6 space-y-2 text-sm list-disc list-inside\" style=\"color: var(--text-primary)\"><li><strong>Permanently removed, no archive or undo:</strong> the library, its folder mappings, and all book records — with their reading progress, highlights, notes, bookmarks, and ratings.</li><li><strong>Not touched:</strong> media files on disk.</li></ul><div class=\"flex justify-end space-x-3\"><button type=\"button\" onclick=\"document.getElementById('delete-library-modal').classList.add('hidden')\" class=\"btn btn-secondary\">Cancel</button> <button type=\"button\" id=\"delete-library-confirm\" class=\"btn btn-danger\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -148,7 +148,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCoun
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Delete</button></div></div></div><script>\n\t\t\t\tfunction toggleLibraryPanel(btn) {\n\t\t\t\t\tvar libId = btn.dataset.libId;\n\t\t\t\t\tvar panel = document.getElementById('library-panel-' + libId);\n\t\t\t\t\tif (panel.innerHTML.trim() !== '') {\n\t\t\t\t\t\tpanel.innerHTML = '';\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtmx.ajax('GET', '/admin/library/' + libId + '/panel', {\n\t\t\t\t\t\t\ttarget: '#library-panel-' + libId,\n\t\t\t\t\t\t\tswap: 'innerHTML'\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfunction openEditModal(id, name, description) {\n\t\t\t\t\tdocument.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);\n\t\t\t\t\tdocument.getElementById('edit-library-name').value = name;\n\t\t\t\t\tdocument.getElementById('edit-library-desc').value = description;\n\t\t\t\t\thtmx.process(document.getElementById('edit-library-form'));\n\t\t\t\t\tdocument.getElementById('edit-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openDeleteModal(id, name) {\n\t\t\t\t\tconst btn = document.getElementById('delete-library-confirm');\n\t\t\t\t\tbtn.setAttribute('hx-delete', '/admin/library/' + id);\n\t\t\t\t\tbtn.setAttribute('hx-target', '#libraries-container');\n\t\t\t\t\tbtn.setAttribute('hx-swap', 'innerHTML');\n\t\t\t\t\tdocument.getElementById('delete-library-name').textContent = name;\n\t\t\t\t\thtmx.process(btn);\n\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openFolderBrowser(targetInputId, libraryId) {\n\t\t\t\t\tconst content = document.getElementById('folder-browser-content');\n\t\t\t\t\tcontent.setAttribute('hx-get', '/admin/library/browse');\n\t\t\t\t\tcontent.setAttribute('hx-vals', '{\"target_input\": \"' + targetInputId + '\", \"library_id\": \"' + libraryId + '\"}');\n\t\t\t\t\tcontent.setAttribute('hx-trigger', 'load');\n\t\t\t\t\thtmx.process(content);\n\t\t\t\t\tdocument.getElementById('folder-browser-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t</script></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Delete</button></div></div></div><script>\n\t\t\t\tfunction toggleLibraryPanel(btn) {\n\t\t\t\t\tvar libId = btn.dataset.libId;\n\t\t\t\t\tvar panel = document.getElementById('library-panel-' + libId);\n\t\t\t\t\tif (panel.innerHTML.trim() !== '') {\n\t\t\t\t\t\tpanel.innerHTML = '';\n\t\t\t\t\t} else {\n\t\t\t\t\t\thtmx.ajax('GET', '/admin/library/' + libId + '/panel', {\n\t\t\t\t\t\t\ttarget: '#library-panel-' + libId,\n\t\t\t\t\t\t\tswap: 'innerHTML'\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tfunction openEditModal(id, name, description) {\n\t\t\t\t\tdocument.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);\n\t\t\t\t\tdocument.getElementById('edit-library-name').value = name;\n\t\t\t\t\tdocument.getElementById('edit-library-desc').value = description;\n\t\t\t\t\thtmx.process(document.getElementById('edit-library-form'));\n\t\t\t\t\tdocument.getElementById('edit-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openDeleteModal(id, name) {\n\t\t\t\t\tconst btn = document.getElementById('delete-library-confirm');\n\t\t\t\t\tbtn.setAttribute('hx-delete', '/admin/library/' + id);\n\t\t\t\t\tbtn.setAttribute('hx-target', '#libraries-container');\n\t\t\t\t\tbtn.setAttribute('hx-swap', 'innerHTML');\n\t\t\t\t\tdocument.getElementById('delete-library-name').textContent = name;\n\t\t\t\t\thtmx.process(btn);\n\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\tfunction openFolderBrowser(targetInputId, libraryId) {\n\t\t\t\t\tconst content = document.getElementById('folder-browser-content');\n\t\t\t\t\tcontent.setAttribute('hx-get', '/admin/library/browse');\n\t\t\t\t\tcontent.setAttribute('hx-vals', '{\"target_input\": \"' + targetInputId + '\", \"library_id\": \"' + libraryId + '\"}');\n\t\t\t\t\tcontent.setAttribute('hx-trigger', 'load');\n\t\t\t\t\thtmx.process(content);\n\t\t\t\t\tdocument.getElementById('folder-browser-modal').classList.remove('hidden');\n\t\t\t\t}\n\t\t\t\t// Close the delete-library modal after a successful delete: the\n\t\t\t\t// htmx swap already removes the deleted row from the libraries\n\t\t\t\t// list, but the modal lives outside the swapped container and\n\t\t\t\t// would otherwise stay open.\n\t\t\t\tdocument.getElementById('delete-library-confirm').addEventListener('htmx:afterRequest', function(evt) {\n\t\t\t\t\tif (evt.detail.successful) {\n\t\t\t\t\t\tdocument.getElementById('delete-library-modal').classList.add('hidden');\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t</script></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -211,7 +211,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(library.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 229, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 246, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -229,7 +229,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(library.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 231, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 248, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -251,7 +251,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(library.TypeName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 236, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 253, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -273,7 +273,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(library.FolderCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 241, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 258, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -291,7 +291,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + library.ID + "/scan")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 247, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 264, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -312,7 +312,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 257, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 274, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -333,7 +333,7 @@ func LibraryList(user User, libraries []LibraryData, users []User) templ.Compone
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("library-panel-" + library.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 266, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 283, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -408,7 +408,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(folder.FolderPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 288, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 305, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -421,7 +421,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 291, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 308, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -434,7 +434,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"folder_path": "` + folder.FolderPath + `"}`)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 292, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 309, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -447,7 +447,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 293, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 310, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -478,7 +478,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/folders")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -491,7 +491,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 303, Col: 124}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 320, Col: 124}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -504,7 +504,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 307, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 324, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -517,7 +517,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 314, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 331, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -530,7 +530,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("folder-input-" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 315, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 332, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -587,7 +587,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/" + libraryID + "/visibility")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 344, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 361, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -600,7 +600,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(`{"user_id": "` + u.ID + `"}`)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 345, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 362, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -613,7 +613,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("#library-panel-" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 347, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 364, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -626,7 +626,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(u.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 350, Col: 76}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 367, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -639,7 +639,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(u.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 351, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 368, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -667,7 +667,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var26 templ.SafeURL
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.JoinURLErrs("/admin/libraries/" + libraryID + "/issues")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 360, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 377, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -694,7 +694,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(issueCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 365, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 382, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -721,7 +721,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 374, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 391, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -734,7 +734,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 375, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 392, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -747,7 +747,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 376, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 393, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -768,7 +768,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 384, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 401, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -781,7 +781,7 @@ func LibraryPanel(user User, libraryID string, library LibraryData, folders []Fo
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(library.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 385, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 402, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -839,7 +839,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(currentPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 400, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 417, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -857,7 +857,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 406, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 423, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -888,7 +888,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 419, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 436, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var36)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -909,7 +909,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(entry.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 424, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 441, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -927,7 +927,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(targetInput)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 431, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 448, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -940,7 +940,7 @@ func FolderBrowserContent(currentPath string, parentPath string, entries []DirEn
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 432, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_library.templ`, Line: 449, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -70,7 +70,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ErrorCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 36, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 36, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -97,7 +97,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.WarningCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 45, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 45, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -124,7 +124,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.InfoCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 54, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 54, Col: 91}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -166,7 +166,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("issue-" + issue.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 70, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 70, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -179,7 +179,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 73, Col: 98}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 73, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -192,7 +192,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 74, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -205,7 +205,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 76, Col: 140}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 76, Col: 140}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -218,7 +218,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 77, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 77, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -231,7 +231,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 139}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 78, Col: 139}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -244,7 +244,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 149}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 79, Col: 149}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -262,7 +262,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 84, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -280,7 +280,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 86, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 86, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -298,7 +298,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 88, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 88, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -321,7 +321,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/libraries/" + libraryID + "/issues/" + issue.ID + "/" + issue.MediaItemID + "/resolve")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 95, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 95, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -334,7 +334,7 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue("#issue-" + issue.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 96, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_processing_issues.templ`, Line: 96, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package templates
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"bookhoard/internal/version"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) {
|
||||
<!DOCTYPE html>
|
||||
@@ -111,6 +114,7 @@ templ AdminSettings(user User, systemConfig map[string]string, scanSettings Scan
|
||||
@ScanSettingsSection(scanSettings)
|
||||
@TunableSettingsSection(liveGroups, false)
|
||||
@TunableSettingsSection(restartGroups, true)
|
||||
@AboutSection()
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
@@ -255,3 +259,18 @@ templ TunableSettingRow(e SettingEntry) {
|
||||
<span id={ "status-" + e.Key } class="text-xs w-24 text-right" style="color: var(--text-secondary)"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
// AboutSection shows build metadata, rendered at the bottom of the System
|
||||
// Settings page. Add future rows (disk space, uptime, Go version, …) to the
|
||||
// list below.
|
||||
templ AboutSection() {
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
@Icon("info", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">About</h3>
|
||||
</div>
|
||||
<div class="space-y-2 text-sm" style="color: var(--text-secondary);">
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Version:</span> { version.Version }</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,10 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"bookhoard/internal/version"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
@@ -67,7 +70,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 31, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 34, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -93,7 +96,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 45, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 48, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -370,7 +373,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 106, Col: 145}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 109, Col: 145}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -383,7 +386,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 107, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 110, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -396,7 +399,7 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(systemConfig["base_url"])
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 108, Col: 152}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 111, Col: 152}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -418,6 +421,10 @@ func AdminSettings(user User, systemConfig map[string]string, scanSettings ScanS
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = AboutSection().Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "</div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
@@ -472,7 +479,7 @@ func ScanSettingsSection(scanSettings ScanSettingsData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 154, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 158, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -564,7 +571,7 @@ func TunableSettingsSection(groups []SettingGroup, restartRequired bool) templ.C
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(g.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 194, Col: 112}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 198, Col: 112}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -622,7 +629,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(e.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 209, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 213, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -640,7 +647,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 211, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 215, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -658,7 +665,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(e.Key)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 213, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 217, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -676,7 +683,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("#status-" + e.Key)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 219, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 223, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -689,7 +696,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Key)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 223, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 227, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -732,7 +739,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 233, Col: 20}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 237, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -750,7 +757,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Min)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 235, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 239, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -769,7 +776,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Max)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 238, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 242, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -792,7 +799,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(e.Value)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 246, Col: 20}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 250, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -818,7 +825,7 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue("status-" + e.Key)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 255, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 259, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -832,4 +839,57 @@ func TunableSettingRow(e SettingEntry) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
// AboutSection shows build metadata, rendered at the bottom of the System
|
||||
// Settings page. Add future rows (disk space, uptime, Go version, …) to the
|
||||
// list below.
|
||||
func AboutSection() templ.Component {
|
||||
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
return templ_7745c5c3_CtxErr
|
||||
}
|
||||
templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
|
||||
if !templ_7745c5c3_IsBuffer {
|
||||
defer func() {
|
||||
templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err == nil {
|
||||
templ_7745c5c3_Err = templ_7745c5c3_BufErr
|
||||
}
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 107, "<div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("info", "h-5 w-5 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 108, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">About</h3></div><div class=\"space-y-2 text-sm\" style=\"color: var(--text-secondary);\"><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Version:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(version.Version)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_settings.templ`, Line: 273, Col: 134}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 109, "</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
|
||||
@@ -60,7 +60,7 @@ func Admin(user User, stats AdminStats) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.LibraryCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 32, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 32, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -81,7 +81,7 @@ func Admin(user User, stats AdminStats) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 39, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 39, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -102,7 +102,7 @@ func Admin(user User, stats AdminStats) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.UserCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 46, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 46, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -123,7 +123,7 @@ func Admin(user User, stats AdminStats) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.DeviceCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin.templ`, Line: 53, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin.templ`, Line: 53, Col: 91}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -57,7 +57,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("user-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 42, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 42, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -78,7 +78,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 50, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 50, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -101,7 +101,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 59, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 59, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -124,7 +124,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 77, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 77, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -137,7 +137,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("#role-result-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 78, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 78, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -170,7 +170,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("role-result-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 87, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 87, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -188,7 +188,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(user.CreatedAt, currentUser.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 92, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 92, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -201,7 +201,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue("/admin/users/" + user.ID + "/profile-modal")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 98, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 98, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -240,7 +240,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/profile/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 117, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 117, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -253,7 +253,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue("#user-" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 118, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `admin_users.templ`, Line: 118, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -42,7 +42,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 8, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 8, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -60,7 +60,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 10, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 10, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -78,7 +78,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 12, Col: 120}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 12, Col: 120}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -96,7 +96,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 14, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 14, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -114,7 +114,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 16, Col: 88}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 16, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -127,7 +127,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 24, Col: 131}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 24, Col: 131}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -140,7 +140,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Response)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 28, Col: 128}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 28, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -179,7 +179,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 45, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 45, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -197,7 +197,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 47, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 47, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -215,7 +215,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 49, Col: 120}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 49, Col: 120}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -233,7 +233,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Method)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 51, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 51, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -251,7 +251,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.Path)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 53, Col: 88}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 53, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -304,7 +304,7 @@ func APIExplorer(explorer APIExplorerData) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(explorer.Endpoint.RequestBody)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/api_explorer.templ`, Line: 70, Col: 136}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `api_explorer.templ`, Line: 70, Col: 136}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -49,7 +49,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 25, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 25, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -88,7 +88,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(data.Source)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 57, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 57, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -101,7 +101,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(data.Timestamp, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 60, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 60, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -119,7 +119,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", data.Data["percentage"].(float64)*100))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 66, Col: 73}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 66, Col: 73}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -147,7 +147,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 74, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -160,7 +160,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["total_pages"].(float64)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 74, Col: 128}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 74, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -174,7 +174,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f", data.Data["page"].(float64)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 76, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 76, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -194,7 +194,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(data.Data["epubcfi"].(string))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 82, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 82, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -212,7 +212,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("resolveConflict('" + book.ActiveConflict.ID + "', '" + source + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 87, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 87, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -235,7 +235,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 98, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 98, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -248,7 +248,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 102, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 102, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -261,7 +261,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 105, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 105, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -279,7 +279,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 109, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -292,7 +292,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 109, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 109, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -311,7 +311,7 @@ func ProgressSyncModal(user User, book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 114, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 114, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -371,7 +371,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 148, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 148, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -392,7 +392,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.HighlightsCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 163, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 163, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -405,7 +405,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 167, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 167, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -418,7 +418,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 171, Col: 106}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 171, Col: 106}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -441,7 +441,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(len(book.DeletedAnnotations))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 183, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 183, Col: 125}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -459,7 +459,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(ann.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 190, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 190, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -472,7 +472,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(ann.AnnotationType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 193, Col: 119}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 193, Col: 119}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -485,7 +485,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(ann.DeletedAt, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 195, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 195, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -498,7 +498,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("restoreDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 200, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 200, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -519,7 +519,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue("purgeDeletedAnnotation('" + ann.AnnotationType + "', '" + ann.ID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 207, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 207, Col: 91}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -545,7 +545,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(ann.DisplayText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 217, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 217, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -569,7 +569,7 @@ func NotesHighlightsModal(user User, book handlers.MediaDetail) templ.Component
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(ann.SecondaryText)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 222, Col: 103}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 222, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -647,7 +647,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 275, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 275, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -660,7 +660,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 276, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 276, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -678,7 +678,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 282, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 282, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -712,7 +712,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 317, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 317, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -725,7 +725,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 321, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 321, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var35)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -738,7 +738,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Description))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 326, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 326, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -751,7 +751,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.Summary))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 331, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 331, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -777,7 +777,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.ResolveAttributeValue(tag)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 344, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 344, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var38)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -795,7 +795,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f", book.CommunityRating.Float64))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 378, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 378, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var39)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -824,7 +824,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Publisher))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 396, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 396, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var40)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -837,7 +837,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.ResolveAttributeValue(formatDateForInput(book.DatePublished))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 401, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 401, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var41)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -850,7 +850,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Edition))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 406, Col: 76}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 406, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var42)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -863,7 +863,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Language))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 410, Col: 78}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 410, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var43)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -876,7 +876,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Genre))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 414, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 414, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var44)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -889,7 +889,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.CopyrightYear.Int32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 419, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 419, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var45)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -918,7 +918,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Series))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 437, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 437, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var46)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -931,7 +931,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesNumber.Int32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 443, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 443, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var47)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -944,7 +944,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.SeriesCount.Int32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 449, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 449, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var48)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -957,7 +957,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var49 string
|
||||
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.Volume.Int32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 455, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 455, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var49)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -986,7 +986,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var50 string
|
||||
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Isbn))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 474, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 474, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var50)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -999,7 +999,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var51 string
|
||||
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Asin))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 478, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 478, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var51)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1012,7 +1012,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var52 string
|
||||
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoodreadsID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 482, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 482, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var52)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1025,7 +1025,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var53 string
|
||||
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.OpenlibraryID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 486, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 486, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var53)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1038,7 +1038,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var54 string
|
||||
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.GoogleBooksID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 490, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 490, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var54)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1051,7 +1051,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var55 string
|
||||
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.WebUrl))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 494, Col: 74}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 494, Col: 74}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var55)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1080,7 +1080,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var56 string
|
||||
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "unknown")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 512, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 512, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var56)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1093,7 +1093,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var57 string
|
||||
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "no")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 513, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 513, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var57)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1106,7 +1106,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var58 string
|
||||
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 514, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 514, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var58)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1119,7 +1119,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var59 string
|
||||
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MangaType) == "yes_and_right_to_left")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 515, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 515, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var59)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1132,7 +1132,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var60 string
|
||||
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "auto")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 521, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 521, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var60)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1145,7 +1145,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var61 string
|
||||
templ_7745c5c3_Var61, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "ltr")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 522, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 522, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var61)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1158,7 +1158,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var62 string
|
||||
templ_7745c5c3_Var62, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "rtl")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 523, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 523, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var62)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1171,7 +1171,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var63 string
|
||||
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ReadingDirection) == "vertical")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 524, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 524, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var63)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1184,7 +1184,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var64 string
|
||||
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.AgeRating))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 529, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 529, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var64)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1197,7 +1197,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var65 string
|
||||
templ_7745c5c3_Var65, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.StoryArc))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 533, Col: 79}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 533, Col: 79}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var65)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1210,7 +1210,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var66 string
|
||||
templ_7745c5c3_Var66, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.Imprint))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 537, Col: 76}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 537, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var66)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1223,7 +1223,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var67 string
|
||||
templ_7745c5c3_Var67, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.ScanInformation))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 541, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 541, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var67)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1236,7 +1236,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var68 string
|
||||
templ_7745c5c3_Var68, templ_7745c5c3_Err = templ.JoinStringErrs(textToString(book.MetadataNotes))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 546, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 546, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var68))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1275,7 +1275,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var69 string
|
||||
templ_7745c5c3_Var69, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", book.PageCount.Int32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 570, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 570, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var69)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1288,7 +1288,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var70 string
|
||||
templ_7745c5c3_Var70, templ_7745c5c3_Err = templ.ResolveAttributeValue(stringSliceToString(book.Contributors))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 575, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 575, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var70)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1301,7 +1301,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var71 string
|
||||
templ_7745c5c3_Var71, templ_7745c5c3_Err = templ.ResolveAttributeValue(textToString(book.MimeType))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 580, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 580, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var71)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1314,7 +1314,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var72 string
|
||||
templ_7745c5c3_Var72, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%.1f MB", float64(book.FileSize.Int64)/1024/1024))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 585, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 585, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var72)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1327,7 +1327,7 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
|
||||
var templ_7745c5c3_Var73 string
|
||||
templ_7745c5c3_Var73, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FileLocation)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail_modals.templ`, Line: 591, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail_modals.templ`, Line: 591, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var73)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -45,7 +45,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 18, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 18, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -58,7 +58,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.FormatGroup)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 26, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 26, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -71,7 +71,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(uuidToString(book.LibraryID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 27, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 27, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -84,7 +84,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%d", getBookRating(book.Rating)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 28, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 28, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -97,7 +97,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflictID(book.ActiveConflict))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 29, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 29, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -110,7 +110,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflictWinnerSource(book.ActiveConflict))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 30, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 30, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -136,7 +136,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url('" + book.CoverImagePath.String + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 36, Col: 101}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 36, Col: 101}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -167,7 +167,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 48, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 48, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -180,7 +180,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 49, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 49, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -198,7 +198,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 57, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 57, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -216,7 +216,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 65, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 65, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -234,7 +234,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 68, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 68, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -252,7 +252,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("window.location.href = '/readers/" + uuidToString(book.ID) + "'")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 74, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 74, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -331,7 +331,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(book.NotesCount + book.HighlightsCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 107, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 107, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -380,7 +380,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f / 10", book.CommunityRating.Float64))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 162, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 162, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -403,7 +403,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var17 templ.SafeURL
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(book.Series.String) + "&library_id=" + uuidToString(book.LibraryID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 169, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 169, Col: 125}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -416,7 +416,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Series.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 173, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 173, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -434,7 +434,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesNumber.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 175, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 175, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -454,7 +454,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ToUpper(book.ReadingDirection.String))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 180, Col: 142}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 180, Col: 142}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -473,7 +473,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(book.AgeRating.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 183, Col: 123}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 183, Col: 123}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -498,7 +498,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(book.StoryArc.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 189, Col: 122}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 189, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -526,7 +526,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinURLErrs("/tags/detail?name=" + url.QueryEscape(tag) + "&library_id=" + uuidToString(book.LibraryID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 196, Col: 109}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 196, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -539,7 +539,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinStringErrs(tag)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 200, Col: 16}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 200, Col: 16}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -595,7 +595,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.JoinStringErrs(book.MetadataNotes.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 230, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 230, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -636,7 +636,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues(fmt.Sprintf("width: %.1f%%; background-color: var(--accent);", book.ReadingProgress.Percentage.Float64*100))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 247, Col: 124}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 247, Col: 124}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -649,7 +649,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.1f", book.ReadingProgress.Percentage.Float64*100))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 254, Col: 134}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 254, Col: 134}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -667,7 +667,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.CurrentPage.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 259, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 259, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -680,7 +680,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.TotalPages.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 259, Col: 131}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 259, Col: 131}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -699,7 +699,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(FormatTimestamptzInTimezone(book.ReadingProgress.LastReadAt, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 265, Col: 140}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 265, Col: 140}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -718,7 +718,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(book.ReadingProgress.LastSyncSource.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 271, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 271, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -746,7 +746,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Publisher.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 284, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 284, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -765,7 +765,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.JoinStringErrs(book.DatePublished.Time.Format("01-02-2006"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 290, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 290, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -784,7 +784,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.JoinStringErrs(book.Isbn.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 296, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 296, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -803,7 +803,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(book.Language.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 302, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 302, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -822,7 +822,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var36 string
|
||||
templ_7745c5c3_Var36, templ_7745c5c3_Err = templ.JoinStringErrs(book.Edition.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 308, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 308, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -841,7 +841,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.JoinStringErrs(book.PageCount.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 314, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 314, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -860,7 +860,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var38 string
|
||||
templ_7745c5c3_Var38, templ_7745c5c3_Err = templ.JoinStringErrs(book.Genre.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 320, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 320, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -879,7 +879,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var39 string
|
||||
templ_7745c5c3_Var39, templ_7745c5c3_Err = templ.JoinStringErrs(book.SeriesCount.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 326, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 326, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -898,7 +898,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var40 string
|
||||
templ_7745c5c3_Var40, templ_7745c5c3_Err = templ.JoinStringErrs(book.Volume.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 332, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 332, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -917,7 +917,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var41 string
|
||||
templ_7745c5c3_Var41, templ_7745c5c3_Err = templ.JoinStringErrs(book.Imprint.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 338, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 338, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -936,7 +936,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var42 string
|
||||
templ_7745c5c3_Var42, templ_7745c5c3_Err = templ.JoinStringErrs(book.CopyrightYear.Int32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 344, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 344, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -955,7 +955,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var43 string
|
||||
templ_7745c5c3_Var43, templ_7745c5c3_Err = templ.JoinStringErrs(strings.ReplaceAll(book.MangaType.String, "_", " "))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 350, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 350, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -974,7 +974,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var44 string
|
||||
templ_7745c5c3_Var44, templ_7745c5c3_Err = templ.JoinStringErrs(book.ScanInformation.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 356, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 356, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -993,7 +993,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var45 string
|
||||
templ_7745c5c3_Var45, templ_7745c5c3_Err = templ.JoinStringErrs(altSeries)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 362, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 362, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1012,7 +1012,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var46 string
|
||||
templ_7745c5c3_Var46, templ_7745c5c3_Err = templ.JoinStringErrs(strings.Join(book.Contributors, ", "))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 368, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 368, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1030,7 +1030,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var47 string
|
||||
templ_7745c5c3_Var47, templ_7745c5c3_Err = templ.JoinStringErrs(book.MimeType.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 373, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 373, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1048,7 +1048,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var48 string
|
||||
templ_7745c5c3_Var48, templ_7745c5c3_Err = templ.JoinStringErrs(formatFileSize(book.FileSize.Int64))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 378, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 378, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1067,7 +1067,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var49 string
|
||||
templ_7745c5c3_Var49, templ_7745c5c3_Err = templ.JoinStringErrs(book.FileLocation)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 384, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 384, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1095,7 +1095,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var50 templ.SafeURL
|
||||
templ_7745c5c3_Var50, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("goodreads", book.GoodreadsID.String, book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 391, Col: 106}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 391, Col: 106}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1113,7 +1113,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var51 templ.SafeURL
|
||||
templ_7745c5c3_Var51, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("goodreads", "", book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 393, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 393, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1132,7 +1132,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var52 templ.SafeURL
|
||||
templ_7745c5c3_Var52, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("openlibrary", book.OpenlibraryID.String, book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 396, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 396, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1150,7 +1150,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var53 templ.SafeURL
|
||||
templ_7745c5c3_Var53, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("openlibrary", "", book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 398, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 398, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1169,7 +1169,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var54 templ.SafeURL
|
||||
templ_7745c5c3_Var54, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("googlebooks", book.GoogleBooksID.String, book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 401, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 401, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1187,7 +1187,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var55 templ.SafeURL
|
||||
templ_7745c5c3_Var55, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("googlebooks", "", book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 403, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 403, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1206,7 +1206,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var56 templ.SafeURL
|
||||
templ_7745c5c3_Var56, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("amazon", book.Asin.String, book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 406, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 406, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1224,7 +1224,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var57 templ.SafeURL
|
||||
templ_7745c5c3_Var57, templ_7745c5c3_Err = templ.JoinURLErrs(getExternalURL("amazon", "", book.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 408, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 408, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1243,7 +1243,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var58 templ.SafeURL
|
||||
templ_7745c5c3_Var58, templ_7745c5c3_Err = templ.JoinURLErrs(book.WebUrl.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 411, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 411, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1256,7 +1256,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var59 string
|
||||
templ_7745c5c3_Var59, templ_7745c5c3_Err = templ.JoinStringErrs(getDomainName(book.WebUrl.String))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 411, Col: 238}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 411, Col: 238}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1289,7 +1289,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var60 templ.SafeURL
|
||||
templ_7745c5c3_Var60, templ_7745c5c3_Err = templ.JoinURLErrs("/collections/" + uuidToString(col.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 423, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 423, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1302,7 +1302,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var61 string
|
||||
templ_7745c5c3_Var61, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("border-color: " + col.Color.String + "; background-color: var(--bg-primary); text-decoration: none;")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 425, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 425, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1315,7 +1315,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var62 string
|
||||
templ_7745c5c3_Var62, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("color: " + col.Color.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 427, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 427, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1328,7 +1328,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var63 string
|
||||
templ_7745c5c3_Var63, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 427, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 427, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1341,7 +1341,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var64 string
|
||||
templ_7745c5c3_Var64, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 428, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `book_detail.templ`, Line: 428, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var64))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -97,7 +97,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(uuidToString(filter.ID))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 93, Col: 135}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 93, Col: 135}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -110,7 +110,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(filter.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 95, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 95, Col: 25}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -173,7 +173,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libraries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 147, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 147, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -191,7 +191,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libraries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 149, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 149, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -212,7 +212,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -225,7 +225,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -238,7 +238,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 154, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 154, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -256,7 +256,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -269,7 +269,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -282,7 +282,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 156, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 156, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -301,7 +301,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -314,7 +314,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -327,7 +327,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 159, Col: 76}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 159, Col: 76}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -345,7 +345,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -358,7 +358,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -371,7 +371,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 161, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 161, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -400,7 +400,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 275, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 275, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -423,7 +423,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset-limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 284, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 284, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -454,7 +454,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinStringErrs(offset/limit + 1)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 293, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 293, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -467,7 +467,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("/api/media-items/search?library_id=%s&limit=%d&offset=%d", currentLibraryID, limit, offset+limit))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 298, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `bookshelf.templ`, Line: 298, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -41,7 +41,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(pageTitle)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 14, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 14, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -62,7 +62,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs(backUrl)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 22, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 22, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -83,7 +83,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(backLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 24, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 24, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -96,7 +96,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(badgeIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 31, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -109,7 +109,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(badgeLabel)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 31, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 31, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -122,7 +122,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 33, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 33, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -135,7 +135,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", len(books)))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 34, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 34, Col: 125}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -168,7 +168,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(emptyIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 44, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 44, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -181,7 +181,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(emptyMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 46, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `browse_detail.templ`, Line: 46, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -72,7 +72,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 23, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 23, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -85,7 +85,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 26, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 26, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -98,7 +98,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 32, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 32, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -111,7 +111,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 45, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 45, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -124,7 +124,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 58, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 58, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -137,7 +137,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 74, Col: 86}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 74, Col: 86}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -155,7 +155,7 @@ func CollectionModal(collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_modal.templ`, Line: 116, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_modal.templ`, Line: 116, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 9, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 9, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -65,7 +65,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 23, Col: 176}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 23, Col: 176}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -78,7 +78,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 25, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 25, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -99,7 +99,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 45, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collection_rules.templ`, Line: 45, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -97,7 +97,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 71, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 71, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -110,7 +110,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(col.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 74, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 74, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -123,7 +123,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 77, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 77, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -136,7 +136,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue("/collections/" + col.ID + "/edit-modal")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 80, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 80, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -157,7 +157,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/collections/" + col.ID + "")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 89, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 89, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -178,7 +178,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 99, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 99, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -191,7 +191,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 100, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 100, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -246,7 +246,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 117, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 117, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -279,7 +279,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 132, Col: 166}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 132, Col: 166}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -292,7 +292,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 134, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 134, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -305,7 +305,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 135, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 135, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -355,7 +355,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -368,7 +368,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 112}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 112}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -381,7 +381,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 179, Col: 147}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 179, Col: 147}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -404,7 +404,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var17 templ.SafeURL
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 194, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 194, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -417,7 +417,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 196, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 196, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -435,7 +435,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 201, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 201, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -453,7 +453,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var20 templ.SafeURL
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + book.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 206, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 206, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -471,7 +471,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 209, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 209, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -551,7 +551,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 385, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 385, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -564,7 +564,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(libraryID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 386, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 386, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -577,7 +577,7 @@ func CollectionDetail(user User, collection CollectionData, books []handlers.Boo
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(collection.IsSystem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 387, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 387, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -620,7 +620,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.toggleBook('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 397, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 397, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -638,7 +638,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 401, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 401, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -651,7 +651,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 401, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 401, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -669,7 +669,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 403, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 403, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -687,7 +687,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 406, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 406, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -700,7 +700,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue("$store.bookPicker.isSelected('" + book.MediaItemID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 411, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 411, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -713,7 +713,7 @@ func BookPickerGrid(books []handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 418, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `collections.templ`, Line: 418, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -54,7 +54,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(total)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 45, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 45, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -67,7 +67,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(unresolved)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 49, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 49, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -80,7 +80,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(total - unresolved)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 53, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 53, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -128,7 +128,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflict.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 111, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 111, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -141,7 +141,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(conflict.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 117, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 117, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -162,7 +162,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.MediaItemTitle)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 125, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 125, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -175,7 +175,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.ConflictType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 126, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 126, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -188,7 +188,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(conflict.CreatedAt, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 133, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 133, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -206,7 +206,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(conflict.ResolvedBy)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 135, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `conflicts.templ`, Line: 135, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -65,7 +65,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 74, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `custom_section.templ`, Line: 74, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -78,7 +78,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 74, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `custom_section.templ`, Line: 74, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -104,7 +104,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 40, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 40, Col: 33}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -117,7 +117,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.IsSystem)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 41, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 41, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -130,7 +130,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 45, Col: 130}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 45, Col: 130}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -143,7 +143,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 47, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 47, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -161,7 +161,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(section.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 49, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 49, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -184,7 +184,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var8 templ.SafeURL
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinURLErrs(section.ViewAllURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 55, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 55, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -210,7 +210,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 68, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 68, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -231,7 +231,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("carousel-track-" + section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 76, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 76, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -249,7 +249,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 81, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 81, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -289,7 +289,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 95, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 95, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -339,7 +339,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var14 templ.SafeURL
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 108, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -357,7 +357,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 115, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -370,7 +370,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 116, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 116, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -388,7 +388,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 124, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 124, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -406,7 +406,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 130, Col: 117}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -419,7 +419,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 131, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -437,7 +437,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 134, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -450,7 +450,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 135, Col: 19}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 135, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -473,7 +473,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var22 templ.SafeURL
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 143, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 143, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -486,7 +486,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue("Resolve progress conflict for " + item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 145, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 145, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -512,7 +512,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var24 templ.SafeURL
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.JoinURLErrs("/readers/" + item.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 152, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 152, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -525,7 +525,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue("Read " + item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 154, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 154, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -593,7 +593,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 184, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -606,7 +606,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(fmt.Sprintf("%v", section.IsSystem))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 185, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -627,7 +627,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 190, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -640,7 +640,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 192, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -668,7 +668,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 202, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 202, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -701,7 +701,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 128}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 228, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -714,7 +714,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `dashboard.templ`, Line: 235, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+16
-16
@@ -111,7 +111,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("showShelfMappings('" + device.ID.String() + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 64, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 64, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -132,7 +132,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue("showDeviceSettings('" + device.ID.String() + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 67, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 67, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -153,7 +153,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 72, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 72, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -166,7 +166,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 73, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 73, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -199,7 +199,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(*device.LastSync, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 86, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 86, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -227,7 +227,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(FormatInTimezone(*device.LastSeen, user.Timezone))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 94, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 94, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -255,7 +255,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(baseURL + "/api/sync/kobo/" + device.AuthToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 110, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 110, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -282,7 +282,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(device.AuthToken)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 132, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 132, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -308,7 +308,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/devices/" + device.ID.String() + "/regenerate-token")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 147, Col: 78}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 147, Col: 78}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -349,7 +349,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 168, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 168, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -362,7 +362,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 170, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 170, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -375,7 +375,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 170, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 170, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -388,7 +388,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("approveDevice('" + reg.RegistrationID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 174, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 174, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -401,7 +401,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue("rejectDevice('" + reg.RegistrationID + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 177, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 177, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -440,7 +440,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(baseURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 242, Col: 148}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 242, Col: 148}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -453,7 +453,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("copyToClipboard('" + baseURL + "', 'Server URL')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 244, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `devices.templ`, Line: 244, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+15
-15
@@ -38,7 +38,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 11, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 11, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -60,7 +60,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var3).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -102,7 +102,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 50, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 50, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -133,7 +133,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var6 templ.SafeURL
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 56, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 56, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -146,7 +146,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 57, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 57, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -159,7 +159,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 58, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 58, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -187,7 +187,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var9 templ.SafeURL
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 65, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 65, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -200,7 +200,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 66, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 66, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -213,7 +213,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 67, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 67, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -257,7 +257,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var12 templ.SafeURL
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 82, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 82, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -270,7 +270,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 82, Col: 129}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 82, Col: 129}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -293,7 +293,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 86, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 86, Col: 104}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -324,7 +324,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var15 templ.SafeURL
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 96, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 96, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -337,7 +337,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level) + "; color: var(--text-secondary);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 98, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 98, Col: 105}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -350,7 +350,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 100, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 100, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func ErrorPage(message string, errorType string) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(message)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/error.templ`, Line: 84, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `error.templ`, Line: 84, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -49,7 +49,7 @@ func ErrorPage(message string, errorType string) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(errorType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/error.templ`, Line: 90, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `error.templ`, Line: 90, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func FilterItem(id string, name string) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(id)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 6, Col: 21}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `filter_item.templ`, Line: 6, Col: 21}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -49,7 +49,7 @@ func FilterItem(id string, name string) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/filter_item.templ`, Line: 14, Col: 9}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `filter_item.templ`, Line: 14, Col: 9}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+22
-22
@@ -53,7 +53,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -83,7 +83,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var4).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -113,7 +113,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var6).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -143,7 +143,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -173,7 +173,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -203,7 +203,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var12).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -260,7 +260,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeTheme('" + opt.Name + "'); openPanel = null")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 103, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 103, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -273,7 +273,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 105, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 105, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -286,7 +286,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + opt.Color)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 108, Col: 130}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 108, Col: 130}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -299,7 +299,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 109, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 109, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -321,7 +321,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var18).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -352,7 +352,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var20 string
|
||||
templ_7745c5c3_Var20, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeWoodPaneling('" + w.Name + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 120, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 120, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -365,7 +365,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.ResolveAttributeValue(w.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 122, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 122, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -388,7 +388,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-image: url(/static/textures/thumb-" + w.Name + ".webp); background-size: cover;")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 128, Col: 191}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 128, Col: 191}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -406,7 +406,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.JoinStringErrs(w.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 130, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 130, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -446,7 +446,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var25 string
|
||||
templ_7745c5c3_Var25, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var24).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var25)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -476,7 +476,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var27 string
|
||||
templ_7745c5c3_Var27, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var26).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -506,7 +506,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var29 string
|
||||
templ_7745c5c3_Var29, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var28).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var29)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -536,7 +536,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var30).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var31)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -566,7 +566,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var33 string
|
||||
templ_7745c5c3_Var33, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var32).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var33)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -646,7 +646,7 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
var templ_7745c5c3_Var35 string
|
||||
templ_7745c5c3_Var35, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 220, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 220, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -730,7 +730,7 @@ func SidebarSignIn(currentPath string) templ.Component {
|
||||
var templ_7745c5c3_Var37 string
|
||||
templ_7745c5c3_Var37, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 268, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `header.templ`, Line: 268, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var37)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -45,7 +45,7 @@ func Icon(name string, extra string) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var2).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/icons.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `icons.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -41,7 +41,7 @@ func Index(loggedIn bool) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 24, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 24, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -54,7 +54,7 @@ func Index(loggedIn bool) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 24, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `index.templ`, Line: 24, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(TotalMediaCount(libData))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 16, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 16, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -56,7 +56,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -69,7 +69,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -82,7 +82,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 20, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 20, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -100,7 +100,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -113,7 +113,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -126,7 +126,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 22, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 22, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -145,7 +145,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -158,7 +158,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -171,7 +171,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 25, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 25, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -189,7 +189,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(lib.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -202,7 +202,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(lib.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 41}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -215,7 +215,7 @@ func LibrarySwitcher(libData []LibraryData, currentLibraryID string, actions ...
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(lib.MediaCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/library_switcher.templ`, Line: 27, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `library_switcher.templ`, Line: 27, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -41,7 +41,7 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(opt.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 24, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `login.templ`, Line: 24, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -54,7 +54,7 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 24, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `login.templ`, Line: 24, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -36,7 +36,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(actionURL)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 5, Col: 20}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 5, Col: 20}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -57,7 +57,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 22, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 22, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -70,7 +70,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 31, Col: 24}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 31, Col: 24}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var4)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -83,7 +83,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.FirstName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 40, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 40, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -96,7 +96,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.ResolveAttributeValue(user.LastName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 50, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 50, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -475,7 +475,7 @@ func ProfileForm(user User, actionURL string, requireCurrentPassword bool, showR
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.ResolveAttributeValue("/api/auth/password/" + user.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_form.templ`, Line: 189, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_form.templ`, Line: 189, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -44,7 +44,7 @@ func ProfileModal(user User) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_modal.templ`, Line: 14, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -57,7 +57,7 @@ func ProfileModal(user User) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(user.Email)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/profile_modal.templ`, Line: 14, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `profile_modal.templ`, Line: 14, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+15
-15
@@ -74,7 +74,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 44, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 44, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -87,7 +87,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var3 templ.SafeURL
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + item.MediaItemID.String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 54, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 54, Col: 56}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -100,7 +100,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 58, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 58, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -118,7 +118,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 61, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 61, Col: 91}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -136,7 +136,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f", item.ProgressPercentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 64, Col: 127}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 64, Col: 127}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -149,7 +149,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("width: " + fmt.Sprintf("%.1f", item.ProgressPercentage) + "%; background-color: var(--accent);")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 68, Col: 162}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 68, Col: 162}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -164,7 +164,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("Page %d of %d (est.)", item.CurrentPage, item.EstimatedPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 77, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 77, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -174,7 +174,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.2f%%", item.ProgressPercentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 79, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 79, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -185,7 +185,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d / %d", item.CurrentPage, item.TotalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 82, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 82, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -199,7 +199,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 88, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 88, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -212,7 +212,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 93, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 93, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -225,7 +225,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 94, Col: 98}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 94, Col: 98}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -251,7 +251,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 101, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 101, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -264,7 +264,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 102, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 102, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -291,7 +291,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 108, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `progress.templ`, Line: 108, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+13
-13
@@ -62,7 +62,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(stats.PendingCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 41, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 41, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -75,7 +75,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(stats.ProcessingCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 45, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 45, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -88,7 +88,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(stats.CompletedCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 49, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 49, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -101,7 +101,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(stats.FailedCount)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 53, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 53, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -149,7 +149,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Status)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 130, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 130, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -162,7 +162,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.SyncType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 131, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 131, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -175,7 +175,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Priority)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 133, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 133, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -188,7 +188,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 136, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 136, Col: 34}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -206,7 +206,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(*item.MediaTitle)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 138, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 138, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -220,7 +220,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Attempts)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 142, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -233,7 +233,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.MaxAttempts)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 142, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 142, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -246,7 +246,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(item.CreatedAt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 143, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 143, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -272,7 +272,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(*item.ErrorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 148, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `queue.templ`, Line: 148, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+95
-98
@@ -16,6 +16,7 @@ func readerInitExpr(metadata ReaderMetadata) string {
|
||||
"formatGroup": metadata.FormatGroup,
|
||||
"readingDirection": metadata.ReadingDirection,
|
||||
"mangaType": metadata.MangaType,
|
||||
"treatAsComic": metadata.TreatAsComic,
|
||||
}
|
||||
jsonBytes, _ := json.Marshal(config)
|
||||
return fmt.Sprintf("initReader(%s)", string(jsonBytes))
|
||||
@@ -23,7 +24,7 @@ func readerInitExpr(metadata ReaderMetadata) string {
|
||||
|
||||
templ Reader(user User, metadata ReaderMetadata) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="overscroll-none">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"/>
|
||||
@@ -38,7 +39,7 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
<body
|
||||
x-data="readerShell"
|
||||
x-init={ readerInitExpr(metadata) }
|
||||
class={ "theme-" + user.Theme + " h-screen overflow-hidden" }
|
||||
class={ "theme-" + user.Theme + " h-screen overflow-hidden overscroll-none" }
|
||||
>
|
||||
<!-- Reading surface: edge-to-edge. Chrome overlays translucently;
|
||||
drawers and scrim float above everything. -->
|
||||
@@ -68,9 +69,7 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ReaderChrome(metadata)
|
||||
|
||||
<!-- Drawer scrim -->
|
||||
<div
|
||||
x-show="tocOpen || settingsOpen || bookmarksOpen || searchOpen"
|
||||
@@ -79,7 +78,6 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
class="drawer-scrim"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
|
||||
<!-- TOC drawer (left) -->
|
||||
<div
|
||||
id="toc-drawer"
|
||||
@@ -96,7 +94,6 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
>
|
||||
@ReaderTOCDrawer()
|
||||
</div>
|
||||
|
||||
<!-- Settings drawer (right) -->
|
||||
<div
|
||||
id="settings-drawer"
|
||||
@@ -113,7 +110,6 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
>
|
||||
@ReaderSettingsDrawer()
|
||||
</div>
|
||||
|
||||
<!-- Selection popover (highlights) -->
|
||||
<div
|
||||
id="selection-popover"
|
||||
@@ -139,7 +135,13 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
:style="'background-color:' + c"
|
||||
:title="selectionPopover.mode === 'create' ? 'Highlight' : 'Set color'"
|
||||
:aria-label="'Highlight color ' + c"
|
||||
@click="selectionPopover.mode === 'create' ? createHighlight(c) : (selectionPopover.color = c, saveHighlightChanges())"
|
||||
@click="selectionPopover.noteOpen
|
||||
? (selectionPopover.mode === 'create'
|
||||
? (selectionPopover.color = c)
|
||||
: (selectionPopover.color = c, saveHighlightChanges(true)))
|
||||
: (selectionPopover.mode === 'create'
|
||||
? createHighlight(c)
|
||||
: (selectionPopover.color = c, saveHighlightChanges()))"
|
||||
></button>
|
||||
</template>
|
||||
<div class="w-px h-5 reader-sep"></div>
|
||||
@@ -188,12 +190,11 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
type="button"
|
||||
class="flex-1 py-1 text-xs border rounded hover:opacity-80"
|
||||
style="border-color: var(--border);"
|
||||
@click="selectionPopover.noteOpen = false"
|
||||
@click="hideSelectionPopover()"
|
||||
>Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Annotations drawer (right): highlights / notes / bookmarks -->
|
||||
<div
|
||||
id="annotations-drawer"
|
||||
@@ -210,7 +211,6 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
>
|
||||
@ReaderAnnotationsDrawer()
|
||||
</div>
|
||||
|
||||
<!-- Search drawer (right, reflowable only) -->
|
||||
<div
|
||||
id="search-drawer"
|
||||
@@ -227,7 +227,6 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
>
|
||||
@ReaderSearchDrawer()
|
||||
</div>
|
||||
|
||||
<!-- Shortcuts & gestures help modal -->
|
||||
<div
|
||||
x-show="helpOpen"
|
||||
@@ -263,17 +262,14 @@ templ Reader(user User, metadata ReaderMetadata) {
|
||||
<div class="help-row"><span>Settings</span><span class="help-keys"><kbd class="kbd">s</kbd></span></div>
|
||||
<div class="help-row"><span>Add bookmark</span><span class="help-keys"><kbd class="kbd">b</kbd></span></div>
|
||||
<div class="help-row"><span>Close menus / toggle chrome</span><span class="help-keys"><kbd class="kbd">Esc</kbd></span></div>
|
||||
|
||||
<h4 class="reader-help-section" x-show="isFixedLayout">Zoom & pan <span class="help-note">(comics, PDFs)</span></h4>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Zoom in / out / reset</span><span class="help-keys"><kbd class="kbd">+</kbd><kbd class="kbd">−</kbd><kbd class="kbd">0</kbd> or wheel</span></div>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Move the page</span><span class="help-keys">drag with the mouse</span></div>
|
||||
<div class="help-row" x-show="isFixedLayout"><span>Recenter <span class="help-note">(keeps zoom)</span></span><span class="help-keys">⊙ button</span></div>
|
||||
<div class="help-row" x-show="isPDF"><span>Select text in a PDF</span><span class="help-keys">Smart mode: drag over text · <kbd class="kbd">Shift</kbd>+drag anywhere · Text mode</span></div>
|
||||
|
||||
<h4 class="reader-help-section">Highlights</h4>
|
||||
<div class="help-row"><span>Create</span><span class="help-keys">select text → pick a color</span></div>
|
||||
<div class="help-row"><span>Edit / delete</span><span class="help-keys">click the highlight</span></div>
|
||||
|
||||
<h4 class="reader-help-section">Touch</h4>
|
||||
<div class="help-row"><span>Page turn</span><span class="help-keys">swipe, or tap outer edges</span></div>
|
||||
<div class="help-row"><span>Toggle chrome</span><span class="help-keys">tap center</span></div>
|
||||
@@ -295,7 +291,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
|
||||
<h1 class="text-base sm:text-lg font-semibold hidden sm:block sm:truncate">{ metadata.Title }</h1>
|
||||
<div class="flex items-center gap-1">
|
||||
<button @click="addBookmark()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Bookmark this position (b)">🏷️</button>
|
||||
<button x-show="!isFixedLayout || isPDF" @click="toggleSearch()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Search (/)">🔍</button>
|
||||
<button x-show="(!isFixedLayout || isPDF) && !treatAsComic" @click="toggleSearch()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Search (/)">🔍</button>
|
||||
<button @click="toggleBookmarks()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Annotations">📝</button>
|
||||
<button @click="toggleSettings()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Settings (s)">Aa</button>
|
||||
<button @click="toggleHelp()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700 text-sm font-bold" title="Shortcuts & gestures (?)" aria-label="Shortcuts and gestures">?</button>
|
||||
@@ -338,7 +334,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<div id="progress-display" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[4rem] max-w-[5rem] sm:max-w-none text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">—</span>
|
||||
<span class="hidden sm:inline" x-text="progressLabel"></span><span x-text="progressMain">—</span>
|
||||
</div>
|
||||
<div class="w-px h-6 reader-sep"></div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
@@ -348,92 +344,93 @@ templ ReaderChrome(metadata ReaderMetadata) {
|
||||
<div x-show="isFixedLayout">
|
||||
<!-- Full toolbar: desktop/tablet (>= 768px, md). Wraps gracefully in between. -->
|
||||
<div class="hidden md:flex flex-wrap items-center gap-x-1 gap-y-1 px-1.5 py-1.5 sm:px-2 sm:py-2 sm:gap-1">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left (←)" aria-label="Go left">
|
||||
<div class="flex items-center gap-0.5">
|
||||
<button @click="goLeft()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Left (←)" aria-label="Go left">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
|
||||
<circle cx="12" cy="9.5" r="2"></circle>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
id="progress-slider-fx"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="grow min-w-[3rem]"
|
||||
/>
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right (→)" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 15 6 L 9 12 L 15 18"></path>
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button x-show="backStack.length > 0" @click="goBackToLocation()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Return to previous location (Alt+←)" aria-label="Return to previous location">
|
||||
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||
<!-- Fit mode + zoom cluster (not applicable in webtoon flow) -->
|
||||
<div class="flex items-center gap-1" x-show="comicFlow !== 'webtoon'">
|
||||
<select
|
||||
@change="applyFitMode($event.target.value)"
|
||||
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
||||
title="Fit mode"
|
||||
aria-label="Fit mode"
|
||||
>
|
||||
<option value="fit-page">Fit Page</option>
|
||||
<option value="fit-width">Fit Width</option>
|
||||
</select>
|
||||
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out (-)" aria-label="Zoom out">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom (0)" aria-label="Reset zoom">
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In (+)" aria-label="Zoom in">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Recenter: reset pan, keep zoom -->
|
||||
<button @click="recenterView()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Recenter page (reset pan, keep zoom)" aria-label="Recenter page">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Pointer mode (PDF only): Smart / Pan / Text — hidden for
|
||||
comic-treated PDFs: no text selection, pan only -->
|
||||
<div class="reader-seg shrink-0" x-show="isPDF && !treatAsComic" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
|
||||
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
|
||||
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
|
||||
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
|
||||
</div>
|
||||
<!-- Double page spread (not applicable in webtoon flow) -->
|
||||
<button x-show="comicFlow !== 'webtoon'" @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 12 21 C 9 18 5 14.4 5 9.5 C 5 6.5 8 4 12 4 C 16 4 19 6.5 19 9.5 C 19 14.4 15 18 12 21 Z"></path>
|
||||
<circle cx="12" cy="9.5" r="2"></circle>
|
||||
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
id="progress-slider-fx"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="any"
|
||||
@input="goToFraction($event.target.value)"
|
||||
class="grow min-w-[3rem]"
|
||||
/>
|
||||
<button @click="goRight()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Go Right (→)" aria-label="Go right">
|
||||
<svg class="reader-icon" width="24" height="24" aria-hidden="true">
|
||||
<path d="M 9 6 L 15 12 L 9 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="w-px h-6 mx-1 reader-sep"></div>
|
||||
<!-- Fit mode + zoom cluster (not applicable in webtoon flow) -->
|
||||
<div class="flex items-center gap-1" x-show="comicFlow !== 'webtoon'">
|
||||
<select
|
||||
@change="applyFitMode($event.target.value)"
|
||||
class="reader-select text-xs px-1.5 py-1 rounded-lg shrink-0"
|
||||
title="Fit mode"
|
||||
aria-label="Fit mode"
|
||||
>
|
||||
<option value="fit-page">Fit Page</option>
|
||||
<option value="fit-width">Fit Width</option>
|
||||
</select>
|
||||
<button @click="zoomOut()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom Out (-)" aria-label="Zoom out">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<button @click="resetZoom()" class="text-xs px-1 rounded-lg hover:bg-gray-700 min-w-[3rem]" title="Reset Zoom (0)" aria-label="Reset zoom">
|
||||
<span x-text="zoomPercent + '%'">100%</span>
|
||||
</button>
|
||||
<button @click="zoomIn()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Zoom In (+)" aria-label="Zoom in">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 10 5 L 10 15 M 5 10 L 15 10"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Recenter: reset pan, keep zoom -->
|
||||
<button @click="recenterView()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Recenter page (reset pan, keep zoom)" aria-label="Recenter page">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="4"></circle>
|
||||
<path d="M 12 2 V 5 M 12 19 V 22 M 2 12 H 5 M 19 12 H 22"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Magnifier -->
|
||||
<button @click="toggleMagnifier()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="magnifierEnabled ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Magnifier" aria-label="Toggle magnifier">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<circle cx="9" cy="9" r="5"></circle>
|
||||
<path d="M 13 13 L 18 18"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Pointer mode (PDF only): Smart / Pan / Text -->
|
||||
<div class="reader-seg shrink-0" x-show="isPDF" role="group" aria-label="Pointer mode" title="Smart: drag text to select, drag blank to pan · Pan: drag anywhere to move · Text: selection only. Shift always force-pans.">
|
||||
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')" title="Smart: drag text to select, drag blank to pan">Smart</button>
|
||||
<button :class="interactionMode === 'pan' ? 'active' : ''" @click="setInteractionMode('pan')" title="Pan: drag anywhere to move">Pan</button>
|
||||
<button :class="interactionMode === 'text' ? 'active' : ''" @click="setInteractionMode('text')" title="Text: selection only, never pan (Shift overrides)">Text</button>
|
||||
</div>
|
||||
<!-- Double page spread (not applicable in webtoon flow) -->
|
||||
<button x-show="comicFlow !== 'webtoon'" @click="toggleSpread()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" :class="doublePageSpread ? 'bg-blue-600 hover:bg-blue-700' : ''" title="Double Page Spread" aria-label="Toggle double page spread">
|
||||
<svg class="reader-icon" width="20" height="20" aria-hidden="true">
|
||||
<path d="M 3 5 h 6 v 14 h -6 z M 15 5 h 6 v 14 h -6 z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
<!-- Progress + TOC -->
|
||||
<div class="flex items-center gap-1">
|
||||
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span x-text="progressMain">—</span>
|
||||
<!-- Progress + TOC -->
|
||||
<div class="flex items-center gap-1">
|
||||
<div id="progress-display-fx" @click="cycleProgressMode()" :title="progressTooltip()" class="text-sm min-w-[3.5rem] text-center cursor-pointer truncate whitespace-nowrap overflow-hidden">
|
||||
<span x-text="progressMain">—</span>
|
||||
</div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
<button @click="toggleTOC()" class="p-1.5 sm:p-2 rounded-lg hover:bg-gray-700" title="Table of Contents (t)">📖</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Compact row (phone-sized windows): single line + ⋯ overflow.
|
||||
Standard mobile-reader pattern: paging, slider, progress stay;
|
||||
@@ -528,7 +525,7 @@ templ ReaderChrome(metadata ReaderMetadata) {
|
||||
<span>Night mode</span>
|
||||
<button @click="fxInvert = !fxInvert; applyFxFilter(true)" class="reader-popover-btn text-xs px-2" :class="fxInvert ? 'text-blue-400' : ''" x-text="fxInvert ? 'On' : 'Off'">Off</button>
|
||||
</div>
|
||||
<div class="reader-tools-row" x-show="isPDF">
|
||||
<div class="reader-tools-row" x-show="isPDF && !treatAsComic">
|
||||
<span>Pointer</span>
|
||||
<div class="reader-seg" role="group" aria-label="Pointer mode">
|
||||
<button :class="interactionMode === 'select' ? 'active' : ''" @click="setInteractionMode('select')">Smart</button>
|
||||
@@ -700,7 +697,7 @@ templ ReaderAnnotationsDrawer() {
|
||||
@keydown.enter="renameBookmark(bookmark)"
|
||||
@keydown.escape="bookmark.renameOpen = false"
|
||||
placeholder="Bookmark name…"
|
||||
></input>
|
||||
/>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<button @click="renameBookmark(bookmark)" class="flex-1 py-1 text-xs bg-blue-600 text-white rounded hover:bg-blue-700">Save</button>
|
||||
<button @click="bookmark.renameOpen = false" class="flex-1 py-1 text-xs border rounded hover:opacity-80" style="border-color: var(--border);">Cancel</button>
|
||||
|
||||
+11
-10
File diff suppressed because one or more lines are too long
+12
-12
@@ -99,7 +99,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
var templ_7745c5c3_Var2 templ.SafeURL
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage-1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 50, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 50, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -125,7 +125,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", currentPage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 58, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 58, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -138,7 +138,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", totalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 58, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 58, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -156,7 +156,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs(fmt.Sprintf("/series?library_id=%s&page=%d", currentLibraryID, currentPage+1))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 62, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 62, Col: 93}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -224,7 +224,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var7 templ.SafeURL
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs("/series/detail?name=" + url.QueryEscape(series.Name))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 79, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 79, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -246,7 +246,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var8).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -269,7 +269,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(cover)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 84, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 84, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -282,7 +282,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var10).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 1, Col: 0}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var12)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -295,7 +295,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.ResolveAttributeValue(series.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 86, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 86, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -327,7 +327,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(series.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 98, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 98, Col: 99}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -340,7 +340,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.BookCount))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 100, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 100, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -353,7 +353,7 @@ func SeriesCard(series SeriesCardData) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d", series.TotalInSeries))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 100, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `series.templ`, Line: 100, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -37,7 +37,7 @@ func ErrorToast(message string) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue("showErrorToast('" + message + "')")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/toast.templ`, Line: 5, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `toast.templ`, Line: 5, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+19
-10
@@ -52,17 +52,23 @@ type FolderData struct {
|
||||
}
|
||||
|
||||
// ArchivedItem feeds the admin archived-items page: media hidden from
|
||||
// libraries because their files vanished, with their pending fate.
|
||||
// libraries because their files vanished, with their pending fate. When an
|
||||
// active item with identical content (same SHA-256) exists, MatchID and
|
||||
// friends are set so the UI can offer merging the archived row's reading
|
||||
// data into its active twin.
|
||||
type ArchivedItem struct {
|
||||
ID string
|
||||
Title string
|
||||
Author string
|
||||
LibraryName string
|
||||
FilePath string
|
||||
MissingScans int32
|
||||
ArchivedAt *time.Time
|
||||
PurgeAt *time.Time
|
||||
Status string
|
||||
ID string
|
||||
Title string
|
||||
Author string
|
||||
LibraryName string
|
||||
FilePath string
|
||||
MissingScans int32
|
||||
ArchivedAt *time.Time
|
||||
PurgeAt *time.Time
|
||||
Status string
|
||||
MatchID string
|
||||
MatchTitle string
|
||||
MatchLibraryName string
|
||||
}
|
||||
|
||||
type DirEntry struct {
|
||||
@@ -243,6 +249,9 @@ type ReaderMetadata struct {
|
||||
LibraryID string `json:"library_id"`
|
||||
TotalCharacters int64 `json:"total_characters"`
|
||||
EstimatedPages int `json:"estimated_pages"`
|
||||
// TreatAsComic: fixed-layout item in a comics/manga library gets
|
||||
// comic reader treatment (bookmarks only — no highlights, no search).
|
||||
TreatAsComic bool `json:"treat_as_comic"`
|
||||
}
|
||||
|
||||
type ReadingProgress struct {
|
||||
|
||||
@@ -105,7 +105,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.ProgressID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 63, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 63, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -118,7 +118,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.TitleFromDevice)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 63, Col: 136}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 63, Col: 136}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -151,7 +151,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(book.TitleFromDevice)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 77, Col: 111}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 77, Col: 111}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -164,7 +164,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 79, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -177,7 +177,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(book.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 79, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 79, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -194,7 +194,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(book.FilePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 88, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 88, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -212,7 +212,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.ResolveAttributeValue(book.SHA256)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 94, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 94, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var8)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -225,7 +225,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(book.SHA256)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 94, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 94, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -243,7 +243,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.LastSyncTime)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 99, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 99, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -261,7 +261,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.ConfidenceScore)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 105, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `unlinked_books.templ`, Line: 105, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -339,6 +339,39 @@ async function deleteArchivedItem(id: string): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
// Merge an archived item's reading data into its active duplicate with the
|
||||
// same content hash (POST /api/media-items/:id/merge), then remove the
|
||||
// archived entry. Covers copy-then-delete-later moves between libraries.
|
||||
async function mergeArchivedItem(sourceId: string, targetId: string): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
if (
|
||||
!window.confirm(
|
||||
"Merge this archived copy's reading progress, notes, and highlights into the active copy? The archived entry is removed.",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(`/api/media-items/${sourceId}/merge`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ target_id: targetId }),
|
||||
});
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({}));
|
||||
throw new Error(err.error || "Failed to merge item");
|
||||
}
|
||||
showToast("Reading data merged into active copy", "success");
|
||||
setTimeout(() => window.location.reload(), 500);
|
||||
} catch (e) {
|
||||
showToast(e instanceof Error ? e.message : "Failed to merge item", "error");
|
||||
}
|
||||
}
|
||||
|
||||
// The archived-items page renders per-row buttons with data attributes
|
||||
// (escaping-safe); bind them here. Module scripts run after DOM parse.
|
||||
document.querySelectorAll<HTMLElement>("[data-unarchive]").forEach((el) => {
|
||||
@@ -349,6 +382,11 @@ document.querySelectorAll<HTMLElement>("[data-delete-archived]").forEach((el) =>
|
||||
deleteArchivedItem(el.dataset.deleteArchived || ""),
|
||||
);
|
||||
});
|
||||
document.querySelectorAll<HTMLElement>("[data-merge-source]").forEach((el) => {
|
||||
el.addEventListener("click", () =>
|
||||
mergeArchivedItem(el.dataset.mergeSource || "", el.dataset.mergeTarget || ""),
|
||||
);
|
||||
});
|
||||
|
||||
export {
|
||||
hideScanProgress,
|
||||
|
||||
+601
-130
@@ -358,6 +358,15 @@ const getCSS = ({
|
||||
text-align: ${justify ? "justify" : "start"} !important;
|
||||
hyphens: ${hyphenate ? "auto" : "none"};
|
||||
}
|
||||
/* Chrome's touch word-selection walks hyphen fragments: dragging from a
|
||||
line-start word re-anchors at the line-end fragment (silently dropping
|
||||
the selected word) and fast drags overshoot into the next column.
|
||||
Coarse pointers get whole words only. */
|
||||
@media (pointer: coarse) {
|
||||
p, li, blockquote, dd {
|
||||
hyphens: ${hyphenate ? "manual" : "none"};
|
||||
}
|
||||
}
|
||||
[align="left"] { text-align: left; }
|
||||
[align="right"] { text-align: right; }
|
||||
[align="center"] { text-align: center; }
|
||||
@@ -389,6 +398,10 @@ document.addEventListener("alpine:init", () => {
|
||||
isFixedLayout: false,
|
||||
isPDF: false,
|
||||
isComic: false,
|
||||
// Fixed-layout item in a comics/manga library (e.g. a manga PDF):
|
||||
// comic reader treatment — bookmarks only, no text-selection
|
||||
// highlights, no in-book search.
|
||||
treatAsComic: false as boolean,
|
||||
comicFlow: "paged" as string,
|
||||
fxBrightness: 1 as number,
|
||||
fxContrast: 1 as number,
|
||||
@@ -404,12 +417,20 @@ document.addEventListener("alpine:init", () => {
|
||||
pointerFine: true as boolean,
|
||||
fxZoomed: false as boolean,
|
||||
tapZonesEnabled: true as boolean,
|
||||
tapZoneSize: 30 as number,
|
||||
tapZoneSize: 12 as number,
|
||||
tapZoneTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
// Any live text selection, host document or content iframe. Fed by
|
||||
// selectionchange listeners (touch devices); tap zones stand down
|
||||
// while one exists.
|
||||
anySelection: false as boolean,
|
||||
// The section document most recently loaded (for clearing its
|
||||
// selection after actions that consume it).
|
||||
currentDoc: null as any,
|
||||
// Timestamp of the last touch press (pointer:coarse surfaces); a
|
||||
// selection appearing during a short press is Chrome's tap-to-select,
|
||||
// not a selection gesture.
|
||||
lastTouchDownT: 0 as number,
|
||||
selPopTimer: null as ReturnType<typeof setTimeout> | null,
|
||||
highlightItems: [] as {
|
||||
id: string;
|
||||
text: string;
|
||||
@@ -571,8 +592,10 @@ document.addEventListener("alpine:init", () => {
|
||||
formatGroup: string;
|
||||
readingDirection: string;
|
||||
mangaType: string;
|
||||
treatAsComic: boolean;
|
||||
}) {
|
||||
this.mediaItemId = config.mediaItemId;
|
||||
this.treatAsComic = config.treatAsComic;
|
||||
// Reading state (position, bookmarks, annotations) is never baked
|
||||
// into the rendered page: the web reader is intrinsically tied to
|
||||
// the server, so it reads all of it from the APIs at open time —
|
||||
@@ -598,7 +621,7 @@ document.addEventListener("alpine:init", () => {
|
||||
this.chromeBehavior =
|
||||
behavior === "always-visible" ? "always-visible" : "auto-hide";
|
||||
this.tapZonesEnabled = this.settings.tap_zones_enabled ?? true;
|
||||
this.tapZoneSize = this.settings.tap_zone_size || 30;
|
||||
this.tapZoneSize = this.settings.tap_zone_size || 12;
|
||||
const mode = this.settings.pdf_interaction_mode;
|
||||
if (mode === "pan" || mode === "text" || mode === "select") {
|
||||
this.interactionMode = mode;
|
||||
@@ -679,6 +702,7 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
this.view.addEventListener("load", (e: any) => {
|
||||
const { doc, index } = e.detail;
|
||||
this.currentDoc = doc;
|
||||
const link = doc.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
link.href = "/static/reader-fonts.css";
|
||||
@@ -702,64 +726,61 @@ document.addEventListener("alpine:init", () => {
|
||||
this.noteSelectionActivity(
|
||||
!!sel && !sel.isCollapsed && !!sel.toString(),
|
||||
);
|
||||
// The gesture takeover swallows pointerup for touch selection
|
||||
// drags, so the popover's pointerup trigger never fires for
|
||||
// them; a selection that has been stable for a moment is the
|
||||
// settled gesture — open the popover for it.
|
||||
if (
|
||||
this.isFixedLayout &&
|
||||
!this.treatAsComic &&
|
||||
doc.querySelector(".textLayer")
|
||||
)
|
||||
this.schedulePDFSelectionPopover(doc, index);
|
||||
else if (!this.isFixedLayout)
|
||||
this.scheduleSelectionPopover(doc, index);
|
||||
});
|
||||
}
|
||||
// Clicks in the book dismiss the popover in ANY mode and for ANY
|
||||
// content doc (EPUB sections and fixed-layout pages alike): iframe
|
||||
// events never bubble to the host document (so the host
|
||||
// outside-click dismiss never sees them). Clicking a painted
|
||||
// highlight still works: this hides, then foliate's
|
||||
// show-annotation re-opens it in edit mode.
|
||||
doc.addEventListener(
|
||||
"pointerdown",
|
||||
() => {
|
||||
if (this.selectionPopover.open)
|
||||
this.hideSelectionPopover();
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
// Text selection → highlight popover (reflowable EPUB only;
|
||||
// fixed-layout highlight overlays are a later milestone).
|
||||
if (!this.isFixedLayout) {
|
||||
const checkSelection = () => {
|
||||
const checkSelection = (e?: PointerEvent) => {
|
||||
const sel = doc.getSelection();
|
||||
// Chrome on touch selects the word under a quick tap — that is
|
||||
// a page-turn tap, not a selection gesture; clear it and show
|
||||
// nothing. Real hold selections never reach this pointerup
|
||||
// (the gesture takeover swallows it) — they open the popover
|
||||
// through the selectionchange debounce below.
|
||||
if (
|
||||
e && e.pointerType === "touch" && this.lastTouchDownT &&
|
||||
Date.now() - this.lastTouchDownT < 300
|
||||
) {
|
||||
sel?.removeAllRanges();
|
||||
return;
|
||||
}
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
||||
if (this.selectionPopover.mode === "create")
|
||||
this.hideSelectionPopover();
|
||||
return;
|
||||
}
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
let cfi: string;
|
||||
let cfiEnd: string;
|
||||
try {
|
||||
cfi = this.view.getCFI(index, range);
|
||||
// Collapse to the end point for a distinct end anchor —
|
||||
// KOReader sync renders the highlight box from pos0/pos1, and
|
||||
// pos1 == pos0 would be a degenerate (zero-length) range.
|
||||
const endRange = range.cloneRange();
|
||||
endRange.collapse(false);
|
||||
cfiEnd = this.view.getCFI(index, endRange);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
const iframeRect = frame?.getBoundingClientRect();
|
||||
const rect = range.getBoundingClientRect();
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: (iframeRect?.left ?? 0) + rect.left + rect.width / 2,
|
||||
y: (iframeRect?.top ?? 0) + rect.top,
|
||||
text,
|
||||
cfi,
|
||||
cfiEnd,
|
||||
});
|
||||
this.openPopoverForSelection(sel, doc, index);
|
||||
};
|
||||
doc.addEventListener(
|
||||
"pointerup",
|
||||
() => setTimeout(checkSelection, 0),
|
||||
{ passive: true },
|
||||
);
|
||||
// Clicks in the book dismiss the popover in ANY mode: iframe
|
||||
// events never bubble to the host document (so the host
|
||||
// outside-click dismiss never sees them), and the collapsed
|
||||
// check above only covers create mode — edit mode had no
|
||||
// outside-click path at all, leaving Esc as the only way out.
|
||||
// Clicking a painted highlight still works: this hides, then
|
||||
// foliate's show-annotation re-opens it in edit mode.
|
||||
doc.addEventListener(
|
||||
"pointerdown",
|
||||
() => {
|
||||
if (this.selectionPopover.open)
|
||||
this.hideSelectionPopover();
|
||||
},
|
||||
(e: PointerEvent) => setTimeout(() => checkSelection(e), 0),
|
||||
{ passive: true },
|
||||
);
|
||||
doc.addEventListener(
|
||||
@@ -774,66 +795,29 @@ document.addEventListener("alpine:init", () => {
|
||||
// Detected structurally: renderer.isPDF isn't set until the first
|
||||
// spread renders (during view.init), which is after this listener
|
||||
// attaches — the stale copy here would always be falsy.
|
||||
if (this.isFixedLayout && doc.querySelector(".textLayer")) {
|
||||
const checkPDFSelection = () => {
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
// Denominator: the element whose post-transform screen rect IS
|
||||
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
||||
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
||||
// is dpr× too small and would inflate every fraction (highlight
|
||||
// shifted right/oversized on any dpr != 1 display). The canvas's
|
||||
// rect is in the same transform-inclusive space as the textLayer
|
||||
// span rects, so the dpr scaling cancels exactly.
|
||||
const denom =
|
||||
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
||||
(doc.querySelector("img") as HTMLElement);
|
||||
let dr = denom?.getBoundingClientRect();
|
||||
if (!dr || !dr.width || !dr.height) {
|
||||
const vw = doc.defaultView;
|
||||
dr = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: vw?.innerWidth || 1,
|
||||
height: vw?.innerHeight || 1,
|
||||
} as DOMRect;
|
||||
}
|
||||
const rects: number[][] = [];
|
||||
for (const r of range.getClientRects()) {
|
||||
const x = (r.left - dr.left) / dr.width;
|
||||
const y = (r.top - dr.top) / dr.height;
|
||||
const w = r.width / dr.width;
|
||||
const h = r.height / dr.height;
|
||||
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
||||
}
|
||||
if (!rects.length) return;
|
||||
// Map the first rect to host-space for popover placement.
|
||||
// The canvas's screen rect maps 1:1 onto the host iframe box
|
||||
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
||||
// kept general for the comic img fallback.
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
if (!frame) return;
|
||||
const fr = frame.getBoundingClientRect();
|
||||
const first = range.getBoundingClientRect();
|
||||
const sx = fr.width / dr.width;
|
||||
const sy = fr.height / dr.height;
|
||||
this.pdfSelDoc = doc;
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
||||
y: fr.top + first.top * sy,
|
||||
text,
|
||||
cfi: "",
|
||||
pdfPage: index,
|
||||
pdfRects: rects,
|
||||
});
|
||||
};
|
||||
// Comic-treated PDFs never attach it: no highlights, like comics.
|
||||
if (
|
||||
this.isFixedLayout &&
|
||||
!this.treatAsComic &&
|
||||
doc.querySelector(".textLayer")
|
||||
) {
|
||||
doc.addEventListener(
|
||||
"pointerup",
|
||||
() => setTimeout(checkPDFSelection, 0),
|
||||
(e: PointerEvent) =>
|
||||
setTimeout(() => {
|
||||
// Chrome on touch selects the word under a quick tap —
|
||||
// that is a page-turn tap, not a selection gesture; clear
|
||||
// it and show nothing (same guard as the reflowable path).
|
||||
if (
|
||||
e.pointerType === "touch" &&
|
||||
this.lastTouchDownT &&
|
||||
Date.now() - this.lastTouchDownT < 300
|
||||
) {
|
||||
doc.getSelection()?.removeAllRanges();
|
||||
return;
|
||||
}
|
||||
this.checkPDFSelectionFor(doc, index);
|
||||
}, 0),
|
||||
{ passive: true },
|
||||
);
|
||||
}
|
||||
@@ -885,7 +869,18 @@ document.addEventListener("alpine:init", () => {
|
||||
this.view.addEventListener("relocate", (e: any) => {
|
||||
const { fraction, location, pageItem, cfi, tocItem, section } =
|
||||
e.detail;
|
||||
this.hideSelectionPopover();
|
||||
// Only dismiss the popover on an actual position change — the
|
||||
// paginator also emits no-op relocates (e.g. a tap's page-aligned
|
||||
// anchor), which would otherwise close a just-opened highlight
|
||||
// edit popover. NOTE: detail.section is an object (recreated on
|
||||
// every relocate), so it can't be compared by reference; the
|
||||
// fraction threshold alone covers section changes (any section
|
||||
// boundary moves the whole-book fraction far more than 0.1%).
|
||||
const prev = this.lastRelocateDetail;
|
||||
const moved =
|
||||
!prev ||
|
||||
Math.abs((prev.fraction ?? 0) - (fraction ?? 0)) > 0.001;
|
||||
if (moved) this.hideSelectionPopover();
|
||||
this.lastCfi = cfi || "";
|
||||
this.lastRelocateDetail = {
|
||||
fraction,
|
||||
@@ -959,6 +954,9 @@ document.addEventListener("alpine:init", () => {
|
||||
// Smart|Pan|Text control appear for PDFs.
|
||||
this.isPDF = !!this.renderer?.isPDF;
|
||||
if (this.isPDF) {
|
||||
// Comic-treated PDFs are paged through like comics: no text
|
||||
// selection, so pointer interactions stay in pan mode.
|
||||
if (this.treatAsComic) this.interactionMode = "pan";
|
||||
this.renderer.setAttribute("interaction-mode", this.interactionMode);
|
||||
}
|
||||
this.fxZoomed = this.isFixedLayout && this.renderer?.zoom != null;
|
||||
@@ -1006,6 +1004,10 @@ document.addEventListener("alpine:init", () => {
|
||||
this.pokeChrome();
|
||||
},
|
||||
pokeChrome() {
|
||||
// Never raise the chrome while a touch selection is live — the
|
||||
// selection popover owns the screen until the selection is
|
||||
// consumed or dismissed.
|
||||
if (this.anySelection) return;
|
||||
this.chromeVisible = true;
|
||||
if (this.chromeBehavior !== "auto-hide") return;
|
||||
if (this.hideTimer) clearTimeout(this.hideTimer);
|
||||
@@ -1057,6 +1059,15 @@ document.addEventListener("alpine:init", () => {
|
||||
clearTimeout(this.tapZoneTimer);
|
||||
this.tapZoneTimer = null;
|
||||
}
|
||||
// Hide the chrome the moment a selection appears — it competes
|
||||
// with the selection popover and handles for screen space.
|
||||
if (hasSelection && this.chromeVisible) {
|
||||
this.chromeVisible = false;
|
||||
if (this.hideTimer) {
|
||||
clearTimeout(this.hideTimer);
|
||||
this.hideTimer = null;
|
||||
}
|
||||
}
|
||||
},
|
||||
attachTapZoneListeners(surface: HTMLElement, isDoc: boolean) {
|
||||
let downX = 0;
|
||||
@@ -1080,6 +1091,7 @@ document.addEventListener("alpine:init", () => {
|
||||
downId = e.pointerId;
|
||||
moved = false;
|
||||
longPressed = false;
|
||||
this.lastTouchDownT = downT;
|
||||
},
|
||||
{ passive: true },
|
||||
);
|
||||
@@ -1129,16 +1141,26 @@ document.addEventListener("alpine:init", () => {
|
||||
// No tap actions while a fixed-layout page is zoomed — taps then
|
||||
// belong to the content (and double-tap zoom).
|
||||
if (this.isFixedLayout && this.renderer?.zoom != null) return;
|
||||
const width =
|
||||
(isDoc
|
||||
? (surface as any).documentElement.clientWidth
|
||||
: (surface as HTMLElement).getBoundingClientRect().width) || 1;
|
||||
const relX =
|
||||
(e.clientX -
|
||||
(isDoc
|
||||
? 0
|
||||
: (surface as HTMLElement).getBoundingClientRect().left)) /
|
||||
width;
|
||||
// Content iframes are laid out at the full section width (the
|
||||
// paginator shows one column slice of a much wider document), so
|
||||
// clientX must be mapped into the visible page before it can be
|
||||
// compared against the tap zones — dividing by the iframe's
|
||||
// clientWidth puts every tap in the left zone.
|
||||
let relX: number;
|
||||
if (isDoc) {
|
||||
const r: any = this.renderer;
|
||||
const docWidth = (surface as any).documentElement.clientWidth || 1;
|
||||
if (r && !r.scrolled && r.size) {
|
||||
const base = r.start - r.size;
|
||||
relX = (e.clientX - base) / r.size;
|
||||
} else {
|
||||
relX = e.clientX / docWidth;
|
||||
}
|
||||
if (this.view?.book?.dir === "rtl") relX = 1 - relX;
|
||||
} else {
|
||||
const rect = (surface as HTMLElement).getBoundingClientRect();
|
||||
relX = (e.clientX - rect.left) / (rect.width || 1);
|
||||
}
|
||||
this.routeTapDebounced(relX);
|
||||
},
|
||||
{ passive: true },
|
||||
@@ -1154,6 +1176,13 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
this.tapZoneTimer = setTimeout(() => {
|
||||
this.tapZoneTimer = null;
|
||||
// Touch pages by swipe only (the paginator's pan + snap); a tap
|
||||
// anywhere just toggles the chrome. The edge zones remain for
|
||||
// mouse-driven taps.
|
||||
if (window.matchMedia("(pointer: coarse)").matches) {
|
||||
this.toggleChromeManually();
|
||||
return;
|
||||
}
|
||||
const zone = this.tapZoneSize / 100;
|
||||
if (relX < zone) this.goLeft();
|
||||
else if (relX > 1 - zone) this.goRight();
|
||||
@@ -1199,10 +1228,14 @@ document.addEventListener("alpine:init", () => {
|
||||
p.noteOpen = !!p.note && opts.mode === "edit";
|
||||
p.pdfPage = opts.pdfPage ?? -1;
|
||||
p.pdfRects = opts.pdfRects ?? [];
|
||||
// Clamp so the popover stays on screen (it anchors bottom-center).
|
||||
// Clamp so the popover stays on screen (it anchors bottom-center;
|
||||
// the popover is ~240px wide, so the fixed margins must cover half
|
||||
// of it). NOTE: do not mutate the position after opening — touching
|
||||
// the style mid-enter-transition leaves the popover invisible to
|
||||
// compositor hit-testing (taps fall through to the content iframe).
|
||||
const w = window.innerWidth;
|
||||
const h = window.innerHeight;
|
||||
p.x = Math.min(Math.max(opts.x, 90), w - 90);
|
||||
p.x = Math.min(Math.max(opts.x, 130), w - 130);
|
||||
p.y = Math.min(Math.max(opts.y, 60), h - 60);
|
||||
p.open = true;
|
||||
},
|
||||
@@ -1210,6 +1243,266 @@ document.addEventListener("alpine:init", () => {
|
||||
this.selectionPopover.open = false;
|
||||
this.selectionPopover.noteOpen = false;
|
||||
},
|
||||
scheduleSelectionPopover(doc: any, index: number) {
|
||||
if (this.selPopTimer) clearTimeout(this.selPopTimer);
|
||||
this.selPopTimer = setTimeout(() => {
|
||||
this.selPopTimer = null;
|
||||
// A tap on a painted highlight re-opens the popover in edit mode
|
||||
// via its own path — never clobber it.
|
||||
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
|
||||
return;
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
||||
if (
|
||||
this.selectionPopover.open &&
|
||||
this.selectionPopover.mode === "create"
|
||||
)
|
||||
this.hideSelectionPopover();
|
||||
this.hideSelectionHandles();
|
||||
return;
|
||||
}
|
||||
// The browser's selection auto-scroll (drags near a viewport
|
||||
// edge) scrolls the paginated container off the page grid, and
|
||||
// a selection that crosses the page edge keeps its reveal-scroll
|
||||
// chasing the off-page text — the view bounces between pages
|
||||
// forever. Correct the view to the anchor's page and clamp the
|
||||
// selection to the visible page; scrollBy clamps to the current
|
||||
// section, so the correction can never cross sections.
|
||||
const r: any = this.renderer;
|
||||
if (r && !r.scrolled && r.size && this.view?.book?.dir !== "rtl") {
|
||||
try {
|
||||
const anchorProbe = doc.createRange();
|
||||
anchorProbe.setStart(sel.anchorNode, sel.anchorOffset);
|
||||
// collapsed ranges report degenerate (0,0) rects — extend by
|
||||
// one character to get a measurable one
|
||||
if (
|
||||
sel.anchorNode?.nodeType === 3 &&
|
||||
sel.anchorOffset < sel.anchorNode.length
|
||||
)
|
||||
anchorProbe.setEnd(sel.anchorNode, sel.anchorOffset + 1);
|
||||
const rects = anchorProbe.getClientRects();
|
||||
const ar =
|
||||
rects[rects.length - 1] || anchorProbe.getBoundingClientRect();
|
||||
// rect x is already in the iframe's own (content) space,
|
||||
// i.e. scroll-invariant: page = floor(x / column stride)
|
||||
const desired = (Math.floor(ar.x / r.size) + 1) * r.size;
|
||||
const needsScroll = Math.abs(r.start - desired) > r.size * 0.02;
|
||||
if (needsScroll) r.scrollBy(desired - r.start, 0);
|
||||
const clamped = this.clampSelectionToPage(sel, doc, r);
|
||||
if (clamped)
|
||||
// the clamp's selectionchange re-arms this settle; the
|
||||
// next pass finds view and selection in place and opens
|
||||
// the popover
|
||||
return;
|
||||
if (needsScroll) {
|
||||
setTimeout(() => {
|
||||
const s = doc.getSelection();
|
||||
if (s && s.rangeCount && !s.isCollapsed && s.type === "Range")
|
||||
this.openPopoverForSelection(s, doc, index);
|
||||
}, 350);
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* anchor geometry unavailable */
|
||||
}
|
||||
}
|
||||
this.openPopoverForSelection(sel, doc, index);
|
||||
}, 400);
|
||||
},
|
||||
// PDF mirror of scheduleSelectionPopover: the gesture takeover
|
||||
// swallows pointerup for touch selection drags in the fx iframe too,
|
||||
// so a selection that has been stable for a moment is the settled
|
||||
// gesture — open the popover for it.
|
||||
schedulePDFSelectionPopover(doc: any, index: number) {
|
||||
if (this.selPopTimer) clearTimeout(this.selPopTimer);
|
||||
this.selPopTimer = setTimeout(() => {
|
||||
this.selPopTimer = null;
|
||||
// A tap on a painted highlight re-opens the popover in edit mode
|
||||
// via its own path — never clobber it.
|
||||
if (this.selectionPopover.open && this.selectionPopover.mode === "edit")
|
||||
return;
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) {
|
||||
if (
|
||||
this.selectionPopover.open &&
|
||||
this.selectionPopover.mode === "create"
|
||||
)
|
||||
this.hideSelectionPopover();
|
||||
return;
|
||||
}
|
||||
this.checkPDFSelectionFor(doc, index);
|
||||
}, 400);
|
||||
},
|
||||
checkPDFSelectionFor(doc: any, index: number) {
|
||||
const sel = doc.getSelection();
|
||||
if (!sel || sel.isCollapsed || !sel.rangeCount) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
// Denominator: the element whose post-transform screen rect IS
|
||||
// the visible page. For PDFs that's the rendered canvas — pdf.js
|
||||
// scales <html> by 1/devicePixelRatio, so documentElement's rect
|
||||
// is dpr× too small and would inflate every fraction (highlight
|
||||
// shifted right/oversized on any dpr != 1 display). The canvas's
|
||||
// rect is in the same transform-inclusive space as the textLayer
|
||||
// span rects, so the dpr scaling cancels exactly.
|
||||
const denom =
|
||||
(doc.querySelector("#canvas canvas") as HTMLElement) ||
|
||||
(doc.querySelector("img") as HTMLElement);
|
||||
let dr = denom?.getBoundingClientRect();
|
||||
if (!dr || !dr.width || !dr.height) {
|
||||
const vw = doc.defaultView;
|
||||
dr = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
width: vw?.innerWidth || 1,
|
||||
height: vw?.innerHeight || 1,
|
||||
} as DOMRect;
|
||||
}
|
||||
const rects: number[][] = [];
|
||||
for (const r of range.getClientRects()) {
|
||||
const x = (r.left - dr.left) / dr.width;
|
||||
const y = (r.top - dr.top) / dr.height;
|
||||
const w = r.width / dr.width;
|
||||
const h = r.height / dr.height;
|
||||
if (w > 0 && h > 0) rects.push([x, y, w, h]);
|
||||
}
|
||||
if (!rects.length) return;
|
||||
// Map the first rect to host-space for popover placement.
|
||||
// The canvas's screen rect maps 1:1 onto the host iframe box
|
||||
// (the visible page fills the iframe), so sx/sy are 1 for PDFs;
|
||||
// kept general for the comic img fallback.
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
if (!frame) return;
|
||||
const fr = frame.getBoundingClientRect();
|
||||
const first = range.getBoundingClientRect();
|
||||
const sx = fr.width / dr.width;
|
||||
const sy = fr.height / dr.height;
|
||||
// On touch the browser's own selection menu attaches ABOVE the
|
||||
// selection and its drag handles sit just below it — place ours
|
||||
// below both (same +90 clearance the reflowable path uses). Fine
|
||||
// pointers (desktop) keep the above-the-selection placement.
|
||||
const coarse = window.matchMedia("(pointer: coarse)").matches;
|
||||
const y = coarse
|
||||
? fr.top + first.bottom * sy + 90
|
||||
: fr.top + first.top * sy;
|
||||
this.pdfSelDoc = doc;
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: fr.left + first.left * sx + (first.width * sx) / 2,
|
||||
y,
|
||||
text,
|
||||
cfi: "",
|
||||
pdfPage: index,
|
||||
pdfRects: rects,
|
||||
});
|
||||
},
|
||||
// Trim a touch selection to the visible page. The visible column
|
||||
// occupies content x [start - size, start]; endpoints beyond it are
|
||||
// re-mapped to the page edge on their own line via caretRangeFromPoint.
|
||||
clampSelectionToPage(sel: any, doc: any, r: any): boolean {
|
||||
const left = r.start - r.size;
|
||||
const right = r.start;
|
||||
const TOL = 8;
|
||||
const backwardProbe = doc.createRange();
|
||||
backwardProbe.setStart(sel.anchorNode, sel.anchorOffset);
|
||||
backwardProbe.setEnd(sel.focusNode, sel.focusOffset);
|
||||
const backward = backwardProbe.collapsed;
|
||||
const range = sel.getRangeAt(0);
|
||||
const probeRect = (node: any, offset: number, forward: boolean) => {
|
||||
const p = doc.createRange();
|
||||
p.setStart(node, offset);
|
||||
if (node?.nodeType === 3) {
|
||||
if (forward && offset < node.length)
|
||||
p.setEnd(node, offset + 1);
|
||||
else if (!forward && offset > 0)
|
||||
p.setStart(node, offset - 1);
|
||||
}
|
||||
if (p.collapsed) return null;
|
||||
const rects = p.getClientRects();
|
||||
return rects.length ? rects[forward ? 0 : rects.length - 1] : null;
|
||||
};
|
||||
const caretAt = (x: number, y: number) => {
|
||||
const caret = doc.caretRangeFromPoint?.(x, y)
|
||||
?? doc.caretPositionFromPoint?.(x, y);
|
||||
if (!caret) return null;
|
||||
return {
|
||||
node: caret.startContainer ?? caret.offsetNode,
|
||||
offset: caret.startOffset ?? caret.offset,
|
||||
};
|
||||
};
|
||||
// selection end beyond the visible page (into the next column):
|
||||
// trim to the last position on the visible page (bottom-right
|
||||
// corner of the column — nearest-text mapping is unambiguous
|
||||
// from the margin)
|
||||
const endRect = probeRect(range.endContainer, range.endOffset, false);
|
||||
if (endRect && endRect.x + endRect.width > right + TOL) {
|
||||
const caret = caretAt(
|
||||
right - 15, doc.documentElement.clientHeight - 40);
|
||||
if (caret && range.isPointInRange(caret.node, caret.offset)) {
|
||||
if (backward)
|
||||
sel.setBaseAndExtent(caret.node, caret.offset,
|
||||
sel.focusNode, sel.focusOffset);
|
||||
else sel.extend(caret.node, caret.offset);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// selection start before the visible page (previous column):
|
||||
// trim to the first position on the visible page (top-left corner)
|
||||
const startRect = probeRect(range.startContainer, range.startOffset, true);
|
||||
if (startRect && startRect.x < left - TOL) {
|
||||
const caret = caretAt(left + 15, 40);
|
||||
if (caret && range.isPointInRange(caret.node, caret.offset)) {
|
||||
if (backward)
|
||||
sel.extend(caret.node, caret.offset);
|
||||
else
|
||||
sel.setBaseAndExtent(caret.node, caret.offset,
|
||||
sel.focusNode, sel.focusOffset);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
openPopoverForSelection(sel: any, doc: any, index: number) {
|
||||
const range = sel.getRangeAt(0);
|
||||
const text = sel.toString().replace(/\s+/g, " ").trim();
|
||||
if (!text) return;
|
||||
let cfi: string;
|
||||
let cfiEnd: string;
|
||||
try {
|
||||
cfi = this.view.getCFI(index, range);
|
||||
// Collapse to the end point for a distinct end anchor —
|
||||
// KOReader sync renders the highlight box from pos0/pos1, and
|
||||
// pos1 == pos0 would be a degenerate (zero-length) range.
|
||||
const endRange = range.cloneRange();
|
||||
endRange.collapse(false);
|
||||
cfiEnd = this.view.getCFI(index, endRange);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const frame = doc.defaultView?.frameElement as HTMLElement | null;
|
||||
const iframeRect = frame?.getBoundingClientRect();
|
||||
const rect = range.getBoundingClientRect();
|
||||
// On touch the browser's own selection menu attaches ABOVE the
|
||||
// selection (and paints over page content) — place ours BELOW it
|
||||
// so both are visible. The popover anchors bottom-center at y-10
|
||||
// and is ~44px tall; the drag handles occupy the first 24px below
|
||||
// the selection, so the offset must clear them too.
|
||||
const coarse = window.matchMedia("(pointer: coarse)").matches;
|
||||
const y = coarse
|
||||
? (iframeRect?.top ?? 0) + rect.bottom + 90
|
||||
: (iframeRect?.top ?? 0) + rect.top;
|
||||
this.openSelectionPopover({
|
||||
mode: "create",
|
||||
x: (iframeRect?.left ?? 0) + rect.left + rect.width / 2,
|
||||
y,
|
||||
text,
|
||||
cfi,
|
||||
cfiEnd,
|
||||
});
|
||||
if (coarse && this.view?.book?.dir !== "rtl")
|
||||
this.showSelectionHandles(doc);
|
||||
},
|
||||
renderAllHighlights() {
|
||||
for (const hl of this.highlightItems) {
|
||||
if (hl.pdfPage >= 0) {
|
||||
@@ -1332,7 +1625,12 @@ document.addEventListener("alpine:init", () => {
|
||||
const rows = await hlResp.json();
|
||||
this.highlightItems = (rows as any[])
|
||||
.map((r) => this.mapHighlightRow(r))
|
||||
.filter((hl: any) => hl.cfi || hl.pdfPage >= 0);
|
||||
// Comic-treated items show no highlights (bookmarks only),
|
||||
// and previously-created PDF highlights stop rendering.
|
||||
.filter(
|
||||
(hl: any) =>
|
||||
!this.treatAsComic && (hl.cfi || hl.pdfPage >= 0),
|
||||
);
|
||||
this.renderAllHighlights();
|
||||
}
|
||||
if (noteResp.ok) {
|
||||
@@ -1351,6 +1649,9 @@ document.addEventListener("alpine:init", () => {
|
||||
const p = this.selectionPopover;
|
||||
const token = getToken();
|
||||
if (!token || !this.mediaItemId || (!p.cfi && p.pdfPage < 0)) return;
|
||||
// The note editor writes into p.note live; "Highlight with note"
|
||||
// creates the highlight AND its note in one POST.
|
||||
const note = p.note.trim();
|
||||
const pdfAnchor =
|
||||
p.pdfPage >= 0
|
||||
? JSON.stringify({ v: 1, page: p.pdfPage, rects: p.pdfRects })
|
||||
@@ -1371,7 +1672,7 @@ document.addEventListener("alpine:init", () => {
|
||||
epubcfi_start: p.pdfPage >= 0 ? pdfAnchor : p.cfi,
|
||||
epubcfi_end: p.pdfPage >= 0 ? pdfAnchor : p.cfiEnd,
|
||||
color,
|
||||
note_text: "",
|
||||
note_text: note,
|
||||
percentage_start: this.lastRelocateDetail?.fraction ?? 0,
|
||||
}),
|
||||
},
|
||||
@@ -1400,16 +1701,23 @@ document.addEventListener("alpine:init", () => {
|
||||
? ""
|
||||
: this.toRangeCfi(p.cfi, p.cfiEnd, p.text) || p.cfi,
|
||||
color,
|
||||
note: "",
|
||||
note,
|
||||
id: row.id,
|
||||
});
|
||||
// Clear the DOM selection: the gesture is consumed, and it is
|
||||
// what keeps Chrome's native copy/share menu on screen.
|
||||
try {
|
||||
this.currentDoc?.getSelection()?.removeAllRanges();
|
||||
} catch {
|
||||
/* doc may be gone */
|
||||
}
|
||||
}
|
||||
this.hideSelectionPopover();
|
||||
} catch (_e) {
|
||||
/* ignore highlight errors */
|
||||
}
|
||||
},
|
||||
async saveHighlightChanges() {
|
||||
async saveHighlightChanges(keepNoteOpen = false) {
|
||||
const p = this.selectionPopover;
|
||||
const token = getToken();
|
||||
if (!token || !this.mediaItemId || !p.id) return;
|
||||
@@ -1473,7 +1781,9 @@ document.addEventListener("alpine:init", () => {
|
||||
id: p.id,
|
||||
});
|
||||
}
|
||||
p.noteOpen = false;
|
||||
// Explicit saves (the Save button) close the whole popover; color
|
||||
// tweaks made while the note editor is open keep everything open.
|
||||
if (!keepNoteOpen) this.hideSelectionPopover();
|
||||
} catch (_e) {
|
||||
/* ignore highlight errors */
|
||||
}
|
||||
@@ -1504,12 +1814,170 @@ document.addEventListener("alpine:init", () => {
|
||||
}
|
||||
},
|
||||
async copySelectionText() {
|
||||
const text = this.selectionPopover.text;
|
||||
try {
|
||||
await navigator.clipboard.writeText(this.selectionPopover.text);
|
||||
this.hideSelectionPopover();
|
||||
} catch (_e) {
|
||||
/* clipboard unavailable */
|
||||
if (navigator.clipboard?.writeText) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
this.finishCopy();
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
/* fall through to the legacy path */
|
||||
}
|
||||
// Plain HTTP (typical LAN access) has no navigator.clipboard;
|
||||
// a transient textarea + execCommand works from this user
|
||||
// gesture on insecure contexts.
|
||||
try {
|
||||
const ta = document.createElement("textarea");
|
||||
ta.value = text;
|
||||
ta.style.position = "fixed";
|
||||
ta.style.opacity = "0";
|
||||
document.body.append(ta);
|
||||
ta.select();
|
||||
document.execCommand("copy");
|
||||
ta.remove();
|
||||
this.finishCopy();
|
||||
} catch {
|
||||
/* ignore clipboard errors */
|
||||
}
|
||||
},
|
||||
finishCopy() {
|
||||
try {
|
||||
this.currentDoc?.getSelection()?.removeAllRanges();
|
||||
} catch {
|
||||
/* doc may be gone */
|
||||
}
|
||||
this.hideSelectionPopover();
|
||||
},
|
||||
// ----- touch selection handles -----
|
||||
// The native selection handles cannot be used (the custom gesture's
|
||||
// touchstart preventDefault disables them along with the rest of the
|
||||
// native controller), so adjustment after lift happens through these.
|
||||
// Purely imperative DOM — position updates run on every drag frame.
|
||||
selectionHandles: {
|
||||
root: null as HTMLElement | null,
|
||||
start: null as HTMLElement | null,
|
||||
end: null as HTMLElement | null,
|
||||
drag: null as "start" | "end" | null,
|
||||
doc: null as any,
|
||||
},
|
||||
ensureSelectionHandles() {
|
||||
const h = this.selectionHandles as any;
|
||||
if (h.root) return;
|
||||
const root = document.createElement("div");
|
||||
root.id = "selection-handles";
|
||||
const make = (which: "start" | "end") => {
|
||||
const el = document.createElement("div");
|
||||
el.className = "sel-handle";
|
||||
el.addEventListener("pointerdown", (e: PointerEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
h.drag = which;
|
||||
el.setPointerCapture(e.pointerId);
|
||||
});
|
||||
el.addEventListener("pointermove", (e: PointerEvent) => {
|
||||
if (h.drag !== which) return;
|
||||
e.preventDefault();
|
||||
this.dragSelectionHandle(e);
|
||||
});
|
||||
const release = () => {
|
||||
h.drag = null;
|
||||
};
|
||||
el.addEventListener("pointerup", release);
|
||||
el.addEventListener("pointercancel", release);
|
||||
root.append(el);
|
||||
return el;
|
||||
};
|
||||
h.start = make("start");
|
||||
h.end = make("end");
|
||||
h.root = root;
|
||||
document.body.append(root);
|
||||
},
|
||||
showSelectionHandles(doc: any) {
|
||||
this.ensureSelectionHandles();
|
||||
const h = this.selectionHandles as any;
|
||||
h.doc = doc;
|
||||
h.root!.style.display = "block";
|
||||
this.positionSelectionHandles(doc);
|
||||
},
|
||||
hideSelectionHandles() {
|
||||
const h = this.selectionHandles as any;
|
||||
if (!h.root) return;
|
||||
h.root.style.display = "none";
|
||||
h.drag = null;
|
||||
},
|
||||
// Host-space position of a selection boundary: the left edge of the
|
||||
// start caret or the right edge of the end caret, at the line bottom.
|
||||
caretHostPos(doc: any, node: any, offset: number, forward: boolean) {
|
||||
try {
|
||||
const p = doc.createRange();
|
||||
p.setStart(node, offset);
|
||||
if (node?.nodeType === 3) {
|
||||
if (forward && offset < node.length) p.setEnd(node, offset + 1);
|
||||
else if (!forward && offset > 0) p.setStart(node, offset - 1);
|
||||
}
|
||||
if (p.collapsed) return null;
|
||||
const rects = p.getClientRects();
|
||||
const r = rects[forward ? 0 : rects.length - 1];
|
||||
if (!r) return null;
|
||||
const off = doc.defaultView?.frameElement?.getBoundingClientRect();
|
||||
return {
|
||||
x: (off?.left ?? 0) + (forward ? r.left : r.right),
|
||||
y: (off?.top ?? 0) + r.bottom,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
positionSelectionHandles(doc: any) {
|
||||
const h = this.selectionHandles as any;
|
||||
if (!h.root || h.root.style.display === "none") return;
|
||||
const sel = doc.getSelection();
|
||||
if (!sel?.rangeCount || sel.isCollapsed) {
|
||||
this.hideSelectionHandles();
|
||||
return;
|
||||
}
|
||||
const range = sel.getRangeAt(0);
|
||||
const startPos = this.caretHostPos(doc, range.startContainer,
|
||||
range.startOffset, true);
|
||||
const endPos = this.caretHostPos(doc, range.endContainer,
|
||||
range.endOffset, false);
|
||||
if (!startPos || !endPos) {
|
||||
this.hideSelectionHandles();
|
||||
return;
|
||||
}
|
||||
h.start!.style.transform = `translate(${startPos.x - 6}px, ${startPos.y}px)`;
|
||||
h.end!.style.transform = `translate(${endPos.x - 6}px, ${endPos.y}px)`;
|
||||
},
|
||||
dragSelectionHandle(ev: PointerEvent) {
|
||||
const h = this.selectionHandles as any;
|
||||
const doc = h.doc;
|
||||
const r: any = this.renderer;
|
||||
if (!doc || !r || r.scrolled) return;
|
||||
if (this.view?.book?.dir === "rtl") return;
|
||||
const frame = doc.defaultView?.frameElement;
|
||||
if (!frame) return;
|
||||
const off = frame.getBoundingClientRect();
|
||||
// pointer → iframe content space, clamped to the visible page
|
||||
const size = r.size;
|
||||
const left = r.start - size;
|
||||
const right = r.start;
|
||||
const x = Math.min(Math.max(ev.clientX - off.left, left + 2), right - 2);
|
||||
const y = ev.clientY - off.top;
|
||||
const caret = doc.caretRangeFromPoint?.(x, y)
|
||||
?? doc.caretPositionFromPoint?.(x, y);
|
||||
if (!caret) return;
|
||||
const node = caret.startContainer ?? caret.offsetNode;
|
||||
const offset = caret.startOffset ?? caret.offset;
|
||||
const sel = doc.getSelection();
|
||||
if (!sel?.rangeCount || !node) return;
|
||||
const range = sel.getRangeAt(0);
|
||||
if (h.drag === "start")
|
||||
sel.setBaseAndExtent(node, offset, range.endContainer, range.endOffset);
|
||||
else
|
||||
sel.setBaseAndExtent(range.startContainer, range.startOffset,
|
||||
node, offset);
|
||||
this.positionSelectionHandles(doc);
|
||||
},
|
||||
goToHighlight(hl: {
|
||||
cfi: string;
|
||||
@@ -1698,6 +2166,8 @@ document.addEventListener("alpine:init", () => {
|
||||
},
|
||||
setInteractionMode(mode: string) {
|
||||
if (!this.isFixedLayout || !this.isPDF) return;
|
||||
// Comic-treated PDFs have no text selection: pan only.
|
||||
if (this.treatAsComic) return;
|
||||
if (mode !== "select" && mode !== "pan" && mode !== "text") return;
|
||||
this.renderer.setAttribute("interaction-mode", mode);
|
||||
this.interactionMode = mode;
|
||||
@@ -1792,8 +2262,9 @@ document.addEventListener("alpine:init", () => {
|
||||
},
|
||||
toggleSearch() {
|
||||
// Reflowable books use foliate's DOM search; PDFs use extracted text.
|
||||
// Comics have no text at all.
|
||||
if (this.isFixedLayout && !this.isPDF) return;
|
||||
// Comics have no text at all — and comic-treated PDFs (manga scans)
|
||||
// opt out of search to match comic treatment: bookmarks only.
|
||||
if ((this.isFixedLayout && !this.isPDF) || this.treatAsComic) return;
|
||||
const opening = !this.searchOpen;
|
||||
this.closeDrawers();
|
||||
this.searchOpen = opening;
|
||||
@@ -2201,8 +2672,8 @@ document.addEventListener("alpine:init", () => {
|
||||
this.hyphenate = true;
|
||||
this.chromeBehavior = "auto-hide";
|
||||
this.applyChromeBehavior();
|
||||
this.interactionMode = "select";
|
||||
this.setInteractionMode("select");
|
||||
this.interactionMode = this.treatAsComic ? "pan" : "select";
|
||||
this.setInteractionMode(this.interactionMode);
|
||||
this.fxBrightness = 1;
|
||||
this.fxContrast = 1;
|
||||
this.fxInvert = false;
|
||||
|
||||
Vendored
+1
-1
File diff suppressed because one or more lines are too long
@@ -1434,3 +1434,34 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch selection handles (custom gesture — the native ones are disabled) */
|
||||
#selection-handles {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
z-index: 55;
|
||||
}
|
||||
#selection-handles .sel-handle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 12px;
|
||||
height: 24px;
|
||||
pointer-events: auto;
|
||||
touch-action: none;
|
||||
background: #8ab4f8;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.45);
|
||||
opacity: 0.95;
|
||||
}
|
||||
#selection-handles .sel-handle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 50%;
|
||||
width: 2px;
|
||||
height: 5px;
|
||||
background: inherit;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user