# Conflicts: # templates/admin_library.templ # templates/admin_library_templ.go # templates/conflicts_templ.go # templates/unlinked_books_templ.go
This commit is contained in:
+26
-6
@@ -50,6 +50,16 @@ func main() {
|
||||
}
|
||||
log.Println("✅ Database schema initialized and verified, starting server...")
|
||||
|
||||
// Load tunable settings from the DB into the registry. All values fall back
|
||||
// to compiled defaults if a row is missing, so this never blocks startup.
|
||||
registry := database.NewSettingsRegistry(queries)
|
||||
if err := registry.Load(ctx); err != nil {
|
||||
log.Printf("⚠️ Could not load system settings (using defaults): %v", err)
|
||||
}
|
||||
// Wire the registry into the package-level password validator so live
|
||||
// rule changes apply to the echo struct-tag validator and ValidatePassword.
|
||||
middleware.SetDefaultPasswordSettings(registry)
|
||||
|
||||
// Seed base_url from env var if not already configured. Uses conditional
|
||||
// UPDATE so admin-set values are never overwritten on restart.
|
||||
if cfg.BaseURL != "" {
|
||||
@@ -79,15 +89,20 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
// Create login attempt tracker: 5 failed attempts = 15 minute lockout
|
||||
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(5, 15*time.Minute, 5*time.Minute)
|
||||
// Create login attempt tracker from configured (or default) lockout policy.
|
||||
loginMaxAttempts, loginLockout := registry.LoginLockout()
|
||||
loginAttemptTracker := ratelimit.NewLoginAttemptTracker(loginMaxAttempts, loginLockout, 5*time.Minute)
|
||||
|
||||
authHandler := handlers.NewAuthHandler(queries, cfg.JWTSecret, loginAttemptTracker)
|
||||
authHandler.SetSettings(registry)
|
||||
systemSettingsHandler := handlers.NewSystemSettingsHandler(queries)
|
||||
systemSettingsHandler.SetSettings(registry)
|
||||
sidecarHandler := handlers.NewSidecarHandler(queries, cfg)
|
||||
sidecarHandler.SetSettings(registry)
|
||||
libraryHandler := handlers.NewLibraryHandler(queries)
|
||||
deviceHandler := handlers.NewDeviceHandler(queries, cfg.JWTSecret, cfg)
|
||||
deviceAuthMiddleware := middleware.NewDeviceAuthMiddleware(queries)
|
||||
deviceAuthMiddleware.SetSettings(registry)
|
||||
processingIssuesHandler := handlers.NewProcessingIssuesHandler(queries)
|
||||
|
||||
// Create WebSocket connection manager
|
||||
@@ -95,10 +110,11 @@ func main() {
|
||||
|
||||
progressService := sync.NewProgressService(queries, connManager)
|
||||
annotationService := sync.NewAnnotationService(queries, connManager)
|
||||
tombstonePurgerCancel := annotationService.StartTombstonePurger()
|
||||
defer tombstonePurgerCancel()
|
||||
annotationService.SetSettings(registry)
|
||||
maintenanceCancel := annotationService.StartDailyMaintenance()
|
||||
defer maintenanceCancel()
|
||||
|
||||
queueProcessor := sync.NewSyncQueueProcessor(queries)
|
||||
queueProcessor := sync.NewSyncQueueProcessorWithConfig(queries, registry.SyncQueueConfig().Interval, registry.SyncQueueConfig().BatchSize)
|
||||
queueProcessor.SetProgressService(progressService)
|
||||
queueProcessor.SetAnnotationService(annotationService)
|
||||
|
||||
@@ -109,7 +125,8 @@ func main() {
|
||||
libraryService.SyncAllowedExtensions(context.Background())
|
||||
|
||||
// Create worker for background tasks
|
||||
worker := services.NewWorker(3, connManager)
|
||||
workerCfg := registry.WorkerPoolConfig()
|
||||
worker := services.NewWorkerWithConfig(workerCfg.Size, workerCfg.QueueCap, connManager)
|
||||
services.WorkerInstance = worker
|
||||
|
||||
koreaderHandler := handlers.NewKOReaderHandler(queries, connManager, queueProcessor)
|
||||
@@ -122,7 +139,9 @@ func main() {
|
||||
queueHandler := handlers.NewQueueHandler(queries, queueProcessor)
|
||||
|
||||
conversionService := services.NewConversionService(queries, "/var/bookhoard/cache/kepub")
|
||||
conversionService.SetSettings(registry)
|
||||
opdsHandler := handlers.NewOPDSHandler(queries, libraryService, conversionService)
|
||||
opdsHandler.SetSettings(registry)
|
||||
|
||||
collectionHandler := handlers.NewCollectionHandler(queries, libraryService, connManager)
|
||||
dashboardService := services.NewDashboardService(queries)
|
||||
@@ -175,6 +194,7 @@ func main() {
|
||||
Echo: e,
|
||||
Queries: queries,
|
||||
Cfg: cfg,
|
||||
Settings: registry,
|
||||
DBPool: dbPool,
|
||||
AuthHandler: authHandler,
|
||||
LibraryHandler: libraryHandler,
|
||||
|
||||
@@ -45,11 +45,47 @@ CREATE TABLE IF NOT EXISTS system_settings (
|
||||
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Insert default system settings
|
||||
INSERT INTO system_settings (setting_key, setting_value, description) VALUES
|
||||
('scan_poll_interval_seconds', '60', 'How often to scan all libraries in minutes'),
|
||||
('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide'),
|
||||
('default_timezone', 'UTC', 'System default timezone')
|
||||
-- Extend system_settings with typed metadata so it can back the admin UI's
|
||||
-- configurable tunables. All columns are nullable for backward compatibility
|
||||
-- with the original three rows and any pre-existing data.
|
||||
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS setting_type VARCHAR(20);
|
||||
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS min_value TEXT;
|
||||
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS max_value TEXT;
|
||||
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS requires_restart BOOLEAN DEFAULT FALSE;
|
||||
ALTER TABLE system_settings ADD COLUMN IF NOT EXISTS category VARCHAR(40);
|
||||
|
||||
-- Insert default system settings (original scan/timezone rows + tunables).
|
||||
-- Values match the previous hardcoded literals, so behavior is unchanged on upgrade.
|
||||
-- ON CONFLICT DO NOTHING preserves any admin-modified values.
|
||||
INSERT INTO system_settings (setting_key, setting_value, description, setting_type, min_value, max_value, requires_restart, category) VALUES
|
||||
('scan_poll_interval_seconds', '60', 'How often to scan all libraries (seconds)', 'int', '1', '3600', FALSE, 'scanner'),
|
||||
('auto_scan_enabled', 'true', 'Whether auto-scanning is enabled system-wide', 'bool', NULL, NULL, FALSE, 'scanner'),
|
||||
('default_timezone', 'UTC', 'System default timezone', 'string', NULL, NULL, FALSE, 'general'),
|
||||
-- security / auth (live)
|
||||
('session_duration_seconds', '604800', 'How long a login session stays valid', 'int', '300', '31536000', FALSE, 'security'),
|
||||
('password_min_length', '8', 'Minimum password length', 'int', '1', '128', FALSE, 'security'),
|
||||
('password_require_upper', 'true', 'Require at least one uppercase letter (A-Z)', 'bool', NULL, NULL, FALSE, 'security'),
|
||||
('password_require_lower', 'true', 'Require at least one lowercase letter (a-z)', 'bool', NULL, NULL, FALSE, 'security'),
|
||||
('password_require_number', 'true', 'Require at least one number (0-9)', 'bool', NULL, NULL, FALSE, 'security'),
|
||||
('password_require_special', 'true', 'Require at least one special character', 'bool', NULL, NULL, FALSE, 'security'),
|
||||
-- security / auth (restart required)
|
||||
('auth_rate_limit_per_min', '10', 'Global auth API rate limit (requests per minute)', 'int', '1', '10000', TRUE, 'security'),
|
||||
('login_max_attempts', '5', 'Failed login attempts before lockout', 'int', '1', '100', TRUE, 'security'),
|
||||
('login_lockout_minutes', '15', 'Lockout duration after too many failed logins', 'int', '1', '10080', TRUE, 'security'),
|
||||
-- api (live)
|
||||
('opds_default_page_size', '50', 'Default OPDS page size', 'int', '1', '500', FALSE, 'api'),
|
||||
('opds_max_page_size', '200', 'Maximum OPDS page size', 'int', '1', '1000', FALSE, 'api'),
|
||||
('device_rate_sync_per_min', '60', 'Device sync requests per minute', 'int', '1', '10000', FALSE, 'api'),
|
||||
('device_rate_progress_per_min', '120', 'Device progress requests per minute', 'int', '1', '10000', FALSE, 'api'),
|
||||
('device_rate_metadata_per_min', '30', 'Device metadata requests per minute', 'int', '1', '10000', FALSE, 'api'),
|
||||
-- sync / performance (live)
|
||||
('annotation_tombstone_ttl_days', '30', 'How long deleted annotations are kept before purge', 'int', '1', '3650', FALSE, 'sync'),
|
||||
('conversion_cache_ttl_hours', '24', 'How long converted (kepub) files are cached', 'int', '1', '720', FALSE, 'performance'),
|
||||
-- sync / performance (restart required)
|
||||
('sync_queue_interval_seconds', '5', 'How often the sync queue flushes', 'int', '1', '3600', TRUE, 'sync'),
|
||||
('sync_queue_batch_size', '50', 'Maximum items processed per sync queue flush', 'int', '1', '10000', TRUE, 'sync'),
|
||||
('worker_pool_size', '3', 'Number of background worker goroutines', 'int', '1', '100', TRUE, 'performance'),
|
||||
('worker_queue_cap', '100', 'Background worker job queue capacity', 'int', '1', '10000', TRUE, 'performance')
|
||||
ON CONFLICT (setting_key) DO NOTHING;
|
||||
|
||||
-- Create refresh_tokens table
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
@@ -484,11 +484,16 @@ type SystemConfig struct {
|
||||
}
|
||||
|
||||
type SystemSettings struct {
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
SettingKey string `db:"setting_key" json:"setting_key"`
|
||||
SettingValue string `db:"setting_value" json:"setting_value"`
|
||||
Description pgtype.Text `db:"description" json:"description"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
ID pgtype.UUID `db:"id" json:"id"`
|
||||
SettingKey string `db:"setting_key" json:"setting_key"`
|
||||
SettingValue string `db:"setting_value" json:"setting_value"`
|
||||
Description pgtype.Text `db:"description" json:"description"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
SettingType pgtype.Text `db:"setting_type" json:"setting_type"`
|
||||
MinValue pgtype.Text `db:"min_value" json:"min_value"`
|
||||
MaxValue pgtype.Text `db:"max_value" json:"max_value"`
|
||||
RequiresRestart pgtype.Bool `db:"requires_restart" json:"requires_restart"`
|
||||
Category pgtype.Text `db:"category" json:"category"`
|
||||
}
|
||||
|
||||
type UnlinkedBooks struct {
|
||||
@@ -532,3 +537,4 @@ type Users struct {
|
||||
Timezone pgtype.Text `db:"timezone" json:"timezone"`
|
||||
UpdatedAt pgtype.Timestamptz `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// sqlc v1.31.1
|
||||
|
||||
package database
|
||||
|
||||
@@ -26,7 +26,7 @@ type Querier interface {
|
||||
CheckForProgressConflicts(ctx context.Context, arg CheckForProgressConflictsParams) (int64, error)
|
||||
// Cleanup expired OPDS tokens
|
||||
CleanupExpiredOpdsTokens(ctx context.Context) error
|
||||
CleanupExpiredRefreshTokens(ctx context.Context) error
|
||||
CleanupExpiredRefreshTokens(ctx context.Context, dollar_1 float64) error
|
||||
ClearDeviceSyncQueue(ctx context.Context, deviceID pgtype.UUID) error
|
||||
ClearKoboShelf(ctx context.Context, deviceID pgtype.UUID) error
|
||||
ClearKoboShelfByName(ctx context.Context, arg ClearKoboShelfByNameParams) error
|
||||
@@ -137,6 +137,7 @@ type Querier interface {
|
||||
// Get all system config
|
||||
GetAllSystemConfig(ctx context.Context) ([]SystemConfig, error)
|
||||
GetAllSystemSettings(ctx context.Context) ([]GetAllSystemSettingsRow, error)
|
||||
GetAllSystemSettingsFull(ctx context.Context) ([]SystemSettings, error)
|
||||
GetAnnotationsForBook(ctx context.Context, arg GetAnnotationsForBookParams) ([]GetAnnotationsForBookRow, error)
|
||||
GetBooksByTag(ctx context.Context, arg GetBooksByTagParams) ([]MediaItems, error)
|
||||
// Get collection
|
||||
@@ -278,6 +279,7 @@ type Querier interface {
|
||||
GetSystemConfig(ctx context.Context, key string) (SystemConfig, error)
|
||||
// System Settings queries
|
||||
GetSystemSetting(ctx context.Context, settingKey string) (string, error)
|
||||
GetSystemSettingFull(ctx context.Context, settingKey string) (SystemSettings, error)
|
||||
GetSystemTimezone(ctx context.Context) (string, error)
|
||||
GetTombstonedAnnotationsForBook(ctx context.Context, arg GetTombstonedAnnotationsForBookParams) ([]GetTombstonedAnnotationsForBookRow, error)
|
||||
// Get universal progress for a book
|
||||
@@ -428,6 +430,7 @@ type Querier interface {
|
||||
UpsertDashboardPreferences(ctx context.Context, arg UpsertDashboardPreferencesParams) (UserDashboardPreferences, error)
|
||||
UpsertPanelData(ctx context.Context, arg UpsertPanelDataParams) (PanelData, error)
|
||||
UpsertReaderSettings(ctx context.Context, arg UpsertReaderSettingsParams) (ReaderSettings, error)
|
||||
UpsertSystemSetting(ctx context.Context, arg UpsertSystemSettingParams) (SystemSettings, error)
|
||||
}
|
||||
|
||||
var _ Querier = (*Queries)(nil)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.30.0
|
||||
// sqlc v1.31.1
|
||||
// source: queries.sql
|
||||
|
||||
package database
|
||||
@@ -186,11 +186,11 @@ func (q *Queries) CleanupExpiredOpdsTokens(ctx context.Context) error {
|
||||
}
|
||||
|
||||
const CleanupExpiredRefreshTokens = `-- name: CleanupExpiredRefreshTokens :exec
|
||||
DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked_at IS NOT NULL AND revoked_at < NOW() - INTERVAL '7 days')
|
||||
DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked_at IS NOT NULL AND revoked_at < NOW() - make_interval(secs => $1::double precision))
|
||||
`
|
||||
|
||||
func (q *Queries) CleanupExpiredRefreshTokens(ctx context.Context) error {
|
||||
_, err := q.db.Exec(ctx, CleanupExpiredRefreshTokens)
|
||||
func (q *Queries) CleanupExpiredRefreshTokens(ctx context.Context, dollar_1 float64) error {
|
||||
_, err := q.db.Exec(ctx, CleanupExpiredRefreshTokens, dollar_1)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2258,6 +2258,41 @@ func (q *Queries) GetAllSystemSettings(ctx context.Context) ([]GetAllSystemSetti
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetAllSystemSettingsFull = `-- name: GetAllSystemSettingsFull :many
|
||||
SELECT id, setting_key, setting_value, description, updated_at, setting_type, min_value, max_value, requires_restart, category FROM system_settings ORDER BY category, setting_key
|
||||
`
|
||||
|
||||
func (q *Queries) GetAllSystemSettingsFull(ctx context.Context) ([]SystemSettings, error) {
|
||||
rows, err := q.db.Query(ctx, GetAllSystemSettingsFull)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
items := []SystemSettings{}
|
||||
for rows.Next() {
|
||||
var i SystemSettings
|
||||
if err := rows.Scan(
|
||||
&i.ID,
|
||||
&i.SettingKey,
|
||||
&i.SettingValue,
|
||||
&i.Description,
|
||||
&i.UpdatedAt,
|
||||
&i.SettingType,
|
||||
&i.MinValue,
|
||||
&i.MaxValue,
|
||||
&i.RequiresRestart,
|
||||
&i.Category,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const GetAnnotationsForBook = `-- name: GetAnnotationsForBook :many
|
||||
SELECT
|
||||
mh.id,
|
||||
@@ -6587,6 +6622,28 @@ func (q *Queries) GetSystemSetting(ctx context.Context, settingKey string) (stri
|
||||
return setting_value, err
|
||||
}
|
||||
|
||||
const GetSystemSettingFull = `-- name: GetSystemSettingFull :one
|
||||
SELECT id, setting_key, setting_value, description, updated_at, setting_type, min_value, max_value, requires_restart, category FROM system_settings WHERE setting_key = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSystemSettingFull(ctx context.Context, settingKey string) (SystemSettings, error) {
|
||||
row := q.db.QueryRow(ctx, GetSystemSettingFull, settingKey)
|
||||
var i SystemSettings
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SettingKey,
|
||||
&i.SettingValue,
|
||||
&i.Description,
|
||||
&i.UpdatedAt,
|
||||
&i.SettingType,
|
||||
&i.MinValue,
|
||||
&i.MaxValue,
|
||||
&i.RequiresRestart,
|
||||
&i.Category,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const GetSystemTimezone = `-- name: GetSystemTimezone :one
|
||||
SELECT setting_value FROM system_settings WHERE setting_key = 'default_timezone'
|
||||
`
|
||||
@@ -12280,3 +12337,56 @@ func (q *Queries) UpsertReaderSettings(ctx context.Context, arg UpsertReaderSett
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const UpsertSystemSetting = `-- name: UpsertSystemSetting :one
|
||||
INSERT INTO system_settings (setting_key, setting_value, description, setting_type, min_value, max_value, requires_restart, category)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (setting_key) DO UPDATE
|
||||
SET setting_value = EXCLUDED.setting_value,
|
||||
description = EXCLUDED.description,
|
||||
setting_type = EXCLUDED.setting_type,
|
||||
min_value = EXCLUDED.min_value,
|
||||
max_value = EXCLUDED.max_value,
|
||||
requires_restart = EXCLUDED.requires_restart,
|
||||
category = EXCLUDED.category,
|
||||
updated_at = NOW()
|
||||
RETURNING id, setting_key, setting_value, description, updated_at, setting_type, min_value, max_value, requires_restart, category
|
||||
`
|
||||
|
||||
type UpsertSystemSettingParams struct {
|
||||
SettingKey string `db:"setting_key" json:"setting_key"`
|
||||
SettingValue string `db:"setting_value" json:"setting_value"`
|
||||
Description pgtype.Text `db:"description" json:"description"`
|
||||
SettingType pgtype.Text `db:"setting_type" json:"setting_type"`
|
||||
MinValue pgtype.Text `db:"min_value" json:"min_value"`
|
||||
MaxValue pgtype.Text `db:"max_value" json:"max_value"`
|
||||
RequiresRestart pgtype.Bool `db:"requires_restart" json:"requires_restart"`
|
||||
Category pgtype.Text `db:"category" json:"category"`
|
||||
}
|
||||
|
||||
func (q *Queries) UpsertSystemSetting(ctx context.Context, arg UpsertSystemSettingParams) (SystemSettings, error) {
|
||||
row := q.db.QueryRow(ctx, UpsertSystemSetting,
|
||||
arg.SettingKey,
|
||||
arg.SettingValue,
|
||||
arg.Description,
|
||||
arg.SettingType,
|
||||
arg.MinValue,
|
||||
arg.MaxValue,
|
||||
arg.RequiresRestart,
|
||||
arg.Category,
|
||||
)
|
||||
var i SystemSettings
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.SettingKey,
|
||||
&i.SettingValue,
|
||||
&i.Description,
|
||||
&i.UpdatedAt,
|
||||
&i.SettingType,
|
||||
&i.MinValue,
|
||||
&i.MaxValue,
|
||||
&i.RequiresRestart,
|
||||
&i.Category,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -373,9 +373,29 @@ SELECT setting_value FROM system_settings WHERE setting_key = $1;
|
||||
-- name: UpdateSystemSetting :exec
|
||||
UPDATE system_settings SET setting_value = $2, updated_at = NOW() WHERE setting_key = $1;
|
||||
|
||||
-- name: UpsertSystemSetting :one
|
||||
INSERT INTO system_settings (setting_key, setting_value, description, setting_type, min_value, max_value, requires_restart, category)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (setting_key) DO UPDATE
|
||||
SET setting_value = EXCLUDED.setting_value,
|
||||
description = EXCLUDED.description,
|
||||
setting_type = EXCLUDED.setting_type,
|
||||
min_value = EXCLUDED.min_value,
|
||||
max_value = EXCLUDED.max_value,
|
||||
requires_restart = EXCLUDED.requires_restart,
|
||||
category = EXCLUDED.category,
|
||||
updated_at = NOW()
|
||||
RETURNING *;
|
||||
|
||||
-- name: GetSystemSettingFull :one
|
||||
SELECT * FROM system_settings WHERE setting_key = $1;
|
||||
|
||||
-- name: GetAllSystemSettings :many
|
||||
SELECT setting_key, setting_value, description FROM system_settings ORDER BY setting_key;
|
||||
|
||||
-- name: GetAllSystemSettingsFull :many
|
||||
SELECT * FROM system_settings ORDER BY category, setting_key;
|
||||
|
||||
-- name: CreateMediaRating :one
|
||||
INSERT INTO media_ratings (media_item_id, user_id, rating)
|
||||
VALUES ($1, $2, $3)
|
||||
@@ -1007,7 +1027,7 @@ UPDATE refresh_tokens SET revoked_at = NOW() WHERE token = $1;
|
||||
UPDATE refresh_tokens SET revoked_at = NOW() WHERE user_id = $1 AND revoked_at IS NULL;
|
||||
|
||||
-- name: CleanupExpiredRefreshTokens :exec
|
||||
DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked_at IS NOT NULL AND revoked_at < NOW() - INTERVAL '7 days');
|
||||
DELETE FROM refresh_tokens WHERE expires_at < NOW() OR (revoked_at IS NOT NULL AND revoked_at < NOW() - make_interval(secs => $1::double precision));
|
||||
|
||||
-- ============================================
|
||||
-- FORMAT DETECTION & PROGRESS
|
||||
|
||||
@@ -0,0 +1,342 @@
|
||||
package database
|
||||
|
||||
// SettingsRegistry provides a typed, cached view over the system_settings table.
|
||||
// It is the single source of truth for tunable runtime values that used to be
|
||||
// hardcoded as Go literals.
|
||||
//
|
||||
// Consumers call the domain-specific getters (SessionDuration, OpdsPageSize,
|
||||
// etc.) which read from an in-memory cache. The cache is populated by Load at
|
||||
// startup and refreshed by Reload whenever a setting is written. Getters always
|
||||
// fall back to a compiled-in default if the DB value is missing or unparsable,
|
||||
// so a corrupt or deleted row can never break the app.
|
||||
//
|
||||
// SettingsRegistry lives in the database package (rather than its own package)
|
||||
// so that every consumer already imports database and does not need to take on
|
||||
// a new package import.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SettingType enumerates the value types stored in system_settings.setting_type.
|
||||
const (
|
||||
SettingTypeInt = "int"
|
||||
SettingTypeBool = "bool"
|
||||
SettingTypeString = "string"
|
||||
SettingTypeStringList = "string_list"
|
||||
)
|
||||
|
||||
// SecondsPerDay / SecondsPerHour are conversion helpers used by defaults.
|
||||
const (
|
||||
SecondsPerMinute = 60
|
||||
SecondsPerHour = 3600
|
||||
SecondsPerDay = 86400
|
||||
)
|
||||
|
||||
// SettingDefault holds the fallback value for a key. These mirror the literals that
|
||||
// were previously hardcoded in the source so an empty/corrupt DB row preserves
|
||||
// prior behavior exactly.
|
||||
type SettingDefault struct {
|
||||
Key string
|
||||
Value string
|
||||
Type string
|
||||
Min string
|
||||
Max string
|
||||
RequiresRestart bool
|
||||
Category string
|
||||
Group string
|
||||
Description string
|
||||
}
|
||||
|
||||
// SettingDefaults is the source of truth for fallback values and metadata. New keys
|
||||
// must be added here AND seeded in database/schema/schema.sql. Entries are ordered
|
||||
// by (RequiresRestart, Group) so the admin UI renders coherent sub-sections.
|
||||
var SettingDefaults = []SettingDefault{
|
||||
{Key: "scan_poll_interval_seconds", Value: "60", Type: SettingTypeInt, Min: "1", Max: "3600", Category: "scanner", Group: "Scanning", Description: "How often to scan all libraries (seconds)"},
|
||||
{Key: "auto_scan_enabled", Value: "true", Type: SettingTypeBool, Category: "scanner", Group: "Scanning", Description: "Whether auto-scanning is enabled system-wide"},
|
||||
{Key: "default_timezone", Value: "UTC", Type: SettingTypeString, Category: "general", Group: "System Defaults", Description: "System default timezone"},
|
||||
{Key: "session_duration_seconds", Value: "604800", Type: SettingTypeInt, Min: "300", Max: "31536000", Category: "security", Group: "Session", Description: "How long a login session stays valid"},
|
||||
{Key: "password_min_length", Value: "8", Type: SettingTypeInt, Min: "1", Max: "128", Category: "security", Group: "Password Quality", Description: "Minimum password length"},
|
||||
{Key: "password_require_upper", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one uppercase letter (A-Z)"},
|
||||
{Key: "password_require_lower", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one lowercase letter (a-z)"},
|
||||
{Key: "password_require_number", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one number (0-9)"},
|
||||
{Key: "password_require_special", Value: "true", Type: SettingTypeBool, Category: "security", Group: "Password Quality", Description: "Require at least one special character"},
|
||||
{Key: "opds_default_page_size", Value: "50", Type: SettingTypeInt, Min: "1", Max: "500", Category: "api", Group: "OPDS Catalog", Description: "Default OPDS page size"},
|
||||
{Key: "opds_max_page_size", Value: "200", Type: SettingTypeInt, Min: "1", Max: "1000", Category: "api", Group: "OPDS Catalog", Description: "Maximum OPDS page size"},
|
||||
{Key: "device_rate_sync_per_min", Value: "60", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device sync requests per minute"},
|
||||
{Key: "device_rate_progress_per_min", Value: "120", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device progress requests per minute"},
|
||||
{Key: "device_rate_metadata_per_min", Value: "30", Type: SettingTypeInt, Min: "1", Max: "10000", Category: "api", Group: "Device Rate Limits", Description: "Device metadata requests per minute"},
|
||||
{Key: "annotation_tombstone_ttl_days", Value: "30", Type: SettingTypeInt, Min: "1", Max: "3650", Category: "sync", Group: "Annotation Retention", Description: "How long deleted annotations are kept before purge"},
|
||||
{Key: "conversion_cache_ttl_hours", Value: "24", Type: SettingTypeInt, Min: "1", Max: "720", Category: "performance", Group: "Conversion Cache", Description: "How long converted (kepub) files are cached"},
|
||||
{Key: "auth_rate_limit_per_min", Value: "10", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "security", Group: "Auth Rate Limiting", Description: "Global auth API rate limit (requests per minute)"},
|
||||
{Key: "login_max_attempts", Value: "5", Type: SettingTypeInt, Min: "1", Max: "100", RequiresRestart: true, Category: "security", Group: "Login Lockout", Description: "Failed login attempts before lockout"},
|
||||
{Key: "login_lockout_minutes", Value: "15", Type: SettingTypeInt, Min: "1", Max: "10080", RequiresRestart: true, Category: "security", Group: "Login Lockout", Description: "Lockout duration after too many failed logins"},
|
||||
{Key: "sync_queue_interval_seconds", Value: "5", Type: SettingTypeInt, Min: "1", Max: "3600", RequiresRestart: true, Category: "sync", Group: "Sync Queue", Description: "How often the sync queue flushes"},
|
||||
{Key: "sync_queue_batch_size", Value: "50", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "sync", Group: "Sync Queue", Description: "Maximum items processed per sync queue flush"},
|
||||
{Key: "worker_pool_size", Value: "3", Type: SettingTypeInt, Min: "1", Max: "100", RequiresRestart: true, Category: "performance", Group: "Worker Pool", Description: "Number of background worker goroutines"},
|
||||
{Key: "worker_queue_cap", Value: "100", Type: SettingTypeInt, Min: "1", Max: "10000", RequiresRestart: true, Category: "performance", Group: "Worker Pool", Description: "Background worker job queue capacity"},
|
||||
}
|
||||
|
||||
// defaultBy indexes SettingDefaults by key for O(1) lookup.
|
||||
var defaultBy = func() map[string]SettingDefault {
|
||||
m := make(map[string]SettingDefault, len(SettingDefaults))
|
||||
for _, d := range SettingDefaults {
|
||||
m[d.Key] = d
|
||||
}
|
||||
return m
|
||||
}()
|
||||
|
||||
// Registry caches system_settings values in memory. The zero value is not
|
||||
// usable; construct with New.
|
||||
type SettingsRegistry struct {
|
||||
q *Queries
|
||||
|
||||
mu sync.RWMutex
|
||||
values map[string]string
|
||||
loadedAt time.Time
|
||||
}
|
||||
|
||||
// New returns a Registry backed by the given queries. The cache is empty
|
||||
// until Load is called.
|
||||
func NewSettingsRegistry(q *Queries) *SettingsRegistry {
|
||||
return &SettingsRegistry{q: q, values: make(map[string]string)}
|
||||
}
|
||||
|
||||
// Load populates the cache from the database. Missing rows fall back to the
|
||||
// compiled defaults. Safe to call multiple times.
|
||||
func (r *SettingsRegistry) Load(ctx context.Context) error {
|
||||
rows, err := r.q.GetAllSystemSettings(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fresh := make(map[string]string, len(SettingDefaults))
|
||||
for _, d := range SettingDefaults {
|
||||
fresh[d.Key] = d.Value
|
||||
}
|
||||
for _, row := range rows {
|
||||
if _, ok := fresh[row.SettingKey]; ok {
|
||||
fresh[row.SettingKey] = row.SettingValue
|
||||
}
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.values = fresh
|
||||
r.loadedAt = time.Now()
|
||||
r.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reload refreshes the cache from the database. Should be called after any
|
||||
// setting write. On error the cache is left untouched and the error is logged.
|
||||
func (r *SettingsRegistry) Reload(ctx context.Context) {
|
||||
if err := r.Load(ctx); err != nil {
|
||||
log.Printf("settings: reload failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// raw returns the cached string value for a key (or the default), clamped to
|
||||
// [min, max] for int-typed keys.
|
||||
func (r *SettingsRegistry) raw(key string) string {
|
||||
r.mu.RLock()
|
||||
v, ok := r.values[key]
|
||||
r.mu.RUnlock()
|
||||
if !ok || v == "" {
|
||||
v = defaultBy[key].Value
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *SettingsRegistry) getInt(key string) int {
|
||||
d := defaultBy[key]
|
||||
v := r.raw(key)
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
n, _ = strconv.Atoi(d.Value)
|
||||
}
|
||||
if d.Min != "" {
|
||||
if mn, err := strconv.Atoi(d.Min); err == nil && n < mn {
|
||||
n = mn
|
||||
}
|
||||
}
|
||||
if d.Max != "" {
|
||||
if mx, err := strconv.Atoi(d.Max); err == nil && n > mx {
|
||||
n = mx
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (r *SettingsRegistry) getBool(key string) bool {
|
||||
v := r.raw(key)
|
||||
b, err := strconv.ParseBool(v)
|
||||
if err != nil {
|
||||
b, _ = strconv.ParseBool(defaultBy[key].Value)
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// ---- Domain-specific getters (call sites use these) ----
|
||||
|
||||
// ScanPollInterval is how often the scanner polls, as a duration.
|
||||
func (r *SettingsRegistry) ScanPollInterval() time.Duration {
|
||||
return time.Duration(r.getInt("scan_poll_interval_seconds")) * time.Second
|
||||
}
|
||||
|
||||
// AutoScanEnabled reports whether auto-scanning is on.
|
||||
func (r *SettingsRegistry) AutoScanEnabled() bool { return r.getBool("auto_scan_enabled") }
|
||||
|
||||
// DefaultTimezone returns the configured default timezone name.
|
||||
func (r *SettingsRegistry) DefaultTimezone() string { return r.raw("default_timezone") }
|
||||
|
||||
// SessionDuration is how long a login session / refresh token stays valid.
|
||||
func (r *SettingsRegistry) SessionDuration() time.Duration {
|
||||
return time.Duration(r.getInt("session_duration_seconds")) * time.Second
|
||||
}
|
||||
|
||||
// PasswordMinLength is the minimum password length.
|
||||
func (r *SettingsRegistry) PasswordMinLength() int { return r.getInt("password_min_length") }
|
||||
|
||||
// PasswordRules bundles the active complexity requirements.
|
||||
type PasswordRules struct {
|
||||
MinLength int
|
||||
Upper bool
|
||||
Lower bool
|
||||
Number bool
|
||||
Special bool
|
||||
}
|
||||
|
||||
// PasswordRules returns the active password complexity configuration.
|
||||
func (r *SettingsRegistry) PasswordRules() PasswordRules {
|
||||
return PasswordRules{
|
||||
MinLength: r.PasswordMinLength(),
|
||||
Upper: r.getBool("password_require_upper"),
|
||||
Lower: r.getBool("password_require_lower"),
|
||||
Number: r.getBool("password_require_number"),
|
||||
Special: r.getBool("password_require_special"),
|
||||
}
|
||||
}
|
||||
|
||||
// AuthRateLimit is the global auth endpoint rate limit (requests/minute). Read
|
||||
// once at startup.
|
||||
func (r *SettingsRegistry) AuthRateLimit() int { return r.getInt("auth_rate_limit_per_min") }
|
||||
|
||||
// LoginLockout returns (max attempts, lockout duration). Read once at startup.
|
||||
func (r *SettingsRegistry) LoginLockout() (int, time.Duration) {
|
||||
return r.getInt("login_max_attempts"), time.Duration(r.getInt("login_lockout_minutes")) * time.Minute
|
||||
}
|
||||
|
||||
// OpdsDefaultPageSize is the default OPDS items-per-page.
|
||||
func (r *SettingsRegistry) OpdsDefaultPageSize() int { return r.getInt("opds_default_page_size") }
|
||||
|
||||
// OpdsMaxPageSize is the maximum items-per-page a client may request.
|
||||
func (r *SettingsRegistry) OpdsMaxPageSize() int { return r.getInt("opds_max_page_size") }
|
||||
|
||||
// DeviceRateLimits bundles the per-route device rate limits (requests/minute).
|
||||
type DeviceRateLimits struct {
|
||||
Sync int
|
||||
Progress int
|
||||
Metadata int
|
||||
}
|
||||
|
||||
// DeviceRateLimits returns the active device rate limits.
|
||||
func (r *SettingsRegistry) DeviceRateLimits() DeviceRateLimits {
|
||||
return DeviceRateLimits{
|
||||
Sync: r.getInt("device_rate_sync_per_min"),
|
||||
Progress: r.getInt("device_rate_progress_per_min"),
|
||||
Metadata: r.getInt("device_rate_metadata_per_min"),
|
||||
}
|
||||
}
|
||||
|
||||
// TombstoneTTL is how long deleted annotations are retained before purge.
|
||||
func (r *SettingsRegistry) TombstoneTTL() time.Duration {
|
||||
return time.Duration(r.getInt("annotation_tombstone_ttl_days")) * 24 * time.Hour
|
||||
}
|
||||
|
||||
// ConversionCacheTTL is how long converted (kepub) files are served from cache.
|
||||
func (r *SettingsRegistry) ConversionCacheTTL() time.Duration {
|
||||
return time.Duration(r.getInt("conversion_cache_ttl_hours")) * time.Hour
|
||||
}
|
||||
|
||||
// SyncQueueConfig bundles the sync queue interval and batch size. Read at
|
||||
// startup; changes require a restart.
|
||||
type SyncQueueConfig struct {
|
||||
Interval time.Duration
|
||||
BatchSize int
|
||||
}
|
||||
|
||||
// SyncQueueConfig returns the active sync queue configuration.
|
||||
func (r *SettingsRegistry) SyncQueueConfig() SyncQueueConfig {
|
||||
return SyncQueueConfig{
|
||||
Interval: time.Duration(r.getInt("sync_queue_interval_seconds")) * time.Second,
|
||||
BatchSize: r.getInt("sync_queue_batch_size"),
|
||||
}
|
||||
}
|
||||
|
||||
// WorkerPoolConfig bundles worker count and queue capacity. Read at startup;
|
||||
// changes require a restart.
|
||||
type WorkerPoolConfig struct {
|
||||
Size int
|
||||
QueueCap int
|
||||
}
|
||||
|
||||
// WorkerPoolConfig returns the active worker pool configuration.
|
||||
func (r *SettingsRegistry) WorkerPoolConfig() WorkerPoolConfig {
|
||||
return WorkerPoolConfig{
|
||||
Size: r.getInt("worker_pool_size"),
|
||||
QueueCap: r.getInt("worker_queue_cap"),
|
||||
}
|
||||
}
|
||||
|
||||
// SettingEntry exposes one setting's metadata + current value, for the admin UI/API.
|
||||
type SettingEntry struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
Type string `json:"type"`
|
||||
Min string `json:"min,omitempty"`
|
||||
Max string `json:"max,omitempty"`
|
||||
RequiresRestart bool `json:"requires_restart"`
|
||||
Category string `json:"category"`
|
||||
Group string `json:"group"`
|
||||
Description string `json:"description"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
}
|
||||
|
||||
// All returns metadata + current values for every known setting, grouped by
|
||||
// the in-memory cache (which reflects the DB after Load/Reload).
|
||||
func (r *SettingsRegistry) All() []SettingEntry {
|
||||
r.mu.RLock()
|
||||
vals := make(map[string]string, len(r.values))
|
||||
for k, v := range r.values {
|
||||
vals[k] = v
|
||||
}
|
||||
r.mu.RUnlock()
|
||||
out := make([]SettingEntry, 0, len(SettingDefaults))
|
||||
for _, d := range SettingDefaults {
|
||||
v, ok := vals[d.Key]
|
||||
if !ok {
|
||||
v = d.Value
|
||||
}
|
||||
out = append(out, SettingEntry{
|
||||
Key: d.Key,
|
||||
Value: v,
|
||||
Type: d.Type,
|
||||
Min: d.Min,
|
||||
Max: d.Max,
|
||||
RequiresRestart: d.RequiresRestart,
|
||||
Category: d.Category,
|
||||
Group: d.Group,
|
||||
Description: d.Description,
|
||||
IsDefault: v == d.Value,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// LookupDefault returns the compiled-in SettingDefault for a key (ok=false if unknown).
|
||||
func LookupDefault(key string) (SettingDefault, bool) {
|
||||
d, ok := defaultBy[key]
|
||||
return d, ok
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestSettingDefaults ensures every seeded setting has a compiled default with
|
||||
// a valid value for its declared type. This guards against typos that would
|
||||
// silently fall back at runtime.
|
||||
func TestSettingDefaults(t *testing.T) {
|
||||
if len(SettingDefaults) == 0 {
|
||||
t.Fatal("SettingDefaults is empty")
|
||||
}
|
||||
for _, d := range SettingDefaults {
|
||||
if d.Key == "" {
|
||||
t.Errorf("default has empty key: %+v", d)
|
||||
continue
|
||||
}
|
||||
switch d.Type {
|
||||
case SettingTypeInt:
|
||||
if _, err := strconv.Atoi(d.Value); err != nil {
|
||||
t.Errorf("int setting %s default %q is not an int: %v", d.Key, d.Value, err)
|
||||
}
|
||||
if d.Min != "" {
|
||||
if _, err := strconv.Atoi(d.Min); err != nil {
|
||||
t.Errorf("int setting %s min %q is not an int", d.Key, d.Min)
|
||||
}
|
||||
}
|
||||
if d.Max != "" {
|
||||
if _, err := strconv.Atoi(d.Max); err != nil {
|
||||
t.Errorf("int setting %s max %q is not an int", d.Key, d.Max)
|
||||
}
|
||||
}
|
||||
case SettingTypeBool:
|
||||
if _, err := strconv.ParseBool(d.Value); err != nil {
|
||||
t.Errorf("bool setting %s default %q is not a bool", d.Key, d.Value)
|
||||
}
|
||||
case SettingTypeString:
|
||||
if d.Value == "" {
|
||||
t.Errorf("string setting %s has empty default", d.Key)
|
||||
}
|
||||
default:
|
||||
t.Errorf("setting %s has unknown type %q", d.Key, d.Type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSettingsRegistryGetIntClamping verifies that out-of-range DB values are
|
||||
// clamped to the declared min/max, and that garbage falls back to the default.
|
||||
func TestSettingsRegistryGetIntClamping(t *testing.T) {
|
||||
r := &SettingsRegistry{values: map[string]string{}, q: nil}
|
||||
|
||||
// Seed with an over-max value; expect clamping to the max (3600).
|
||||
r.values["scan_poll_interval_seconds"] = "999999"
|
||||
if got := r.ScanPollInterval(); got.Seconds() != 3600 {
|
||||
t.Errorf("expected clamp to 3600, got %v", got)
|
||||
}
|
||||
|
||||
// Seed with an under-min value; expect clamp to min (1).
|
||||
r.values["scan_poll_interval_seconds"] = "0"
|
||||
if got := r.ScanPollInterval(); got.Seconds() != 1 {
|
||||
t.Errorf("expected clamp to 1, got %v", got)
|
||||
}
|
||||
|
||||
// Seed with garbage; expect fallback to default (60).
|
||||
r.values["scan_poll_interval_seconds"] = "not-a-number"
|
||||
if got := r.ScanPollInterval(); got.Seconds() != 60 {
|
||||
t.Errorf("expected fallback default 60, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSettingsRegistryGetBoolFallback verifies bool parsing and fallback.
|
||||
func TestSettingsRegistryGetBoolFallback(t *testing.T) {
|
||||
r := &SettingsRegistry{values: map[string]string{}, q: nil}
|
||||
|
||||
r.values["auto_scan_enabled"] = "true"
|
||||
if !r.AutoScanEnabled() {
|
||||
t.Error("expected true")
|
||||
}
|
||||
|
||||
r.values["auto_scan_enabled"] = "garbage"
|
||||
// garbage falls back to default ("true")
|
||||
if !r.AutoScanEnabled() {
|
||||
t.Error("expected fallback to default true")
|
||||
}
|
||||
}
|
||||
|
||||
// TestLookupDefaultUnknownKey verifies unknown keys return ok=false.
|
||||
func TestLookupDefaultUnknownKey(t *testing.T) {
|
||||
if _, ok := LookupDefault("does_not_exist"); ok {
|
||||
t.Error("expected ok=false for unknown key")
|
||||
}
|
||||
if _, ok := LookupDefault("session_duration_seconds"); !ok {
|
||||
t.Error("expected ok=true for known key")
|
||||
}
|
||||
}
|
||||
+32
-11
@@ -26,14 +26,34 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// Session duration constants
|
||||
// Follows same pattern as refresh_token.go
|
||||
SessionDuration = 7 * 24 * time.Hour // 7 days
|
||||
// DefaultSessionDuration is the fallback session duration used when no
|
||||
// settings registry is wired (matches the historical 7-day value).
|
||||
DefaultSessionDuration = 7 * 24 * time.Hour
|
||||
)
|
||||
|
||||
// SessionDurationSec is the session duration in seconds for use in cookies and API responses
|
||||
// Note: This is computed from SessionDuration to avoid magic numbers
|
||||
var SessionDurationSec = int(SessionDuration.Seconds())
|
||||
// SessionDurationSec is retained for backward compatibility; new code uses the
|
||||
// registry via AuthHandler.sessionDuration().
|
||||
var SessionDurationSec = int(DefaultSessionDuration.Seconds())
|
||||
|
||||
// SetSettings wires the tunable settings registry (optional).
|
||||
func (h *AuthHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }
|
||||
|
||||
// sessionDuration returns the active session duration from the registry.
|
||||
func (h *AuthHandler) sessionDuration() time.Duration {
|
||||
if h.settings != nil {
|
||||
return h.settings.SessionDuration()
|
||||
}
|
||||
return DefaultSessionDuration
|
||||
}
|
||||
|
||||
// refreshTokenTTL returns the active refresh-token lifetime (shared with the
|
||||
// session duration), with a compiled-default fallback.
|
||||
func (h *AuthHandler) refreshTokenTTL() time.Duration {
|
||||
if h.settings != nil {
|
||||
return h.settings.SessionDuration()
|
||||
}
|
||||
return DefaultSessionDuration
|
||||
}
|
||||
|
||||
var secure = os.Getenv("COOKIE_SECURE")
|
||||
|
||||
@@ -41,6 +61,7 @@ type AuthHandler struct {
|
||||
db *database.Queries
|
||||
jwtKey []byte
|
||||
loginAttemptTracker *middleware.LoginAttemptTracker
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewAuthHandler(db *database.Queries, jwtSecret string, loginAttemptTracker *middleware.LoginAttemptTracker) *AuthHandler {
|
||||
@@ -265,7 +286,7 @@ func (h *AuthHandler) Register(c *echo.Context) error {
|
||||
HttpOnly: true,
|
||||
Secure: secure == "true", // TODO: Set to true in production with HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: SessionDurationSec,
|
||||
MaxAge: int(h.sessionDuration().Seconds()),
|
||||
}
|
||||
c.SetCookie(cookie)
|
||||
|
||||
@@ -298,7 +319,7 @@ window.location.href = '/dashboard';
|
||||
Token: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: SessionDurationSec,
|
||||
ExpiresIn: int(h.sessionDuration().Seconds()),
|
||||
User: UserProfile{
|
||||
ID: uuid.UUID(user.ID.Bytes).String(),
|
||||
Email: user.Email,
|
||||
@@ -411,7 +432,7 @@ func (h *AuthHandler) Login(c *echo.Context) error {
|
||||
HttpOnly: true,
|
||||
Secure: secure == "true", // TODO: Set to true in production with HTTPS
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
MaxAge: SessionDurationSec,
|
||||
MaxAge: int(h.sessionDuration().Seconds()),
|
||||
}
|
||||
c.SetCookie(cookie)
|
||||
|
||||
@@ -450,7 +471,7 @@ window.location.href = '%s';
|
||||
Token: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: SessionDurationSec,
|
||||
ExpiresIn: int(h.sessionDuration().Seconds()),
|
||||
User: UserProfile{
|
||||
ID: uuid.UUID(user.ID.Bytes).String(),
|
||||
Email: user.Email,
|
||||
@@ -1014,7 +1035,7 @@ func (h *AuthHandler) generateJWTWithAllClaims(userID, userRole, userEmail, user
|
||||
"user_role": userRole,
|
||||
"user_email": userEmail,
|
||||
"user_username": userUsername,
|
||||
"exp": time.Now().Add(SessionDuration).Unix(),
|
||||
"exp": time.Now().Add(h.sessionDuration()).Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
}
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
|
||||
@@ -623,7 +623,7 @@ func (h *KoboHandler) Markup(c *echo.Context) error {
|
||||
}
|
||||
|
||||
if h.annotationSvc != nil && len(processedBooks) > 0 {
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true}
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
|
||||
for mediaItemID, contentId := range processedBooks {
|
||||
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
||||
MediaItemID: mediaItemID,
|
||||
|
||||
@@ -854,7 +854,7 @@ func (h *KOReaderHandler) GetMetadata(c *echo.Context) error {
|
||||
annotationsResponse.Bookmarks = append(annotationsResponse.Bookmarks, koreaderBookmark)
|
||||
}
|
||||
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-wsync.TombstoneTTL), Valid: true}
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-h.annotationSvc.ActiveTombstoneTTL()), Valid: true}
|
||||
tombstones, _ := h.db.GetTombstonedAnnotationsForBook(c.Request().Context(), database.GetTombstonedAnnotationsForBookParams{
|
||||
MediaItemID: pgBookUUID,
|
||||
UserID: pgUserID,
|
||||
|
||||
@@ -26,6 +26,26 @@ type OPDSHandler struct {
|
||||
conversionService interface {
|
||||
ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*services.ConvertedKEPUB, error)
|
||||
}
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry (OPDS page size).
|
||||
func (h *OPDSHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }
|
||||
|
||||
// opdsDefaultPageSize returns the configured default page size (50 if unset).
|
||||
func (h *OPDSHandler) opdsDefaultPageSize() int {
|
||||
if h.settings != nil {
|
||||
return h.settings.OpdsDefaultPageSize()
|
||||
}
|
||||
return 50
|
||||
}
|
||||
|
||||
// opdsMaxPageSize returns the configured maximum page size (200 if unset).
|
||||
func (h *OPDSHandler) opdsMaxPageSize() int {
|
||||
if h.settings != nil {
|
||||
return h.settings.OpdsMaxPageSize()
|
||||
}
|
||||
return 200
|
||||
}
|
||||
|
||||
func NewOPDSHandler(db *database.Queries, libraryService *services.LibraryService, conversionService interface {
|
||||
@@ -168,9 +188,10 @@ func (h *OPDSHandler) GetDeviceCatalog(c *echo.Context) error {
|
||||
}
|
||||
}
|
||||
|
||||
perPageNum := 50
|
||||
perPageNum := h.opdsDefaultPageSize()
|
||||
maxPerPage := h.opdsMaxPageSize()
|
||||
if perPage != "" {
|
||||
if num, err := strconv.Atoi(perPage); err == nil && num > 0 && num <= 200 {
|
||||
if num, err := strconv.Atoi(perPage); err == nil && num > 0 && num <= maxPerPage {
|
||||
perPageNum = num
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"context"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -139,3 +140,8 @@ func (h *ProcessingIssuesHandler) DeleteProcessingIssue(c *echo.Context) error {
|
||||
"message": "Issue deleted",
|
||||
})
|
||||
}
|
||||
|
||||
// GetProcessingIssueStatsData returns stats for SSR (not JSON response)
|
||||
func (h *ProcessingIssuesHandler) GetProcessingIssueStatsData(ctx context.Context, libraryID pgtype.UUID) (database.GetProcessingIssueStatsRow, error) {
|
||||
return h.db.GetProcessingIssueStats(ctx, libraryID)
|
||||
}
|
||||
|
||||
@@ -14,10 +14,6 @@ import (
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
refreshTokenExpiration = 7 * 24 * time.Hour // 7 days
|
||||
)
|
||||
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
@@ -72,7 +68,7 @@ func (h *AuthHandler) RefreshAccessToken(c *echo.Context) error {
|
||||
return c.JSON(http.StatusOK, RefreshTokenResponse{
|
||||
AccessToken: accessToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: SessionDurationSec,
|
||||
ExpiresIn: int(h.refreshTokenTTL().Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -101,7 +97,7 @@ func (h *AuthHandler) CreateRefreshToken(userID uuid.UUID) (string, string, erro
|
||||
tokenUUID := uuid.New()
|
||||
refreshToken := tokenUUID.String()
|
||||
|
||||
expiresAt := time.Now().Add(refreshTokenExpiration)
|
||||
expiresAt := time.Now().Add(h.refreshTokenTTL())
|
||||
_, err := h.db.CreateRefreshToken(context.Background(), database.CreateRefreshTokenParams{
|
||||
UserID: pgtype.UUID{Bytes: userID, Valid: true},
|
||||
Token: pgtype.UUID{Bytes: tokenUUID, Valid: true},
|
||||
|
||||
@@ -15,14 +15,19 @@ import (
|
||||
)
|
||||
|
||||
type SidecarHandler struct {
|
||||
db *database.Queries
|
||||
cfg *config.Config
|
||||
db *database.Queries
|
||||
cfg *config.Config
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewSidecarHandler(db *database.Queries, cfg *config.Config) *SidecarHandler {
|
||||
return &SidecarHandler{db: db, cfg: cfg}
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry so the timezone write path
|
||||
// keeps the cache consistent.
|
||||
func (h *SidecarHandler) SetSettings(s *database.SettingsRegistry) { h.settings = s }
|
||||
|
||||
type SidecarConfig struct {
|
||||
Version string `json:"version"`
|
||||
Bookhoard SidecarBookhoardConfig `json:"bookhoard"`
|
||||
@@ -400,6 +405,9 @@ func (h *SidecarHandler) UpdateSystemConfiguration(c *echo.Context) error {
|
||||
"error": "failed to update default timezone",
|
||||
})
|
||||
}
|
||||
if h.settings != nil {
|
||||
h.settings.Reload(ctx)
|
||||
}
|
||||
continue
|
||||
}
|
||||
_, err := h.db.SetSystemConfig(ctx, database.SetSystemConfigParams{
|
||||
|
||||
@@ -2,17 +2,21 @@ package handlers
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
type SystemSettingsHandler struct {
|
||||
db *database.Queries
|
||||
db *database.Queries
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewSystemSettingsHandler(db *database.Queries) *SystemSettingsHandler {
|
||||
@@ -21,6 +25,154 @@ func NewSystemSettingsHandler(db *database.Queries) *SystemSettingsHandler {
|
||||
}
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry. Required for the unified
|
||||
// /api/system/settings endpoints and for cache invalidation after writes.
|
||||
func (h *SystemSettingsHandler) SetSettings(s *database.SettingsRegistry) {
|
||||
h.settings = s
|
||||
}
|
||||
|
||||
// reload refreshes the in-memory cache after a write.
|
||||
func (h *SystemSettingsHandler) reload(c *echo.Context) {
|
||||
if h.settings != nil {
|
||||
h.settings.Reload(c.Request().Context())
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Unified /api/system/settings endpoints ----
|
||||
|
||||
// GetSettings handles GET /api/system/settings.
|
||||
func (h *SystemSettingsHandler) GetSettings(c *echo.Context) error {
|
||||
if h.settings == nil {
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "settings registry not initialized"})
|
||||
}
|
||||
return c.JSON(http.StatusOK, h.settings.All())
|
||||
}
|
||||
|
||||
// UpdateSettingRequest is the body for PUT /api/system/settings.
|
||||
type UpdateSettingRequest struct {
|
||||
Key string `json:"key" form:"key"`
|
||||
Value string `json:"value" form:"value"`
|
||||
}
|
||||
|
||||
// UpdateSettingResponse mirrors a settings entry plus a reload hint.
|
||||
type UpdateSettingResponse struct {
|
||||
database.SettingEntry
|
||||
ReloadRequired bool `json:"reload_required"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// UpdateSetting handles PUT /api/system/settings.
|
||||
func (h *SystemSettingsHandler) UpdateSetting(c *echo.Context) error {
|
||||
if h.settings == nil {
|
||||
return c.JSON(http.StatusServiceUnavailable, map[string]string{"error": "settings registry not initialized"})
|
||||
}
|
||||
var req UpdateSettingRequest
|
||||
if err := c.Bind(&req); err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": "invalid request"})
|
||||
}
|
||||
resp, err := h.ApplySetting(c.Request().Context(), req.Key, req.Value)
|
||||
if err != nil {
|
||||
return c.JSON(http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
}
|
||||
return c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// ApplySetting validates, persists, and reloads a single setting. Shared by the
|
||||
// JSON API and the HTMX admin endpoint.
|
||||
func (h *SystemSettingsHandler) ApplySetting(ctx context.Context, key, value string) (UpdateSettingResponse, error) {
|
||||
if h.settings == nil {
|
||||
return UpdateSettingResponse{}, fmt.Errorf("settings registry not initialized")
|
||||
}
|
||||
if key == "" {
|
||||
return UpdateSettingResponse{}, fmt.Errorf("key is required")
|
||||
}
|
||||
def, ok := database.LookupDefault(key)
|
||||
if !ok {
|
||||
return UpdateSettingResponse{}, fmt.Errorf("unknown setting key: %s", key)
|
||||
}
|
||||
if err := validateSettingValue(def, value); err != nil {
|
||||
return UpdateSettingResponse{}, err
|
||||
}
|
||||
|
||||
desc := def.Description
|
||||
rType := pgtype.Text{}
|
||||
if def.Type != "" {
|
||||
rType = pgtype.Text{String: def.Type, Valid: true}
|
||||
}
|
||||
var minP, maxP pgtype.Text
|
||||
if def.Min != "" {
|
||||
minP = pgtype.Text{String: def.Min, Valid: true}
|
||||
}
|
||||
if def.Max != "" {
|
||||
maxP = pgtype.Text{String: def.Max, Valid: true}
|
||||
}
|
||||
if _, err := h.db.UpsertSystemSetting(ctx, database.UpsertSystemSettingParams{
|
||||
SettingKey: key,
|
||||
SettingValue: value,
|
||||
Description: pgtype.Text{String: desc, Valid: desc != ""},
|
||||
SettingType: rType,
|
||||
MinValue: minP,
|
||||
MaxValue: maxP,
|
||||
RequiresRestart: pgtype.Bool{Bool: def.RequiresRestart, Valid: true},
|
||||
Category: pgtype.Text{String: def.Category, Valid: def.Category != ""},
|
||||
}); err != nil {
|
||||
return UpdateSettingResponse{}, err
|
||||
}
|
||||
|
||||
h.settings.Reload(ctx)
|
||||
|
||||
resp := UpdateSettingResponse{ReloadRequired: def.RequiresRestart}
|
||||
for _, e := range h.settings.All() {
|
||||
if e.Key == key {
|
||||
resp.SettingEntry = e
|
||||
break
|
||||
}
|
||||
}
|
||||
if def.RequiresRestart {
|
||||
resp.Message = "Saved. Restart the server for this change to take full effect."
|
||||
} else {
|
||||
resp.Message = "Saved."
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// validateSettingValue checks a candidate value against the setting's type and bounds.
|
||||
func validateSettingValue(def database.SettingDefault, value string) error {
|
||||
switch def.Type {
|
||||
case database.SettingTypeInt:
|
||||
n, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("value must be an integer")
|
||||
}
|
||||
if def.Min != "" {
|
||||
if mn, err := strconv.Atoi(def.Min); err == nil && n < mn {
|
||||
return fmt.Errorf("value must be >= %s", def.Min)
|
||||
}
|
||||
}
|
||||
if def.Max != "" {
|
||||
if mx, err := strconv.Atoi(def.Max); err == nil && n > mx {
|
||||
return fmt.Errorf("value must be <= %s", def.Max)
|
||||
}
|
||||
}
|
||||
case database.SettingTypeBool:
|
||||
if _, err := strconv.ParseBool(value); err != nil {
|
||||
return fmt.Errorf("value must be true or false")
|
||||
}
|
||||
case database.SettingTypeString:
|
||||
if value == "" {
|
||||
return fmt.Errorf("value must not be empty")
|
||||
}
|
||||
if def.Key == "default_timezone" {
|
||||
if _, err := time.LoadLocation(value); err != nil {
|
||||
return fmt.Errorf("invalid timezone: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ---- Legacy scan-settings endpoints (retained for backward compatibility) ----
|
||||
|
||||
type UpdateScanSettingsRequest struct {
|
||||
ScanPollIntervalSeconds int32 `json:"scan_poll_interval_seconds" validate:"required,min=1,max=3600"`
|
||||
AutoScanEnabled bool `json:"auto_scan_enabled"`
|
||||
@@ -51,6 +203,7 @@ func (h *SystemSettingsHandler) UpdateTimezoneSettings(c *echo.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.reload(c)
|
||||
return c.JSON(http.StatusOK, map[string]string{"message": "Timezone updated"})
|
||||
}
|
||||
|
||||
@@ -88,6 +241,8 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
h.reload(c)
|
||||
|
||||
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
||||
ScanPollIntervalSeconds: req.ScanPollIntervalSeconds,
|
||||
AutoScanEnabled: req.AutoScanEnabled,
|
||||
@@ -96,6 +251,15 @@ func (h *SystemSettingsHandler) UpdateScanSettings(c *echo.Context) error {
|
||||
}
|
||||
|
||||
func (h *SystemSettingsHandler) GetScanSettings(c *echo.Context) error {
|
||||
// Prefer the registry (single source of truth after Load).
|
||||
if h.settings != nil {
|
||||
interval := int32(h.settings.ScanPollInterval().Seconds())
|
||||
return c.JSON(http.StatusOK, ScanSettingsResponse{
|
||||
ScanPollIntervalSeconds: interval,
|
||||
AutoScanEnabled: h.settings.AutoScanEnabled(),
|
||||
})
|
||||
}
|
||||
|
||||
scanFrequencySetting, err := h.db.GetSystemSetting(c.Request().Context(), "scan_poll_interval_seconds")
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
|
||||
@@ -25,6 +25,7 @@ type DeviceContext struct {
|
||||
type DeviceAuthMiddleware struct {
|
||||
db *database.Queries
|
||||
rateLimiter *DeviceRateLimiter
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware {
|
||||
@@ -34,6 +35,41 @@ func NewDeviceAuthMiddleware(db *database.Queries) *DeviceAuthMiddleware {
|
||||
}
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry so device rate limits are
|
||||
// read live on each authenticated request.
|
||||
func (m *DeviceAuthMiddleware) SetSettings(s *database.SettingsRegistry) { m.settings = s }
|
||||
|
||||
// rateLimitConfig returns the active device rate limits from the registry, or
|
||||
// the historical defaults when no registry is wired.
|
||||
func (m *DeviceAuthMiddleware) rateLimitConfig() DeviceRateLimitConfig {
|
||||
if m.settings != nil {
|
||||
dl := m.settings.DeviceRateLimits()
|
||||
return DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: dl.Sync,
|
||||
ProgressUpdatesPerMinute: dl.Progress,
|
||||
MetadataRequestsPerMinute: dl.Metadata,
|
||||
}
|
||||
}
|
||||
return DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: DefaultSyncRequestsPerMinute,
|
||||
ProgressUpdatesPerMinute: DefaultProgressUpdatesPerMinute,
|
||||
MetadataRequestsPerMinute: DefaultMetadataRequestsPerMinute,
|
||||
}
|
||||
}
|
||||
|
||||
// rateLimitForRequestType returns the configured per-minute limit for a given
|
||||
// request type, for use in X-RateLimit-* headers.
|
||||
func (m *DeviceAuthMiddleware) rateLimitForRequestType(requestType string, config DeviceRateLimitConfig) int {
|
||||
switch requestType {
|
||||
case "progress":
|
||||
return config.ProgressUpdatesPerMinute
|
||||
case "metadata":
|
||||
return config.MetadataRequestsPerMinute
|
||||
default: // "sync" and any unknown type
|
||||
return config.SyncRequestsPerMinute
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c *echo.Context) error {
|
||||
var device database.Devices
|
||||
@@ -115,15 +151,12 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF
|
||||
deviceUUID := uuid.UUID(device.ID.Bytes)
|
||||
deviceID := deviceUUID.String()
|
||||
|
||||
config := DeviceRateLimitConfig{
|
||||
SyncRequestsPerMinute: 60,
|
||||
ProgressUpdatesPerMinute: 120,
|
||||
MetadataRequestsPerMinute: 30,
|
||||
}
|
||||
config := m.rateLimitConfig()
|
||||
limitForType := m.rateLimitForRequestType(requestType, config)
|
||||
|
||||
if !m.rateLimiter.CheckRateLimit(deviceID, requestType, config) {
|
||||
remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config)
|
||||
c.Response().Header().Set("X-RateLimit-Limit", "60")
|
||||
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(limitForType))
|
||||
c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
|
||||
c.Response().Header().Set("X-RateLimit-Reset", "60")
|
||||
return c.JSON(http.StatusTooManyRequests, map[string]string{
|
||||
@@ -134,7 +167,7 @@ func (m *DeviceAuthMiddleware) Authenticate(next echo.HandlerFunc) echo.HandlerF
|
||||
}
|
||||
|
||||
remaining := m.rateLimiter.GetRemainingRequests(deviceID, requestType, config)
|
||||
c.Response().Header().Set("X-RateLimit-Limit", "60")
|
||||
c.Response().Header().Set("X-RateLimit-Limit", strconv.Itoa(limitForType))
|
||||
c.Response().Header().Set("X-RateLimit-Remaining", strconv.Itoa(remaining))
|
||||
|
||||
ctx := DeviceContext{
|
||||
|
||||
@@ -1,96 +1,146 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"sync"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
)
|
||||
|
||||
// PasswordValidator validates password complexity requirements
|
||||
type PasswordValidator struct{}
|
||||
// specialCharRegex matches the historical "special character" set used by the
|
||||
// password complexity rules.
|
||||
const specialCharRegex = `[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`
|
||||
|
||||
// Validate checks if a password meets complexity requirements:
|
||||
// - Minimum 8 characters
|
||||
// - At least one uppercase letter
|
||||
// - At least one lowercase letter
|
||||
// - At least one number
|
||||
// - At least one special character
|
||||
// PasswordValidator validates password complexity against the configured rules.
|
||||
// When a database.SettingsRegistry is wired via SetSettings, rules are read live and the
|
||||
// regex set is recompiled under a mutex on each validation. Without a registry
|
||||
// the historical hardcoded defaults (8+ chars, upper/lower/number/special) apply.
|
||||
type PasswordValidator struct {
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry.
|
||||
func (v *PasswordValidator) SetSettings(s *database.SettingsRegistry) { v.settings = s }
|
||||
|
||||
// compileSpecialRegex isolates the regexp compile (which is safe to call
|
||||
// concurrently, but we keep it behind a cached var for the no-registry path).
|
||||
var (
|
||||
specialOnce sync.Once
|
||||
specialRe *regexp.Regexp
|
||||
)
|
||||
|
||||
func specialRegex() *regexp.Regexp {
|
||||
specialOnce.Do(func() {
|
||||
specialRe = regexp.MustCompile(specialCharRegex)
|
||||
})
|
||||
return specialRe
|
||||
}
|
||||
|
||||
func (v *PasswordValidator) rules() database.PasswordRules {
|
||||
if v.settings != nil {
|
||||
return v.settings.PasswordRules()
|
||||
}
|
||||
return database.PasswordRules{MinLength: 8, Upper: true, Lower: true, Number: true, Special: true}
|
||||
}
|
||||
|
||||
// Validate checks if a password meets the configured complexity requirements.
|
||||
func (v *PasswordValidator) Validate(fl validator.FieldLevel) bool {
|
||||
password := fl.Field().String()
|
||||
return v.CheckPassword(fl.Field().String())
|
||||
}
|
||||
|
||||
// Check minimum length
|
||||
if len(password) < 8 {
|
||||
// CheckPassword applies the active rules to a single password.
|
||||
func (v *PasswordValidator) CheckPassword(password string) bool {
|
||||
r := v.rules()
|
||||
if len(password) < r.MinLength {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for uppercase
|
||||
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(password)
|
||||
if !hasUpper {
|
||||
if r.Upper && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for lowercase
|
||||
hasLower := regexp.MustCompile(`[a-z]`).MatchString(password)
|
||||
if !hasLower {
|
||||
if r.Lower && !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for number
|
||||
hasNumber := regexp.MustCompile(`[0-9]`).MatchString(password)
|
||||
if !hasNumber {
|
||||
if r.Number && !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check for special character
|
||||
hasSpecial := regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password)
|
||||
if !hasSpecial {
|
||||
if r.Special && !specialRegex().MatchString(password) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// GetPasswordRequirements returns a human-readable list of password requirements
|
||||
// GetPasswordRequirements returns a human-readable list of the active password
|
||||
// requirements, driven by the configured rules when a registry is wired.
|
||||
func GetPasswordRequirements() []string {
|
||||
return []string{
|
||||
"At least 8 characters long",
|
||||
"At least one uppercase letter (A-Z)",
|
||||
"At least one lowercase letter (a-z)",
|
||||
"At least one number (0-9)",
|
||||
"At least one special character (!@#$%^&*()_+-=[]{}|;':\",./<>?)",
|
||||
}
|
||||
return defaultPasswordValidator.Requirements()
|
||||
}
|
||||
|
||||
// ValidatePassword checks a password and returns an error if it doesn't meet requirements
|
||||
func ValidatePassword(password string) error {
|
||||
if len(password) < 8 {
|
||||
return fmt.Errorf("password must be at least 8 characters long")
|
||||
// Requirements returns the human-readable list for the receiver's active rules.
|
||||
func (v *PasswordValidator) Requirements() []string {
|
||||
r := v.rules()
|
||||
var out []string
|
||||
out = append(out, fmt.Sprintf("At least %d characters long", r.MinLength))
|
||||
if r.Upper {
|
||||
out = append(out, "At least one uppercase letter (A-Z)")
|
||||
}
|
||||
if r.Lower {
|
||||
out = append(out, "At least one lowercase letter (a-z)")
|
||||
}
|
||||
if r.Number {
|
||||
out = append(out, "At least one number (0-9)")
|
||||
}
|
||||
if r.Special {
|
||||
out = append(out, "At least one special character (!@#$%^&*()_+-=[]{}|;':\",./<>?)")
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
// ValidatePassword checks a password against the default (hardcoded) rules and
|
||||
// returns an error describing the first unmet requirement. Retained for callers
|
||||
// that don't have access to a configured PasswordValidator instance.
|
||||
func ValidatePassword(password string) error {
|
||||
v := defaultPasswordValidator
|
||||
r := v.rules()
|
||||
if len(password) < r.MinLength {
|
||||
return fmt.Errorf("password must be at least %d characters long", r.MinLength)
|
||||
}
|
||||
if r.Upper && !regexp.MustCompile(`[A-Z]`).MatchString(password) {
|
||||
return fmt.Errorf("password must contain at least one uppercase letter")
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
if r.Lower && !regexp.MustCompile(`[a-z]`).MatchString(password) {
|
||||
return fmt.Errorf("password must contain at least one lowercase letter")
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
if r.Number && !regexp.MustCompile(`[0-9]`).MatchString(password) {
|
||||
return fmt.Errorf("password must contain at least one number")
|
||||
}
|
||||
|
||||
if !regexp.MustCompile(`[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]`).MatchString(password) {
|
||||
if r.Special && !specialRegex().MatchString(password) {
|
||||
return fmt.Errorf("password must contain at least one special character")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterPasswordValidation registers the password validator with the validator instance
|
||||
// defaultPasswordValidator is used by the package-level helpers
|
||||
// (GetPasswordRequirements, ValidatePassword) and as the fallback inside
|
||||
// RegisterPasswordValidation when no registry has been wired. Callers that want
|
||||
// live rule updates should construct their own PasswordValidator and call
|
||||
// SetSettings.
|
||||
var defaultPasswordValidator = &PasswordValidator{}
|
||||
|
||||
// RegisterPasswordValidation registers the password validator with the
|
||||
// validator instance. The registered func re-evaluates rules on every call, so
|
||||
// changes to the wired registry take effect immediately.
|
||||
func RegisterPasswordValidation(v *validator.Validate) error {
|
||||
return v.RegisterValidation("passwordcomplex", func(fl validator.FieldLevel) bool {
|
||||
pv := &PasswordValidator{}
|
||||
return pv.Validate(fl)
|
||||
return defaultPasswordValidator.CheckPassword(fl.Field().String())
|
||||
})
|
||||
}
|
||||
|
||||
// SetDefaultPasswordSettings wires the settings registry into the package-level
|
||||
// default validator so that the struct-tag validator (used by echo's
|
||||
// CustomValidator) and ValidatePassword follow live configuration. Intended to
|
||||
// be called once at startup.
|
||||
func SetDefaultPasswordSettings(s *database.SettingsRegistry) {
|
||||
defaultPasswordValidator.SetSettings(s)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,447 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"bookhoard/internal/database"
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/templates"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/labstack/echo/v5"
|
||||
)
|
||||
|
||||
func registerAdminLibraryRoutes(cfg *Config, frontendProtected *echo.Group) {
|
||||
g := frontendProtected.Group("", handlers.AdminMiddleware)
|
||||
|
||||
// HTMX: Create library
|
||||
g.POST("/admin/library/create", func(c *echo.Context) error {
|
||||
user := c.Get("user").(database.Users)
|
||||
|
||||
name := c.FormValue("name")
|
||||
desc := c.FormValue("description")
|
||||
libType := c.FormValue("type")
|
||||
if name == "" || libType == "" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Name and type are required</div>`)
|
||||
}
|
||||
|
||||
_, err := cfg.LibraryService.CreateLibrary(
|
||||
c.Request().Context(),
|
||||
name,
|
||||
desc,
|
||||
libType,
|
||||
user.ID,
|
||||
)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-sm" style="color: var(--status-danger);">Failed to create library</div>`)
|
||||
}
|
||||
|
||||
return renderLibraryList(c, cfg)
|
||||
})
|
||||
|
||||
// HTMX: Update library
|
||||
g.PUT("/admin/library/:id", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
name := c.FormValue("name")
|
||||
desc := c.FormValue("description")
|
||||
if name == "" {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Name is required</div>`)
|
||||
}
|
||||
|
||||
_, err = cfg.LibraryService.UpdateLibrary(c.Request().Context(), libraryID, name, desc)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-sm" style="color: var(--status-danger);">Failed to update library</div>`)
|
||||
}
|
||||
|
||||
return renderLibraryList(c, cfg)
|
||||
})
|
||||
|
||||
// HTMX: Delete library
|
||||
g.DELETE("/admin/library/:id", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
err = cfg.LibraryService.DeleteLibrary(c.Request().Context(), libraryID)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-sm" style="color: var(--status-danger);">Failed to delete library</div>`)
|
||||
}
|
||||
|
||||
return renderLibraryList(c, cfg)
|
||||
})
|
||||
|
||||
// HTMX: Library expanded panel
|
||||
g.GET("/admin/library/:id/panel", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
})
|
||||
|
||||
// HTMX: Add folder
|
||||
g.POST("/admin/library/:id/folders", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
folderPath := c.FormValue("folder_path")
|
||||
if folderPath == "" {
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
}
|
||||
|
||||
if strings.Contains(folderPath, "..") {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "Path traversal not allowed")
|
||||
}
|
||||
|
||||
cleanPath := filepath.Clean(folderPath)
|
||||
fileInfo, err := os.Stat(cleanPath)
|
||||
if err != nil {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "Folder path does not exist")
|
||||
}
|
||||
if !fileInfo.IsDir() {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "Path must be a directory")
|
||||
}
|
||||
|
||||
_, err = cfg.LibraryService.AddLibraryFolder(c.Request().Context(), libraryID, cleanPath)
|
||||
if err != nil {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "Failed to add folder: "+err.Error())
|
||||
}
|
||||
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
})
|
||||
|
||||
// HTMX: Remove folder
|
||||
g.DELETE("/admin/library/:id/folders", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
folderPath := c.FormValue("folder_path")
|
||||
if folderPath == "" {
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
}
|
||||
|
||||
err = cfg.LibraryService.DeleteLibraryFolder(c.Request().Context(), libraryID, folderPath)
|
||||
if err != nil {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "Failed to remove folder")
|
||||
}
|
||||
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
})
|
||||
|
||||
// HTMX: Folder browser
|
||||
g.GET("/admin/library/browse", func(c *echo.Context) error {
|
||||
path := c.QueryParam("path")
|
||||
if path == "" {
|
||||
path = "/"
|
||||
}
|
||||
targetInput := c.QueryParam("target_input")
|
||||
libraryID := c.QueryParam("library_id")
|
||||
|
||||
dirs, currentPath, parentPath, err := cfg.LibraryService.BrowseDirectories(c.Request().Context(), path)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Cannot browse: `+err.Error()+`</div>`)
|
||||
}
|
||||
|
||||
entries := make([]templates.DirEntry, len(dirs))
|
||||
for i, d := range dirs {
|
||||
fullPath := filepath.Join(currentPath, d)
|
||||
entries[i] = templates.DirEntry{Name: d, Path: fullPath}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.FolderBrowserContent(currentPath, parentPath, entries, targetInput, libraryID).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// HTMX: Set user visibility for library
|
||||
g.POST("/admin/library/:id/visibility", func(c *echo.Context) error {
|
||||
libraryID, err := parseAdminUUID(c.Param("id"))
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid library ID</div>`)
|
||||
}
|
||||
|
||||
userIDStr := c.FormValue("user_id")
|
||||
isVisible := c.FormValue("is_visible") == "true"
|
||||
|
||||
userID, err := parseAdminUUID(userIDStr)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Invalid user ID</div>`)
|
||||
}
|
||||
|
||||
_, err = cfg.LibraryService.SetLibraryVisibility(c.Request().Context(), userID, libraryID, isVisible)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-sm" style="color: var(--status-danger);">Failed to update visibility</div>`)
|
||||
}
|
||||
|
||||
return renderLibraryPanel(c, cfg, libraryID)
|
||||
})
|
||||
}
|
||||
|
||||
// renderLibraryList fetches all libraries + users and renders the LibraryList partial.
|
||||
func renderLibraryList(c *echo.Context, cfg *Config) error {
|
||||
libraries, err := cfg.LibraryHandler.ListLibrariesData(c.Request().Context())
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, `<div class="text-sm" style="color: var(--status-danger);">Failed to load libraries</div>`)
|
||||
}
|
||||
|
||||
libData := make([]templates.LibraryData, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
||||
folderCount := getFolderCount(c.Request().Context(), cfg, lib.ID)
|
||||
libData[i] = templates.LibraryData{
|
||||
ID: libUUID.String(),
|
||||
Name: lib.Name,
|
||||
Description: getText(lib.Description),
|
||||
TypeName: lib.TypeName,
|
||||
TypeValue: lib.TypeName,
|
||||
FolderCount: folderCount,
|
||||
}
|
||||
}
|
||||
|
||||
users, err := cfg.Queries.ListUsers(c.Request().Context())
|
||||
if err != nil {
|
||||
log.Printf("ListUsers failed: %v", err)
|
||||
users = []database.ListUsersRow{}
|
||||
}
|
||||
userData := make([]templates.User, len(users))
|
||||
for i, u := range users {
|
||||
userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
|
||||
userData[i] = templates.User{
|
||||
ID: userUUID.String(),
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
Role: u.Role,
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.LibraryList(templates.User{}, libData, userData).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
}
|
||||
|
||||
// renderLibraryPanel fetches library details and renders the LibraryPanel partial.
|
||||
func renderLibraryPanel(c *echo.Context, cfg *Config, libraryID pgtype.UUID) error {
|
||||
return renderLibraryPanelWithError(c, cfg, libraryID, "")
|
||||
}
|
||||
|
||||
func renderLibraryPanelWithError(c *echo.Context, cfg *Config, libraryID pgtype.UUID, errMsg string) error {
|
||||
ctx := c.Request().Context()
|
||||
libraryIDStr := uuid.UUID(libraryID.Bytes).String()
|
||||
|
||||
// Get library details
|
||||
lib, err := cfg.LibraryService.GetLibrary(ctx, libraryID)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusNotFound, `<div class="text-sm" style="color: var(--status-danger);">Library not found</div>`)
|
||||
}
|
||||
|
||||
libData := templates.LibraryData{
|
||||
ID: libraryIDStr,
|
||||
Name: lib.Name,
|
||||
Description: getText(lib.Description),
|
||||
TypeName: lib.TypeName,
|
||||
TypeValue: lib.TypeName,
|
||||
}
|
||||
|
||||
// Get folders
|
||||
dbFolders, err := cfg.LibraryService.GetLibraryFolders(ctx, libraryID)
|
||||
if err != nil {
|
||||
log.Printf("GetLibraryFolders failed: %v", err)
|
||||
}
|
||||
folders := make([]templates.FolderData, len(dbFolders))
|
||||
for i, f := range dbFolders {
|
||||
folders[i] = templates.FolderData{FolderPath: f.FolderPath}
|
||||
}
|
||||
libData.FolderCount = len(folders)
|
||||
|
||||
// Get users
|
||||
dbUsers, err := cfg.Queries.ListUsers(ctx)
|
||||
if err != nil {
|
||||
log.Printf("ListUsers failed: %v", err)
|
||||
dbUsers = []database.ListUsersRow{}
|
||||
}
|
||||
userData := make([]templates.User, len(dbUsers))
|
||||
for i, u := range dbUsers {
|
||||
userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
|
||||
userData[i] = templates.User{
|
||||
ID: userUUID.String(),
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
}
|
||||
}
|
||||
|
||||
// Get visibility for all users
|
||||
visibility := make([]templates.UserVisibilityData, len(userData))
|
||||
for i, u := range userData {
|
||||
userUUID, _ := parseAdminUUID(u.ID)
|
||||
visibleLibs, err := cfg.LibraryService.GetUserVisibleLibraries(ctx, userUUID)
|
||||
if err != nil {
|
||||
log.Printf("GetUserVisibleLibraries failed: %v", err)
|
||||
}
|
||||
isVisible := false
|
||||
for _, vl := range visibleLibs {
|
||||
if vl.ID.Bytes == libraryID.Bytes {
|
||||
isVisible = true
|
||||
break
|
||||
}
|
||||
}
|
||||
visibility[i] = templates.UserVisibilityData{
|
||||
UserID: u.ID,
|
||||
Username: u.Username,
|
||||
Email: u.Email,
|
||||
IsVisible: isVisible,
|
||||
}
|
||||
}
|
||||
|
||||
// Get issue count
|
||||
issueStats, err := cfg.ProcessingIssuesHandler.GetProcessingIssueStatsData(ctx, libraryID)
|
||||
if err != nil {
|
||||
log.Printf("GetProcessingIssueStats failed: %v", err)
|
||||
}
|
||||
issueCount := issueStats.ErrorCount + issueStats.WarningCount + issueStats.InfoCount
|
||||
|
||||
// Get current user for template
|
||||
tmplUser := templates.User{}
|
||||
if u, ok := c.Get("user").(database.Users); ok {
|
||||
userUUID, _ := uuid.FromBytes(u.ID.Bytes[0:16])
|
||||
tmplUser = templates.User{
|
||||
ID: userUUID.String(),
|
||||
Username: u.Username,
|
||||
Role: u.Role,
|
||||
}
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.LibraryPanel(tmplUser, libraryIDStr, libData, folders, userData, visibility, int(issueCount)).Render(ctx, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
html := buf.String()
|
||||
if errMsg != "" {
|
||||
html = `<div class="p-3 mb-3 rounded-lg text-sm" style="background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); color: var(--status-danger);">` + errMsg + `</div>` + html
|
||||
}
|
||||
return c.HTML(http.StatusOK, html)
|
||||
}
|
||||
|
||||
func getFolderCount(ctx context.Context, cfg *Config, libraryID pgtype.UUID) int {
|
||||
folders, err := cfg.LibraryService.GetLibraryFolders(ctx, libraryID)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return len(folders)
|
||||
}
|
||||
|
||||
func parseAdminUUID(s string) (pgtype.UUID, error) {
|
||||
parsed, err := uuid.Parse(s)
|
||||
if err != nil {
|
||||
return pgtype.UUID{}, err
|
||||
}
|
||||
return pgtype.UUID{Bytes: parsed, Valid: true}, nil
|
||||
}
|
||||
|
||||
func getAdminStats(ctx context.Context, cfg *Config) templates.AdminStats {
|
||||
stats := templates.AdminStats{}
|
||||
|
||||
libs, _ := cfg.LibraryHandler.ListLibrariesData(ctx)
|
||||
stats.LibraryCount = len(libs)
|
||||
|
||||
users, _ := cfg.Queries.ListUsers(ctx)
|
||||
stats.UserCount = len(users)
|
||||
|
||||
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {
|
||||
_ = pool.QueryRow(ctx, "SELECT COUNT(*) FROM media_items").Scan(&stats.MediaCount)
|
||||
_ = pool.QueryRow(ctx, "SELECT COUNT(*) FROM devices").Scan(&stats.DeviceCount)
|
||||
}
|
||||
|
||||
return stats
|
||||
}
|
||||
|
||||
func registerAdminSettingsRoutes(cfg *Config, frontendProtected *echo.Group) {
|
||||
g := frontendProtected.Group("", handlers.AdminMiddleware)
|
||||
|
||||
g.PUT("/admin/settings/scan", func(c *echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
|
||||
autoScan := c.FormValue("auto_scan_enabled") == "true"
|
||||
intervalStr := c.FormValue("scan_poll_interval_seconds")
|
||||
interval, err := strconv.Atoi(intervalStr)
|
||||
if err != nil || interval < 1 || interval > 3600 {
|
||||
return c.HTML(http.StatusBadRequest, `<div class="text-sm" style="color: var(--status-danger);">Interval must be between 1 and 3600 seconds</div>`)
|
||||
}
|
||||
|
||||
autoScanStr := "false"
|
||||
if autoScan {
|
||||
autoScanStr = "true"
|
||||
}
|
||||
_ = cfg.Queries.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
|
||||
SettingKey: "auto_scan_enabled",
|
||||
SettingValue: autoScanStr,
|
||||
})
|
||||
_ = cfg.Queries.UpdateSystemSetting(ctx, database.UpdateSystemSettingParams{
|
||||
SettingKey: "scan_poll_interval_seconds",
|
||||
SettingValue: strconv.Itoa(interval),
|
||||
})
|
||||
|
||||
// Refresh the registry cache so the change is visible immediately.
|
||||
if cfg.Settings != nil {
|
||||
cfg.Settings.Reload(ctx)
|
||||
}
|
||||
|
||||
scanSettings := templates.ScanSettingsData{
|
||||
AutoScanEnabled: autoScan,
|
||||
ScanPollIntervalSeconds: interval,
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
_ = templates.ScanSettingsSection(scanSettings).Render(ctx, &buf)
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
})
|
||||
|
||||
// HTMX endpoint for saving a single tunable setting. Returns a small HTML
|
||||
// status snippet rendered into the row's status span.
|
||||
g.PUT("/admin/settings/tunable", func(c *echo.Context) error {
|
||||
ctx := c.Request().Context()
|
||||
key := c.FormValue("key")
|
||||
value := c.FormValue("value")
|
||||
|
||||
if cfg.SystemSettingsHandler == nil {
|
||||
return c.HTML(http.StatusServiceUnavailable, `<span style="color: var(--status-danger);">settings unavailable</span>`)
|
||||
}
|
||||
resp, err := cfg.SystemSettingsHandler.ApplySetting(ctx, key, value)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusBadRequest, fmt.Sprintf(`<span style="color: var(--status-danger);">%s</span>`, err.Error()))
|
||||
}
|
||||
|
||||
color := "var(--status-success)"
|
||||
msg := "Saved"
|
||||
if resp.ReloadRequired {
|
||||
color = "var(--status-warning)"
|
||||
msg = "Saved — restart required"
|
||||
}
|
||||
return c.HTML(http.StatusOK, fmt.Sprintf(`<span style="color: %s;">%s</span>`, color, msg))
|
||||
})
|
||||
}
|
||||
@@ -810,8 +810,9 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
stats := getAdminStats(c.Request().Context(), cfg)
|
||||
var buf bytes.Buffer
|
||||
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
||||
err = templates.Admin(user, stats).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -823,8 +824,9 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
if err != nil {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
stats := getAdminStats(c.Request().Context(), cfg)
|
||||
var buf bytes.Buffer
|
||||
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
||||
err = templates.Admin(user, stats).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -848,11 +850,14 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
libData := make([]templates.LibraryData, len(libraries))
|
||||
for i, lib := range libraries {
|
||||
libUUID, _ := uuid.FromBytes(lib.ID.Bytes[0:16])
|
||||
folders, _ := cfg.LibraryService.GetLibraryFolders(c.Request().Context(), lib.ID)
|
||||
libData[i] = templates.LibraryData{
|
||||
ID: libUUID.String(),
|
||||
Name: lib.Name,
|
||||
Description: getText(lib.Description),
|
||||
TypeName: lib.TypeName,
|
||||
TypeValue: lib.TypeName,
|
||||
FolderCount: len(folders),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -990,21 +995,67 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
return renderErrorPage(c, "Error loading user", "user_load_error")
|
||||
}
|
||||
|
||||
ctx := c.Request().Context()
|
||||
|
||||
// Fetch current system configuration - just base_url
|
||||
baseURL := cfg.getBaseURL(c.Request().Context())
|
||||
baseURL := cfg.getBaseURL(ctx)
|
||||
|
||||
systemConfig := map[string]string{
|
||||
"base_url": baseURL,
|
||||
"default_timezone": "UTC",
|
||||
}
|
||||
|
||||
defaultTimezone, err := cfg.Queries.GetSystemTimezone(c.Request().Context())
|
||||
defaultTimezone, err := cfg.Queries.GetSystemTimezone(ctx)
|
||||
if err == nil && defaultTimezone != "" {
|
||||
systemConfig["default_timezone"] = defaultTimezone
|
||||
}
|
||||
|
||||
// Fetch scan settings
|
||||
scanSettings := templates.ScanSettingsData{
|
||||
AutoScanEnabled: true,
|
||||
ScanPollIntervalSeconds: 60,
|
||||
}
|
||||
if val, err := cfg.Queries.GetSystemSetting(ctx, "auto_scan_enabled"); err == nil {
|
||||
scanSettings.AutoScanEnabled = val == "true"
|
||||
}
|
||||
if val, err := cfg.Queries.GetSystemSetting(ctx, "scan_poll_interval_seconds"); err == nil {
|
||||
if n, err := strconv.Atoi(val); err == nil {
|
||||
scanSettings.ScanPollIntervalSeconds = n
|
||||
}
|
||||
}
|
||||
|
||||
// Load tunable settings entries from the registry. Exclude keys that
|
||||
// already have their own dedicated UI cards (timezone dropdown, scan
|
||||
// settings) so they aren't listed twice.
|
||||
dedicatedUI := map[string]bool{
|
||||
"default_timezone": true,
|
||||
"scan_poll_interval_seconds": true,
|
||||
"auto_scan_enabled": true,
|
||||
}
|
||||
var tunableSettings []templates.SettingEntry
|
||||
if cfg.Settings != nil {
|
||||
for _, e := range cfg.Settings.All() {
|
||||
if dedicatedUI[e.Key] {
|
||||
continue
|
||||
}
|
||||
tunableSettings = append(tunableSettings, templates.SettingEntry{
|
||||
Key: e.Key,
|
||||
Value: e.Value,
|
||||
Type: e.Type,
|
||||
Min: e.Min,
|
||||
Max: e.Max,
|
||||
RequiresRestart: e.RequiresRestart,
|
||||
Category: e.Category,
|
||||
Group: e.Group,
|
||||
Description: e.Description,
|
||||
IsDefault: e.IsDefault,
|
||||
})
|
||||
}
|
||||
}
|
||||
liveGroups, restartGroups := templates.GroupTunableSettings(tunableSettings)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = templates.AdminSettings(user, systemConfig, "").Render(c.Request().Context(), &buf)
|
||||
err = templates.AdminSettings(user, systemConfig, scanSettings, liveGroups, restartGroups, "").Render(ctx, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1046,6 +1097,11 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
return c.HTML(http.StatusOK, buf.String())
|
||||
}))
|
||||
|
||||
// Admin library HTMX endpoints
|
||||
registerAdminLibraryRoutes(cfg, frontendProtected)
|
||||
// Admin settings HTMX endpoints
|
||||
registerAdminSettingsRoutes(cfg, frontendProtected)
|
||||
|
||||
// ============================================================================
|
||||
// LEGACY API ROUTES (for backward compatibility)
|
||||
// ============================================================================
|
||||
|
||||
@@ -38,6 +38,8 @@ func registerLibraryRoutes(cfg *Config) {
|
||||
adminLibrary.GET("/:id/stats", cfg.LibraryHandler.GetLibraryStats)
|
||||
adminLibrary.GET("/:id/issues/list", cfg.ProcessingIssuesHandler.ListProcessingIssues)
|
||||
adminLibrary.GET("/:id/issues/stats", cfg.ProcessingIssuesHandler.GetProcessingIssueStats)
|
||||
adminLibrary.POST("/:id/issues/:issueId/:mediaItemId/resolve", cfg.ProcessingIssuesHandler.ResolveProcessingIssue)
|
||||
adminLibrary.DELETE("/:id/issues/:issueId", cfg.ProcessingIssuesHandler.DeleteProcessingIssue)
|
||||
adminLibrary.POST("/:id/scan", func(c *echo.Context) error {
|
||||
libraryID := c.Param("id")
|
||||
scanReq := map[string]interface{}{
|
||||
|
||||
@@ -39,6 +39,7 @@ type Config struct {
|
||||
Echo *echo.Echo
|
||||
Queries *database.Queries
|
||||
Cfg *config.Config
|
||||
Settings *database.SettingsRegistry
|
||||
DBPool interface{} // pgxpool.Pool interface
|
||||
AuthHandler *handlers.AuthHandler
|
||||
LibraryHandler *handlers.LibraryHandler
|
||||
@@ -211,10 +212,12 @@ func RegisterRoutes(cfg *Config) *handlers.Handler {
|
||||
// Setup redirect middleware - must run before all routes
|
||||
e.Pre(setupRedirectMiddleware(cfg))
|
||||
|
||||
// Rate limiter
|
||||
// Rate limiter. The per-minute value comes from the settings registry (DB);
|
||||
// the enabled flag stays env-driven since disabling rate limiting is a
|
||||
// deployment-time decision, not a runtime tunable.
|
||||
rateLimiterConfig := ratelimit.RateLimiterConfig{
|
||||
Enabled: cfg.Cfg.RateLimitEnabled,
|
||||
RequestsPerMinute: cfg.Cfg.RequestsPerMinute,
|
||||
RequestsPerMinute: cfg.Settings.AuthRateLimit(),
|
||||
CleanupInterval: 5 * time.Minute,
|
||||
}
|
||||
rateLimiter := ratelimit.NewRateLimiter(rateLimiterConfig)
|
||||
|
||||
@@ -17,4 +17,10 @@ func registerSystemRoutes(cfg *Config) {
|
||||
// System configuration routes (admin-only)
|
||||
system.GET("/config", cfg.SidecarHandler.GetSystemConfiguration)
|
||||
system.PUT("/config", cfg.SidecarHandler.UpdateSystemConfiguration)
|
||||
|
||||
// Unified tunable settings (admin-only). These back the admin UI's
|
||||
// editable System Settings sections and supersede the legacy
|
||||
// /api/libraries/scan-settings JSON routes.
|
||||
system.GET("/settings", cfg.SystemSettingsHandler.GetSettings)
|
||||
system.PUT("/settings", cfg.SystemSettingsHandler.UpdateSetting)
|
||||
}
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// defaultConversionCacheTTL is the fallback kepub cache lifetime when no
|
||||
// settings registry is wired. Matches the historical hardcoded 24h.
|
||||
const defaultConversionCacheTTL = 24 * time.Hour
|
||||
|
||||
type ConvertedKEPUB struct {
|
||||
Path string
|
||||
SHA256 string
|
||||
@@ -27,6 +31,7 @@ type ConversionService struct {
|
||||
cacheDir string
|
||||
conversionTool string
|
||||
conversionCacheTTL time.Duration
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewConversionService(db *database.Queries, cacheDir string) *ConversionService {
|
||||
@@ -34,17 +39,32 @@ func NewConversionService(db *database.Queries, cacheDir string) *ConversionServ
|
||||
db: db,
|
||||
cacheDir: cacheDir,
|
||||
conversionTool: "/usr/bin/kepubify",
|
||||
conversionCacheTTL: 24 * time.Hour,
|
||||
conversionCacheTTL: defaultConversionCacheTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry. When wired, the cache TTL
|
||||
// is read live on each conversion request.
|
||||
func (s *ConversionService) SetSettings(reg *database.SettingsRegistry) { s.settings = reg }
|
||||
|
||||
// cacheTTL returns the active conversion cache TTL.
|
||||
func (s *ConversionService) cacheTTL() time.Duration {
|
||||
if s.settings != nil {
|
||||
return s.settings.ConversionCacheTTL()
|
||||
}
|
||||
if s.conversionCacheTTL > 0 {
|
||||
return s.conversionCacheTTL
|
||||
}
|
||||
return defaultConversionCacheTTL
|
||||
}
|
||||
|
||||
func (s *ConversionService) ConvertEPUBToKEPUB(ctx context.Context, mediaItemID pgtype.UUID, epubPath string) (*ConvertedKEPUB, error) {
|
||||
existing, err := s.db.GetMediaItemFormatByType(ctx, database.GetMediaItemFormatByTypeParams{
|
||||
MediaItemID: mediaItemID,
|
||||
FormatType: "kepub",
|
||||
})
|
||||
if err == nil && existing.FilePath.Valid {
|
||||
if time.Since(existing.CreatedAt.Time) < s.conversionCacheTTL {
|
||||
if time.Since(existing.CreatedAt.Time) < s.cacheTTL() {
|
||||
return &ConvertedKEPUB{
|
||||
Path: existing.FilePath.String,
|
||||
SHA256: existing.FileSha256.String,
|
||||
|
||||
@@ -65,5 +65,7 @@ func TestConversionServiceDefaults(t *testing.T) {
|
||||
assert.NotNil(t, service)
|
||||
assert.Equal(t, cacheDir, service.cacheDir)
|
||||
assert.Equal(t, "/usr/bin/kepubify", service.conversionTool)
|
||||
assert.Equal(t, int64(24*3600*1000000000), service.conversionCacheTTL.Nanoseconds(), "Default TTL should be 24 hours")
|
||||
assert.Equal(t, int64(24*3600*1000000000), service.conversionCacheTTL.Nanoseconds(), "Default TTL field should be 24 hours")
|
||||
// cacheTTL() must reflect the same default when no registry is wired.
|
||||
assert.Equal(t, int64(24*3600*1000000000), service.cacheTTL().Nanoseconds(), "Default TTL accessor should return 24 hours")
|
||||
}
|
||||
|
||||
@@ -176,10 +176,17 @@ func (w *Worker) GetActiveJobCount() int {
|
||||
return count
|
||||
}
|
||||
func NewWorker(numWorkers int, connManager *wsync.ConnectionManager) *Worker {
|
||||
return NewWorkerWithConfig(numWorkers, 100, connManager)
|
||||
}
|
||||
|
||||
// NewWorkerWithConfig constructs a worker pool with the given worker count and
|
||||
// job-queue capacity. Used at startup to source values from the settings
|
||||
// registry.
|
||||
func NewWorkerWithConfig(numWorkers, queueCap int, connManager *wsync.ConnectionManager) *Worker {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
w := &Worker{
|
||||
jobQueue: make(chan *Job, 100),
|
||||
jobQueue: make(chan *Job, queueCap),
|
||||
results: make(map[string]*JobResult),
|
||||
ctx: ctx,
|
||||
cancel: cancel,
|
||||
@@ -995,3 +1002,4 @@ func (w *Worker) Shutdown() {
|
||||
close(w.jobQueue)
|
||||
w.wg.Wait()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,38 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
// TombstoneTTL is the fallback retention for soft-deleted annotations when no
|
||||
// settings registry is wired (e.g. in tests). It matches the historical value.
|
||||
const TombstoneTTL = 30 * 24 * time.Hour
|
||||
|
||||
type AnnotationService struct {
|
||||
db *database.Queries
|
||||
connMgr *ConnectionManager
|
||||
settings *database.SettingsRegistry
|
||||
}
|
||||
|
||||
func NewAnnotationService(db *database.Queries, connMgr *ConnectionManager) *AnnotationService {
|
||||
return &AnnotationService{db: db, connMgr: connMgr}
|
||||
}
|
||||
|
||||
// SetSettings wires the tunable settings registry. When wired, the tombstone
|
||||
// TTL is read live from the DB; otherwise the package const TombstoneTTL is
|
||||
// used.
|
||||
func (s *AnnotationService) SetSettings(reg *database.SettingsRegistry) { s.settings = reg }
|
||||
|
||||
// tombstoneTTL returns the active tombstone retention window.
|
||||
func (s *AnnotationService) tombstoneTTL() time.Duration {
|
||||
if s.settings != nil {
|
||||
return s.settings.TombstoneTTL()
|
||||
}
|
||||
return TombstoneTTL
|
||||
}
|
||||
|
||||
// ActiveTombstoneTTL exposes the configured tombstone retention window for
|
||||
// callers outside the sync package (e.g. kobo/koreader handlers) that need to
|
||||
// compute cutoffs consistently with the service.
|
||||
func (s *AnnotationService) ActiveTombstoneTTL() time.Duration { return s.tombstoneTTL() }
|
||||
|
||||
type SaveOutcome string
|
||||
|
||||
const (
|
||||
@@ -29,15 +59,6 @@ const (
|
||||
SaveOutcomeDeleted SaveOutcome = "deleted"
|
||||
)
|
||||
|
||||
type AnnotationService struct {
|
||||
db *database.Queries
|
||||
connMgr *ConnectionManager
|
||||
}
|
||||
|
||||
func NewAnnotationService(db *database.Queries, connMgr *ConnectionManager) *AnnotationService {
|
||||
return &AnnotationService{db: db, connMgr: connMgr}
|
||||
}
|
||||
|
||||
type SaveHighlightRequest struct {
|
||||
MediaItemID pgtype.UUID
|
||||
UserID pgtype.UUID
|
||||
@@ -79,7 +100,7 @@ func (s *AnnotationService) SaveHighlight(ctx context.Context, req SaveHighlight
|
||||
}
|
||||
|
||||
if existing.Deleted.Bool {
|
||||
if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < TombstoneTTL {
|
||||
if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < s.tombstoneTTL() {
|
||||
return &SaveHighlightResult{Highlight: existing, Outcome: SaveOutcomeDeleted}, nil
|
||||
}
|
||||
return s.createHighlight(ctx, req, dedupKey)
|
||||
@@ -238,7 +259,7 @@ func (s *AnnotationService) TombstoneHighlightByID(
|
||||
}
|
||||
|
||||
func (s *AnnotationService) PurgeExpiredTombstones(ctx context.Context) error {
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-TombstoneTTL), Valid: true}
|
||||
cutoff := pgtype.Timestamptz{Time: time.Now().Add(-s.tombstoneTTL()), Valid: true}
|
||||
if err := s.db.PurgeExpiredHighlightTombstones(ctx, cutoff); err != nil {
|
||||
return fmt.Errorf("purge highlight tombstones: %w", err)
|
||||
}
|
||||
@@ -251,7 +272,13 @@ func (s *AnnotationService) PurgeExpiredTombstones(ctx context.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *AnnotationService) StartTombstonePurger() context.CancelFunc {
|
||||
// StartDailyMaintenance launches a single background goroutine that runs all
|
||||
// periodic cleanup tasks once every 24 hours: expired annotation tombstones,
|
||||
// expired/revoked refresh tokens (retention follows the configured session
|
||||
// duration), and expired OPDS tokens. Each task is independent; a failure in
|
||||
// one is logged and does not skip the others. The returned CancelFunc stops the
|
||||
// goroutine and the underlying ticker; it must be invoked on shutdown.
|
||||
func (s *AnnotationService) StartDailyMaintenance() context.CancelFunc {
|
||||
ticker := time.NewTicker(24 * time.Hour)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
@@ -262,9 +289,7 @@ func (s *AnnotationService) StartTombstonePurger() context.CancelFunc {
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
if err := s.PurgeExpiredTombstones(ctx); err != nil {
|
||||
log.Printf("AnnotationService: tombstone purge failed: %v", err)
|
||||
}
|
||||
s.runDailyMaintenance(ctx)
|
||||
}
|
||||
}
|
||||
}()
|
||||
@@ -272,6 +297,28 @@ func (s *AnnotationService) StartTombstonePurger() context.CancelFunc {
|
||||
return cancel
|
||||
}
|
||||
|
||||
// runDailyMaintenance executes every periodic cleanup task. Tasks run
|
||||
// sequentially under the single daily-tick goroutine so there is no added
|
||||
// concurrency. All three queries only delete rows that are already unusable
|
||||
// (expired or revoked), so this never logs out active sessions.
|
||||
func (s *AnnotationService) runDailyMaintenance(ctx context.Context) {
|
||||
if err := s.PurgeExpiredTombstones(ctx); err != nil {
|
||||
log.Printf("maintenance: tombstone purge failed: %v", err)
|
||||
}
|
||||
if err := s.db.CleanupExpiredOpdsTokens(ctx); err != nil {
|
||||
log.Printf("maintenance: OPDS token purge failed: %v", err)
|
||||
}
|
||||
// Refresh-token retention follows the configured session duration; re-read
|
||||
// on every tick so live settings changes are honored. Guarded so unwired
|
||||
// test paths simply skip cleanup (production always wires the registry).
|
||||
if s.settings != nil {
|
||||
retention := s.settings.SessionDuration().Seconds()
|
||||
if err := s.db.CleanupExpiredRefreshTokens(ctx, retention); err != nil {
|
||||
log.Printf("maintenance: refresh token purge failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type SaveNoteRequest struct {
|
||||
MediaItemID pgtype.UUID
|
||||
UserID pgtype.UUID
|
||||
@@ -457,7 +504,7 @@ func (s *AnnotationService) SaveBookmark(ctx context.Context, req SaveBookmarkRe
|
||||
}
|
||||
|
||||
if existing.Deleted.Bool {
|
||||
if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < TombstoneTTL {
|
||||
if existing.DeletedAt.Valid && time.Since(existing.DeletedAt.Time) < s.tombstoneTTL() {
|
||||
return &SaveBookmarkResult{Bookmark: existing, Outcome: SaveOutcomeDeleted}, nil
|
||||
}
|
||||
return s.createBookmark(ctx, req, dedupKey)
|
||||
|
||||
@@ -74,11 +74,18 @@ type SyncQueueItem struct {
|
||||
}
|
||||
|
||||
func NewSyncQueueProcessor(db *database.Queries) *SyncQueueProcessor {
|
||||
return NewSyncQueueProcessorWithConfig(db, 5*time.Second, 50)
|
||||
}
|
||||
|
||||
// NewSyncQueueProcessorWithConfig constructs a processor with the given flush
|
||||
// interval and batch size. Used at startup to source values from the settings
|
||||
// registry.
|
||||
func NewSyncQueueProcessorWithConfig(db *database.Queries, interval time.Duration, batchSize int) *SyncQueueProcessor {
|
||||
return &SyncQueueProcessor{
|
||||
db: db,
|
||||
progressChan: make(chan *ProgressUpdate, 100),
|
||||
interval: 5 * time.Second,
|
||||
batchSize: 50,
|
||||
interval: interval,
|
||||
batchSize: batchSize,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+112
-116
@@ -1,140 +1,136 @@
|
||||
package templates
|
||||
|
||||
templ Admin(user User) {
|
||||
templ Admin(user User, stats AdminStats) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Admin Dashboard - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="admin" x-init="loadWatchStatus(); initializeScanWebSocket()" class="theme-tokyo-night">
|
||||
<body x-data="admin" x-init="loadWatchStatus()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin")
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("grid", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Dashboard</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Overview of your Bookhoard library and settings</p>
|
||||
<main class="p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("grid", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Dashboard</h1>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">Library</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage your ebook collection</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/" class="btn btn-secondary mt-4 text-sm">
|
||||
@Icon("arrow-right", "h-4 w-4")
|
||||
View Library
|
||||
</a>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("sync", "h-5 w-5")
|
||||
</span>
|
||||
<div>
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">Scan Watch Status</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Auto-detecting new files</p>
|
||||
</div>
|
||||
</div>
|
||||
<div id="watch-status" class="mt-4 text-sm" style="color: var(--text-secondary)">
|
||||
<span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: var(--status-success);"></span>
|
||||
Watching <span id="watch-count">0</span> libraries
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Overview of your Bookhoard instance</p>
|
||||
</div>
|
||||
<!-- Stats Grid -->
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--text-secondary);">
|
||||
@Icon("library", "h-4 w-4")
|
||||
<span class="text-xs font-semibold uppercase tracking-wide">Libraries</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ stats.LibraryCount }</p>
|
||||
</div>
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<button @click="scanAllLibraries()" class="btn btn-primary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Rescan Library
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Re-scan existing files and fix metadata</span>
|
||||
</button>
|
||||
<a href="/admin/library" class="btn btn-secondary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("library", "h-5 w-5")
|
||||
Manage Libraries
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Add or remove libraries and scan directories</span>
|
||||
</a>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--text-secondary);">
|
||||
@Icon("book", "h-4 w-4")
|
||||
<span class="text-xs font-semibold uppercase tracking-wide">Books</span>
|
||||
</div>
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ stats.MediaCount }</p>
|
||||
</div>
|
||||
<!-- Scan Progress Section -->
|
||||
<div id="scan-progress-container" class="card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Scanning Libraries
|
||||
</h3>
|
||||
<button @click="hideScanProgress()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--text-secondary);">
|
||||
@Icon("users", "h-4 w-4")
|
||||
<span class="text-xs font-semibold uppercase tracking-wide">Users</span>
|
||||
</div>
|
||||
<!-- Overall Progress -->
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span style="color: var(--text-secondary)">Overall Progress</span>
|
||||
<span id="scan-progress-text" style="color: var(--text-primary)">0%</span>
|
||||
</div>
|
||||
<div class="w-full rounded-full h-3" style="background-color: var(--surface-hover);">
|
||||
<div
|
||||
id="scan-progress-bar"
|
||||
class="h-3 rounded-full transition-all duration-500"
|
||||
style="width: 0%; background-color: var(--accent);"
|
||||
></div>
|
||||
</div>
|
||||
<div id="scan-status" class="text-sm mt-2" style="color: var(--text-secondary)">
|
||||
Starting scan...
|
||||
</div>
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ stats.UserCount }</p>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="flex items-center gap-2 mb-2" style="color: var(--text-secondary);">
|
||||
@Icon("device", "h-4 w-4")
|
||||
<span class="text-xs font-semibold uppercase tracking-wide">Devices</span>
|
||||
</div>
|
||||
<!-- Per-Library Progress -->
|
||||
<div id="library-progress-list" class="space-y-3">
|
||||
<!-- Dynamically populated -->
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">{ stats.DeviceCount }</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Watch Status -->
|
||||
<div class="stat-card mb-6">
|
||||
<div class="flex items-center gap-3">
|
||||
<span class="grid place-items-center h-11 w-11 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("sync", "h-5 w-5")
|
||||
</span>
|
||||
<div class="flex-1">
|
||||
<h3 class="font-semibold" style="color: var(--text-primary)">File Watcher</h3>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Auto-detects new files in library folders</p>
|
||||
</div>
|
||||
<!-- Results Summary -->
|
||||
<div id="scan-results" class="hidden mt-6 p-4 rounded-xl border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h4 class="font-semibold mb-2 flex items-center gap-2" style="color: var(--status-success);">
|
||||
@Icon("check-circle", "h-5 w-5")
|
||||
Scan Complete!
|
||||
</h4>
|
||||
<div id="scan-results-content" style="color: var(--text-secondary)">
|
||||
<!-- Results populated by JS -->
|
||||
</div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button
|
||||
@click="window.location.reload()"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
Refresh to View Books
|
||||
</button>
|
||||
<button
|
||||
@click="hideScanProgress()"
|
||||
class="btn btn-secondary"
|
||||
>
|
||||
Dismiss
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-right text-sm" style="color: var(--text-secondary)">
|
||||
<span class="inline-block w-2 h-2 rounded-full mr-2" style="background-color: var(--status-success);"></span>
|
||||
Watching <span id="watch-count">0</span> libraries
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Quick Actions -->
|
||||
<div class="card p-6">
|
||||
<h3 class="text-lg font-semibold mb-4" style="color: var(--text-primary)">Quick Actions</h3>
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<button @click="scanAllLibraries()" class="btn btn-primary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Scan All Libraries
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Re-scan existing files and detect new items</span>
|
||||
</button>
|
||||
<a href="/admin/library" class="btn btn-secondary py-4 flex-col items-start gap-1">
|
||||
<span class="flex items-center gap-2 font-medium">
|
||||
@Icon("library", "h-5 w-5")
|
||||
Manage Libraries
|
||||
</span>
|
||||
<span class="text-xs font-normal opacity-80">Add or remove libraries and folders</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Scan Progress Section -->
|
||||
<div id="scan-progress-container" class="card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold flex items-center gap-2" style="color: var(--text-primary)">
|
||||
@Icon("refresh", "h-5 w-5")
|
||||
Scanning Libraries
|
||||
</h3>
|
||||
<button @click="hideScanProgress()" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div class="mb-4">
|
||||
<div class="flex justify-between text-sm mb-2">
|
||||
<span style="color: var(--text-secondary)">Overall Progress</span>
|
||||
<span id="scan-progress-text" style="color: var(--text-primary)">0%</span>
|
||||
</div>
|
||||
<div class="w-full rounded-full h-3" style="background-color: var(--surface-hover);">
|
||||
<div
|
||||
id="scan-progress-bar"
|
||||
class="h-3 rounded-full transition-all duration-500"
|
||||
style="width: 0%; background-color: var(--accent);"
|
||||
></div>
|
||||
</div>
|
||||
<div id="scan-status" class="text-sm mt-2" style="color: var(--text-secondary)">
|
||||
Starting scan...
|
||||
</div>
|
||||
</div>
|
||||
<div id="library-progress-list" class="space-y-3"></div>
|
||||
<div id="scan-results" class="hidden mt-6 p-4 rounded-xl border" style="background-color: var(--bg-primary); border-color: var(--border);">
|
||||
<h4 class="font-semibold mb-2 flex items-center gap-2" style="color: var(--status-success);">
|
||||
@Icon("check-circle", "h-5 w-5")
|
||||
Scan Complete!
|
||||
</h4>
|
||||
<div id="scan-results-content" style="color: var(--text-secondary)"></div>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<button @click="window.location.reload()" class="btn btn-primary">Refresh to View Books</button>
|
||||
<button @click="hideScanProgress()" class="btn btn-secondary">Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
+351
-125
@@ -6,126 +6,54 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Library Management - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/library")
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin/library")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-5xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<a href="/admin" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Dashboard
|
||||
</a>
|
||||
<button data-action="show-create-modal" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create Library
|
||||
</button>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Library Management</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage libraries and configure media scanning</p>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Libraries Section -->
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Libraries</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage media libraries and their folders</p>
|
||||
<div id="libraries-list" class="space-y-3 mb-2">
|
||||
if len(libraries) == 0 {
|
||||
<p class="text-center py-8 text-sm" style="color: var(--text-secondary)">
|
||||
No libraries yet. Create your first library to get started.
|
||||
</p>
|
||||
} else {
|
||||
for _, library := range libraries {
|
||||
<div class="p-4 rounded-xl border transition-colors hover:bg-surface-hover" style="background-color: var(--bg-primary); border-color: var(--border)">
|
||||
<div class="flex justify-between items-start gap-3 mb-2">
|
||||
<div class="min-w-0">
|
||||
<h4 class="font-semibold" style="color: var(--text-primary)">{ library.Name }</h4>
|
||||
if library.Description != "" {
|
||||
<p class="text-sm" style="color: var(--text-secondary)">{ library.Description }</p>
|
||||
}
|
||||
<span class="chip mt-1">
|
||||
@Icon("tag", "h-3 w-3")
|
||||
{ library.TypeName }
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-1 shrink-0">
|
||||
<button data-library-id={ library.ID } data-action="show-folders" class="btn btn-secondary text-xs px-2.5 py-1">
|
||||
@Icon("folder", "h-4 w-4")
|
||||
Folders
|
||||
</button>
|
||||
<button data-library-id={ library.ID } data-action="edit" class="btn btn-primary text-xs px-2.5 py-1">
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Edit
|
||||
</button>
|
||||
<button data-library-id={ library.ID } data-action="delete" class="btn btn-danger text-xs px-2.5 py-1">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id={ "library-folders-" + library.ID } class="hidden mt-3 space-y-2"></div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
<main class="p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Libraries</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Manage media libraries, folders, and scanning</p>
|
||||
</div>
|
||||
<!-- User Library Visibility Section -->
|
||||
<div class="card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("check-circle", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Library Visibility</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Control which libraries are visible to users</p>
|
||||
<div id="visibility-controls" class="space-y-4">
|
||||
<!-- Visibility controls will be loaded here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- User Visibility Management -->
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-1">
|
||||
@Icon("users", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">User Library Access</h3>
|
||||
</div>
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Manage individual user access to specific libraries</p>
|
||||
<div class="mb-4">
|
||||
<select id="user-select" onchange="loadUserVisibility()" class="input w-auto">
|
||||
<option value="">Select a user...</option>
|
||||
for _, user := range users {
|
||||
<option value={ user.ID }>{ user.Username } ({ user.Email })</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
<div id="user-libraries" class="space-y-3">
|
||||
<!-- User library checkboxes will be loaded here -->
|
||||
</div>
|
||||
<button
|
||||
onclick="document.getElementById('create-library-modal').classList.remove('hidden')"
|
||||
class="btn btn-primary"
|
||||
>
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create Library
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
<!-- Create Library Modal -->
|
||||
<div id="libraries-container">
|
||||
@LibraryList(user, libraries, users)
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<!-- Create / Edit Library Modal -->
|
||||
<div id="create-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Create Library</h2>
|
||||
<button type="button" data-action="hide-create-modal" class="icon-btn" aria-label="Close">
|
||||
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<form id="create-library-form">
|
||||
<input type="hidden" id="library-id" name="id"/>
|
||||
<form
|
||||
hx-post="/admin/library/create"
|
||||
hx-target="#libraries-container"
|
||||
hx-swap="innerHTML"
|
||||
onsubmit="document.getElementById('create-library-modal').classList.add('hidden')"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
||||
<input type="text" name="name" placeholder="My Ebook Library" class="input" required/>
|
||||
@@ -143,7 +71,7 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" data-action="hide-create-modal" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" onclick="document.getElementById('create-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Create
|
||||
@@ -152,42 +80,340 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Folder Browser Modal -->
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
|
||||
<button type="button" data-action="browse-cancel" class="icon-btn" aria-label="Close">
|
||||
<!-- Edit Library Modal -->
|
||||
<div id="edit-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Edit Library</h2>
|
||||
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="folder-browser-content">
|
||||
<!-- Directory listings will be rendered here -->
|
||||
</div>
|
||||
<form id="edit-library-form" hx-target="#libraries-container" hx-swap="innerHTML" onsubmit="document.getElementById('edit-library-modal').classList.add('hidden')">
|
||||
<div class="mb-4">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Library Name</label>
|
||||
<input type="text" name="name" id="edit-library-name" class="input" required/>
|
||||
</div>
|
||||
<div class="mb-6">
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Description</label>
|
||||
<textarea name="description" id="edit-library-desc" rows="3" class="input"></textarea>
|
||||
</div>
|
||||
<div class="flex justify-end space-x-3">
|
||||
<button type="button" onclick="document.getElementById('edit-library-modal').classList.add('hidden')" class="btn btn-secondary">Cancel</button>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Library Confirmation Modal -->
|
||||
<!-- Folder Browser Modal -->
|
||||
<div id="folder-browser-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-lg mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Browse Folders</h2>
|
||||
<button type="button" onclick="document.getElementById('folder-browser-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="folder-browser-content"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Delete Library Modal -->
|
||||
<div id="delete-library-modal" class="hidden fixed inset-0 z-50 flex items-center justify-center p-4" style="background-color: var(--surface-overlay);">
|
||||
<div class="card p-6 w-full max-w-md mx-4" style="box-shadow: var(--shadow-pop);">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold" style="color: var(--text-primary)">Delete Library</h2>
|
||||
<button type="button" data-action="hide-delete-modal" class="icon-btn" aria-label="Close">
|
||||
<button type="button" onclick="document.getElementById('delete-library-modal').classList.add('hidden')" class="icon-btn" aria-label="Close">
|
||||
@Icon("close", "h-5 w-5")
|
||||
</button>
|
||||
</div>
|
||||
<div id="delete-modal-content" class="mb-6" style="color: var(--text-primary)">
|
||||
<!-- Dynamic content will be injected here -->
|
||||
</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" data-action="hide-delete-modal" class="btn btn-secondary">Cancel</button>
|
||||
<button type="button" data-action="confirm-delete" class="btn btn-danger">
|
||||
<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">
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script>
|
||||
function toggleLibraryPanel(btn) {
|
||||
var libId = btn.dataset.libId;
|
||||
var panel = document.getElementById('library-panel-' + libId);
|
||||
if (panel.innerHTML.trim() !== '') {
|
||||
panel.innerHTML = '';
|
||||
} else {
|
||||
htmx.ajax('GET', '/admin/library/' + libId + '/panel', {
|
||||
target: '#library-panel-' + libId,
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
}
|
||||
}
|
||||
function openEditModal(id, name, description) {
|
||||
document.getElementById('edit-library-form').setAttribute('hx-put', '/admin/library/' + id);
|
||||
document.getElementById('edit-library-name').value = name;
|
||||
document.getElementById('edit-library-desc').value = description;
|
||||
htmx.process(document.getElementById('edit-library-form'));
|
||||
document.getElementById('edit-library-modal').classList.remove('hidden');
|
||||
}
|
||||
function openDeleteModal(id, name) {
|
||||
const btn = document.getElementById('delete-library-confirm');
|
||||
btn.setAttribute('hx-delete', '/admin/library/' + id);
|
||||
btn.setAttribute('hx-target', '#libraries-container');
|
||||
btn.setAttribute('hx-swap', 'innerHTML');
|
||||
document.getElementById('delete-library-name').textContent = name;
|
||||
htmx.process(btn);
|
||||
document.getElementById('delete-library-modal').classList.remove('hidden');
|
||||
}
|
||||
function openFolderBrowser(targetInputId, libraryId) {
|
||||
const content = document.getElementById('folder-browser-content');
|
||||
content.setAttribute('hx-get', '/admin/library/browse');
|
||||
content.setAttribute('hx-vals', '{"target_input": "' + targetInputId + '", "library_id": "' + libraryId + '"}');
|
||||
content.setAttribute('hx-trigger', 'load');
|
||||
htmx.process(content);
|
||||
document.getElementById('folder-browser-modal').classList.remove('hidden');
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ LibraryList(user User, libraries []LibraryData, users []User) {
|
||||
if len(libraries) == 0 {
|
||||
<div class="card text-center py-16">
|
||||
<span class="grid place-items-center h-14 w-14 mx-auto mb-4 rounded-2xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-7 w-7")
|
||||
</span>
|
||||
<h3 class="text-xl font-semibold mb-2" style="color: var(--text-primary)">No Libraries Yet</h3>
|
||||
<p class="mb-4 text-sm" style="color: var(--text-secondary)">Create your first library to get started</p>
|
||||
<button onclick="document.getElementById('create-library-modal').classList.remove('hidden')" class="btn btn-primary">Create Your First Library</button>
|
||||
</div>
|
||||
} else {
|
||||
<div class="space-y-4">
|
||||
for _, library := range libraries {
|
||||
<div class="card overflow-hidden">
|
||||
<div class="flex items-center justify-between gap-4 p-5">
|
||||
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("library", "h-5 w-5")
|
||||
</span>
|
||||
<div class="min-w-0">
|
||||
<h3 class="font-semibold truncate" style="color: var(--text-primary)">{ library.Name }</h3>
|
||||
if library.Description != "" {
|
||||
<p class="text-sm truncate" style="color: var(--text-secondary)">{ library.Description }</p>
|
||||
}
|
||||
</div>
|
||||
<span class="chip shrink-0">
|
||||
@Icon("tag", "h-3 w-3")
|
||||
{ library.TypeName }
|
||||
</span>
|
||||
if library.FolderCount > 0 {
|
||||
<span class="chip shrink-0">
|
||||
@Icon("folder", "h-3 w-3")
|
||||
{ library.FolderCount } folders
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
hx-post={ "/api/libraries/" + library.ID + "/scan" }
|
||||
hx-vals='{"force": "true"}'
|
||||
hx-target="#scan-indicator"
|
||||
hx-swap="innerHTML"
|
||||
class="btn btn-secondary text-xs px-3 py-1.5"
|
||||
>
|
||||
@Icon("refresh", "h-4 w-4")
|
||||
Scan
|
||||
</button>
|
||||
<button
|
||||
data-lib-id={ library.ID }
|
||||
onclick="toggleLibraryPanel(this)"
|
||||
class="btn btn-secondary text-xs px-3 py-1.5"
|
||||
>
|
||||
@Icon("chevron-down", "h-4 w-4")
|
||||
Manage
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id={ "library-panel-" + library.ID }></div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div id="scan-indicator"></div>
|
||||
}
|
||||
|
||||
templ LibraryPanel(user User, libraryID string, library LibraryData, folders []FolderData, users []User, visibility []UserVisibilityData, issueCount int) {
|
||||
<div class="border-t p-5 space-y-6" style="border-color: var(--border);">
|
||||
<!-- Folders -->
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@Icon("folder", "h-4 w-4 shrink-0")
|
||||
<h4 class="text-sm font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">Folders</h4>
|
||||
</div>
|
||||
if len(folders) == 0 {
|
||||
<p class="text-sm mb-3" style="color: var(--text-secondary)">No folders configured. Add a folder to enable scanning.</p>
|
||||
} else {
|
||||
<div class="space-y-2 mb-3">
|
||||
for _, folder := range folders {
|
||||
<div class="flex items-center justify-between gap-2 p-2 rounded-lg" style="background-color: var(--bg-primary);">
|
||||
<code class="text-xs flex-1 truncate" style="color: var(--text-primary)">{ folder.FolderPath }</code>
|
||||
<button
|
||||
class="icon-btn h-7 w-7 shrink-0"
|
||||
hx-delete={ "/admin/library/" + libraryID + "/folders" }
|
||||
hx-vals={ `{"folder_path": "` + folder.FolderPath + `"}` }
|
||||
hx-target={ "#library-panel-" + libraryID }
|
||||
hx-swap="innerHTML"
|
||||
hx-confirm="Remove this folder from the library?"
|
||||
>
|
||||
@Icon("trash", "h-3.5 w-3.5")
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<form class="flex gap-2" hx-post={ "/admin/library/" + libraryID + "/folders" } hx-target={ "#library-panel-" + libraryID } hx-swap="innerHTML">
|
||||
<input
|
||||
type="text"
|
||||
name="folder_path"
|
||||
id={ "folder-input-" + libraryID }
|
||||
placeholder="/path/to/books"
|
||||
class="input flex-1"
|
||||
required
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
data-library-id={ libraryID }
|
||||
data-target-input={ "folder-input-" + libraryID }
|
||||
onclick="openFolderBrowser(this.dataset.targetInput, this.dataset.libraryId)"
|
||||
class="btn btn-secondary text-sm shrink-0"
|
||||
>
|
||||
@Icon("folder", "h-4 w-4")
|
||||
Browse
|
||||
</button>
|
||||
<button type="submit" class="btn btn-primary text-sm shrink-0">
|
||||
@Icon("plus", "h-4 w-4")
|
||||
Add
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
<!-- User Visibility -->
|
||||
if len(users) > 0 {
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3">
|
||||
@Icon("users", "h-4 w-4 shrink-0")
|
||||
<h4 class="text-sm font-semibold uppercase tracking-wide" style="color: var(--text-secondary)">User Access</h4>
|
||||
</div>
|
||||
<div class="space-y-1">
|
||||
for _, u := range users {
|
||||
<label class="flex items-center gap-3 cursor-pointer p-2 rounded-lg transition-colors hover:bg-surface-hover">
|
||||
<input
|
||||
type="checkbox"
|
||||
class="w-4 h-4 rounded"
|
||||
name="is_visible"
|
||||
value="true"
|
||||
checked?={ isUserVisible(u.ID, visibility) }
|
||||
hx-post={ "/admin/library/" + libraryID + "/visibility" }
|
||||
hx-vals={ `{"user_id": "` + u.ID + `"}` }
|
||||
hx-trigger="change"
|
||||
hx-target={ "#library-panel-" + libraryID }
|
||||
hx-swap="innerHTML"
|
||||
/>
|
||||
<span class="text-sm" style="color: var(--text-primary)">{ u.Username }</span>
|
||||
<span class="text-xs" style="color: var(--text-secondary)">{ u.Email }</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<!-- Processing Issues -->
|
||||
if issueCount > 0 {
|
||||
<div>
|
||||
<a href={ "/admin/libraries/" + libraryID + "/issues" } class="flex items-center gap-2 text-sm" style="color: var(--status-warning);">
|
||||
@Icon("alert", "h-4 w-4")
|
||||
if issueCount == 1 {
|
||||
<span>1 processing issue</span>
|
||||
} else {
|
||||
<span>{ issueCount } processing issues</span>
|
||||
}
|
||||
@Icon("chevron-right", "h-4 w-4")
|
||||
</a>
|
||||
</div>
|
||||
}
|
||||
<!-- Actions -->
|
||||
<div class="flex gap-2 pt-2 border-t" style="border-color: var(--border);">
|
||||
<button
|
||||
data-edit-id={ libraryID }
|
||||
data-edit-name={ library.Name }
|
||||
data-edit-desc={ library.Description }
|
||||
onclick="openEditModal(this.dataset.editId, this.dataset.editName, this.dataset.editDesc)"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
@Icon("edit", "h-4 w-4")
|
||||
Edit Details
|
||||
</button>
|
||||
<button
|
||||
data-delete-id={ libraryID }
|
||||
data-delete-name={ library.Name }
|
||||
onclick="openDeleteModal(this.dataset.deleteId, this.dataset.deleteName)"
|
||||
class="btn btn-danger text-sm"
|
||||
>
|
||||
@Icon("trash", "h-4 w-4")
|
||||
Delete Library
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
templ FolderBrowserContent(currentPath string, parentPath string, entries []DirEntry, targetInput string, libraryID string) {
|
||||
<div>
|
||||
<div class="flex items-center gap-2 mb-3 p-2 rounded-lg" style="background-color: var(--bg-primary);">
|
||||
@Icon("folder", "h-4 w-4 shrink-0")
|
||||
<code class="text-xs flex-1 truncate" style="color: var(--text-primary)">{ currentPath }</code>
|
||||
</div>
|
||||
if parentPath != "" {
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-secondary);"
|
||||
hx-get={ "/admin/library/browse?path=" + parentPath + "&target_input=" + targetInput + "&library_id=" + libraryID }
|
||||
hx-target="#folder-browser-content"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
<span>..</span>
|
||||
</button>
|
||||
}
|
||||
<div class="space-y-1 max-h-64 overflow-y-auto">
|
||||
for _, entry := range entries {
|
||||
<button
|
||||
class="w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
style="color: var(--text-primary);"
|
||||
hx-get={ "/admin/library/browse?path=" + entry.Path + "&target_input=" + targetInput + "&library_id=" + libraryID }
|
||||
hx-target="#folder-browser-content"
|
||||
hx-swap="innerHTML"
|
||||
>
|
||||
@Icon("folder", "h-4 w-4 shrink-0")
|
||||
<span class="truncate">{ entry.Name }</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
data-target-input={ targetInput }
|
||||
data-current-path={ currentPath }
|
||||
onclick="document.getElementById(this.dataset.targetInput).value = this.dataset.currentPath; document.getElementById('folder-browser-modal').classList.add('hidden')"
|
||||
class="btn btn-primary text-sm"
|
||||
>
|
||||
@Icon("check", "h-4 w-4")
|
||||
Select This Folder
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+853
-258
File diff suppressed because it is too large
Load Diff
@@ -7,28 +7,23 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
|
||||
<meta charset="UTF-8"/>
|
||||
<title>Processing Issues - Bookhoard</title>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="processingIssues" x-init="initializeProcessingIssues('{ libraryID }')" class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/libraries/"+libraryID)
|
||||
<main class="flex-1 p-8">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Processing Issues</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Items that couldn't be processed in this library</p>
|
||||
</div>
|
||||
<a href="/admin/libraries/{ libraryID }" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Library
|
||||
</a>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/library")
|
||||
<main class="p-8">
|
||||
<div class="mx-auto max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("alert", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">Processing Issues</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Items that couldn't be processed in this library</p>
|
||||
</div>
|
||||
</div>
|
||||
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
|
||||
<!-- Stats Cards -->
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
@@ -72,7 +67,7 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
|
||||
<!-- Issues List -->
|
||||
<div class="space-y-4">
|
||||
for _, issue := range issues {
|
||||
<div class="card p-6">
|
||||
<div class="card p-6" id={ "issue-" + issue.ID }>
|
||||
<div class="flex justify-between items-start gap-4 mb-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="text-lg font-semibold mb-2" style="color: var(--text-primary)">{ issue.Title }</h4>
|
||||
@@ -94,17 +89,20 @@ templ AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssu
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-3 mt-4">
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
<button
|
||||
@click="dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
@Icon("close", "h-4 w-4")
|
||||
Dismiss
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
<div class="flex gap-3 mt-4">
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
<button
|
||||
hx-post={ "/api/libraries/" + libraryID + "/issues/" + issue.ID + "/" + issue.MediaItemID + "/resolve" }
|
||||
hx-target={ "#issue-" + issue.ID }
|
||||
hx-swap="outerHTML"
|
||||
hx-confirm="Dismiss this issue?"
|
||||
class="btn btn-secondary text-sm"
|
||||
>
|
||||
@Icon("close", "h-4 w-4")
|
||||
Dismiss
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -29,15 +29,15 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Processing Issues - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"processingIssues\" x-init=\"initializeProcessingIssues('{ libraryID }')\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Processing Issues - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Header(user, "/admin/libraries/"+libraryID).Render(ctx, templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = Header(user, "/admin/library").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"flex-1 p-8\"><div class=\"mx-auto max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"p-8\"><div class=\"mx-auto max-w-4xl\"><div class=\"mb-8\"><div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -45,25 +45,17 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Processing Issues</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Items that couldn't be processed in this library</p></div><a href=\"/admin/libraries/{ libraryID }\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Library</a></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Processing Issues</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Items that couldn't be processed in this library</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if stats.ErrorCount > 0 || stats.WarningCount > 0 || stats.InfoCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<!-- Stats Cards --> <div class=\"grid grid-cols-1 md:grid-cols-3 gap-6 mb-8\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if stats.ErrorCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-danger);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-danger);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-danger);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -71,26 +63,26 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Errors</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Errors</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 41, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if stats.WarningCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-warning);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-warning);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-warning);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-warning);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -98,26 +90,26 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Warnings</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Warnings</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 50, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if stats.InfoCount > 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-info);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-info);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<div class=\"stat-card\" style=\"border-left: 4px solid var(--status-info);\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--status-info);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -125,31 +117,31 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Info</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<h3 class=\"text-sm font-semibold uppercase tracking-wide\">Info</h3></div><p class=\"text-3xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 59, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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 {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if len(issues) == 0 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<div class=\"card p-8 text-center\"><span class=\"grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<div class=\"card p-8 text-center\"><span class=\"grid place-items-center h-12 w-12 mx-auto mb-3 rounded-2xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -157,26 +149,39 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "</span><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No processing issues found for this library.</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</span><p class=\"text-sm\" style=\"color: var(--text-secondary)\">No processing issues found for this library.</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<!-- Issues List --> <div class=\"space-y-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<!-- Issues List --> <div class=\"space-y-4\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, issue := range issues {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<div class=\"card p-6\"><div class=\"flex justify-between items-start gap-4 mb-4\"><div class=\"flex-1 min-w-0\"><h4 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<div class=\"card p-6\" id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Title)
|
||||
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: 78, Col: 98}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 70, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\"><div class=\"flex justify-between items-start gap-4 mb-4\"><div class=\"flex-1 min-w-0\"><h4 class=\"text-lg font-semibold mb-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -184,12 +189,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueDescription)
|
||||
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: 79, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 74, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -197,12 +202,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(issue.IssueType)
|
||||
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: 81, Col: 140}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 76, Col: 140}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -210,12 +215,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FormatGroup)
|
||||
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: 82, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 77, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -223,12 +228,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(issue.FilePath)
|
||||
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: 83, Col: 139}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 78, Col: 139}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -236,12 +241,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(issue.LibraryTypeName)
|
||||
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: 84, Col: 149}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 79, Col: 149}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -254,12 +259,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
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: 89, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 84, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -272,12 +277,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
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: 91, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 86, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -290,12 +295,12 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(issue.Severity)
|
||||
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: 93, Col: 66}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_processing_issues.templ`, Line: 88, Col: 66}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -309,7 +314,33 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if issue.Severity == "warning" || issue.Severity == "info" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button @click=\"dismissIssue('{ issue.ID }', '{ issue.MediaItemID }')\" class=\"btn btn-secondary text-sm\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button hx-post=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" hx-swap=\"outerHTML\" hx-confirm=\"Dismiss this issue?\" class=\"btn btn-secondary text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -317,22 +348,22 @@ func AdminProcessingIssues(user User, libraryID string, issues []ProcessingIssue
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "Dismiss</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "Dismiss</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></main></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+161
-25
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ AdminSettings(user User, systemConfig map[string]string, errorMessage string) {
|
||||
import "fmt"
|
||||
|
||||
templ AdminSettings(user User, systemConfig map[string]string, scanSettings ScanSettingsData, liveGroups []SettingGroup, restartGroups []SettingGroup, errorMessage string) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -8,28 +10,21 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
|
||||
<title>System Settings - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="adminSettings">
|
||||
@Header(user, "/admin/settings")
|
||||
<div class="flex">
|
||||
@AdminSidebar(user, "/admin/settings")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap mb-4">
|
||||
<a href="/admin" class="btn btn-secondary">
|
||||
@Icon("arrow-left", "h-4 w-4")
|
||||
Back to Dashboard
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("settings", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">System Settings</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
|
||||
</div>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/admin/settings")
|
||||
<main class="p-8">
|
||||
<div class="max-w-4xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("gear", "h-5 w-5")
|
||||
</span>
|
||||
<h1 class="text-2xl font-bold tracking-tight" style="color: var(--text-primary)">System Settings</h1>
|
||||
</div>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Configure your Bookhoard instance</p>
|
||||
</div>
|
||||
if errorMessage != "" {
|
||||
<div class="mb-6 p-4 rounded-xl border flex items-start gap-3" style="background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);">
|
||||
@Icon("alert", "h-5 w-5 shrink-0 mt-0.5")
|
||||
@@ -113,9 +108,150 @@ templ AdminSettings(user User, systemConfig map[string]string, errorMessage stri
|
||||
<p><span class="font-medium uppercase tracking-wide text-xs" style="color: var(--text-secondary)">Device Sync:</span> { systemConfig["base_url"] }/api/sync</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@ScanSettingsSection(scanSettings)
|
||||
@TunableSettingsSection(liveGroups, false)
|
||||
@TunableSettingsSection(restartGroups, true)
|
||||
</div>
|
||||
</body>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
templ ScanSettingsSection(scanSettings ScanSettingsData) {
|
||||
<div id="scan-settings-section" class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-6">
|
||||
@Icon("refresh", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Scanning</h3>
|
||||
</div>
|
||||
<form
|
||||
hx-put="/admin/settings/scan"
|
||||
hx-target="#scan-settings-section"
|
||||
hx-swap="outerHTML"
|
||||
>
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary)">Auto-Scan</label>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Watch libraries for file changes on startup</p>
|
||||
</div>
|
||||
<label class="relative inline-flex items-center cursor-pointer shrink-0">
|
||||
<input
|
||||
type="checkbox"
|
||||
name="auto_scan_enabled"
|
||||
value="true"
|
||||
checked?={ scanSettings.AutoScanEnabled }
|
||||
class="sr-only peer"
|
||||
/>
|
||||
<div class="w-11 h-6 rounded-full peer peer-checked:bg-brand transition-colors" style="background-color: color-mix(in srgb, var(--text-primary) 15%, transparent);"></div>
|
||||
<div class="absolute left-0.5 top-0.5 bg-white rounded-full w-5 h-5 transition-transform peer-checked:translate-x-5"></div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-xs font-semibold uppercase tracking-wide mb-2" style="color: var(--text-secondary)">Scan Interval (seconds)</label>
|
||||
<input
|
||||
type="number"
|
||||
name="scan_poll_interval_seconds"
|
||||
value={ fmt.Sprintf("%d", scanSettings.ScanPollIntervalSeconds) }
|
||||
min="1"
|
||||
max="3600"
|
||||
class="input"
|
||||
required
|
||||
/>
|
||||
<p class="text-sm mt-2" style="color: var(--text-secondary)">How often to poll libraries for changes (1–3600 seconds). Default: 60.</p>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
@Icon("save", "h-4 w-4")
|
||||
Save Scan Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
|
||||
// TunableSettingsSection renders the editable tunables for a given bucket
|
||||
// (live vs restart-required). Within the card, settings are clustered into
|
||||
// labeled sub-sections by Group (e.g. "Password Quality", "Device Rate Limits").
|
||||
templ TunableSettingsSection(groups []SettingGroup, restartRequired bool) {
|
||||
<div class="mt-6 card p-6">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
if restartRequired {
|
||||
@Icon("alert", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Tunable Settings — Restart Required</h3>
|
||||
} else {
|
||||
@Icon("settings", "h-5 w-5 shrink-0")
|
||||
<h3 class="text-lg font-semibold" style="color: var(--text-primary)">Tunable Settings — Live</h3>
|
||||
}
|
||||
</div>
|
||||
if restartRequired {
|
||||
<p class="text-sm mb-4" style="color: var(--status-warning)">Changes are saved immediately but only take effect after the server restarts.</p>
|
||||
} else {
|
||||
<p class="text-sm mb-4" style="color: var(--text-secondary)">Changes apply immediately — no restart needed.</p>
|
||||
}
|
||||
for _, g := range groups {
|
||||
<div class="mt-5 first:mt-0">
|
||||
<h4 class="text-xs font-semibold uppercase tracking-wide mb-1" style="color: var(--text-secondary)">{ g.Name }</h4>
|
||||
<div>
|
||||
for _, e := range g.Entries {
|
||||
@TunableSettingRow(e)
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
// TunableSettingRow renders a single editable setting as an inline HTMX form.
|
||||
templ TunableSettingRow(e SettingEntry) {
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 py-3" style="border-top: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent);">
|
||||
<div class="flex-1 min-w-0">
|
||||
<label class="block text-sm font-medium" style="color: var(--text-primary)">{ e.Description }</label>
|
||||
if !e.IsDefault {
|
||||
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ e.Key } — modified from default</p>
|
||||
} else {
|
||||
<p class="text-xs mt-0.5" style="color: var(--text-secondary)">{ e.Key }</p>
|
||||
}
|
||||
</div>
|
||||
<form
|
||||
class="flex items-center gap-2 shrink-0"
|
||||
hx-put="/admin/settings/tunable"
|
||||
hx-target={ "#status-" + e.Key }
|
||||
hx-swap="innerHTML"
|
||||
hx-disinherit="*"
|
||||
>
|
||||
<input type="hidden" name="key" value={ e.Key }/>
|
||||
if e.Type == "bool" {
|
||||
<select name="value" class="input py-1.5 text-sm w-28">
|
||||
<option value="true" selected?={ e.Value == "true" }>Yes</option>
|
||||
<option value="false" selected?={ e.Value != "true" }>No</option>
|
||||
</select>
|
||||
} else if e.Type == "int" {
|
||||
<input
|
||||
type="number"
|
||||
name="value"
|
||||
value={ e.Value }
|
||||
if e.Min != "" {
|
||||
min={ e.Min }
|
||||
}
|
||||
if e.Max != "" {
|
||||
max={ e.Max }
|
||||
}
|
||||
class="input py-1.5 text-sm w-32"
|
||||
/>
|
||||
} else {
|
||||
<input
|
||||
type="text"
|
||||
name="value"
|
||||
value={ e.Value }
|
||||
class="input py-1.5 text-sm w-40"
|
||||
/>
|
||||
}
|
||||
<button type="submit" class="btn btn-secondary px-3 py-1.5 text-sm">
|
||||
@Icon("save", "h-3.5 w-3.5")
|
||||
Save
|
||||
</button>
|
||||
</form>
|
||||
<span id={ "status-" + e.Key } class="text-xs w-24 text-right" style="color: var(--text-secondary)"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func AdminSettings(user User, systemConfig map[string]string, errorMessage string) templ.Component {
|
||||
import "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) {
|
||||
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
|
||||
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
|
||||
@@ -29,7 +31,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>System Settings - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"adminSettings\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>System Settings - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -37,36 +39,20 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = AdminSidebar(user, "/admin/settings").Render(ctx, templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = Icon("gear", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center justify-between gap-4 flex-wrap mb-4\"><a href=\"/admin\" class=\"btn btn-secondary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-left", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Back to Dashboard</a></div><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("settings", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">System Settings</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">System Settings</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Configure your Bookhoard instance</p></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if errorMessage != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<div class=\"mb-6 p-4 rounded-xl border flex items-start gap-3\" style=\"background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<div class=\"mb-6 p-4 rounded-xl border flex items-start gap-3\" style=\"background-color: color-mix(in srgb, var(--status-danger) 12%, var(--bg-secondary)); border-color: var(--status-danger); color: var(--status-danger);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -74,25 +60,25 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 36, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 31, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</span></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<form id=\"settings-form\" hx-put=\"/api/system/config\" hx-target=\"#settings-form\" hx-swap=\"outerHTML\"><div class=\"card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -100,20 +86,20 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Base URL</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Base URL</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Base URL</label> <input type=\"url\" name=\"base_url\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 50, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 45, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "\" placeholder=\"https://books.example.com\" class=\"input\" required><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn btn-primary\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\" placeholder=\"https://books.example.com\" class=\"input\" required><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">The public URL of your Bookhoard instance (e.g., https://books.example.com). Used for device sync, OPDS, and API endpoints.</p></div><div class=\"mt-6 flex justify-end\"><button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -121,7 +107,7 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Save Settings</button></div></div><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Save Settings</button></div></div><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -129,247 +115,247 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">System Defaults</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"input\"><option value=\"UTC\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">System Defaults</h3></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Default Timezone</label> <select name=\"default_timezone\" id=\"default_timezone\" class=\"input\"><option value=\"UTC\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "UTC" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Honolulu" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">UTC (UTC+0)</option> <option value=\"Pacific/Honolulu\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Honolulu" {
|
||||
if systemConfig["default_timezone"] == "America/Anchorage" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Hawaii (UTC-10)</option> <option value=\"America/Anchorage\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Anchorage" {
|
||||
if systemConfig["default_timezone"] == "America/Los_Angeles" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Alaska (UTC-9/-8)</option> <option value=\"America/Los_Angeles\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Los_Angeles" {
|
||||
if systemConfig["default_timezone"] == "America/Denver" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Pacific (UTC-8/-7)</option> <option value=\"America/Denver\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Denver" {
|
||||
if systemConfig["default_timezone"] == "America/Phoenix" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Mountain (UTC-7/-6)</option> <option value=\"America/Phoenix\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Phoenix" {
|
||||
if systemConfig["default_timezone"] == "America/Chicago" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Mountain - no DST (UTC-7)</option> <option value=\"America/Chicago\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Chicago" {
|
||||
if systemConfig["default_timezone"] == "America/New_York" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Central (UTC-6/-5)</option> <option value=\"America/New_York\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/New_York" {
|
||||
if systemConfig["default_timezone"] == "America/Sao_Paulo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Eastern (UTC-5/-4)</option> <option value=\"America/Sao_Paulo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "America/Sao_Paulo" {
|
||||
if systemConfig["default_timezone"] == "Europe/London" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">Brasilia (UTC-3/-2)</option> <option value=\"Europe/London\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/London" {
|
||||
if systemConfig["default_timezone"] == "Europe/Paris" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">British (UTC+0/+1)</option> <option value=\"Europe/Paris\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Paris" {
|
||||
if systemConfig["default_timezone"] == "Europe/Helsinki" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Central European (UTC+1/+2)</option> <option value=\"Europe/Helsinki\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Helsinki" {
|
||||
if systemConfig["default_timezone"] == "Europe/Moscow" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Eastern European (UTC+2/+3)</option> <option value=\"Europe/Moscow\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Europe/Moscow" {
|
||||
if systemConfig["default_timezone"] == "Asia/Tehran" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Moscow (UTC+3)</option> <option value=\"Asia/Tehran\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Tehran" {
|
||||
if systemConfig["default_timezone"] == "Asia/Dubai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Iran (UTC+3:30)</option> <option value=\"Asia/Dubai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Dubai" {
|
||||
if systemConfig["default_timezone"] == "Asia/Karachi" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Gulf (UTC+4)</option> <option value=\"Asia/Karachi\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Karachi" {
|
||||
if systemConfig["default_timezone"] == "Asia/Kolkata" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">Pakistan (UTC+5)</option> <option value=\"Asia/Kolkata\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Kolkata" {
|
||||
if systemConfig["default_timezone"] == "Asia/Dhaka" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">India (UTC+5:30)</option> <option value=\"Asia/Dhaka\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Dhaka" {
|
||||
if systemConfig["default_timezone"] == "Asia/Bangkok" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 48, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Bangladesh (UTC+6)</option> <option value=\"Asia/Bangkok\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Bangkok" {
|
||||
if systemConfig["default_timezone"] == "Asia/Shanghai" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">Indochina (UTC+7)</option> <option value=\"Asia/Shanghai\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 51, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Shanghai" {
|
||||
if systemConfig["default_timezone"] == "Asia/Tokyo" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">China (UTC+8)</option> <option value=\"Asia/Tokyo\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Asia/Tokyo" {
|
||||
if systemConfig["default_timezone"] == "Australia/Darwin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">Japan/Korea (UTC+9)</option> <option value=\"Australia/Darwin\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Darwin" {
|
||||
if systemConfig["default_timezone"] == "Australia/Sydney" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Australian Central (UTC+9:30)</option> <option value=\"Australia/Sydney\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Australia/Sydney" {
|
||||
if systemConfig["default_timezone"] == "Pacific/Auckland" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">Australian Eastern (UTC+10/+11)</option> <option value=\"Pacific/Auckland\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if systemConfig["default_timezone"] == "Pacific/Auckland" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-4\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, ">New Zealand (UTC+12/+13)</option></select><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Default timezone for users who haven't set their own.</p></div></div></form><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
|
||||
}
|
||||
@@ -377,46 +363,468 @@ func AdminSettings(user User, systemConfig map[string]string, errorMessage strin
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">URL Paths</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)\">OPDS:</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">URL Paths</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)\">OPDS:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 111, Col: 145}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 106, Col: 145}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/opds</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">API:</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "/opds</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">API:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 112, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 107, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 64, "/api</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Device Sync:</span> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "/api</p><p><span class=\"font-medium uppercase tracking-wide text-xs\" style=\"color: var(--text-secondary)\">Device Sync:</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 113, Col: 152}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_settings.templ`, Line: 108, Col: 152}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "/api/sync</p></div></div></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "/api/sync</p></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = ScanSettingsSection(scanSettings).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = TunableSettingsSection(liveGroups, false).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = TunableSettingsSection(restartGroups, true).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
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func ScanSettingsSection(scanSettings ScanSettingsData) 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_Var7 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var7 == nil {
|
||||
templ_7745c5c3_Var7 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 65, "<div id=\"scan-settings-section\" class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-6\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("refresh", "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, 66, "<h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Scanning</h3></div><form hx-put=\"/admin/settings/scan\" hx-target=\"#scan-settings-section\" hx-swap=\"outerHTML\"><div class=\"space-y-4\"><div class=\"flex items-center justify-between gap-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary)\">Auto-Scan</label><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Watch libraries for file changes on startup</p></div><label class=\"relative inline-flex items-center cursor-pointer shrink-0\"><input type=\"checkbox\" name=\"auto_scan_enabled\" value=\"true\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if scanSettings.AutoScanEnabled {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 67, " checked")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 68, " class=\"sr-only peer\"><div class=\"w-11 h-6 rounded-full peer peer-checked:bg-brand transition-colors\" style=\"background-color: color-mix(in srgb, var(--text-primary) 15%, transparent);\"></div><div class=\"absolute left-0.5 top-0.5 bg-white rounded-full w-5 h-5 transition-transform peer-checked:translate-x-5\"></div></label></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Scan Interval (seconds)</label> <input type=\"number\" name=\"scan_poll_interval_seconds\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, 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, 69, "\" min=\"1\" max=\"3600\" class=\"input\" required><p class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">How often to poll libraries for changes (1–3600 seconds). Default: 60.</p></div><div class=\"flex justify-end\"><button type=\"submit\" class=\"btn btn-primary\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 70, "Save Scan Settings</button></div></div></form></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TunableSettingsSection renders the editable tunables for a given bucket
|
||||
// (live vs restart-required). Within the card, settings are clustered into
|
||||
// labeled sub-sections by Group (e.g. "Password Quality", "Device Rate Limits").
|
||||
func TunableSettingsSection(groups []SettingGroup, restartRequired bool) 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_Var9 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var9 == nil {
|
||||
templ_7745c5c3_Var9 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 71, "<div class=\"mt-6 card p-6\"><div class=\"flex items-center gap-2 mb-2\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if restartRequired {
|
||||
templ_7745c5c3_Err = Icon("alert", "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, 72, " <h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Tunable Settings — Restart Required</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = Icon("settings", "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, 73, " <h3 class=\"text-lg font-semibold\" style=\"color: var(--text-primary)\">Tunable Settings — Live</h3>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 74, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if restartRequired {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 75, "<p class=\"text-sm mb-4\" style=\"color: var(--status-warning)\">Changes are saved immediately but only take effect after the server restarts.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 76, "<p class=\"text-sm mb-4\" style=\"color: var(--text-secondary)\">Changes apply immediately — no restart needed.</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
for _, g := range groups {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 77, "<div class=\"mt-5 first:mt-0\"><h4 class=\"text-xs font-semibold uppercase tracking-wide mb-1\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 78, "</h4><div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, e := range g.Entries {
|
||||
templ_7745c5c3_Err = TunableSettingRow(e).Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 79, "</div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 80, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// TunableSettingRow renders a single editable setting as an inline HTMX form.
|
||||
func TunableSettingRow(e SettingEntry) 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_Var11 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var11 == nil {
|
||||
templ_7745c5c3_Var11 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 81, "<div class=\"flex flex-col sm:flex-row sm:items-center gap-3 py-3\" style=\"border-top: 1px solid color-mix(in srgb, var(--text-primary) 7%, transparent);\"><div class=\"flex-1 min-w-0\"><label class=\"block text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 82, "</label> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if !e.IsDefault {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 83, "<p class=\"text-xs mt-0.5\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 84, " — modified from default</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 85, "<p class=\"text-xs mt-0.5\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 86, "</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 87, "</div><form class=\"flex items-center gap-2 shrink-0\" hx-put=\"/admin/settings/tunable\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 88, "\" hx-swap=\"innerHTML\" hx-disinherit=\"*\"><input type=\"hidden\" name=\"key\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 89, "\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if e.Type == "bool" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 90, "<select name=\"value\" class=\"input py-1.5 text-sm w-28\"><option value=\"true\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if e.Value == "true" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 91, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 92, ">Yes</option> <option value=\"false\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if e.Value != "true" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 93, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 94, ">No</option></select> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else if e.Type == "int" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 95, "<input type=\"number\" name=\"value\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 96, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if e.Min != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 97, " min=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 98, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
if e.Max != "" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 99, " max=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 100, "\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 101, " class=\"input py-1.5 text-sm w-32\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 102, "<input type=\"text\" name=\"value\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 103, "\" class=\"input py-1.5 text-sm w-40\"> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 104, "<button type=\"submit\" class=\"btn btn-secondary px-3 py-1.5 text-sm\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("save", "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, 105, "Save</button></form><span id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 106, "\" class=\"text-xs w-24 text-right\" style=\"color: var(--text-secondary)\"></span></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
package templates
|
||||
|
||||
templ AdminSidebar(user User, currentPath string) {
|
||||
<aside class="w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r" style="background-color: var(--bg-secondary); border-color: var(--border);">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center gap-2 mb-6 px-2">
|
||||
<span class="grid place-items-center h-8 w-8 rounded-lg shrink-0" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("shield", "h-4 w-4")
|
||||
</span>
|
||||
<h2 class="text-xs font-bold uppercase tracking-wide" style="color: var(--text-primary)">Admin Panel</h2>
|
||||
</div>
|
||||
<nav class="space-y-0.5">
|
||||
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
|
||||
@Icon("grid", "h-5 w-5 shrink-0")
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/admin/users" class={ activeClass(currentPath, "/admin/users") }>
|
||||
@Icon("users", "h-5 w-5 shrink-0")
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="/admin/library" class={ activeClass(currentPath, "/admin/library") }>
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<span>Library</span>
|
||||
</a>
|
||||
<a href="/admin/settings" class={ activeClass(currentPath, "/admin/settings") }>
|
||||
@Icon("settings", "h-5 w-5 shrink-0")
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
// Code generated by templ - DO NOT EDIT.
|
||||
|
||||
// templ: version: v0.3.1020
|
||||
package templates
|
||||
|
||||
//lint:file-ignore SA4006 This context is only used if a nested component is present.
|
||||
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func AdminSidebar(user User, currentPath string) 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_Var1 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var1 == nil {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<aside class=\"w-64 shrink-0 self-start sticky top-16 h-[calc(100vh-4rem)] overflow-y-auto border-r\" style=\"background-color: var(--bg-secondary); border-color: var(--border);\"><div class=\"p-5\"><div class=\"flex items-center gap-2 mb-6 px-2\"><span class=\"grid place-items-center h-8 w-8 rounded-lg shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("shield", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span><h2 class=\"text-xs font-bold uppercase tracking-wide\" style=\"color: var(--text-primary)\">Admin Panel</h2></div><nav class=\"space-y-0.5\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var2 = []any{activeClass(currentPath, "/admin")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/admin\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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/admin_sidebar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grid", "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, 5, "<span>Dashboard</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var4 = []any{activeClass(currentPath, "/admin/users")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var4...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/admin/users\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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/admin_sidebar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "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, 8, "<span>Users</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var6 = []any{activeClass(currentPath, "/admin/library")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var6...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"/admin/library\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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/admin_sidebar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "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, 11, "<span>Library</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var8 = []any{activeClass(currentPath, "/admin/settings")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"/admin/settings\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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/admin_sidebar.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, 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, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("settings", "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, 14, "<span>Settings</span></a></nav></div></aside>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
var _ = templruntime.GeneratedTemplate
|
||||
+82
-22
@@ -8,7 +8,7 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Admin(user User) templ.Component {
|
||||
func Admin(user User, stats AdminStats) 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 {
|
||||
@@ -29,7 +29,7 @@ func Admin(user User) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Admin Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"admin\" x-init=\"loadWatchStatus(); initializeScanWebSocket()\" class=\"theme-tokyo-night\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Admin Dashboard - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"admin\" x-init=\"loadWatchStatus()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -37,15 +37,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = AdminSidebar(user, "/admin").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<main class=\"p-8\"><div class=\"max-w-4xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -53,23 +45,91 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Dashboard</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Overview of your Bookhoard library and settings</p></div><div class=\"grid grid-cols-1 md:grid-cols-2 gap-6 mb-6\"><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Dashboard</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Overview of your Bookhoard instance</p></div><!-- Stats Grid --><div class=\"grid grid-cols-2 md:grid-cols-4 gap-4 mb-6\"><div class=\"stat-card\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "h-5 w-5").Render(ctx, templ_7745c5c3_Buffer)
|
||||
templ_7745c5c3_Err = Icon("library", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</span><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Library</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage your ebook collection</p></div></div><a href=\"/\" class=\"btn btn-secondary mt-4 text-sm\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span class=\"text-xs font-semibold uppercase tracking-wide\">Libraries</span></div><p class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("arrow-right", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "View Library</a></div><div class=\"stat-card\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "</p></div><div class=\"stat-card\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<span class=\"text-xs font-semibold uppercase tracking-wide\">Books</span></div><p class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</p></div><div class=\"stat-card\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"text-xs font-semibold uppercase tracking-wide\">Users</span></div><p class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</p></div><div class=\"stat-card\"><div class=\"flex items-center gap-2 mb-2\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("device", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span class=\"text-xs font-semibold uppercase tracking-wide\">Devices</span></div><p class=\"text-2xl font-bold\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</p></div></div><!-- Watch Status --><div class=\"stat-card mb-6\"><div class=\"flex items-center gap-3\"><span class=\"grid place-items-center h-11 w-11 rounded-xl shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -77,7 +137,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">Scan Watch Status</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Auto-detecting new files</p></div></div><div id=\"watch-status\" class=\"mt-4 text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full mr-2\" style=\"background-color: var(--status-success);\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn btn-primary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "</span><div class=\"flex-1\"><h3 class=\"font-semibold\" style=\"color: var(--text-primary)\">File Watcher</h3><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Auto-detects new files in library folders</p></div><div class=\"text-right text-sm\" style=\"color: var(--text-secondary)\"><span class=\"inline-block w-2 h-2 rounded-full mr-2\" style=\"background-color: var(--status-success);\"></span> Watching <span id=\"watch-count\">0</span> libraries</div></div></div><!-- Quick Actions --><div class=\"card p-6\"><h3 class=\"text-lg font-semibold mb-4\" style=\"color: var(--text-primary)\">Quick Actions</h3><div class=\"grid grid-cols-1 sm:grid-cols-2 gap-4\"><button @click=\"scanAllLibraries()\" class=\"btn btn-primary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -85,7 +145,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "Rescan Library</span> <span class=\"text-xs font-normal opacity-80\">Re-scan existing files and fix metadata</span></button> <a href=\"/admin/library\" class=\"btn btn-secondary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "Scan All Libraries</span> <span class=\"text-xs font-normal opacity-80\">Re-scan existing files and detect new items</span></button> <a href=\"/admin/library\" class=\"btn btn-secondary py-4 flex-col items-start gap-1\"><span class=\"flex items-center gap-2 font-medium\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -93,7 +153,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "Manage Libraries</span> <span class=\"text-xs font-normal opacity-80\">Add or remove libraries and scan directories</span></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "Manage Libraries</span> <span class=\"text-xs font-normal opacity-80\">Add or remove libraries and folders</span></a></div></div><!-- Scan Progress Section --><div id=\"scan-progress-container\" class=\"card hidden mt-6 p-6 opacity-0 -translate-y-2.5 transition-all duration-300 ease-out\"><div class=\"flex justify-between items-center mb-4\"><h3 class=\"text-lg font-semibold flex items-center gap-2\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -101,7 +161,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "Scanning Libraries</h3><button @click=\"hideScanProgress()\" class=\"icon-btn\" aria-label=\"Close\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -109,7 +169,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</button></div><!-- Overall Progress --><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full rounded-full h-3\" style=\"background-color: var(--surface-hover);\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><!-- Per-Library Progress --><div id=\"library-progress-list\" class=\"space-y-3\"><!-- Dynamically populated --></div><!-- Results Summary --><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-xl border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2 flex items-center gap-2\" style=\"color: var(--status-success);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "</button></div><div class=\"mb-4\"><div class=\"flex justify-between text-sm mb-2\"><span style=\"color: var(--text-secondary)\">Overall Progress</span> <span id=\"scan-progress-text\" style=\"color: var(--text-primary)\">0%</span></div><div class=\"w-full rounded-full h-3\" style=\"background-color: var(--surface-hover);\"><div id=\"scan-progress-bar\" class=\"h-3 rounded-full transition-all duration-500\" style=\"width: 0%; background-color: var(--accent);\"></div></div><div id=\"scan-status\" class=\"text-sm mt-2\" style=\"color: var(--text-secondary)\">Starting scan...</div></div><div id=\"library-progress-list\" class=\"space-y-3\"></div><div id=\"scan-results\" class=\"hidden mt-6 p-4 rounded-xl border\" style=\"background-color: var(--bg-primary); border-color: var(--border);\"><h4 class=\"font-semibold mb-2 flex items-center gap-2\" style=\"color: var(--status-success);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -117,7 +177,7 @@ func Admin(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"><!-- Results populated by JS --></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn btn-primary\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn btn-secondary\">Dismiss</button></div></div></div></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "Scan Complete!</h4><div id=\"scan-results-content\" style=\"color: var(--text-secondary)\"></div><div class=\"mt-4 flex gap-2\"><button @click=\"window.location.reload()\" class=\"btn btn-primary\">Refresh to View Books</button> <button @click=\"hideScanProgress()\" class=\"btn btn-secondary\">Dismiss</button></div></div></div></div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
+10
-16
@@ -8,16 +8,15 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||
<title>Users - Bookhoard Admin</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ currentUser.Theme }">
|
||||
@Header(currentUser, "/admin/users")
|
||||
<!-- Modal Container (populated by HTMX) -->
|
||||
<div id="modal-container"></div>
|
||||
<div class="flex">
|
||||
@AdminSidebar(currentUser, "/admin/users")
|
||||
<main class="flex-1 p-8">
|
||||
<div class="max-w-5xl">
|
||||
<div class="mb-8">
|
||||
@Header(currentUser, "/admin/users")
|
||||
<!-- Modal Container (populated by HTMX) -->
|
||||
<div id="modal-container"></div>
|
||||
<main class="p-8">
|
||||
<div class="max-w-5xl">
|
||||
<div class="mb-8">
|
||||
<div class="flex items-center gap-3 mb-1">
|
||||
<span class="grid place-items-center h-10 w-10 rounded-xl" style="background-color: var(--accent-muted); color: var(--accent);">
|
||||
@Icon("users", "h-5 w-5")
|
||||
@@ -76,14 +75,11 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||
} else {
|
||||
<select
|
||||
hx-put={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#role-result-" + user.ID }
|
||||
hx-swap="innerHTML"
|
||||
hx-trigger="change"
|
||||
name="role"
|
||||
class="input w-auto py-1 pr-7 text-xs"
|
||||
onchange="this.dispatchEvent(new Event('htmx:trigger'))"
|
||||
hx-trigger="change"
|
||||
hx-vals='{"role": this.value}'
|
||||
>
|
||||
<option value="user" selected?={ user.Role == "user" }>User</option>
|
||||
<option value="admin" selected?={ user.Role == "admin" }>Admin</option>
|
||||
@@ -119,7 +115,6 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||
} else {
|
||||
<button
|
||||
hx-delete={ "/api/auth/profile/" + user.ID }
|
||||
hx-headers='{"Authorization": "Bearer " + localStorage.getItem("token")}'
|
||||
hx-target={ "#user-" + user.ID }
|
||||
hx-swap="outerHTML swap:0.5s"
|
||||
hx-confirm="Are you sure you want to delete this user? This action cannot be undone."
|
||||
@@ -136,9 +131,8 @@ templ AdminUsers(currentUser User, users []User, adminCount int) {
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Users - Bookhoard Admin</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ currentUser.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Users - Bookhoard Admin</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ currentUser.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -37,15 +37,7 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><div class=\"flex\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = AdminSidebar(currentUser, "/admin/users").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<main class=\"flex-1 p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<!-- Modal Container (populated by HTMX) --><div id=\"modal-container\"></div><main class=\"p-8\"><div class=\"max-w-5xl\"><div class=\"mb-8\"><div class=\"flex items-center gap-3 mb-1\"><span class=\"grid place-items-center h-10 w-10 rounded-xl\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -53,25 +45,25 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">User Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card overflow-hidden\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "</span><h1 class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">User Management</h1></div><p class=\"text-sm\" style=\"color: var(--text-secondary)\">Manage user accounts and permissions</p></div><!-- Users Table --><div class=\"card overflow-hidden\"><table class=\"w-full\"><thead style=\"background-color: var(--bg-primary)\"><tr><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Username</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Email</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Role</th><th class=\"px-6 py-3 text-left text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Created</th><th class=\"px-6 py-3 text-right text-xs font-semibold uppercase tracking-wide\" style=\"color: var(--text-secondary)\">Actions</th></tr></thead> <tbody class=\"divide-y\" style=\"divide-color: var(--border)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, user := range users {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<tr id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<tr id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 43, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 42, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var2)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\" class=\"transition-colors hover:bg-surface-hover\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center gap-2\"><span class=\"grid place-items-center h-8 w-8 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "\" class=\"transition-colors hover:bg-surface-hover\"><!-- Username --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"flex items-center gap-2\"><span class=\"grid place-items-center h-8 w-8 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -79,143 +71,143 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</span><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "</span><div><div class=\"text-sm font-medium\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 51, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 50, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "</div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "</div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.ID == currentUser.ID {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">You</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span class=\"badge\" style=\"background-color: var(--accent-muted); color: var(--accent);\">You</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "</div></div></td><!-- Email --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-primary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 60, Col: 80}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 59, Col: 80}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "</div></td><!-- Role Toggle (with last-admin protection) --><td class=\"px-6 py-4 whitespace-nowrap\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<!-- Last admin - disabled --> <div class=\"relative\"><select disabled class=\"input w-auto py-1 pr-7 text-xs opacity-50 cursor-not-allowed\" title=\"Cannot demote the last admin\"><option value=\"user\">User</option> <option value=\"admin\" selected>Admin</option></select></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<select hx-put=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<select hx-put=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 78, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 77, Col: 53}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 80, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 78, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\" hx-swap=\"innerHTML\" name=\"role\" class=\"input w-auto py-1 pr-7 text-xs\" onchange=\"this.dispatchEvent(new Event('htmx:trigger'))\" hx-trigger=\"change\" hx-vals='{\"role\": this.value}'><option value=\"user\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "\" hx-swap=\"innerHTML\" hx-trigger=\"change\" name=\"role\" class=\"input w-auto py-1 pr-7 text-xs\"><option value=\"user\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "user" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, ">User</option> <option value=\"admin\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, ">User</option> <option value=\"admin\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, " selected")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, " selected")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, ">Admin</option></select><div id=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, ">Admin</option></select><div id=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 91, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 87, Col: 47}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var7)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "\" class=\"text-xs mt-1\"></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\" class=\"text-xs mt-1\"></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "</td><!-- Created --><td class=\"px-6 py-4 whitespace-nowrap\"><div class=\"text-sm\" style=\"color: var(--text-secondary)\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 96, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 92, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><div class=\"inline-flex items-center gap-1\"><button hx-get=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "</div></td><!-- Actions --><td class=\"px-6 py-4 whitespace-nowrap text-right\"><div class=\"inline-flex items-center gap-1\"><button hx-get=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 102, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 98, Col: 67}
|
||||
}
|
||||
_, 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, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "\" hx-target=\"#modal-container\" hx-swap=\"innerHTML\" class=\"btn btn-secondary text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -223,12 +215,12 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "Edit</button> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "Edit</button> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" && adminCount == 1 {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "<button disabled class=\"btn btn-danger text-xs px-2.5 py-1\" title=\"Cannot delete the last admin\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<button disabled class=\"btn btn-danger text-xs px-2.5 py-1\" title=\"Cannot delete the last admin\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -236,38 +228,38 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "Delete</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "Delete</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
} else {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "<button hx-delete=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "<button hx-delete=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 121, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 117, Col: 57}
|
||||
}
|
||||
_, 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, 28, "\" hx-headers='{\"Authorization\": \"Bearer \" + localStorage.getItem(\"token\")}' hx-target=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\" hx-target=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 123, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_users.templ`, Line: 118, Col: 45}
|
||||
}
|
||||
_, 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, 29, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "\" hx-swap=\"outerHTML swap:0.5s\" hx-confirm=\"Are you sure you want to delete this user? This action cannot be undone.\" class=\"btn btn-danger text-xs px-2.5 py-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -275,17 +267,17 @@ func AdminUsers(currentUser User, users []User, adminCount int) templ.Component
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "Delete</button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "Delete</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</div></td></tr>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "</div></td></tr>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</tbody></table></div></div></main></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</tbody></table></div></div></main></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ templ Analytics(user User) {
|
||||
<title>Reading Analytics - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
|
||||
@Header(user, "/analytics")
|
||||
|
||||
@@ -29,7 +29,7 @@ func Analytics(user User) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Reading Analytics - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"analytics\" x-init=\"loadAnalytics\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Reading Analytics - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"analytics\" x-init=\"loadAnalytics\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ templ BookDetail(user User, book handlers.MediaDetail, errorMessage string) {
|
||||
<title>{ book.Title } - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body
|
||||
x-data="bookDetail"
|
||||
|
||||
@@ -51,14 +51,14 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"bookDetail\" class=\"theme-{ user.Theme }\" data-format-group=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"bookDetail\" class=\"theme-{ user.Theme }\" data-format-group=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 25, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 26, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 27, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 28, Col: 53}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 29, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 35, Col: 101}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 47, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 48, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 56, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 64, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 67, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 73, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 106, Col: 144}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 107, Col: 144}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -370,7 +370,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: 159, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 160, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -393,7 +393,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: 166, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 167, Col: 125}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -406,7 +406,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: 170, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 171, Col: 30}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -424,7 +424,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: 172, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 173, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -444,7 +444,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: 177, Col: 142}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 178, Col: 142}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -463,7 +463,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: 180, Col: 123}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 181, Col: 123}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -488,7 +488,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: 186, Col: 122}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 187, Col: 122}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -516,7 +516,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: 193, Col: 109}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 194, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -529,7 +529,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: 197, Col: 16}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 198, Col: 16}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var24))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -585,7 +585,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: 227, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 228, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var25))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -626,7 +626,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: 244, Col: 124}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 245, Col: 124}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var26))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -639,7 +639,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: 251, Col: 134}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 252, Col: 134}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var27))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -657,7 +657,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: 256, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 257, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var28))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -670,7 +670,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: 256, Col: 131}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 257, Col: 131}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -689,7 +689,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: 262, Col: 140}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 263, Col: 140}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var30))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -708,7 +708,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: 268, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 269, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var31))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -736,7 +736,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: 281, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 282, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -755,7 +755,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: 287, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 288, Col: 92}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var33))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -774,7 +774,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: 293, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 294, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var34))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -793,7 +793,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: 299, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 300, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var35))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -812,7 +812,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: 305, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 306, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var36))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -831,7 +831,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: 311, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 312, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var37))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -850,7 +850,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: 317, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 318, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var38))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -869,7 +869,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: 323, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 324, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var39))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -888,7 +888,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: 329, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 330, Col: 70}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var40))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -907,7 +907,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: 335, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 336, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var41))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -926,7 +926,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: 341, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 342, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var42))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -945,7 +945,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: 347, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 348, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var43))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -964,7 +964,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: 353, Col: 94}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 354, Col: 94}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var44))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -983,7 +983,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: 359, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 360, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var45))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1002,7 +1002,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: 365, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 366, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var46))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1020,7 +1020,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: 370, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 371, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var47))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1038,7 +1038,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: 375, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 376, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var48))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1066,7 +1066,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var49 templ.SafeURL
|
||||
templ_7745c5c3_Var49, 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: 382, Col: 106}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 383, Col: 106}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var49))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1084,7 +1084,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.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 384, Col: 85}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 385, Col: 85}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var50))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1103,7 +1103,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("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: 387, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 388, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var51))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1121,7 +1121,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.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 389, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 390, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var52))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1140,7 +1140,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("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: 392, Col: 110}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 393, Col: 110}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var53))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1158,7 +1158,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.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 394, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 395, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var54))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1177,7 +1177,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("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: 397, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 398, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var55))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1195,7 +1195,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.Isbn, book.Title, book.Author))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 399, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 400, Col: 82}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var56))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1214,7 +1214,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(book.WebUrl.String)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 402, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 403, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var57))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1227,7 +1227,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var58 string
|
||||
templ_7745c5c3_Var58, 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: 402, Col: 238}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 403, Col: 238}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var58))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1260,7 +1260,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var59 templ.SafeURL
|
||||
templ_7745c5c3_Var59, 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: 414, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 415, Col: 54}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var59))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1273,7 +1273,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var60 string
|
||||
templ_7745c5c3_Var60, 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: 416, Col: 118}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 417, Col: 118}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var60))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1286,7 +1286,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var61 string
|
||||
templ_7745c5c3_Var61, 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: 418, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 419, Col: 51}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var61))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1299,7 +1299,7 @@ func BookDetail(user User, book handlers.MediaDetail, errorMessage string) templ
|
||||
var templ_7745c5c3_Var62 string
|
||||
templ_7745c5c3_Var62, 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: 418, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 419, Col: 71}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var62))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -1312,7 +1312,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.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 419, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/book_detail.templ`, Line: 420, Col: 61}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var63))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -25,6 +25,7 @@ templ BookShelf(
|
||||
<title>Library - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body
|
||||
class="theme-{ user.Theme }"
|
||||
|
||||
@@ -45,7 +45,7 @@ func BookShelf(
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Library - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"bookshelf\" x-init=\"initBookshelf()\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Library - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\" x-data=\"bookshelf\" x-init=\"initBookshelf()\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 92, Col: 135}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 94, Col: 25}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 146, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 148, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 149, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -211,7 +211,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: 152, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var6)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -224,7 +224,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: 152, Col: 57}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 57}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -237,7 +237,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: 152, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 153, Col: 77}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -255,7 +255,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: 154, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var9)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -268,7 +268,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: 154, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -281,7 +281,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: 154, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 155, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -310,7 +310,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(errorMessage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 268, Col: 59}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 269, Col: 59}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -333,7 +333,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var13 string
|
||||
templ_7745c5c3_Var13, 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: 277, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 278, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var13)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -364,7 +364,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, 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: 286, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 287, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -377,7 +377,7 @@ func BookShelf(
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, 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: 291, Col: 126}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/bookshelf.templ`, Line: 292, Col: 126}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -13,6 +13,7 @@ templ BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{ pageTitle } - Bookhoard</title>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, backUrl)
|
||||
|
||||
@@ -47,7 +47,7 @@ func BrowseDetail(user User, badgeIcon string, badgeLabel string, title string,
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 21, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 23, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 30, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 30, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 32, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 33, Col: 125}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 43, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 45, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/browse_detail.templ`, Line: 46, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ templ CollectionRules(user User, collection CollectionData) {
|
||||
<title>Collection Rules - { collection.Name } - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="collectionRules" x-init="initCollectionRules('{ collection.ID }')">
|
||||
@Header(user, "/collections")
|
||||
|
||||
@@ -42,7 +42,7 @@ func CollectionRules(user User, collection CollectionData) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"collectionRules\" x-init=\"initCollectionRules('{ collection.ID }')\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\" x-data=\"collectionRules\" x-init=\"initCollectionRules('{ collection.ID }')\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 22, Col: 176}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 24, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 44, Col: 67}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collection_rules.templ`, Line: 45, Col: 67}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var5)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ templ Collection(user User, collections []CollectionData, errorMessage string) {
|
||||
<title>Collections - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
@@ -116,6 +117,7 @@ templ CollectionDetail(user User, collection CollectionData, books []handlers.Bo
|
||||
<title>{ collection.Name } - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="collections" x-init="initCollectionsPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/collections")
|
||||
|
||||
@@ -31,7 +31,7 @@ func Collection(user User, collections []CollectionData, errorMessage string) te
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Collections - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Collections - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 70, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 73, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 76, Col: 41}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 79, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 88, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 98, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 99, Col: 81}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 100, Col: 81}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -246,13 +246,13 @@ 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: 116, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 117, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, " - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"collections\" x-init=\"initCollectionsPage()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 130, Col: 166}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 132, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 133, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 177, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 177, Col: 112}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 177, Col: 147}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 192, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 194, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 199, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 204, Col: 47}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 207, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 383, Col: 26}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 384, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 385, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 395, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 399, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 399, Col: 54}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 401, Col: 61}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 404, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 409, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 416, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 418, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ templ Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total in
|
||||
<title>Sync Conflicts - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="conflicts" class="theme-{ user.Theme }">
|
||||
@Header(user, "/conflicts")
|
||||
|
||||
@@ -31,7 +31,7 @@ func Conflicts(user User, conflicts []handlers.ConflictDetailResponse, total int
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Sync Conflicts - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"conflicts\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Sync Conflicts - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"conflicts\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 44, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 48, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 52, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 110, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 116, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 124, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 125, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 132, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 134, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/conflicts.templ`, Line: 135, Col: 45}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -9,6 +9,7 @@ templ CustomSectionBuilder(user User, libraries []LibraryData, errorMessage stri
|
||||
<title>Create Custom Section - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/custom-section")
|
||||
|
||||
@@ -29,7 +29,7 @@ func CustomSectionBuilder(user User, libraries []LibraryData, errorMessage strin
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Create Custom Section - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Create Custom Section - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 73, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 73, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/custom_section.templ`, Line: 74, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ templ Dashboard(user User, sections []handlers.SectionData, allSections []handle
|
||||
<title>Dashboard - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="dashboard" x-init="initDashboard()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/dashboard")
|
||||
|
||||
@@ -34,7 +34,7 @@ func Dashboard(user User, sections []handlers.SectionData, allSections []handler
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"dashboard\" x-init=\"initDashboard()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Dashboard - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"dashboard\" x-init=\"initDashboard()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 39, Col: 33}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 40, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 44, Col: 130}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 46, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 48, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 54, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 67, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 75, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 76, Col: 39}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var10)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -276,7 +276,7 @@ func CollectionCarousel(section handlers.SectionData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 94, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 95, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -326,7 +326,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var13 templ.SafeURL
|
||||
templ_7745c5c3_Var13, 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: 107, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 108, Col: 40}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -344,7 +344,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.CoverImagePath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 114, Col: 31}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 31}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(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.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 115, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 116, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -375,7 +375,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: 123, Col: 22}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 124, Col: 22}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var16)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -393,7 +393,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: 129, Col: 117}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 117}
|
||||
}
|
||||
_, 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.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 130, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 131, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var18))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -424,7 +424,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.ResolveAttributeValue(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 133, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 100}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(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.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 134, Col: 19}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 135, Col: 19}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var20))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -460,7 +460,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var21 templ.SafeURL
|
||||
templ_7745c5c3_Var21, 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: 142, Col: 40}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 143, Col: 40}
|
||||
}
|
||||
_, 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 string
|
||||
templ_7745c5c3_Var22, 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: 144, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 145, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var22)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -499,7 +499,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var23 templ.SafeURL
|
||||
templ_7745c5c3_Var23, 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: 151, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 152, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var23))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -512,7 +512,7 @@ func BookCard(item handlers.BookInfo) templ.Component {
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, 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: 153, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 154, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -580,7 +580,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 183, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 37}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
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(fmt.Sprintf("%v", section.IsSystem))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 184, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 185, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var27)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -614,7 +614,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.JoinStringErrs(section.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 189, Col: 43}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 190, Col: 43}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 191, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 192, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var29))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -655,7 +655,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(section.ID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 201, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 202, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -688,7 +688,7 @@ func DashboardSettingsModal(sections []handlers.SectionData, hiddenCollections [
|
||||
var templ_7745c5c3_Var31 string
|
||||
templ_7745c5c3_Var31, templ_7745c5c3_Err = templ.JoinStringErrs(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 227, Col: 128}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 228, Col: 128}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(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.ResolveAttributeValue(itemsPerSection)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 234, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/dashboard.templ`, Line: 235, Col: 28}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var32)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []P
|
||||
<title>Device Management - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="devices" x-init="setupEventDelegation()">
|
||||
@Header(user, "/devices")
|
||||
|
||||
+17
-17
@@ -31,7 +31,7 @@ func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []Pe
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Device Management - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"devices\" x-init=\"setupEventDelegation()\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Device Management - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\" x-data=\"devices\" x-init=\"setupEventDelegation()\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 63, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 66, Col: 77}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 71, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 72, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 85, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 93, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 109, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 131, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 146, Col: 78}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 167, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 169, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 169, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 173, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 176, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 241, Col: 148}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 243, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 244, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ templ DocsLayout(nav Navigation, doc Document, user User, currentPath string) {
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>{ doc.Title } - Bookhoard Documentation</title>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
<link rel="stylesheet" href="/static/highlight-dark.min.css"/>
|
||||
</head>
|
||||
<body x-data="docs" x-init="initializeSearch(); highlightCurrentPage(); initializeCodeCopyButtons()" class={ "theme-" + user.Theme + " page-docs font-sans antialiased" }>
|
||||
|
||||
+14
-14
@@ -44,7 +44,7 @@ func DocsLayout(nav Navigation, doc Document, user User, currentPath string) tem
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Documentation</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"stylesheet\" href=\"/static/highlight-dark.min.css\"></head>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Documentation</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"><link rel=\"stylesheet\" href=\"/static/highlight-dark.min.css\"></head>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 49, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 55, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 56, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 57, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 64, Col: 29}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 65, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 66, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 81, Col: 28}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 81, Col: 129}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 85, Col: 104}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 95, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 97, Col: 105}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 99, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/docs.templ`, Line: 100, Col: 23}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ templ ErrorPage(message string, errorType string) {
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Error - Bookhoard</title>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
|
||||
@@ -29,14 +29,14 @@ func ErrorPage(message string, errorType string) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Error - Bookhoard</title><style>\n\t\t\t\t* {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tbox-sizing: border-box;\n\t\t\t\t}\n\t\t\t\tbody {\n\t\t\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n\t\t\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\t\t\tmin-height: 100vh;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tcolor: #eee;\n\t\t\t\t}\n\t\t\t\t.error-container {\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tpadding: 2rem;\n\t\t\t\t\tmax-width: 500px;\n\t\t\t\t}\n\t\t\t\t.error-icon {\n\t\t\t\t\tfont-size: 4rem;\n\t\t\t\t\tmargin-bottom: 1rem;\n\t\t\t\t}\n\t\t\t\t.error-title {\n\t\t\t\t\tfont-size: 1.5rem;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t\tmargin-bottom: 1rem;\n\t\t\t\t\tcolor: #fff;\n\t\t\t\t}\n\t\t\t\t.error-message {\n\t\t\t\t\tfont-size: 1rem;\n\t\t\t\t\tcolor: #ccc;\n\t\t\t\t\tmargin-bottom: 2rem;\n\t\t\t\t\tline-height: 1.6;\n\t\t\t\t}\n\t\t\t\t.error-actions {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tgap: 1rem;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tflex-wrap: wrap;\n\t\t\t\t}\n\t\t\t\t.btn {\n\t\t\t\t\tpadding: 0.75rem 1.5rem;\n\t\t\t\t\tborder-radius: 0.5rem;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\ttransition: all 0.2s;\n\t\t\t\t\tborder: none;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tfont-size: 0.95rem;\n\t\t\t\t}\n\t\t\t\t.btn-primary {\n\t\t\t\t\tbackground: #7aa2f7;\n\t\t\t\t\tcolor: #1a1b26;\n\t\t\t\t}\n\t\t\t\t.btn-primary:hover {\n\t\t\t\t\tbackground: #89b4fa;\n\t\t\t\t}\n\t\t\t\t.btn-secondary {\n\t\t\t\t\tbackground: transparent;\n\t\t\t\t\tcolor: #7aa2f7;\n\t\t\t\t\tborder: 1px solid #7aa2f7;\n\t\t\t\t}\n\t\t\t\t.btn-secondary:hover {\n\t\t\t\t\tbackground: rgba(122, 162, 247, 0.1);\n\t\t\t\t}\n\t\t\t</style></head><body><div class=\"error-container\"><div class=\"error-icon\">⚠️</div><h1 class=\"error-title\">Something went wrong</h1><p class=\"error-message\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Error - Bookhoard</title><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"><style>\n\t\t\t\t* {\n\t\t\t\t\tmargin: 0;\n\t\t\t\t\tpadding: 0;\n\t\t\t\t\tbox-sizing: border-box;\n\t\t\t\t}\n\t\t\t\tbody {\n\t\t\t\t\tfont-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif;\n\t\t\t\t\tbackground: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);\n\t\t\t\t\tmin-height: 100vh;\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\talign-items: center;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tcolor: #eee;\n\t\t\t\t}\n\t\t\t\t.error-container {\n\t\t\t\t\ttext-align: center;\n\t\t\t\t\tpadding: 2rem;\n\t\t\t\t\tmax-width: 500px;\n\t\t\t\t}\n\t\t\t\t.error-icon {\n\t\t\t\t\tfont-size: 4rem;\n\t\t\t\t\tmargin-bottom: 1rem;\n\t\t\t\t}\n\t\t\t\t.error-title {\n\t\t\t\t\tfont-size: 1.5rem;\n\t\t\t\t\tfont-weight: 600;\n\t\t\t\t\tmargin-bottom: 1rem;\n\t\t\t\t\tcolor: #fff;\n\t\t\t\t}\n\t\t\t\t.error-message {\n\t\t\t\t\tfont-size: 1rem;\n\t\t\t\t\tcolor: #ccc;\n\t\t\t\t\tmargin-bottom: 2rem;\n\t\t\t\t\tline-height: 1.6;\n\t\t\t\t}\n\t\t\t\t.error-actions {\n\t\t\t\t\tdisplay: flex;\n\t\t\t\t\tgap: 1rem;\n\t\t\t\t\tjustify-content: center;\n\t\t\t\t\tflex-wrap: wrap;\n\t\t\t\t}\n\t\t\t\t.btn {\n\t\t\t\t\tpadding: 0.75rem 1.5rem;\n\t\t\t\t\tborder-radius: 0.5rem;\n\t\t\t\t\ttext-decoration: none;\n\t\t\t\t\tfont-weight: 500;\n\t\t\t\t\ttransition: all 0.2s;\n\t\t\t\t\tborder: none;\n\t\t\t\t\tcursor: pointer;\n\t\t\t\t\tfont-size: 0.95rem;\n\t\t\t\t}\n\t\t\t\t.btn-primary {\n\t\t\t\t\tbackground: #7aa2f7;\n\t\t\t\t\tcolor: #1a1b26;\n\t\t\t\t}\n\t\t\t\t.btn-primary:hover {\n\t\t\t\t\tbackground: #89b4fa;\n\t\t\t\t}\n\t\t\t\t.btn-secondary {\n\t\t\t\t\tbackground: transparent;\n\t\t\t\t\tcolor: #7aa2f7;\n\t\t\t\t\tborder: 1px solid #7aa2f7;\n\t\t\t\t}\n\t\t\t\t.btn-secondary:hover {\n\t\t\t\t\tbackground: rgba(122, 162, 247, 0.1);\n\t\t\t\t}\n\t\t\t</style></head><body><div class=\"error-container\"><div class=\"error-icon\">⚠️</div><h1 class=\"error-title\">Something went wrong</h1><p class=\"error-message\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 83, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 89, Col: 75}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/error.templ`, Line: 90, Col: 75}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+46
-11
@@ -20,7 +20,9 @@ templ Header(user User, currentPath string) {
|
||||
class="flex items-center gap-2.5 px-5 h-16 shrink-0 border-b"
|
||||
style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"
|
||||
>
|
||||
<span class="text-xl">📚</span>
|
||||
<span class="grid place-items-center h-6 w-6 shrink-0" style="color: var(--accent);">
|
||||
@Icon("book-open", "h-6 w-6")
|
||||
</span>
|
||||
<span class="font-bold text-lg tracking-tight" style="color: var(--text-primary)">Bookhoard</span>
|
||||
<svg
|
||||
x-show="scanning"
|
||||
@@ -96,14 +98,15 @@ templ Header(user User, currentPath string) {
|
||||
<button
|
||||
type="button"
|
||||
@click={ "changeTheme('" + opt.Name + "'); themeOpen = false" }
|
||||
class="w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
class="theme-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover"
|
||||
data-theme={ opt.Name }
|
||||
style="color: var(--text-secondary);"
|
||||
>
|
||||
<span class="inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10" style={ "background-color: " + opt.Color }></span>
|
||||
<span class="flex-1 text-left">{ opt.Label }</span>
|
||||
if user.Theme == opt.Name {
|
||||
@Icon("check", "h-4 w-4 shrink-0")
|
||||
}
|
||||
<span class={ "theme-check shrink-0" + themeCheckClass(opt.Name, user.Theme) }>
|
||||
@Icon("check", "h-4 w-4")
|
||||
</span>
|
||||
</button>
|
||||
}
|
||||
<div class="my-1.5 mx-3 border-t" style="border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);"></div>
|
||||
@@ -122,6 +125,44 @@ templ Header(user User, currentPath string) {
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<!-- Admin section (visible to admins only) -->
|
||||
if user.Role == "admin" {
|
||||
<div x-data="{ adminOpen: window.location.pathname.startsWith('/admin') }" class="sidebar-panel">
|
||||
<button
|
||||
type="button"
|
||||
@click="adminOpen = !adminOpen"
|
||||
class="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover"
|
||||
>
|
||||
@Icon("shield", "h-5 w-5 shrink-0")
|
||||
<span>Administration</span>
|
||||
<svg
|
||||
class="h-4 w-4 ml-auto transition-transform"
|
||||
:class="{ 'rotate-180': adminOpen }"
|
||||
fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div x-show="adminOpen" x-cloak x-transition class="mt-1 space-y-0.5 pl-1">
|
||||
<a href="/admin" class={ activeClass(currentPath, "/admin") }>
|
||||
@Icon("grid", "h-5 w-5 shrink-0")
|
||||
<span>Dashboard</span>
|
||||
</a>
|
||||
<a href="/admin/library" class={ activeClass(currentPath, "/admin/library") }>
|
||||
@Icon("library", "h-5 w-5 shrink-0")
|
||||
<span>Libraries</span>
|
||||
</a>
|
||||
<a href="/admin/users" class={ activeClass(currentPath, "/admin/users") }>
|
||||
@Icon("users", "h-5 w-5 shrink-0")
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="/admin/settings" class={ activeClass(currentPath, "/admin/settings") }>
|
||||
@Icon("gear", "h-5 w-5 shrink-0")
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
<!-- Topbar -->
|
||||
@@ -173,12 +214,6 @@ templ SidebarUserMenu(user User) {
|
||||
@Icon("user", "h-4 w-4")
|
||||
<span>Profile</span>
|
||||
</a>
|
||||
if user.Role == "admin" {
|
||||
<a href="/admin" class="flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover" style="color: var(--text-secondary);">
|
||||
@Icon("settings", "h-4 w-4")
|
||||
<span>Admin Panel</span>
|
||||
</a>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
@click="logout()"
|
||||
|
||||
+272
-111
@@ -29,7 +29,15 @@ func Header(user User, currentPath string) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"header\" x-init=\"initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators(); restoreLibrarySelection(); initializeScanListener()\"><!-- Mobile backdrop --><div class=\"app-sidebar-backdrop lg:hidden\" :class=\"{ 'is-open': mobileMenuOpen }\" @click=\"mobileMenuOpen = false\" x-cloak></div><!-- Sidebar --><aside class=\"app-sidebar\" :class=\"{ 'is-open': mobileMenuOpen }\"><!-- Logo --><a href=\"/dashboard\" class=\"flex items-center gap-2.5 px-5 h-16 shrink-0 border-b\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><span class=\"text-xl\">📚</span> <span class=\"font-bold text-lg tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span> <svg x-show=\"scanning\" x-cloak x-transition class=\"h-4 w-4 animate-spin ml-auto\" style=\"color: var(--accent);\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg></a> <span x-show=\"scanning && scanProgress > 0\" x-cloak x-transition class=\"block px-5 py-1.5 text-xs font-medium -mt-1\" style=\"color: var(--text-secondary);\" x-text=\"'Scanning… ' + scanProgress + '%'\"></span><!-- Primary navigation --><nav class=\"flex-1 px-3 py-4 space-y-0.5 overflow-y-auto\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<div x-data=\"header\" x-init=\"initializeSearch(); initializeTheme(); loadWoodPaneling(); updateWoodPanelingIndicators(); restoreLibrarySelection(); initializeScanListener()\"><!-- Mobile backdrop --><div class=\"app-sidebar-backdrop lg:hidden\" :class=\"{ 'is-open': mobileMenuOpen }\" @click=\"mobileMenuOpen = false\" x-cloak></div><!-- Sidebar --><aside class=\"app-sidebar\" :class=\"{ 'is-open': mobileMenuOpen }\"><!-- Logo --><a href=\"/dashboard\" class=\"flex items-center gap-2.5 px-5 h-16 shrink-0 border-b\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"><span class=\"grid place-items-center h-6 w-6 shrink-0\" style=\"color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("book-open", "h-6 w-6").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</span> <span class=\"font-bold text-lg tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span> <svg x-show=\"scanning\" x-cloak x-transition class=\"h-4 w-4 animate-spin ml-auto\" style=\"color: var(--accent);\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle> <path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg></a> <span x-show=\"scanning && scanProgress > 0\" x-cloak x-transition class=\"block px-5 py-1.5 text-xs font-medium -mt-1\" style=\"color: var(--text-secondary);\" x-text=\"'Scanning… ' + scanProgress + '%'\"></span><!-- Primary navigation --><nav class=\"flex-1 px-3 py-4 space-y-0.5 overflow-y-auto\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -38,7 +46,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "<a href=\"/dashboard\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "<a href=\"/dashboard\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -51,7 +59,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -59,7 +67,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "<span>Library</span></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<span>Library</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -68,7 +76,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "<a href=\"/bookshelf\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "<a href=\"/bookshelf\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -81,7 +89,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -89,7 +97,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "<span>All Books</span></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<span>All Books</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -98,7 +106,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "<a href=\"/series\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "<a href=\"/series\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -111,7 +119,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -119,7 +127,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "<span>Series</span></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<span>Series</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -128,7 +136,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, "<a href=\"/collections\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "<a href=\"/collections\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -141,7 +149,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -149,7 +157,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "<span>Collections</span></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<span>Collections</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -158,7 +166,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "<a href=\"/progress\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "<a href=\"/progress\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -171,7 +179,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 15, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -179,7 +187,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "<span>Progress</span></a> ")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span>Progress</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -188,7 +196,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<a href=\"/devices\" class=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "<a href=\"/devices\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -201,7 +209,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 18, "\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -209,7 +217,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 19, "<span>Devices</span></a></nav><!-- Bottom: theme picker + user --><div class=\"shrink-0 px-3 py-3 space-y-2 border-t\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<span>Devices</span></a></nav><!-- Bottom: theme picker + user --><div class=\"shrink-0 px-3 py-3 space-y-2 border-t\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -224,7 +232,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "<!-- Theme picker --><div x-data=\"{ themeOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"themeOpen = !themeOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<!-- Theme picker --><div x-data=\"{ themeOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"themeOpen = !themeOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -232,7 +240,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span>Appearance</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "<span>Appearance</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -240,115 +248,286 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 22, "</button><div x-show=\"themeOpen\" x-cloak x-transition class=\"mt-1 space-y-0.5 pl-1\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "</button><div x-show=\"themeOpen\" x-cloak x-transition class=\"mt-1 space-y-0.5 pl-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, opt := range ThemeOptions {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 23, "<button type=\"button\" @click=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "<button type=\"button\" @click=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeTheme('" + opt.Name + "'); themeOpen = false")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 98, Col: 69}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 100, Col: 69}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 24, "\" class=\"w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\"><span class=\"inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10\" style=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\" class=\"theme-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" data-theme=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("background-color: " + opt.Color)
|
||||
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: 102, Col: 130}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 102, Col: 29}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var15)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 25, "\"></span> <span class=\"flex-1 text-left\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "\" style=\"color: var(--text-secondary);\"><span class=\"inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10\" style=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(opt.Label)
|
||||
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: 103, Col: 50}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 105, Col: 130}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 26, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Theme == opt.Name {
|
||||
templ_7745c5c3_Err = Icon("check", "h-4 w-4 shrink-0").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "</button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "<div class=\"my-1.5 mx-3 border-t\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"></div><p class=\"px-3 pb-1 text-xs font-medium uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Bookshelf</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, w := range WoodOptions {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<button type=\"button\" @click=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 27, "\"></span> <span class=\"flex-1 text-left\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var17 string
|
||||
templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.ResolveAttributeValue("changeWoodPaneling('" + w.Name + "')")
|
||||
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: 114, Col: 55}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 106, Col: 50}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var17)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\" class=\"wood-paneling-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors\" data-wood=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 28, "</span> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var18 string
|
||||
templ_7745c5c3_Var18, templ_7745c5c3_Err = templ.ResolveAttributeValue(w.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 116, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var18)
|
||||
var templ_7745c5c3_Var18 = []any{"theme-check shrink-0" + themeCheckClass(opt.Name, user.Theme)}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var18...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "\" style=\"color: var(--text-secondary);\"><span class=\"inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10\" style=\"background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);\"></span> <span class=\"flex-1 text-left\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 29, "<span class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var19 string
|
||||
templ_7745c5c3_Var19, templ_7745c5c3_Err = templ.JoinStringErrs(w.Label)
|
||||
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: 120, Col: 48}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var19))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var19)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "</span></button>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 30, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("check", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 31, "</span></button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "</div></div></div></aside><!-- Topbar --><header class=\"app-topbar\"><div class=\"flex items-center gap-3 px-4 sm:px-6 h-full\"><button type=\"button\" class=\"app-mobile-only icon-btn\" @click=\"mobileMenuOpen = true\" aria-label=\"Open menu\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 32, "<div class=\"my-1.5 mx-3 border-t\" style=\"border-color: color-mix(in srgb, var(--text-primary) 7%, transparent);\"></div><p class=\"px-3 pb-1 text-xs font-medium uppercase tracking-wide\" style=\"color: var(--text-secondary);\">Bookshelf</p>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
for _, w := range WoodOptions {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 33, "<button type=\"button\" @click=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 117, Col: 55}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var20)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "\" class=\"wood-paneling-btn w-full flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-sm transition-colors\" data-wood=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
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: 119, Col: 26}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var21)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "\" style=\"color: var(--text-secondary);\"><span class=\"inline-block w-3.5 h-3.5 rounded-full shrink-0 ring-1 ring-black/10\" style=\"background-color: color-mix(in srgb, var(--text-primary) 12%, transparent);\"></span> <span class=\"flex-1 text-left\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var22 string
|
||||
templ_7745c5c3_Var22, templ_7745c5c3_Err = templ.JoinStringErrs(w.Label)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 123, Col: 48}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var22))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "</span></button>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</div></div><!-- Admin section (visible to admins only) -->")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "<div x-data=\"{ adminOpen: window.location.pathname.startsWith('/admin') }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"adminOpen = !adminOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors text-content-muted hover:text-content hover:bg-surface-hover\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("shield", "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, 39, "<span>Administration</span> <svg class=\"h-4 w-4 ml-auto transition-transform\" :class=\"{ 'rotate-180': adminOpen }\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" viewBox=\"0 0 24 24\"><path stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M19 9l-7 7-7-7\"></path></svg></button><div x-show=\"adminOpen\" x-cloak x-transition class=\"mt-1 space-y-0.5 pl-1\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 = []any{activeClass(currentPath, "/admin")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var23...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<a href=\"/admin\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var24 string
|
||||
templ_7745c5c3_Var24, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var23).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var24)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("grid", "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, 42, "<span>Dashboard</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var25 = []any{activeClass(currentPath, "/admin/library")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var25...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<a href=\"/admin/library\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var26 string
|
||||
templ_7745c5c3_Var26, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var25).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var26)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("library", "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, 45, "<span>Libraries</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var27 = []any{activeClass(currentPath, "/admin/users")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var27...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "<a href=\"/admin/users\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var28 string
|
||||
templ_7745c5c3_Var28, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var27).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var28)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("users", "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, 48, "<span>Users</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var29 = []any{activeClass(currentPath, "/admin/settings")}
|
||||
templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var29...)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 49, "<a href=\"/admin/settings\" class=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var30 string
|
||||
templ_7745c5c3_Var30, templ_7745c5c3_Err = templ.ResolveAttributeValue(templ.CSSClasses(templ_7745c5c3_Var29).String())
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 1, Col: 0}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var30)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 50, "\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("gear", "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, 51, "<span>Settings</span></a></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 52, "</div></aside><!-- Topbar --><header class=\"app-topbar\"><div class=\"flex items-center gap-3 px-4 sm:px-6 h-full\"><button type=\"button\" class=\"app-mobile-only icon-btn\" @click=\"mobileMenuOpen = true\" aria-label=\"Open menu\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -356,7 +535,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 34, "</button><div class=\"relative flex-1 max-w-xl\"><span class=\"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none\" style=\"color: var(--text-secondary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 53, "</button><div class=\"relative flex-1 max-w-xl\"><span class=\"absolute left-3 top-1/2 -translate-y-1/2 pointer-events-none\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -364,7 +543,7 @@ func Header(user User, currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 35, "</span> <input type=\"text\" id=\"header-search\" placeholder=\"Search your library…\" class=\"input pl-10\" autocomplete=\"off\"></div></div></header><script type=\"module\" src=\"/static/main.js\"></script></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 54, "</span> <input type=\"text\" id=\"header-search\" placeholder=\"Search your library…\" class=\"input pl-10\" autocomplete=\"off\"></div></div></header><script type=\"module\" src=\"/static/main.js\"></script></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -389,12 +568,12 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var20 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var20 == nil {
|
||||
templ_7745c5c3_Var20 = templ.NopComponent
|
||||
templ_7745c5c3_Var31 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var31 == nil {
|
||||
templ_7745c5c3_Var31 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 36, "<div x-data=\"{ userOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"userOpen = !userOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover\" style=\"color: var(--text-primary);\"><span class=\"grid place-items-center h-7 w-7 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 55, "<div x-data=\"{ userOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"userOpen = !userOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover\" style=\"color: var(--text-primary);\"><span class=\"grid place-items-center h-7 w-7 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -402,20 +581,20 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 37, "</span> <span class=\"flex-1 text-left truncate\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 56, "</span> <span class=\"flex-1 text-left truncate\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var21 string
|
||||
templ_7745c5c3_Var21, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
var templ_7745c5c3_Var32 string
|
||||
templ_7745c5c3_Var32, templ_7745c5c3_Err = templ.JoinStringErrs(user.Username)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 168, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 209, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var21))
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var32))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 38, "</span>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 57, "</span>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -423,7 +602,7 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 39, "</button><div x-show=\"userOpen\" x-cloak x-transition class=\"mt-1 space-y-0.5 pl-1\"><a href=\"/profile\" class=\"flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 58, "</button><div x-show=\"userOpen\" x-cloak x-transition class=\"mt-1 space-y-0.5 pl-1\"><a href=\"/profile\" class=\"flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -431,25 +610,7 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 40, "<span>Profile</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if user.Role == "admin" {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 41, "<a href=\"/admin\" class=\"flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = Icon("settings", "h-4 w-4").Render(ctx, templ_7745c5c3_Buffer)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 42, "<span>Admin Panel</span></a> ")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 43, "<button type=\"button\" @click=\"logout()\" class=\"w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 59, "<span>Profile</span></a> <button type=\"button\" @click=\"logout()\" class=\"w-full flex items-center gap-3 px-3 py-1.5 rounded-lg text-sm transition-colors hover:bg-surface-hover\" style=\"color: var(--text-secondary);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -457,7 +618,7 @@ func SidebarUserMenu(user User) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 44, "<span>Logout</span></button></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 60, "<span>Logout</span></button></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -483,12 +644,12 @@ func SidebarSignIn(currentPath string) templ.Component {
|
||||
}()
|
||||
}
|
||||
ctx = templ.InitializeContext(ctx)
|
||||
templ_7745c5c3_Var22 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var22 == nil {
|
||||
templ_7745c5c3_Var22 = templ.NopComponent
|
||||
templ_7745c5c3_Var33 := templ.GetChildren(ctx)
|
||||
if templ_7745c5c3_Var33 == nil {
|
||||
templ_7745c5c3_Var33 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 45, "<div x-data=\"{ signInOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"signInOpen = !signInOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover\" style=\"color: var(--text-primary);\"><span class=\"grid place-items-center h-7 w-7 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 61, "<div x-data=\"{ signInOpen: false }\" class=\"sidebar-panel\"><button type=\"button\" @click=\"signInOpen = !signInOpen\" class=\"w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors hover:bg-surface-hover\" style=\"color: var(--text-primary);\"><span class=\"grid place-items-center h-7 w-7 rounded-full shrink-0\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -496,20 +657,20 @@ func SidebarSignIn(currentPath string) templ.Component {
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 46, "</span> <span class=\"flex-1 text-left\">Sign in</span></button><div x-show=\"signInOpen\" x-cloak x-transition class=\"mt-1 px-1\"><form hx-post=\"/api/auth/login\" hx-target=\"#login-result\" hx-swap=\"innerHTML\" class=\"space-y-2\"><input type=\"hidden\" name=\"redirect\" value=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 62, "</span> <span class=\"flex-1 text-left\">Sign in</span></button><div x-show=\"signInOpen\" x-cloak x-transition class=\"mt-1 px-1\"><form hx-post=\"/api/auth/login\" hx-target=\"#login-result\" hx-swap=\"innerHTML\" class=\"space-y-2\"><input type=\"hidden\" name=\"redirect\" value=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var23 string
|
||||
templ_7745c5c3_Var23, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||
var templ_7745c5c3_Var34 string
|
||||
templ_7745c5c3_Var34, templ_7745c5c3_Err = templ.ResolveAttributeValue(currentPath)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 217, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/header.templ`, Line: 252, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var23)
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var34)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 47, "\"> <input type=\"text\" name=\"login\" class=\"input py-1.5 text-sm\" placeholder=\"Email or username\" required> <input type=\"password\" name=\"password\" class=\"input py-1.5 text-sm\" placeholder=\"Password\" required> <button type=\"submit\" class=\"btn btn-primary w-full py-1.5 text-sm\">Sign In</button></form><div id=\"login-result\"></div><a href=\"/register\" class=\"block text-center text-xs mt-2 hover:underline\" style=\"color: var(--text-secondary);\">Create an account</a></div></div>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 63, "\"> <input type=\"text\" name=\"login\" class=\"input py-1.5 text-sm\" placeholder=\"Email or username\" required> <input type=\"password\" name=\"password\" class=\"input py-1.5 text-sm\" placeholder=\"Password\" required> <button type=\"submit\" class=\"btn btn-primary w-full py-1.5 text-sm\">Sign In</button></form><div id=\"login-result\"></div><a href=\"/register\" class=\"block text-center text-xs mt-2 hover:underline\" style=\"color: var(--text-secondary);\">Create an account</a></div></div>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ templ Index(loggedIn bool) {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night" x-data="index" x-init="initIndexTheme(); checkAuthRedirect()">
|
||||
<div class="absolute top-4 right-4 z-10">
|
||||
|
||||
@@ -29,7 +29,7 @@ func Index(loggedIn bool) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><div class=\"absolute top-4 right-4 z-10\"><select id=\"theme-select\" @change=\"changeTheme\" class=\"input w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Bookhoard - Home</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-tokyo-night\" x-data=\"index\" x-init=\"initIndexTheme(); checkAuthRedirect()\"><div class=\"absolute top-4 right-4 z-10\"><select id=\"theme-select\" @change=\"changeTheme\" class=\"input w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 23, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 23, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/index.templ`, Line: 24, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ templ Login(sessionExpired bool, deleted bool) {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="login" x-init="initLoginTheme()">
|
||||
<div class="brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative">
|
||||
|
||||
@@ -29,7 +29,7 @@ func Login(sessionExpired bool, deleted bool) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"login\" x-init=\"initLoginTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative\"><select id=\"theme-select\" @change=\"changeTheme()\" class=\"input absolute top-4 right-4 w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Login - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"login\" x-init=\"initLoginTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4 relative\"><select id=\"theme-select\" @change=\"changeTheme()\" class=\"input absolute top-4 right-4 w-auto py-1.5 pr-8 text-sm cursor-pointer\" aria-label=\"Theme\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 23, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 23, Col: 44}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/login.templ`, Line: 24, Col: 44}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ templ Profile(user User) {
|
||||
<title>Profile Settings - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="profile">
|
||||
@Header(user, "/profile")
|
||||
|
||||
@@ -29,7 +29,7 @@ func Profile(user User) templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Profile Settings - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"profile\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><title>Profile Settings - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\" x-data=\"profile\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ templ Progress(user User, progressData []handlers.ProgressWithMedia, errorMessag
|
||||
<title>Reading Progress - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/progress")
|
||||
|
||||
+16
-16
@@ -32,7 +32,7 @@ func Progress(user User, progressData []handlers.ProgressWithMedia, errorMessage
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Reading Progress - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Reading Progress - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 43, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 53, Col: 56}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 57, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 60, Col: 91}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 63, Col: 127}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 67, Col: 162}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 76, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 78, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 81, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 87, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 92, Col: 51}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 93, Col: 98}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 100, Col: 92}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 101, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 107, Col: 36}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 108, Col: 36}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -11,6 +11,7 @@ templ Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Q
|
||||
<title>Sync Queue - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="queue" class="theme-{ user.Theme }">
|
||||
@Header(user, "/queue")
|
||||
|
||||
+14
-14
@@ -31,7 +31,7 @@ func Queue(user User, queueItems []handlers.QueueItemResponse, stats handlers.Qu
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Sync Queue - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"queue\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Sync Queue - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"queue\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 40, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 44, Col: 97}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 48, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 52, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 129, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 130, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 132, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 135, Col: 34}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 137, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 141, Col: 39}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 141, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 142, Col: 30}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 147, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/queue.templ`, Line: 148, Col: 42}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -43,6 +43,7 @@ templ Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookm
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/reader.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body
|
||||
x-data="readerShell"
|
||||
|
||||
+10
-10
@@ -73,14 +73,14 @@ func Reader(user User, metadata ReaderMetadata, progress ReadingProgress, bookma
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"readerShell\" x-init=\"")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, " - Bookhoard Reader</title><link rel=\"manifest\" href=\"/static/manifest.json\"><link href=\"/static/reader-fonts.css\" rel=\"stylesheet\"><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/reader.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"readerShell\" x-init=\"")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.ResolveAttributeValue(readerInitExpr(metadata, progress))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 49, Col: 46}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 50, Col: 46}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var3)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -170,7 +170,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var5 templ.SafeURL
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinURLErrs("/media/" + metadata.MediaItemID)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 87, Col: 63}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 88, Col: 63}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -183,7 +183,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(metadata.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 90, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 91, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -198,7 +198,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%% · Page %d/%d", progress.Percentage, progress.CurrentPage, metadata.EstimatedPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 169, Col: 113}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 170, Col: 113}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -208,7 +208,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%.0f%%", progress.Percentage))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 171, Col: 52}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 172, Col: 52}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -219,7 +219,7 @@ func ReaderChrome(user User, metadata ReaderMetadata, progress ReadingProgress)
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(fmt.Sprintf("%d/%d", progress.CurrentPage, progress.TotalPages))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 174, Col: 72}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 175, Col: 72}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -359,7 +359,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var14 string
|
||||
templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.ResolveAttributeValue(bookmark.CfiPosition)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 392, Col: 38}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 393, Col: 38}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var14)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -372,7 +372,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var15 string
|
||||
templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 395, Col: 49}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 396, Col: 49}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -385,7 +385,7 @@ func ReaderBookmarksPanel(bookmarks []Bookmark) templ.Component {
|
||||
var templ_7745c5c3_Var16 string
|
||||
templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(bookmark.Position)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 397, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/reader.templ`, Line: 398, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ templ Register() {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="register" x-init="initRegisterTheme()">
|
||||
<div class="brand-gradient min-h-screen flex flex-col items-center justify-center p-4">
|
||||
|
||||
@@ -29,7 +29,7 @@ func Register() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"register\" x-init=\"initRegisterTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4\"><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a><h1 class=\"text-xl font-bold text-center mb-6\" style=\"color: var(--text-primary)\">Create your account</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"input\" required></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"input\"></div></div><div id=\"password-requirements\" class=\"p-4 rounded-xl text-sm\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><p class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">Password Requirements:</p><ul class=\"space-y-1\" style=\"color: var(--text-secondary)\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"input\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn btn-primary w-full py-2.5 opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Already have an account? <a href=\"/login\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Login</a></p></div></div></body></html>")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Register - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"register\" x-init=\"initRegisterTheme()\"><div class=\"brand-gradient min-h-screen flex flex-col items-center justify-center p-4\"><div class=\"w-full max-w-sm\"><a href=\"/\" class=\"flex items-center justify-center gap-2.5 mb-8\"><span class=\"text-3xl\">📚</span> <span class=\"text-2xl font-bold tracking-tight\" style=\"color: var(--text-primary)\">Bookhoard</span></a><h1 class=\"text-xl font-bold text-center mb-6\" style=\"color: var(--text-primary)\">Create your account</h1><form hx-post=\"/api/auth/register\" hx-target=\"#result\" hx-swap=\"innerHTML\" class=\"card p-6 space-y-4\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Email</label> <input type=\"email\" id=\"email\" name=\"email\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Username</label> <input type=\"text\" id=\"username\" name=\"username\" class=\"input\" required></div><div class=\"grid grid-cols-2 gap-3\"><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">First Name</label> <input type=\"text\" name=\"first_name\" placeholder=\"Optional\" class=\"input\"></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Last Name</label> <input type=\"text\" name=\"last_name\" placeholder=\"Optional\" class=\"input\"></div></div><div id=\"password-requirements\" class=\"p-4 rounded-xl text-sm\" style=\"background-color: var(--bg-primary); border: 1px solid var(--border);\"><p class=\"font-semibold mb-2\" style=\"color: var(--text-primary)\">Password Requirements:</p><ul class=\"space-y-1\" style=\"color: var(--text-secondary)\"><li id=\"req-length\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> At least 8 characters</li><li id=\"req-upper\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One uppercase letter</li><li id=\"req-lower\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One lowercase letter</li><li id=\"req-number\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One number</li><li id=\"req-special\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> One special character</li><li id=\"req-match\" class=\"flex items-center gap-2\"><span class=\"requirement-icon\">○</span> Passwords match</li></ul></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Password</label> <input type=\"password\" id=\"password\" name=\"password\" class=\"input\" required></div><div><label class=\"block text-xs font-semibold uppercase tracking-wide mb-2\" style=\"color: var(--text-secondary)\">Confirm Password</label> <input type=\"password\" id=\"confirm-password\" name=\"confirm_password\" class=\"input\" required></div><button type=\"submit\" id=\"register-btn\" class=\"btn btn-primary w-full py-2.5 opacity-50 cursor-not-allowed\" disabled>Register</button></form><div id=\"result\" class=\"mt-4 text-center text-sm\"></div><p class=\"text-center mt-6 text-sm\" style=\"color: var(--text-secondary)\">Already have an account? <a href=\"/login\" class=\"font-medium hover:underline\" style=\"color: var(--accent)\">Login</a></p></div></div></body></html>")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ templ Series(user User, seriesList []SeriesCardData, libData []LibraryData, curr
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||
<title>Series - Bookhoard</title>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body x-data="seriesPage" x-init="initSeriesPage()" class="theme-{ user.Theme }">
|
||||
@Header(user, "/series")
|
||||
|
||||
+11
-11
@@ -34,7 +34,7 @@ func Series(user User, seriesList []SeriesCardData, libData []LibraryData, curre
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Series - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body x-data=\"seriesPage\" x-init=\"initSeriesPage()\" class=\"theme-{ user.Theme }\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Series - Bookhoard</title><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body x-data=\"seriesPage\" x-init=\"initSeriesPage()\" class=\"theme-{ user.Theme }\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 49, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 57, Col: 45}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 57, Col: 82}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 61, Col: 93}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 78, Col: 64}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 79, Col: 64}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
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: 83, Col: 17}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 84, Col: 17}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ_7745c5c3_Var11)
|
||||
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: 85, Col: 23}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 97, Col: 99}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 99, Col: 42}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 99, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/series.templ`, Line: 100, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@ templ Setup() {
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<script type="module" src="/static/main.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-tokyo-night min-h-screen" x-data="setupWizard" x-init="initSetup()">
|
||||
<div class="mx-auto container px-4 py-8 max-w-2xl">
|
||||
|
||||
@@ -29,7 +29,7 @@ func Setup() templ.Component {
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Setup - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"setupWizard\" x-init=\"initSetup()\"><div class=\"mx-auto container px-4 py-8 max-w-2xl\"><div class=\"text-center mb-8\"><span class=\"inline-grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Setup - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><script type=\"module\" src=\"/static/main.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-tokyo-night min-h-screen\" x-data=\"setupWizard\" x-init=\"initSetup()\"><div class=\"mx-auto container px-4 py-8 max-w-2xl\"><div class=\"text-center mb-8\"><span class=\"inline-grid place-items-center h-14 w-14 rounded-2xl mx-auto mb-4\" style=\"background-color: var(--accent-muted); color: var(--accent);\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,59 @@ type LibraryData struct {
|
||||
Name string
|
||||
Description string
|
||||
TypeName string
|
||||
TypeValue string
|
||||
MediaCount int64
|
||||
FolderCount int
|
||||
}
|
||||
|
||||
type FolderData struct {
|
||||
FolderPath string
|
||||
}
|
||||
|
||||
type DirEntry struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
type UserVisibilityData struct {
|
||||
UserID string
|
||||
Username string
|
||||
Email string
|
||||
IsVisible bool
|
||||
}
|
||||
|
||||
type AdminStats struct {
|
||||
LibraryCount int
|
||||
MediaCount int
|
||||
UserCount int
|
||||
DeviceCount int
|
||||
}
|
||||
|
||||
type ScanSettingsData struct {
|
||||
AutoScanEnabled bool
|
||||
ScanPollIntervalSeconds int
|
||||
}
|
||||
|
||||
// SettingEntry mirrors database.SettingEntry for the admin UI. Kept as a
|
||||
// template-local type so the templates package does not import database.
|
||||
type SettingEntry struct {
|
||||
Key string
|
||||
Value string
|
||||
Type string
|
||||
Min string
|
||||
Max string
|
||||
RequiresRestart bool
|
||||
Category string
|
||||
Group string
|
||||
Description string
|
||||
IsDefault bool
|
||||
}
|
||||
|
||||
// SettingGroup is a labeled cluster of related settings rendered as a
|
||||
// sub-section within a tunable-settings card.
|
||||
type SettingGroup struct {
|
||||
Name string
|
||||
Entries []SettingEntry
|
||||
}
|
||||
|
||||
type SeriesCardData struct {
|
||||
|
||||
@@ -9,6 +9,7 @@ templ UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) {
|
||||
<title>Unlinked Books - Bookhoard</title>
|
||||
<script src="/static/htmx.min.js"></script>
|
||||
<link href="/static/style.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg"/>
|
||||
</head>
|
||||
<body class="theme-{ user.Theme }" x-data="unlinkedBooks" x-init="setupEventDelegation()">
|
||||
@Header(user, "/unlinked")
|
||||
|
||||
@@ -29,7 +29,7 @@ func UnlinkedBooks(user User, unlinkedBooks []UnlinkedBookData) templ.Component
|
||||
templ_7745c5c3_Var1 = templ.NopComponent
|
||||
}
|
||||
ctx = templ.ClearChildren(ctx)
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Unlinked Books - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"></head><body class=\"theme-{ user.Theme }\" x-data=\"unlinkedBooks\" x-init=\"setupEventDelegation()\">")
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "<!doctype html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\"><title>Unlinked Books - Bookhoard</title><script src=\"/static/htmx.min.js\"></script><link href=\"/static/style.css\" rel=\"stylesheet\"><link rel=\"icon\" type=\"image/svg+xml\" href=\"/static/favicon.svg\"></head><body class=\"theme-{ user.Theme }\" x-data=\"unlinkedBooks\" x-init=\"setupEventDelegation()\">")
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
@@ -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: 62, Col: 100}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 62, Col: 136}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 76, Col: 111}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 78, Col: 37}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 78, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 87, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 93, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 93, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 98, Col: 71}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/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: 104, Col: 35}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/unlinked_books.templ`, Line: 105, Col: 35}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
+40
-1
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
func activeClass(current, target string) string {
|
||||
base := "flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-colors "
|
||||
if current == target {
|
||||
if strings.TrimRight(current, "/") == strings.TrimRight(target, "/") {
|
||||
return base + "bg-brand/15 text-brand"
|
||||
}
|
||||
return base + "text-content-muted hover:bg-surface-hover hover:text-content"
|
||||
@@ -31,6 +31,17 @@ func navItemClass(current, target string) string {
|
||||
return base + "text-content-muted hover:text-content hover:bg-surface-hover"
|
||||
}
|
||||
|
||||
// themeCheckClass returns the class suffix for a theme option's checkmark
|
||||
// wrapper: empty for the active theme (visible), " hidden" otherwise. The
|
||||
// check element is always rendered so updateThemeIndicators() can move it
|
||||
// client-side when the user picks a different theme.
|
||||
func themeCheckClass(name, current string) string {
|
||||
if name == current {
|
||||
return ""
|
||||
}
|
||||
return " hidden"
|
||||
}
|
||||
|
||||
func ContainsString(slice []string, item string) bool {
|
||||
for _, s := range slice {
|
||||
if s == item {
|
||||
@@ -40,6 +51,15 @@ func ContainsString(slice []string, item string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func isUserVisible(userID string, visibility []UserVisibilityData) bool {
|
||||
for _, v := range visibility {
|
||||
if v.UserID == userID {
|
||||
return v.IsVisible
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// TotalMediaCount sums the MediaCount across the given libraries,
|
||||
// used to display the total next to the "All Libraries" option.
|
||||
func TotalMediaCount(libs []LibraryData) int64 {
|
||||
@@ -294,3 +314,22 @@ func formatDateForInput(d pgtype.Date) string {
|
||||
}
|
||||
return d.Time.Format("2006-01-02")
|
||||
}
|
||||
|
||||
// GroupTunableSettings splits a flat, group-sorted entry list into labeled
|
||||
// sub-section groups, separated into "live" (applies immediately) and
|
||||
// "restart required" buckets. Entries keep their original order so groups stay
|
||||
// coherent.
|
||||
func GroupTunableSettings(entries []SettingEntry) (live, restart []SettingGroup) {
|
||||
var liveGroups, restartGroups []SettingGroup
|
||||
for _, e := range entries {
|
||||
target := &liveGroups
|
||||
if e.RequiresRestart {
|
||||
target = &restartGroups
|
||||
}
|
||||
if len(*target) == 0 || (*target)[len(*target)-1].Name != e.Group {
|
||||
*target = append(*target, SettingGroup{Name: e.Group})
|
||||
}
|
||||
(*target)[len(*target)-1].Entries = append((*target)[len(*target)-1].Entries, e)
|
||||
}
|
||||
return liveGroups, restartGroups
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package templates
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGroupTunableSettings(t *testing.T) {
|
||||
entries := []SettingEntry{
|
||||
{Key: "session_duration_seconds", Group: "Session", RequiresRestart: false},
|
||||
{Key: "password_min_length", Group: "Password Quality", RequiresRestart: false},
|
||||
{Key: "password_require_upper", Group: "Password Quality", RequiresRestart: false},
|
||||
{Key: "opds_default_page_size", Group: "OPDS Catalog", RequiresRestart: false},
|
||||
{Key: "auth_rate_limit_per_min", Group: "Auth Rate Limiting", RequiresRestart: true},
|
||||
{Key: "login_max_attempts", Group: "Login Lockout", RequiresRestart: true},
|
||||
{Key: "login_lockout_minutes", Group: "Login Lockout", RequiresRestart: true},
|
||||
}
|
||||
|
||||
live, restart := GroupTunableSettings(entries)
|
||||
|
||||
if len(live) != 3 {
|
||||
t.Fatalf("expected 3 live groups, got %d", len(live))
|
||||
}
|
||||
if live[0].Name != "Session" || len(live[0].Entries) != 1 {
|
||||
t.Errorf("live[0] = %+v", live[0])
|
||||
}
|
||||
if live[1].Name != "Password Quality" || len(live[1].Entries) != 2 {
|
||||
t.Errorf("live[1] = %+v", live[1])
|
||||
}
|
||||
if live[2].Name != "OPDS Catalog" || len(live[2].Entries) != 1 {
|
||||
t.Errorf("live[2] = %+v", live[2])
|
||||
}
|
||||
|
||||
if len(restart) != 2 {
|
||||
t.Fatalf("expected 2 restart groups, got %d", len(restart))
|
||||
}
|
||||
if restart[0].Name != "Auth Rate Limiting" || len(restart[0].Entries) != 1 {
|
||||
t.Errorf("restart[0] = %+v", restart[0])
|
||||
}
|
||||
if restart[1].Name != "Login Lockout" || len(restart[1].Entries) != 2 {
|
||||
t.Errorf("restart[1] = %+v", restart[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGroupTunableSettingsEmpty(t *testing.T) {
|
||||
live, restart := GroupTunableSettings(nil)
|
||||
if len(live) != 0 || len(restart) != 0 {
|
||||
t.Errorf("expected empty groups, got live=%d restart=%d", len(live), len(restart))
|
||||
}
|
||||
}
|
||||
|
||||
func TestThemeCheckClass(t *testing.T) {
|
||||
if got := themeCheckClass("dracula", "dracula"); got != "" {
|
||||
t.Errorf("active theme should be visible, got %q", got)
|
||||
}
|
||||
if got := themeCheckClass("nord", "dracula"); got != " hidden" {
|
||||
t.Errorf("inactive theme should be hidden, got %q", got)
|
||||
}
|
||||
}
|
||||
+3
-140
@@ -1,134 +1,5 @@
|
||||
import { Alpine } from "./alpine";
|
||||
import { showToast } from "./toast";
|
||||
import { createWebSocket } from "./websocket";
|
||||
|
||||
function initializeScanWebSocket(): void {
|
||||
createWebSocket({
|
||||
onMessage: (message) => {
|
||||
switch (message.type) {
|
||||
case "scan_progress":
|
||||
updateScanProgress(message.data);
|
||||
break;
|
||||
case "scan_complete":
|
||||
showScanComplete(message.data);
|
||||
break;
|
||||
case "scan_error":
|
||||
showScanError(message.data);
|
||||
break;
|
||||
}
|
||||
},
|
||||
enableReconnect: true,
|
||||
reconnectDelay: 5000,
|
||||
});
|
||||
}
|
||||
|
||||
function updateScanProgress(data: { progress: number; files_scanned: number; new_items: number }): void {
|
||||
const progressBar = document.getElementById("scan-progress-bar");
|
||||
if (progressBar) {
|
||||
progressBar.style.width = (data.progress * 100) + "%";
|
||||
}
|
||||
|
||||
const progressText = document.getElementById("scan-progress-text");
|
||||
if (progressText) {
|
||||
progressText.textContent = `${data.files_scanned} files scanned (${data.new_items} new)`;
|
||||
}
|
||||
}
|
||||
|
||||
function showScanComplete(_data: unknown): void {
|
||||
console.log("Scan complete:", _data);
|
||||
}
|
||||
|
||||
function showScanError(_data: unknown): void {
|
||||
console.error("Scan error:", _data);
|
||||
}
|
||||
|
||||
async function triggerLibraryScan(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/libraries/scan", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Library scan started", "success");
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to start scan", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Scan error:", error);
|
||||
showToast("Failed to start library scan", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function triggerQuickScan(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/libraries/quick-scan", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
showToast("Quick scan started", "success");
|
||||
} else {
|
||||
const error = await response.json();
|
||||
showToast(error.error || "Failed to start quick scan", "error");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Quick scan error:", error);
|
||||
showToast("Failed to start quick scan", "error");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSystemStats(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
if (!token) return;
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/admin/stats", {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const stats = await response.json();
|
||||
renderSystemStats(stats);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load stats:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderSystemStats(stats: Record<string, unknown>): void {
|
||||
const container = document.getElementById("system-stats");
|
||||
if (!container) return;
|
||||
|
||||
container.innerHTML = `
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_books || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Total Books</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_users || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Users</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_devices || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Devices</p>
|
||||
</div>
|
||||
<div class="p-4 rounded-lg" style="background-color: var(--bg-secondary)">
|
||||
<p class="text-2xl font-bold" style="color: var(--text-primary)">${stats.total_libraries || 0}</p>
|
||||
<p class="text-sm" style="color: var(--text-secondary)">Libraries</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
async function scanAllLibraries(): Promise<void> {
|
||||
const token = localStorage.getItem("token");
|
||||
@@ -227,6 +98,8 @@ function showScanProgress(
|
||||
pollScanProgress(jobIds, libraryNames);
|
||||
}
|
||||
|
||||
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
function pollScanProgress(
|
||||
jobIds: string[],
|
||||
_libraryNames: Record<string, string>,
|
||||
@@ -283,7 +156,7 @@ function pollScanProgress(
|
||||
}
|
||||
|
||||
if (allComplete) {
|
||||
clearInterval(scanPollInterval);
|
||||
clearInterval(scanPollInterval!);
|
||||
showScanResults(
|
||||
jobIds.length,
|
||||
totalFiles,
|
||||
@@ -369,8 +242,6 @@ async function loadWatchStatus(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
let scanPollInterval: ReturnType<typeof setInterval> | undefined = undefined;
|
||||
|
||||
function stopScanStatusPolling(): void {
|
||||
if (scanPollInterval !== undefined) {
|
||||
clearInterval(scanPollInterval);
|
||||
@@ -380,22 +251,14 @@ function stopScanStatusPolling(): void {
|
||||
|
||||
export {
|
||||
hideScanProgress,
|
||||
initializeScanWebSocket,
|
||||
loadSystemStats,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
stopScanStatusPolling,
|
||||
triggerLibraryScan,
|
||||
triggerQuickScan,
|
||||
};
|
||||
|
||||
Alpine.data("admin", () => ({
|
||||
hideScanProgress,
|
||||
initializeScanWebSocket,
|
||||
loadSystemStats,
|
||||
loadWatchStatus,
|
||||
scanAllLibraries,
|
||||
stopScanStatusPolling,
|
||||
triggerLibraryScan,
|
||||
triggerQuickScan,
|
||||
}));
|
||||
|
||||
@@ -1,695 +0,0 @@
|
||||
// Library management functionality for admin/library page
|
||||
// Procedural/imperative style t
|
||||
// nt
|
||||
// OOP)
|
||||
|
||||
import { Alpine } from "./alpine";
|
||||
import {
|
||||
apiPut,
|
||||
apiPost,
|
||||
apiGet,
|
||||
handleResponse,
|
||||
handleError,
|
||||
handleVoidResponse,
|
||||
apiDelete,
|
||||
} from "./api";
|
||||
import { showToast } from "./toast";
|
||||
|
||||
interface Library {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
library_type_id: string;
|
||||
type_name: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface LibraryFolder {
|
||||
id: string;
|
||||
library_id: string;
|
||||
folder_path: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface LibrariesResponse {
|
||||
data: Library[];
|
||||
}
|
||||
|
||||
// State (already SSR'd, used for updates)
|
||||
let libraries: Library[] = [];
|
||||
|
||||
// Reload libraries from API (called after create/delete/update)
|
||||
async function reloadLibraries(): Promise<void> {
|
||||
try {
|
||||
const response = await apiGet("/libraries");
|
||||
const result = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as LibrariesResponse;
|
||||
libraries = result.data;
|
||||
renderLibraries();
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load libraries");
|
||||
}
|
||||
}
|
||||
|
||||
// Render libraries list (replaces SSR content after updates)
|
||||
function renderLibraries(): void {
|
||||
const container = document.getElementById("libraries-list");
|
||||
if (!container) return;
|
||||
if (libraries.length === 0) {
|
||||
container.innerHTML =
|
||||
'<p style="color: var(--text-secondary)" class="text-center py-8">No libraries yet. Create your first library to get started.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = libraries
|
||||
.map(
|
||||
(library) =>
|
||||
'<div class="p-4 border rounded-lg" style="background-color: var(--bg-primary); border-color: var(--border)">' +
|
||||
'<div class="flex justify-between items-start mb-2">' +
|
||||
"<div>" +
|
||||
`<h4 class="font-semibold" style="color: var(--text-primary)">${escapeHtmlLocal(library.name)}</h4>` +
|
||||
(library.description
|
||||
? `<p class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(library.description)}</p>`
|
||||
: "") +
|
||||
`<span class="inline-block px-2 py-1 text-xs rounded" style="background-color: var(--accent); color: var(--bg-primary)">${escapeHtmlLocal(library.type_name)}</span>` +
|
||||
"</div>" +
|
||||
'<div class="flex space-x-2">' +
|
||||
`<button data-library-id="${library.id}" data-action="show-folders" class="text-xs px-2 py-1 rounded" style="background-color: var(--bg-secondary); color: var(--text-primary)">Folders</button>` +
|
||||
`<button data-library-id="${library.id}" data-action="edit" class="text-xs px-2 py-1 rounded" style="background-color: var(--accent); color: var(--bg-primary)">Edit</button>` +
|
||||
`<button data-library-id="${library.id}" data-action="delete" class="text-xs px-2 py-1 rounded text-red-500">Delete</button>` +
|
||||
"</div>" +
|
||||
"</div>" +
|
||||
`<div id="library-folders-${library.id}" class="hidden mt-3 space-y-2"></div>` +
|
||||
"</div>",
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
// Load user's visible libraries for visibility management
|
||||
async function loadUserVisibility(): Promise<void> {
|
||||
const select = document.getElementById("user-select") as HTMLSelectElement;
|
||||
const userId = select?.value;
|
||||
if (!userId) {
|
||||
const container = document.getElementById("user-libraries");
|
||||
if (container) {
|
||||
container.innerHTML =
|
||||
'<p style="color: var(--text-secondary)">Please select a user</p>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiGet("/libraries/visible");
|
||||
const visibleLibraries = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as Library[];
|
||||
const container = document.getElementById("user-libraries");
|
||||
if (!container) return;
|
||||
|
||||
const visibleIds = new Set(visibleLibraries.map((lib: Library) => lib.id));
|
||||
|
||||
container.innerHTML = libraries
|
||||
.map((library) => {
|
||||
const isVisible = visibleIds.has(library.id);
|
||||
return (
|
||||
'<label class="flex items-center space-x-3 p-2 rounded" style="background-color: var(--bg-primary);">' +
|
||||
`<input type="checkbox" ${isVisible ? "checked" : ""} ` +
|
||||
`data-user-id="${userId}" data-library-id="${library.id}" ` +
|
||||
`onchange="setLibraryVisibility('${userId}', '${library.id}', this.checked)" ` +
|
||||
'class="w-4 h-4">' +
|
||||
`<span style="color: var(--text-primary)">${escapeHtmlLocal(library.name)} (${escapeHtmlLocal(library.type_name)})</span>` +
|
||||
"</label>"
|
||||
);
|
||||
})
|
||||
.join("");
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load user libraries");
|
||||
}
|
||||
}
|
||||
|
||||
// Set library visibility for a user
|
||||
async function setLibraryVisibility(
|
||||
userId: string,
|
||||
libraryId: string,
|
||||
isVisible: boolean,
|
||||
): Promise<void> {
|
||||
// userId is used in the HTML onchange handler but the API gets user from JWT context
|
||||
console.debug(
|
||||
"Setting visibility for user:",
|
||||
userId,
|
||||
"library:",
|
||||
libraryId,
|
||||
"visible:",
|
||||
isVisible,
|
||||
);
|
||||
|
||||
try {
|
||||
const response = await apiPost("/libraries/visibility", {
|
||||
library_id: libraryId,
|
||||
is_visible: isVisible,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Library visibility updated", "success");
|
||||
|
||||
// Refresh visibility controls
|
||||
void loadUserVisibility();
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to update library visibility");
|
||||
}
|
||||
}
|
||||
|
||||
// Create library form handler
|
||||
async function handleCreateLibrarySubmit(event: Event): Promise<void> {
|
||||
event.preventDefault();
|
||||
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const libraryId = (document.getElementById("library-id") as HTMLInputElement)
|
||||
?.value;
|
||||
const isEdit = !!libraryId;
|
||||
|
||||
const libraryData = {
|
||||
name: formData.get("name") as string,
|
||||
description: formData.get("description") as string,
|
||||
type: formData.get("type") as string,
|
||||
};
|
||||
|
||||
try {
|
||||
const url = isEdit ? `/libraries/${libraryId}` : "/libraries";
|
||||
const response = isEdit
|
||||
? await apiPut(url, libraryData)
|
||||
: await apiPost(url, libraryData);
|
||||
|
||||
if (isEdit) {
|
||||
await handleVoidResponse(response);
|
||||
} else {
|
||||
(await handleResponse(response)) as unknown as { data: Library };
|
||||
}
|
||||
|
||||
showToast(
|
||||
isEdit ? "Library updated successfully" : "Library created successfully",
|
||||
"success",
|
||||
);
|
||||
|
||||
hideCreateLibraryModal();
|
||||
form.reset();
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = "";
|
||||
}
|
||||
|
||||
void reloadLibraries();
|
||||
} catch (error) {
|
||||
handleError(
|
||||
error,
|
||||
isEdit ? "Failed to update library" : "Failed to create library",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Delete library
|
||||
async function deleteLibrary(libraryId: string): Promise<void> {
|
||||
const library = libraries.find((l) => l.id === libraryId);
|
||||
if (!library) return;
|
||||
|
||||
showDeleteModal(library);
|
||||
}
|
||||
|
||||
// Show library folders
|
||||
async function showLibraryFolders(libraryId: string): Promise<void> {
|
||||
const container = document.getElementById(`library-folders-${libraryId}`);
|
||||
if (!container) return;
|
||||
|
||||
try {
|
||||
const response = await apiGet(`/libraries/${libraryId}/folders`);
|
||||
const folders = (await handleResponse(
|
||||
response,
|
||||
)) as unknown as LibraryFolder[];
|
||||
|
||||
container.innerHTML = folders
|
||||
.map(
|
||||
(folder: LibraryFolder) =>
|
||||
'<div class="flex justify-between items-center p-2 rounded" style="background-color: var(--bg-secondary); border-color: var(--border)">' +
|
||||
`<span class="text-sm" style="color: var(--text-primary)">${escapeHtmlLocal(folder.folder_path)}</span>` +
|
||||
`<button data-library-id="${libraryId}" data-folder-path="${escapeHtmlLocal(folder.folder_path)}" data-action="remove-folder" ` +
|
||||
'class="text-xs text-red-500">Remove</button>' +
|
||||
"</div>",
|
||||
)
|
||||
.join("");
|
||||
|
||||
container.innerHTML +=
|
||||
'<div class="mt-2 flex space-x-2">' +
|
||||
`<input type="text" id="folder-path-${libraryId}" placeholder="Add folder path" ` +
|
||||
'class="flex-1 px-2 py-1 text-sm border rounded" style="background-color: var(--bg-primary); color: var(--text-primary); border-color: var(--border)">' +
|
||||
`<button data-action="browse-folder" data-input-id="folder-path-${libraryId}" ` +
|
||||
'class="btn-secondary px-2 py-1 text-xs rounded">Browse</button>' +
|
||||
`<button data-library-id="${libraryId}" data-action="add-folder" ` +
|
||||
'class="btn-primary px-2 py-1 text-xs rounded">Add</button>' +
|
||||
"</div>";
|
||||
|
||||
container.classList.remove("hidden");
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load folders");
|
||||
}
|
||||
}
|
||||
|
||||
// Add library folder
|
||||
async function addLibraryFolder(libraryId: string): Promise<void> {
|
||||
const input = document.getElementById(
|
||||
`folder-path-${libraryId}`,
|
||||
) as HTMLInputElement;
|
||||
const folderPath = input?.value.trim();
|
||||
|
||||
if (!folderPath) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiPost(`/libraries/${libraryId}/folders`, {
|
||||
folder_path: folderPath,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Folder added successfully", "success");
|
||||
|
||||
if (input) {
|
||||
input.value = "";
|
||||
}
|
||||
void showLibraryFolders(libraryId); // Refresh
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to add folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Remove library folder
|
||||
async function removeLibraryFolder(
|
||||
libraryId: string,
|
||||
folderPath: string,
|
||||
): Promise<void> {
|
||||
if (!confirm(`Remove folder "${folderPath}" from the library?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await apiDelete(`/libraries/${libraryId}/folders`, {
|
||||
folder_path: folderPath,
|
||||
});
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Folder removed successfully", "success");
|
||||
|
||||
void showLibraryFolders(libraryId); // Refresh
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to remove folder");
|
||||
}
|
||||
}
|
||||
|
||||
// Edit library (placeholder - opens modal or navigates to edit page)
|
||||
function editLibrary(libraryId: string): void {
|
||||
const library = libraries.find((l) => l.id === libraryId);
|
||||
if (!library) {
|
||||
showToast("Library not found", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const form = document.getElementById(
|
||||
"create-library-form",
|
||||
) as HTMLFormElement;
|
||||
if (form) {
|
||||
const nameInput = form.querySelector('[name="name"]') as HTMLInputElement;
|
||||
const descInput = form.querySelector(
|
||||
'[name="description"]',
|
||||
) as HTMLTextAreaElement;
|
||||
const typeInput = form.querySelector('[name="type"]') as HTMLSelectElement;
|
||||
|
||||
if (nameInput) nameInput.value = library.name;
|
||||
if (descInput) descInput.value = library.description || "";
|
||||
if (typeInput) typeInput.value = library.type_name;
|
||||
}
|
||||
|
||||
const modalTitle = document.querySelector("#create-library-modal h2");
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = "Edit Library";
|
||||
}
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = libraryId;
|
||||
}
|
||||
|
||||
showCreateLibraryModal();
|
||||
}
|
||||
|
||||
// Modal controls
|
||||
function showCreateLibraryModal(): void {
|
||||
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.remove("hidden");
|
||||
|
||||
const modalTitle = document.querySelector("#create-library-modal h2");
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = "Create Library";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function hideCreateLibraryModal(): void {
|
||||
const modal = document.getElementById("create-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Delete modal state
|
||||
let libraryToDelete: Library | null = null;
|
||||
|
||||
function showDeleteModal(library: Library): void {
|
||||
libraryToDelete = library;
|
||||
|
||||
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
||||
const content = document.getElementById(
|
||||
"delete-modal-content",
|
||||
) as HTMLElement;
|
||||
|
||||
if (modal && content) {
|
||||
const message = `Are you sure you want to delete "<strong>${escapeHtmlLocal(library.name)}</strong>"?
|
||||
|
||||
This will remove:
|
||||
• Library metadata from the database
|
||||
• All folder references
|
||||
• All book records from the database
|
||||
|
||||
⚠️ Book files on disk will NOT be deleted.
|
||||
|
||||
This action cannot be undone.`;
|
||||
|
||||
content.innerHTML = message.replace(/\n/g, "<br>");
|
||||
modal.classList.remove("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
function hideDeleteModal(): void {
|
||||
const modal = document.getElementById("delete-library-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
libraryToDelete = null;
|
||||
}
|
||||
|
||||
async function confirmDeleteLibrary(): Promise<void> {
|
||||
if (!libraryToDelete) return;
|
||||
|
||||
const libraryId = libraryToDelete.id;
|
||||
hideDeleteModal();
|
||||
|
||||
try {
|
||||
const response = await apiDelete(`/libraries/${libraryId}`);
|
||||
await handleVoidResponse(response);
|
||||
|
||||
showToast("Library deleted successfully", "success");
|
||||
|
||||
await reloadLibraries();
|
||||
|
||||
const libraryIdInput = document.getElementById(
|
||||
"library-id",
|
||||
) as HTMLInputElement | null;
|
||||
if (libraryIdInput) {
|
||||
libraryIdInput.value = "";
|
||||
}
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to delete library");
|
||||
}
|
||||
}
|
||||
|
||||
// Local escape HTML helper
|
||||
function escapeHtmlLocal(text: string): string {
|
||||
const div = document.createElement("div");
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
// Event delegation for handling dynamic button clicks
|
||||
function handleLibraryListClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
const libraryId = button.dataset.libraryId;
|
||||
|
||||
switch (action) {
|
||||
case "show-folders":
|
||||
if (libraryId) showLibraryFolders(libraryId);
|
||||
break;
|
||||
case "delete":
|
||||
if (libraryId) deleteLibrary(libraryId);
|
||||
break;
|
||||
case "edit":
|
||||
if (libraryId) editLibrary(libraryId);
|
||||
break;
|
||||
case "add-folder":
|
||||
if (libraryId) addLibraryFolder(libraryId);
|
||||
break;
|
||||
case "remove-folder":
|
||||
if (libraryId && button.dataset.folderPath) {
|
||||
removeLibraryFolder(libraryId, button.dataset.folderPath);
|
||||
}
|
||||
break;
|
||||
case "browse-folder":
|
||||
if (button.dataset.inputId) showFolderBrowser(button.dataset.inputId);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function handleFolderBrowserClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
const div = target.closest("div[data-action]") as HTMLElement;
|
||||
|
||||
if (button) {
|
||||
const action = button.dataset.action;
|
||||
const path = button.dataset.path;
|
||||
|
||||
switch (action) {
|
||||
case "browse-parent":
|
||||
if (path) navigateFolderBrowser(path);
|
||||
break;
|
||||
case "browse-cancel":
|
||||
hideFolderBrowser();
|
||||
break;
|
||||
case "browse-select":
|
||||
if (path) selectBrowseFolder(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (div && div.dataset.action === "browse-navigate") {
|
||||
const path = div.dataset.path;
|
||||
if (path) navigateFolderBrowser(path);
|
||||
}
|
||||
}
|
||||
|
||||
function handleGlobalClick(event: Event): void {
|
||||
const target = event.target as HTMLElement;
|
||||
const button = target.closest("button") as HTMLElement;
|
||||
if (!button) return;
|
||||
|
||||
const action = button.dataset.action;
|
||||
|
||||
switch (action) {
|
||||
case "show-create-modal":
|
||||
showCreateLibraryModal();
|
||||
break;
|
||||
case "hide-create-modal":
|
||||
hideCreateLibraryModal();
|
||||
break;
|
||||
case "hide-delete-modal":
|
||||
hideDeleteModal();
|
||||
break;
|
||||
case "confirm-delete":
|
||||
void confirmDeleteLibrary();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Folder browser state
|
||||
let currentBrowsePath = "";
|
||||
let currentBrowseInputId = "";
|
||||
|
||||
// Show folder browser modal
|
||||
function showFolderBrowser(inputId: string): void {
|
||||
currentBrowseInputId = inputId;
|
||||
currentBrowsePath = "/";
|
||||
|
||||
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.remove("hidden");
|
||||
void loadBrowseDirectories(currentBrowsePath);
|
||||
}
|
||||
}
|
||||
|
||||
// Load directories for browsing
|
||||
async function loadBrowseDirectories(path: string): Promise<void> {
|
||||
try {
|
||||
const response = await apiGet(
|
||||
`/libraries/browse?path=${encodeURIComponent(path)}`,
|
||||
);
|
||||
const data = (await handleResponse(response)) as unknown as {
|
||||
current_path: string;
|
||||
parent_path: string;
|
||||
directories: string[];
|
||||
};
|
||||
|
||||
currentBrowsePath = data.current_path;
|
||||
renderBrowseDirectories(data);
|
||||
} catch (error) {
|
||||
handleError(error, "Failed to load directories");
|
||||
}
|
||||
}
|
||||
|
||||
// Render browse directories (uses event delegation via data-action attributes)
|
||||
function renderBrowseDirectories(data: {
|
||||
current_path: string;
|
||||
parent_path: string;
|
||||
directories: string[];
|
||||
}): void {
|
||||
const container = document.getElementById("folder-browser-content");
|
||||
if (!container) return;
|
||||
|
||||
let html = `
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
${
|
||||
data.parent_path
|
||||
? `<button type="button" data-action="browse-parent" data-path="${escapeHtmlLocal(data.parent_path)}" class="btn-secondary px-3 py-1 rounded">↑ Parent</button>`
|
||||
: ""
|
||||
}
|
||||
<span class="text-sm" style="color: var(--text-secondary)">${escapeHtmlLocal(data.current_path)}</span>
|
||||
</div>
|
||||
<div class="max-h-64 overflow-y-auto space-y-1">
|
||||
`;
|
||||
|
||||
if (data.directories.length === 0) {
|
||||
html +=
|
||||
'<p style="color: var(--text-secondary)" class="text-center py-4">No subdirectories</p>';
|
||||
} else {
|
||||
data.directories.forEach((dir) => {
|
||||
const fullPath =
|
||||
data.current_path === "/" ? `/${dir}` : `${data.current_path}/${dir}`;
|
||||
html += `
|
||||
<div class="p-2 rounded cursor-pointer hover:opacity-80"
|
||||
style="background-color: var(--bg-secondary); color: var(--text-primary)"
|
||||
data-action="browse-navigate"
|
||||
data-path="${escapeHtmlLocal(fullPath)}">
|
||||
📁 ${escapeHtmlLocal(dir)}
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
}
|
||||
|
||||
html += `
|
||||
</div>
|
||||
<div class="mt-4 flex justify-end gap-2">
|
||||
<button type="button" data-action="browse-cancel" class="btn-secondary px-4 py-2 rounded">Cancel</button>
|
||||
<button type="button" data-action="browse-select" data-path="${escapeHtmlLocal(data.current_path)}" class="btn-primary px-4 py-2 rounded">Select This Folder</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Navigate to subdirectory
|
||||
function navigateFolderBrowser(path: string): void {
|
||||
void loadBrowseDirectories(path);
|
||||
}
|
||||
|
||||
// Select folder and close browser
|
||||
function selectBrowseFolder(path: string): void {
|
||||
const input = document.getElementById(
|
||||
currentBrowseInputId,
|
||||
) as HTMLInputElement;
|
||||
if (input) {
|
||||
input.value = path;
|
||||
}
|
||||
hideFolderBrowser();
|
||||
}
|
||||
|
||||
// Hide folder browser modal
|
||||
function hideFolderBrowser(): void {
|
||||
const modal = document.getElementById("folder-browser-modal") as HTMLElement;
|
||||
if (modal) {
|
||||
modal.classList.add("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize page
|
||||
function initializeLibraryAdmin(): void {
|
||||
// Setup event listeners
|
||||
const librariesList = document.getElementById("libraries-list");
|
||||
if (librariesList) {
|
||||
librariesList.addEventListener("click", handleLibraryListClick);
|
||||
}
|
||||
|
||||
const folderBrowserModal = document.getElementById("folder-browser-modal");
|
||||
if (folderBrowserModal) {
|
||||
folderBrowserModal.addEventListener("click", handleFolderBrowserClick);
|
||||
}
|
||||
|
||||
document.addEventListener("click", handleGlobalClick);
|
||||
|
||||
// Setup form submission
|
||||
const createLibraryForm = document.getElementById("create-library-form");
|
||||
if (createLibraryForm) {
|
||||
createLibraryForm.addEventListener("submit", handleCreateLibrarySubmit);
|
||||
}
|
||||
|
||||
// SSR provides initial library list - no need to fetch on page load
|
||||
// reloadLibraries() is called after create/delete/update operations
|
||||
}
|
||||
|
||||
// Export functions for global access
|
||||
export {
|
||||
addLibraryFolder,
|
||||
confirmDeleteLibrary,
|
||||
deleteLibrary,
|
||||
editLibrary,
|
||||
handleCreateLibrarySubmit,
|
||||
hideDeleteModal,
|
||||
hideFolderBrowser,
|
||||
initializeLibraryAdmin,
|
||||
loadUserVisibility,
|
||||
navigateFolderBrowser,
|
||||
removeLibraryFolder,
|
||||
selectBrowseFolder,
|
||||
setLibraryVisibility,
|
||||
showDeleteModal,
|
||||
showFolderBrowser,
|
||||
showLibraryFolders,
|
||||
};
|
||||
|
||||
Alpine.data("library", () => ({
|
||||
addLibraryFolder,
|
||||
confirmDeleteLibrary,
|
||||
deleteLibrary,
|
||||
editLibrary,
|
||||
handleCreateLibrarySubmit,
|
||||
hideDeleteModal,
|
||||
hideFolderBrowser,
|
||||
initializeLibraryAdmin,
|
||||
loadUserVisibility,
|
||||
navigateFolderBrowser,
|
||||
removeLibraryFolder,
|
||||
selectBrowseFolder,
|
||||
setLibraryVisibility,
|
||||
showDeleteModal,
|
||||
showFolderBrowser,
|
||||
showLibraryFolders,
|
||||
}));
|
||||
@@ -20,7 +20,6 @@ import "./dom";
|
||||
import "./events";
|
||||
import "./header";
|
||||
import "./index";
|
||||
import "./library";
|
||||
import "./library-switcher";
|
||||
import "./linking";
|
||||
import "./login";
|
||||
|
||||
@@ -41,6 +41,7 @@ const loadTheme = (): void => {
|
||||
// Change: Remove DOM element lookup, accept theme as parameter
|
||||
const changeTheme = async (theme: string): Promise<void> => {
|
||||
applyTheme(theme);
|
||||
updateThemeIndicators();
|
||||
|
||||
// Save to server if logged in
|
||||
const token = localStorage.getItem(TOKEN_STORAGE_KEY);
|
||||
@@ -78,6 +79,7 @@ const loadUserTheme = async (): Promise<void> => {
|
||||
const data = await response.json();
|
||||
if (data.theme) {
|
||||
applyTheme(data.theme as string);
|
||||
updateThemeIndicators();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
@@ -85,10 +87,26 @@ const loadUserTheme = async (): Promise<void> => {
|
||||
}
|
||||
};
|
||||
|
||||
// Update the active-theme checkmark in the Appearance menu to follow the
|
||||
// theme currently applied to <body>. Each theme button carries its theme name
|
||||
// in data-theme and a .theme-check element that is shown for the active theme
|
||||
// and hidden for the rest.
|
||||
const updateThemeIndicators = (): void => {
|
||||
const match = document.body.className.match(/theme-([\w-]+)/);
|
||||
const current = match ? match[1] : DEFAULT_THEME;
|
||||
document.querySelectorAll<HTMLElement>(".theme-btn").forEach((btn) => {
|
||||
const check = btn.querySelector(".theme-check");
|
||||
if (!check) return;
|
||||
const theme = btn.getAttribute("data-theme");
|
||||
check.classList.toggle("hidden", theme !== current);
|
||||
});
|
||||
};
|
||||
|
||||
// Initialize theme system
|
||||
const initializeTheme = (): void => {
|
||||
loadTheme();
|
||||
loadUserTheme();
|
||||
updateThemeIndicators();
|
||||
|
||||
// Set theme select value to current theme
|
||||
const themeSelect = document.getElementById(
|
||||
@@ -210,6 +228,7 @@ Alpine.data("theme", () => ({
|
||||
loadTheme,
|
||||
loadUserTheme,
|
||||
loadWoodPaneling,
|
||||
updateThemeIndicators,
|
||||
updateWoodPanelingIndicators,
|
||||
}));
|
||||
|
||||
@@ -221,6 +240,7 @@ export {
|
||||
loadTheme,
|
||||
loadUserTheme,
|
||||
loadWoodPaneling,
|
||||
updateThemeIndicators,
|
||||
updateWoodPanelingIndicators,
|
||||
};
|
||||
export type { ThemeType };
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="#7aa2f7" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 7v14"></path>
|
||||
<path d="M3 4h6a3 3 0 0 1 3 3v14a2 2 0 0 0-2-2H3z"></path>
|
||||
<path d="M21 4h-6a3 3 0 0 0-3 3v14a2 2 0 0 1 2-2h7z"></path>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 340 B |
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user