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) }