Refactor: Eliminate duplicate types - Use handler types directly
- Deleted templates.CollectionDetailData - using templates.CollectionData everywhere - Deleted templates.BookData - using handlers.BookInfo everywhere - Deleted templates.DeviceData - using handlers.DeviceInfo everywhere - Deleted templates.ProgressItemData - using handlers.ProgressWithMedia everywhere - Deleted templates.convertDevices() helper - Use handlers types directly in templates - Enhanced handlers.ProgressWithMedia with device metadata fields - Added handlers.getDeviceIcon() helper - Updated all templates to import handlers package - Cleaned up unused imports This aligns codebase with templ's design philosophy (use Go types directly, no parallel type system)
This commit is contained in:
+41
-17
@@ -42,6 +42,7 @@
|
||||
- `internal/router/device.go` - Add regenerate token route
|
||||
- `internal/handlers/devices.go` - Add regenerate token handler
|
||||
- `templates/devices.templ` - Add copy/regenerate UI
|
||||
- `web/src/device-management.ts` - Add device management TypeScript
|
||||
- `bruno/sync-kobo/api.bru` - Add URL path token requests
|
||||
- `bruno/devices/regenerate-*.bru` - New token regeneration test files
|
||||
- `bruno/opds/*-*.bru` - New OPDS authentication test files
|
||||
@@ -86,14 +87,6 @@ SET
|
||||
updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *;
|
||||
|
||||
-- name: RevokeDevice :exec
|
||||
UPDATE devices
|
||||
SET
|
||||
auth_token = NULL,
|
||||
sync_enabled = false,
|
||||
updated_at = NOW()
|
||||
WHERE id = $1;
|
||||
```
|
||||
|
||||
**Verification**: Run `go build ./...` after adding this query to ensure sqlc generates the new function correctly.
|
||||
@@ -504,11 +497,7 @@ func (h *DeviceHandler) RegenerateDeviceToken(c echo.Context) error {
|
||||
3. Add "Regenerate Token" button for each device
|
||||
4. Add JavaScript functions for copy and regenerate
|
||||
|
||||
#### 1.6.1 Update DeviceData Struct
|
||||
|
||||
**Location**: Find `DeviceData` struct (near top of file)
|
||||
|
||||
**Current struct** (lines vary, find structure):
|
||||
**ADD FIELD** to struct:
|
||||
|
||||
```go
|
||||
type DeviceData struct {
|
||||
@@ -522,6 +511,31 @@ type DeviceData struct {
|
||||
SyncFrequency int
|
||||
CreatedAt string
|
||||
DeviceMetadata json.RawMessage
|
||||
AuthToken string // NEW: Device API key for authentication
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.6.3 Update Collection Detail Template
|
||||
|
||||
**File**: `templates/collection.templ`
|
||||
|
||||
**Location**: Line 213 (function signature)
|
||||
|
||||
**Current**:
|
||||
```templ
|
||||
templ CollectionDetail(user User, collection CollectionDetailData, books []BookData) {
|
||||
```
|
||||
|
||||
**CHANGE** (use handlers.BookInfo):
|
||||
|
||||
```templ
|
||||
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) {
|
||||
```
|
||||
|
||||
**ADD FIELD** to struct:
|
||||
|
||||
```go
|
||||
AuthToken string // NEW: Device API key for authentication
|
||||
}
|
||||
```
|
||||
|
||||
@@ -872,7 +886,7 @@ put {
|
||||
docs {
|
||||
## Regenerate Device Token
|
||||
|
||||
Regenerates the auth token for a device, invalidating the old token immediately.
|
||||
Regenerates auth token for a device, invalidating old token immediately.
|
||||
|
||||
**Method:** PUT
|
||||
|
||||
@@ -899,7 +913,7 @@ docs {
|
||||
**Important Notes:**
|
||||
- Old token stops working immediately
|
||||
- Device must be updated with new token to resume syncing
|
||||
- No data loss - device ID remains the same
|
||||
- No data loss - device ID remains same
|
||||
|
||||
**Example Response:**
|
||||
```json
|
||||
@@ -911,14 +925,24 @@ docs {
|
||||
"device_name": "My Kobo Clara",
|
||||
"device_type": "kobo",
|
||||
"sync_enabled": true,
|
||||
"auto_sync": true
|
||||
"auto_sync": true,
|
||||
"sync_frequency_minutes": 5,
|
||||
"created_at": "2026-02-12T10:00:00Z",
|
||||
"device_metadata": "{...}"
|
||||
},
|
||||
"sync_urls": {
|
||||
"sync_url": "http://localhost:8765/api/sync/kobo/dev_new_token",
|
||||
"markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup"
|
||||
"markup": "http://localhost:8765/api/sync/kobo/dev_new_token/markup",
|
||||
"bookmark": "http://localhost:8765/api/sync/kobo/dev_new_token/bookmark",
|
||||
"init": "http://localhost:8765/api/sync/kobo/dev_new_token/v1/initialization"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Important Notes:**
|
||||
- Old token stops working immediately
|
||||
- Device must be updated with new token to resume syncing
|
||||
- No data loss - device ID remains same
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -0,0 +1,474 @@
|
||||
# Type Duplication Refactoring Plan
|
||||
|
||||
**Document Purpose**: Consolidate duplicate type definitions across templates/ and handlers/ packages to align with templ's design philosophy and reduce maintenance burden.
|
||||
|
||||
**Status**: Planning Phase
|
||||
**Last Updated**: 2026-02-12
|
||||
**Separation from**: IMPLEMENTATION_EXACT.md (this is independent refactoring)
|
||||
|
||||
**Core Principle**: **API wins** - When there's ambiguity between template types and API/handler types, the API/handler types take precedence.
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
**Problem**: Codebase maintains parallel type systems:
|
||||
- `templates/types.go` - Template-specific types
|
||||
- `handlers/*.go` - API response types
|
||||
|
||||
**Core Principle**: **API wins - when in doubt, API/handler types take precedence**
|
||||
|
||||
### Why "API wins"?
|
||||
|
||||
1. **API is public contract**: Handler types define your public API surface - these are more stable
|
||||
2. **Templates are presentation**: HTML templates can format any Go type - they're flexible
|
||||
3. **Single source of truth**: One authoritative type definition vs two parallel definitions
|
||||
4. **Json serialization**: Handler types already have `json:"..."` tags for API responses
|
||||
5. **Future API clients**: If you add mobile apps or CLI tools, they need handler types anyway
|
||||
|
||||
### Trade-offs
|
||||
|
||||
**Pro**: Simpler codebase, one type to maintain, automatic API/HTML consistency
|
||||
**Con**: Templates may have unused fields visible in scope (acceptable - templ handles this well)
|
||||
|
||||
**Impact**:
|
||||
- ~40 lines of duplicate type definitions
|
||||
- Unnecessary conversion functions
|
||||
- Maintenance burden (update 2 places for 1 change)
|
||||
- Violates templ's design philosophy
|
||||
|
||||
**Solution**: Use handler types directly in templates, eliminate template-specific duplicates.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Duplicates Found
|
||||
|
||||
| Template Type | Handler Type | Overlap | Action |
|
||||
|---------------|---------------|----------|--------|
|
||||
| `DeviceData` | `DeviceInfo` | 90% | **DELETE** DeviceData |
|
||||
| `BookData` | `collections.BookInfo` | 100% | **DELETE** BookData |
|
||||
| `CollectionDetailData` | `CollectionData` | 100% | **DELETE** CollectionDetailData |
|
||||
| `ProgressItemData` | `ProgressWithMedia` | 70% | **MERGE** (different fields) |
|
||||
| `QueueItemData` | `QueueItemResponse` | 60% | **KEEP** (template uses subset) |
|
||||
|
||||
### Already Using Handler Types (Good Patterns)
|
||||
|
||||
✅ `Conflicts` template uses `handlers.ConflictDetailResponse`
|
||||
✅ `Queue` template uses `handlers.QueueItemResponse`
|
||||
|
||||
**This is the pattern we want everywhere.**
|
||||
|
||||
---
|
||||
|
||||
## Refactoring Plan
|
||||
|
||||
### Phase 1: Eliminate Complete Duplicates
|
||||
|
||||
#### 1.1 Delete CollectionDetailData
|
||||
|
||||
**Files to Modify**:
|
||||
- `templates/types.go` (lines 26-32) - DELETE struct
|
||||
- `templates/collection_rules.templ` (line 3) - Change signature
|
||||
- `templates/collections.templ` (line 213) - Change signature
|
||||
|
||||
**Changes**:
|
||||
|
||||
**Delete** from `templates/types.go`:
|
||||
```go
|
||||
type CollectionDetailData struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Color string
|
||||
Icon string
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `templates/collection_rules.templ:3`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ CollectionRules(user User, collection CollectionDetailData) {
|
||||
|
||||
// AFTER:
|
||||
templ CollectionRules(user User, collection templates.CollectionData) {
|
||||
```
|
||||
|
||||
**Update** `templates/collections.templ:213`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ CollectionDetail(user User, collection CollectionDetailData, books []BookData) {
|
||||
|
||||
// AFTER:
|
||||
templ CollectionDetail(user User, collection templates.CollectionData, books []collections.BookInfo) {
|
||||
```
|
||||
|
||||
**Test**: `go test ./templates/...` after template regeneration
|
||||
|
||||
---
|
||||
|
||||
#### 1.2 Delete BookData
|
||||
|
||||
**Files to Modify**:
|
||||
- `templates/types.go` (lines 34-39) - DELETE struct
|
||||
- `templates/collections.templ` (line 213) - Change books param
|
||||
|
||||
**Changes**:
|
||||
|
||||
**Delete** from `templates/types.go`:
|
||||
```go
|
||||
type BookData struct {
|
||||
MediaItemID string
|
||||
Title string
|
||||
Author string
|
||||
CoverImagePath string
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `templates/collections.templ:213`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ CollectionDetail(user User, collection templates.CollectionData, books []BookData) {
|
||||
|
||||
// AFTER:
|
||||
templ CollectionDetail(user User, collection templates.CollectionData, books []collections.BookInfo) {
|
||||
```
|
||||
|
||||
**Test**: View collection detail page, verify books display correctly
|
||||
|
||||
---
|
||||
|
||||
#### 1.3 Delete DeviceData and ConvertDevices
|
||||
|
||||
**Files to Modify**:
|
||||
- `templates/types.go` (lines 48-57) - DELETE struct
|
||||
- `templates/devices.templ` (line 3) - Change signature
|
||||
- `internal/router/helpers.go` (lines 46-67) - DELETE function
|
||||
- `internal/router/frontend.go` (lines 161-169) - Remove conversion
|
||||
|
||||
**Changes**:
|
||||
|
||||
**Delete** from `templates/types.go`:
|
||||
```go
|
||||
type DeviceData struct {
|
||||
ID string
|
||||
DeviceName string
|
||||
DeviceType string
|
||||
SyncEnabled bool
|
||||
AutoSync bool
|
||||
SyncFrequency int32
|
||||
LastSync string
|
||||
LastSeen string
|
||||
}
|
||||
```
|
||||
|
||||
**Delete** from `internal/router/helpers.go`:
|
||||
```go
|
||||
func convertDevices(deviceInfos []handlers.DeviceInfo) []templates.DeviceData {
|
||||
// ... entire function (lines 46-67)
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `templates/devices.templ:3`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
|
||||
|
||||
// AFTER:
|
||||
templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) {
|
||||
```
|
||||
|
||||
**Update** `templates/devices.templ` template logic:
|
||||
|
||||
Find these patterns and update:
|
||||
```templ
|
||||
// BEFORE:
|
||||
if device.LastSync != "" {
|
||||
<span>{ device.LastSync }</span>
|
||||
}
|
||||
|
||||
// AFTER:
|
||||
if device.LastSync != nil {
|
||||
<span>{ device.LastSync.Format("2006-01-02 15:04") }</span>
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `internal/router/frontend.go:161-169`:
|
||||
```go
|
||||
// BEFORE:
|
||||
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||
}
|
||||
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
||||
}
|
||||
deviesList := convertDevices(deviceData)
|
||||
pendingList := convertPending(pendingData)
|
||||
|
||||
// AFTER:
|
||||
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||
}
|
||||
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
||||
}
|
||||
// No conversion - use handler data directly
|
||||
```
|
||||
|
||||
**Test**: Devices page renders correctly with proper date formatting
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: Eliminate Remaining Template Types
|
||||
|
||||
#### 2.1 ProgressItemData → Use ProgressWithMedia
|
||||
|
||||
**Current Issue**:
|
||||
- `handlers.ProgressWithMedia` exists as API response type
|
||||
- `templates.ProgressItemData` duplicates it with minor variations
|
||||
|
||||
**Decision**: **API wins** - Use `handlers.ProgressWithMedia` everywhere
|
||||
|
||||
**Files to Modify**:
|
||||
- `internal/handlers/progress.go` - Update `ProgressWithMedia` struct
|
||||
- `templates/types.go` - DELETE `ProgressItemData`
|
||||
- `templates/progress.templ` - Use `handlers.ProgressWithMedia`
|
||||
|
||||
**Changes**:
|
||||
|
||||
**Add fields to** `internal/handlers/progress.go:ProgressWithMedia`:
|
||||
```go
|
||||
// ADD fields to existing struct:
|
||||
type ProgressWithMedia struct {
|
||||
MediaItemID uuid.UUID `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
CurrentPage int32 `json:"current_page"`
|
||||
TotalPages int32 `json:"total_pages"`
|
||||
LastReadAt time.Time `json:"last_read_at"`
|
||||
Epubcfi string `json:"epubcfi"`
|
||||
LastSyncDevice string `json:"last_sync_device"`
|
||||
DeviceName string `json:"device_name,omitempty"` // ADD
|
||||
DeviceType string `json:"device_type,omitempty"` // ADD
|
||||
DeviceIcon string `json:"device_icon,omitempty"` // ADD
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `internal/handlers/progress.go:GetAllProgress()` to populate new device fields.
|
||||
|
||||
**Delete** from `templates/types.go` (lines 99-112):
|
||||
```go
|
||||
type ProgressItemData struct {
|
||||
// ... delete entire struct
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `templates/progress.templ:3`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ Progress(user User, progressData []ProgressItemData) {
|
||||
|
||||
// AFTER:
|
||||
templ Progress(user User, progressData []handlers.ProgressWithMedia) {
|
||||
```
|
||||
|
||||
**Test**: Progress page displays device names and icons
|
||||
|
||||
**Delete** from `templates/types.go` (lines 99-112):
|
||||
```go
|
||||
type ProgressItemData struct {
|
||||
// ... delete entire struct
|
||||
}
|
||||
```
|
||||
|
||||
**Update** `templates/progress.templ:3`:
|
||||
```templ
|
||||
// BEFORE:
|
||||
templ Progress(user User, progressData []ProgressItemData) {
|
||||
|
||||
// AFTER:
|
||||
templ Progress(user User, progressData []handlers.ProgressWithMedia) {
|
||||
```
|
||||
|
||||
**Test**: Progress page displays device names and icons
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Update Get*Data Functions
|
||||
|
||||
**Issue**: Several handler functions return handler types but aren't used directly by templates due to conversion layer.
|
||||
|
||||
**Files to Modify**:
|
||||
- `internal/router/frontend.go` - Remove conversions
|
||||
|
||||
**Changes**:
|
||||
|
||||
**Devices** (already done in Phase 1.3):
|
||||
- `GetDevicesData()` returns `[]handlers.DeviceInfo`
|
||||
- Template uses directly
|
||||
|
||||
**Progress**:
|
||||
```go
|
||||
// frontend.go - update progress route
|
||||
protected.GET("/progress-page", func(c echo.Context) error {
|
||||
// ...
|
||||
progressData, err := cfg.Handler.GetAllProgressData(c)
|
||||
// Returns []handlers.ProgressWithMedia directly - no conversion
|
||||
// ...
|
||||
})
|
||||
```
|
||||
|
||||
**Collections**:
|
||||
```go
|
||||
// collections.go - update to return handler types
|
||||
func (h *CollectionHandler) GetCollectionBooksData(c echo.Context) ([]collections.BookInfo, error) {
|
||||
// Already returns BookInfo - just update template
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Summary Table
|
||||
|
||||
| Phase | Files Changed | Lines Removed | Lines Added | Net Change |
|
||||
|-------|--------------|---------------|--------------|-------------|
|
||||
| 1.1 CollectionDetailData | 3 | 7 | 2 | -5 |
|
||||
| 1.2 BookData | 3 | 6 | 0 | -6 |
|
||||
| 1.3 DeviceData | 4 | 24 | 5 | -19 |
|
||||
| 2.1 ProgressItemData | 4 | 14 | 3 | -11 |
|
||||
| **Total** | **14** | **51** | **10** | **-41** |
|
||||
|
||||
**Note**: QueueItemData already uses handler type (good pattern). Conflicts already uses handler type (good pattern).
|
||||
|
||||
---
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
After each phase:
|
||||
|
||||
### Phase 1.1 (CollectionDetailData)
|
||||
- [ ] View collection rules page
|
||||
- [ ] Edit collection rules
|
||||
- [ ] View collection detail page
|
||||
- [ ] Add books to collection
|
||||
|
||||
### Phase 1.2 (BookData)
|
||||
- [ ] View collection detail page
|
||||
- [ ] Browse books in collection
|
||||
- [ ] Book covers display correctly
|
||||
|
||||
### Phase 1.3 (DeviceData)
|
||||
- [ ] Devices page loads
|
||||
- [ ] Device cards display
|
||||
- [ ] Last sync/last seen dates formatted
|
||||
- [ ] Device type icons show correctly
|
||||
|
||||
### Phase 2.1 (ProgressItemData)
|
||||
- [ ] Progress page loads
|
||||
- [ ] Progress entries show device names/icons
|
||||
- [ ] Percentage display works
|
||||
- [ ] Cover images display
|
||||
|
||||
---
|
||||
|
||||
## Rollback Plan
|
||||
|
||||
If refactoring breaks functionality:
|
||||
|
||||
1. **Git stash** changes before starting
|
||||
2. **Commit after each successful phase**
|
||||
3. **Revert individual commits** if needed
|
||||
4. **Keep conversion functions** commented out temporarily
|
||||
|
||||
```bash
|
||||
# Before starting
|
||||
git stash push -m "pre-refactor-state"
|
||||
|
||||
# After Phase 1.1
|
||||
git commit -m "refactor: delete CollectionDetailData, use CollectionData"
|
||||
|
||||
# If Phase 1.2 breaks
|
||||
git revert HEAD
|
||||
# Continue with 1.3
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Benefits
|
||||
|
||||
### Code Quality
|
||||
- ✅ Aligns with templ's design philosophy
|
||||
- ✅ Single source of truth for types
|
||||
- ✅ Reduced code duplication
|
||||
- ✅ **API-first**: Handler types are authoritative
|
||||
|
||||
### Maintenance
|
||||
- ✅ Update types in 1 place, not 2
|
||||
- ✅ Less conversion logic to maintain
|
||||
- ✅ Clearer data flow
|
||||
- ✅ **Template changes automatically get API updates**
|
||||
|
||||
### Consistency
|
||||
- ✅ All templates use handlers types (like Queue/Conflicts already do)
|
||||
- ✅ API and HTML use same type definitions
|
||||
- ✅ 41 fewer lines of code
|
||||
- ✅ **"API wins"** principle eliminates ambiguity
|
||||
|
||||
---
|
||||
|
||||
## Future Considerations
|
||||
|
||||
### For IMPLEMENTATION_EXACT.md
|
||||
After this refactoring, adding `AuthToken` to devices is simpler:
|
||||
- Add `AuthToken string` to `handlers.DeviceInfo` struct
|
||||
- Add to `GetDevicesData()` function return values
|
||||
- Template automatically gets it (no conversion layer)
|
||||
|
||||
This **solves the critical issue** found in IMPLEMENTATION_EXACT.md analysis automatically.
|
||||
|
||||
### For New Features
|
||||
- Define handler types with json tags first
|
||||
- Templates use same types directly
|
||||
- Only create template-specific types for:
|
||||
- Computed/derived fields not in API
|
||||
- Combining data from multiple sources
|
||||
- Simplifying complex nested structures
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
1. **Run tests** to ensure baseline: `go test ./...`
|
||||
2. **Create feature branch**: `git checkout -b refactor/types-consolidation`
|
||||
3. **Execute Phase 1.1** (CollectionDetailData) - DELETE template type, use API type
|
||||
4. **Commit and test**
|
||||
5. **Execute Phase 1.2** (BookData) - DELETE template type, use API type
|
||||
6. **Commit and test**
|
||||
7. **Execute Phase 1.3** (DeviceData) - DELETE template type, use API type
|
||||
8. **Commit and test**
|
||||
9. **Execute Phase 2.1** (ProgressItemData) - DELETE template type, enhance API type
|
||||
10. **Final commit and test**
|
||||
11. **Update IMPLEMENTATION_EXACT.md** to reference handler types only
|
||||
|
||||
**Total Estimated Time**: 2-3 hours with testing
|
||||
|
||||
**Key Mindset**: Every change removes a template type, never the reverse. API types are authoritative.
|
||||
|
||||
---
|
||||
|
||||
## Questions for User
|
||||
|
||||
1. **Progress template updates**: Since we're using `handlers.ProgressWithMedia` (API type) in templates, any template formatting changes (like date formatting) should be done in the template itself. Is this acceptable?
|
||||
|
||||
2. **Testing**: Do you have automated template tests, or is this manual testing only?
|
||||
|
||||
3. **Implementation plan**: Should I update IMPLEMENTATION_EXACT.md after this refactoring to reference `handlers.DeviceInfo` instead of `DeviceData`?
|
||||
|
||||
4. **Json tags on handler types**: Templates don't need `json:"..."` tags - should we document that these are for API responses only?
|
||||
@@ -63,6 +63,13 @@ type UpdateDeviceMappingRequest struct {
|
||||
SyncDirection string `json:"sync_direction" validate:"required,oneof=bidirectional book_to_device device_to_book none"`
|
||||
}
|
||||
|
||||
type BookInfo struct {
|
||||
MediaItemID string `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
}
|
||||
|
||||
func (h *CollectionHandler) CreateCollection(c echo.Context) error {
|
||||
user := c.Get("user").(database.Users)
|
||||
userUUID := uuid.UUID(user.ID.Bytes)
|
||||
@@ -171,13 +178,6 @@ func (h *CollectionHandler) GetCollection(c echo.Context) error {
|
||||
return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
||||
}
|
||||
|
||||
type BookInfo struct {
|
||||
MediaItemID string `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
}
|
||||
|
||||
bookList := make([]BookInfo, 0, len(books))
|
||||
for _, book := range books {
|
||||
bookList = append(bookList, BookInfo{
|
||||
|
||||
@@ -14,6 +14,20 @@ import (
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// getDeviceIcon returns an emoji icon for device type
|
||||
func getDeviceIcon(deviceType string) string {
|
||||
switch deviceType {
|
||||
case "kobo":
|
||||
return "📚"
|
||||
case "koreader":
|
||||
return "📖"
|
||||
case "kindle":
|
||||
return "📱"
|
||||
default:
|
||||
return "📚"
|
||||
}
|
||||
}
|
||||
|
||||
// GetUniversalProgress retrieves progress with all location references
|
||||
func (h *Handler) GetUniversalProgress(c echo.Context) error {
|
||||
user := MustGetAuthenticatedUser(c)
|
||||
@@ -229,16 +243,22 @@ func (h *Handler) GetProgressHistory(c echo.Context) error {
|
||||
}
|
||||
|
||||
type ProgressWithMedia struct {
|
||||
MediaItemID uuid.UUID
|
||||
Title string
|
||||
Author string
|
||||
CoverImagePath string
|
||||
Percentage float64
|
||||
CurrentPage int32
|
||||
TotalPages int32
|
||||
LastReadAt time.Time
|
||||
Epubcfi string
|
||||
LastSyncDevice string
|
||||
MediaItemID uuid.UUID `json:"media_item_id"`
|
||||
Title string `json:"title"`
|
||||
Author string `json:"author"`
|
||||
CoverImagePath string `json:"cover_image_path"`
|
||||
Percentage float64 `json:"percentage"`
|
||||
CurrentPage int32 `json:"current_page"`
|
||||
TotalPages int32 `json:"total_pages"`
|
||||
LastReadAt time.Time `json:"last_read_at"`
|
||||
Epubcfi string `json:"epubcfi"`
|
||||
LastSyncDevice string `json:"last_sync_device"`
|
||||
DeviceName string `json:"device_name,omitempty"`
|
||||
DeviceType string `json:"device_type,omitempty"`
|
||||
DeviceIcon string `json:"device_icon,omitempty"`
|
||||
ProgressPercentage float64 `json:"-"`
|
||||
EpubCFI string `json:"-"`
|
||||
LastUpdated string `json:"-"`
|
||||
}
|
||||
|
||||
// GetAllProgress retrieves all progress for a user with sync source info
|
||||
@@ -283,6 +303,11 @@ func (h *Handler) GetAllProgress(c echo.Context) error {
|
||||
deviceName = progress.LastSyncDevice.String
|
||||
}
|
||||
|
||||
lastUpdated := ""
|
||||
if progress.LastReadAt.Valid {
|
||||
lastUpdated = progress.LastReadAt.Time.Format("2006-01-02 15:04")
|
||||
}
|
||||
|
||||
progressList = append(progressList, ProgressWithMedia{
|
||||
MediaItemID: progress.MediaItemID.Bytes,
|
||||
Title: mediaItem.Title,
|
||||
@@ -294,6 +319,10 @@ func (h *Handler) GetAllProgress(c echo.Context) error {
|
||||
LastReadAt: progress.LastReadAt.Time,
|
||||
Epubcfi: epubcfi,
|
||||
LastSyncDevice: deviceName,
|
||||
ProgressPercentage: progress.Percentage.Float64,
|
||||
EpubCFI: epubcfi,
|
||||
LastUpdated: lastUpdated,
|
||||
DeviceIcon: getDeviceIcon(deviceName),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -361,4 +390,3 @@ func (h *Handler) GetAllProgressData(c echo.Context) ([]ProgressWithMedia, error
|
||||
|
||||
return progressList, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -158,18 +158,17 @@ func registerFrontendRoutes(cfg *Config) {
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||
}
|
||||
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
||||
devices, err := cfg.DeviceHandler.GetDevicesData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||
}
|
||||
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
pendingMaps, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||
if err != nil {
|
||||
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
||||
}
|
||||
devicesList := convertDevices(deviceData)
|
||||
pendingList := convertPending(pendingData)
|
||||
pendingList := convertPending(pendingMaps)
|
||||
var buf bytes.Buffer
|
||||
err = templates.Devices(user, devicesList, pendingList).Render(c.Request().Context(), &buf)
|
||||
err = templates.Devices(user, devices, pendingList).Render(c.Request().Context(), &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"bookhoard/internal/handlers"
|
||||
"bookhoard/templates"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -43,29 +41,6 @@ func getTemplateUserWithTheme(c echo.Context, cfg *Config) (templates.User, erro
|
||||
}, nil
|
||||
}
|
||||
|
||||
func convertDevices(deviceInfos []handlers.DeviceInfo) []templates.DeviceData {
|
||||
result := make([]templates.DeviceData, len(deviceInfos))
|
||||
for i, d := range deviceInfos {
|
||||
lastSync := ""
|
||||
if d.LastSync != nil {
|
||||
lastSync = d.LastSync.Format(time.RFC3339)
|
||||
}
|
||||
lastSeen := ""
|
||||
if d.LastSeen != nil {
|
||||
lastSeen = d.LastSeen.Format(time.RFC3339)
|
||||
}
|
||||
result[i] = templates.DeviceData{
|
||||
ID: d.ID.String(),
|
||||
DeviceName: d.DeviceName,
|
||||
DeviceType: d.DeviceType,
|
||||
SyncEnabled: d.SyncEnabled,
|
||||
LastSync: lastSync,
|
||||
LastSeen: lastSeen,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
|
||||
result := make([]templates.PendingRegistrationData, len(pending))
|
||||
for i, p := range pending {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package templates
|
||||
|
||||
templ CollectionRules(user User, collection CollectionDetailData) {
|
||||
templ CollectionRules(user User, collection CollectionData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
@@ -8,7 +8,7 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func CollectionRules(user User, collection CollectionDetailData) templ.Component {
|
||||
func CollectionRules(user User, collection CollectionData) 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 {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package templates
|
||||
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
templ Collection(user User, collections []CollectionData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -210,7 +212,7 @@ templ Collection(user User, collections []CollectionData) {
|
||||
</html>
|
||||
}
|
||||
|
||||
templ CollectionDetail(user User, collection CollectionDetailData, books []BookData) {
|
||||
templ CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
@@ -8,6 +8,8 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
func Collection(user User, collections []CollectionData) 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
|
||||
@@ -55,7 +57,7 @@ func Collection(user User, collections []CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(col.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 46, Col: 60}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 48, Col: 60}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -68,7 +70,7 @@ func Collection(user User, collections []CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(col.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 58, Col: 109}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 60, Col: 109}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -81,7 +83,7 @@ func Collection(user User, collections []CollectionData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(col.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 59, Col: 103}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 61, Col: 103}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -100,7 +102,7 @@ func Collection(user User, collections []CollectionData) templ.Component {
|
||||
})
|
||||
}
|
||||
|
||||
func CollectionDetail(user User, collection CollectionDetailData, books []BookData) templ.Component {
|
||||
func CollectionDetail(user User, collection CollectionData, books []handlers.BookInfo) 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 {
|
||||
@@ -128,7 +130,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 219, Col: 32}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 221, Col: 32}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -149,7 +151,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Icon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 234, Col: 95}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 236, Col: 95}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -162,7 +164,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Name)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 236, Col: 107}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 238, Col: 107}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -175,7 +177,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(collection.Description)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 237, Col: 88}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 239, Col: 88}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -199,7 +201,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(book.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 284, Col: 131}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 286, Col: 131}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -217,7 +219,7 @@ func CollectionDetail(user User, collection CollectionDetailData, books []BookDa
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(book.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 286, Col: 108}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/collections.templ`, Line: 288, Col: 108}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) {
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
templ Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
@@ -79,16 +81,16 @@ templ Devices(user User, devices []DeviceData, pendingRegistrations []PendingReg
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--text-secondary)">Last Sync</span>
|
||||
if device.LastSync != "" {
|
||||
<span style="color: var(--text-primary)">{ device.LastSync }</span>
|
||||
if device.LastSync != nil {
|
||||
<span style="color: var(--text-primary)">{ device.LastSync.Format("2006-01-02 15:04") }</span>
|
||||
} else {
|
||||
<span style="color: var(--text-primary)">Never</span>
|
||||
}
|
||||
</div>
|
||||
<div class="flex justify-between">
|
||||
<span style="color: var(--text-secondary)">Last Seen</span>
|
||||
if device.LastSeen != "" {
|
||||
<span style="color: var(--text-primary)">{ device.LastSeen }</span>
|
||||
if device.LastSeen != nil {
|
||||
<span style="color: var(--text-primary)">{ device.LastSeen.Format("2006-01-02 15:04") }</span>
|
||||
} else {
|
||||
<span style="color: var(--text-primary)">Never</span>
|
||||
}
|
||||
|
||||
+14
-12
@@ -8,7 +8,9 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegistrationData) templ.Component {
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
func Devices(user User, devices []handlers.DeviceInfo, pendingRegistrations []PendingRegistrationData) 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 {
|
||||
@@ -89,7 +91,7 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 69, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 71, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -102,7 +104,7 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(device.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 70, Col: 89}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 72, Col: 89}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -127,15 +129,15 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if device.LastSync != "" {
|
||||
if device.LastSync != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "<span 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(device.LastSync)
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSync.Format("2006-01-02 15:04"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 83, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 85, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -155,15 +157,15 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ_7745c5c3_Err
|
||||
}
|
||||
if device.LastSeen != "" {
|
||||
if device.LastSeen != nil {
|
||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 21, "<span 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(device.LastSeen)
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(device.LastSeen.Format("2006-01-02 15:04"))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 91, Col: 70}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 93, Col: 97}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -206,7 +208,7 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 110, Col: 87}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 112, Col: 87}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -219,7 +221,7 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(reg.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 112, Col: 27}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 27}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -232,7 +234,7 @@ func Devices(user User, devices []DeviceData, pendingRegistrations []PendingRegi
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(reg.ExpiresAt)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 112, Col: 58}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/devices.templ`, Line: 114, Col: 58}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package templates
|
||||
|
||||
templ Progress(user User, progressData []ProgressItemData) {
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
templ Progress(user User, progressData []handlers.ProgressWithMedia) {
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
|
||||
+14
-12
@@ -8,7 +8,9 @@ package templates
|
||||
import "github.com/a-h/templ"
|
||||
import templruntime "github.com/a-h/templ/runtime"
|
||||
|
||||
func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
import "bookhoard/internal/handlers"
|
||||
|
||||
func Progress(user User, progressData []handlers.ProgressWithMedia) 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 {
|
||||
@@ -55,7 +57,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var2 string
|
||||
templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 44, Col: 121}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 46, Col: 121}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -73,7 +75,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var3 string
|
||||
templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(item.Author)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 46, Col: 116}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 48, Col: 116}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -91,7 +93,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var4 string
|
||||
templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(item.ProgressPercentage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 50, Col: 127}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 52, Col: 127}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -104,7 +106,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var5 string
|
||||
templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.CurrentPage)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 64, Col: 62}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 66, Col: 62}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -117,7 +119,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var6 string
|
||||
templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.TotalPages)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 64, Col: 84}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 66, Col: 84}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -130,7 +132,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var7 string
|
||||
templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.LastUpdated)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 69, Col: 96}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 71, Col: 96}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -143,7 +145,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var8 string
|
||||
templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceIcon)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 74, Col: 83}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 76, Col: 83}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -156,7 +158,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var9 string
|
||||
templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 75, Col: 102}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 77, Col: 102}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -174,7 +176,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var10 string
|
||||
templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceName)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 84, Col: 90}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 86, Col: 90}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -187,7 +189,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var11 string
|
||||
templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.DeviceType)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 85, Col: 68}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 87, Col: 68}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
@@ -206,7 +208,7 @@ func Progress(user User, progressData []ProgressItemData) templ.Component {
|
||||
var templ_7745c5c3_Var12 string
|
||||
templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(item.EpubCFI)
|
||||
if templ_7745c5c3_Err != nil {
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 93, Col: 65}
|
||||
return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/progress.templ`, Line: 95, Col: 65}
|
||||
}
|
||||
_, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12))
|
||||
if templ_7745c5c3_Err != nil {
|
||||
|
||||
@@ -23,21 +23,6 @@ type CollectionData struct {
|
||||
Icon string
|
||||
}
|
||||
|
||||
type CollectionDetailData struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
Color string
|
||||
Icon string
|
||||
}
|
||||
|
||||
type BookData struct {
|
||||
MediaItemID string
|
||||
Title string
|
||||
Author string
|
||||
CoverImagePath string
|
||||
}
|
||||
|
||||
type LibraryData struct {
|
||||
ID string
|
||||
Name string
|
||||
@@ -45,17 +30,6 @@ type LibraryData struct {
|
||||
TypeName string
|
||||
}
|
||||
|
||||
type DeviceData struct {
|
||||
ID string
|
||||
DeviceName string
|
||||
DeviceType string
|
||||
SyncEnabled bool
|
||||
AutoSync bool
|
||||
SyncFrequency int32
|
||||
LastSync string
|
||||
LastSeen string
|
||||
}
|
||||
|
||||
type PendingRegistrationData struct {
|
||||
RegistrationID string
|
||||
DeviceName string
|
||||
@@ -96,21 +70,6 @@ type DeviceShelfMappingData struct {
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
type ProgressItemData struct {
|
||||
MediaItemID string
|
||||
Title string
|
||||
Author string
|
||||
CoverImagePath string
|
||||
CurrentPage int32
|
||||
TotalPages int32
|
||||
ProgressPercentage float64
|
||||
LastUpdated string
|
||||
DeviceName string
|
||||
DeviceType string
|
||||
DeviceIcon string
|
||||
EpubCFI string
|
||||
}
|
||||
|
||||
type UnlinkedBookData struct {
|
||||
ProgressID string
|
||||
DeviceID string
|
||||
|
||||
Reference in New Issue
Block a user