Fix incorrect struct tags for Page, TotalPages, and PageY fields. Previously used 'int' tag instead of proper JSON field names, which would cause serialization issues.
256 lines
7.2 KiB
Go
256 lines
7.2 KiB
Go
package sync
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math"
|
|
)
|
|
|
|
// ProgressData represents universal progress data with multiple location references
|
|
type ProgressData struct {
|
|
Percentage *float64 `json:"percentage,omitempty"`
|
|
Epubcfi *string `json:"epubcfi,omitempty"`
|
|
Character *int64 `json:"character,omitempty"`
|
|
Chapter *int `json:"chapter,omitempty"`
|
|
ChapterProgress *float64 `json:"chapter_progress,omitempty"`
|
|
ViewportY *float64 `json:"viewport_y,omitempty"`
|
|
Page *int `json:"page,omitempty"`
|
|
TotalPages *int `json:"total_pages,omitempty"`
|
|
PageY *int `json:"page_y,omitempty"`
|
|
Zoom *float64 `json:"zoom,omitempty"`
|
|
ScrollX *float64 `json:"scroll_x,omitempty"`
|
|
ScrollY *float64 `json:"scroll_y,omitempty"`
|
|
Panel *int `json:"panel,omitempty"`
|
|
ReadingMode *string `json:"reading_mode,omitempty"`
|
|
TotalCharacters *int64 `json:"total_characters,omitempty"`
|
|
}
|
|
|
|
// DeviceProgress represents progress from a specific device
|
|
type DeviceProgress struct {
|
|
Source string `json:"source"`
|
|
Data ProgressData `json:"data"`
|
|
Timestamp string `json:"timestamp,omitempty"`
|
|
}
|
|
|
|
// ConvertProgress converts progress between different format groups
|
|
func ConvertProgress(sourceFormat, targetFormat FormatGroup, sourceData map[string]interface{}) (map[string]interface{}, error) {
|
|
percentage := extractPercentage(sourceFormat, sourceData)
|
|
|
|
result := make(map[string]interface{})
|
|
|
|
switch targetFormat {
|
|
case FormatGroupReflowable:
|
|
result["percentage"] = percentage
|
|
if epubcfi, ok := sourceData["epubcfi"].(string); ok {
|
|
result["epubcfi"] = epubcfi
|
|
} else {
|
|
// Generate approximate CFI from percentage
|
|
result["epubcfi"] = fmt.Sprintf("epubcfi(/6/4/2:%d)", int(percentage*100))
|
|
}
|
|
if totalChars, ok := sourceData["total_characters"].(int64); ok {
|
|
result["character"] = int64(float64(totalChars) * percentage)
|
|
}
|
|
|
|
case FormatGroupFixedLayout:
|
|
totalPages := 200.0
|
|
if tp, ok := sourceData["total_pages"].(int); ok {
|
|
totalPages = float64(tp)
|
|
}
|
|
result["page"] = int(math.Round(percentage * totalPages))
|
|
result["total_pages"] = int(totalPages)
|
|
result["percentage"] = percentage
|
|
if pageY, ok := sourceData["page_y"].(int); ok {
|
|
result["page_y"] = pageY
|
|
}
|
|
|
|
case FormatGroupComicArchive:
|
|
totalPages := 32.0
|
|
if tp, ok := sourceData["total_pages"].(int); ok {
|
|
totalPages = float64(tp)
|
|
}
|
|
result["page"] = int(math.Round(percentage * totalPages))
|
|
result["total_pages"] = int(totalPages)
|
|
result["percentage"] = percentage
|
|
if panel, ok := sourceData["panel"].(int); ok {
|
|
result["panel"] = panel
|
|
}
|
|
|
|
default:
|
|
return nil, fmt.Errorf("unsupported target format: %s", targetFormat)
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// extractPercentage extracts the percentage (0.0-1.0) from source data
|
|
func extractPercentage(sourceFormat FormatGroup, sourceData map[string]interface{}) float64 {
|
|
switch sourceFormat {
|
|
case FormatGroupReflowable:
|
|
if p, ok := sourceData["percentage"].(float64); ok {
|
|
return p
|
|
}
|
|
// Try to calculate from character offset
|
|
if char, ok := sourceData["character"].(int64); ok {
|
|
if total, ok := sourceData["total_characters"].(int64); ok && total > 0 {
|
|
return float64(char) / float64(total)
|
|
}
|
|
}
|
|
|
|
case FormatGroupFixedLayout, FormatGroupComicArchive:
|
|
if page, ok := sourceData["page"].(int); ok {
|
|
if total, ok := sourceData["total_pages"].(int); ok && total > 0 {
|
|
return float64(page) / float64(total)
|
|
}
|
|
}
|
|
// Try direct percentage
|
|
if p, ok := sourceData["percentage"].(float64); ok {
|
|
return p
|
|
}
|
|
}
|
|
|
|
return 0.0
|
|
}
|
|
|
|
// PageToPercentage converts page/total_pages to percentage
|
|
func PageToPercentage(page, totalPages int) float64 {
|
|
if totalPages <= 0 {
|
|
return 0.0
|
|
}
|
|
percentage := float64(page) / float64(totalPages)
|
|
if percentage > 1.0 {
|
|
percentage = 1.0
|
|
}
|
|
if percentage < 0.0 {
|
|
percentage = 0.0
|
|
}
|
|
return percentage
|
|
}
|
|
|
|
// PercentageToPage converts percentage to page number
|
|
func PercentageToPage(percentage float64, totalPages int) int {
|
|
if percentage < 0.0 {
|
|
percentage = 0.0
|
|
}
|
|
if percentage > 1.0 {
|
|
percentage = 1.0
|
|
}
|
|
page := int(math.Round(float64(totalPages) * percentage))
|
|
if page < 0 {
|
|
page = 0
|
|
}
|
|
if page > totalPages {
|
|
page = totalPages
|
|
}
|
|
return page
|
|
}
|
|
|
|
// CharacterToPercentage converts character offset to percentage
|
|
func CharacterToPercentage(character, totalCharacters int64) float64 {
|
|
if totalCharacters <= 0 {
|
|
return 0.0
|
|
}
|
|
percentage := float64(character) / float64(totalCharacters)
|
|
if percentage > 1.0 {
|
|
percentage = 1.0
|
|
}
|
|
if percentage < 0.0 {
|
|
percentage = 0.0
|
|
}
|
|
return percentage
|
|
}
|
|
|
|
// PercentageToCharacter converts percentage to character offset
|
|
func PercentageToCharacter(percentage float64, totalCharacters int64) int64 {
|
|
if percentage < 0.0 {
|
|
percentage = 0.0
|
|
}
|
|
if percentage > 1.0 {
|
|
percentage = 1.0
|
|
}
|
|
char := int64(math.Round(float64(totalCharacters) * percentage))
|
|
if char < 0 {
|
|
char = 0
|
|
}
|
|
if char > totalCharacters {
|
|
char = totalCharacters
|
|
}
|
|
return char
|
|
}
|
|
|
|
// MergeProgress merges progress from two sources using "max progress wins" strategy
|
|
func MergeProgress(progressA, progressB map[string]interface{}) map[string]interface{} {
|
|
percA := extractPercentage(FormatGroupReflowable, progressA)
|
|
percB := extractPercentage(FormatGroupReflowable, progressB)
|
|
|
|
winner := progressB
|
|
if percA > percB {
|
|
winner = progressA
|
|
}
|
|
|
|
result := make(map[string]interface{})
|
|
for k, v := range winner {
|
|
result[k] = v
|
|
}
|
|
|
|
sources := []string{}
|
|
if srcA, ok := progressA["source"].(string); ok {
|
|
sources = append(sources, srcA)
|
|
}
|
|
if srcB, ok := progressB["source"].(string); ok {
|
|
sources = append(sources, srcB)
|
|
}
|
|
result["merged_from"] = sources
|
|
result["merge_timestamp"] = "now"
|
|
|
|
return result
|
|
}
|
|
|
|
// FormatProgressForDisplay formats progress for display based on format group
|
|
func FormatProgressForDisplay(formatGroup FormatGroup, progress map[string]interface{}) string {
|
|
percentage := extractPercentage(formatGroup, progress)
|
|
|
|
switch formatGroup {
|
|
case FormatGroupReflowable:
|
|
if chapter, ok := progress["chapter"].(int); ok {
|
|
return fmt.Sprintf("%.1f%% (Chapter %d)", percentage*100, chapter)
|
|
}
|
|
return fmt.Sprintf("%.1f%%", percentage*100)
|
|
|
|
case FormatGroupFixedLayout:
|
|
if page, ok := progress["page"].(int); ok {
|
|
if total, ok := progress["total_pages"].(int); ok {
|
|
return fmt.Sprintf("Page %d of %d (%.1f%%)", page, total, percentage*100)
|
|
}
|
|
return fmt.Sprintf("Page %d (%.1f%%)", page, percentage*100)
|
|
}
|
|
return fmt.Sprintf("%.1f%%", percentage*100)
|
|
|
|
case FormatGroupComicArchive:
|
|
if page, ok := progress["page"].(int); ok {
|
|
if total, ok := progress["total_pages"].(int); ok {
|
|
return fmt.Sprintf("Page %d of %d (%.0f%%)", page, total, percentage*100)
|
|
}
|
|
return fmt.Sprintf("Page %d (%.0f%%)", page, percentage*100)
|
|
}
|
|
return fmt.Sprintf("%.0f%%", percentage*100)
|
|
|
|
default:
|
|
return fmt.Sprintf("%.1f%%", percentage*100)
|
|
}
|
|
}
|
|
|
|
// ParseProgressFromJSON parses progress data from JSON
|
|
func ParseProgressFromJSON(data []byte) (*ProgressData, error) {
|
|
var progress ProgressData
|
|
err := json.Unmarshal(data, &progress)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &progress, nil
|
|
}
|
|
|
|
// MarshalProgressToJSON converts progress data to JSON
|
|
func MarshalProgressToJSON(progress *ProgressData) ([]byte, error) {
|
|
return json.Marshal(progress)
|
|
}
|