WatchSharedModels.swift 9.56 KB
import Foundation
import SwiftUI
import HealthKit

enum HRVStatus: String, Codable {
    case fullOfEnergy = "活力满满"
    case normal = "状态正常"
    case overpressure = "压力过载"
}

struct HRVData: Codable {
    var value: Double?
    var hrvBaseline: Double?
    var date: Date?

    var status: HRVStatus? {
        guard let value else { return nil }
        guard let hrvBaseline, hrvBaseline > 0 else { return .normal }

        let ratio = value / hrvBaseline
        if ratio >= 1.05 {
            return .fullOfEnergy
        }
        if ratio < 0.9 {
            return .overpressure
        }
        return .normal
    }
}

enum UserCharacter: Int, Codable {
    case `default` = 0
    case cat = 1
    case dog = 2
    case rabbit = 3
    case elephant = 4
}

struct AvatarResource: Codable {
    var rawValue: String?

    var url: URL? {
        guard let rawValue else { return nil }
        return URL(string: rawValue)
    }

    init(rawValue: String?) {
        self.rawValue = rawValue
    }

    init(from decoder: Decoder) throws {
        if let value = try? decoder.singleValueContainer().decode(String.self) {
            rawValue = value
            return
        }

        let container = try decoder.container(keyedBy: DynamicCodingKey.self)
        rawValue = Self.decodeString(from: container, keys: ["url", "avatar", "path"])
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(rawValue)
    }

    private static func decodeString(from container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> String? {
        for key in keys {
            if let codingKey = DynamicCodingKey(stringValue: key),
               let value = try? container.decodeIfPresent(String.self, forKey: codingKey) {
                return value
            }
        }
        return nil
    }
}

struct UserInfo: Codable {
    var id: Int?
    var pairId: Int?
    var nickname: String?
    var avatar: AvatarResource?
    var persona: UserCharacter?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: DynamicCodingKey.self)
        id = Self.decodeInt(from: container, keys: ["id", "user_id", "userId"])
        pairId = Self.decodeInt(from: container, keys: ["pair_id", "pairId"])
        nickname = Self.decodeString(from: container, keys: ["nickname", "nick_name", "name"])
        avatar = try Self.decodeAvatar(from: container)
        persona = UserCharacter(rawValue: Self.decodeInt(from: container, keys: ["persona", "character", "user_character", "userCharacter"]) ?? 0)
    }

    func toWatchMap() -> [String: Any] {
        var map: [String: Any] = [:]
        if let id { map["id"] = id }
        if let pairId { map["pair_id"] = pairId }
        if let nickname { map["nickname"] = nickname }
        if let avatar = avatar?.rawValue { map["avatar"] = avatar }
        if let persona { map["persona"] = persona.rawValue }
        return map
    }

    private static func decodeAvatar(from container: KeyedDecodingContainer<DynamicCodingKey>) throws -> AvatarResource? {
        for key in ["avatar", "avatar_url", "avatarUrl"] {
            guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
            if let value = try? container.decodeIfPresent(String.self, forKey: codingKey) {
                return AvatarResource(rawValue: value)
            }
            if let value = try? container.decodeIfPresent(AvatarResource.self, forKey: codingKey) {
                return value
            }
        }
        return nil
    }
}

struct WatchThemeModel: Codable {
    var isDefaultTheme: Bool?
    var positiveDescription: String?
    var normalDescription: String?
    var negativeDescription: String?
    var positiveImageURL: String?
    var normalImageURL: String?
    var negativeImageURL: String?

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: DynamicCodingKey.self)
        isDefaultTheme = Self.decodeBool(from: container, keys: ["isDefaultTheme", "is_default_theme", "isDefault", "is_default"])
        positiveDescription = Self.decodeString(from: container, keys: ["positiveDescription", "positive_description", "excellentDescription", "excellent_description"])
        normalDescription = Self.decodeString(from: container, keys: ["normalDescription", "normal_description"])
        negativeDescription = Self.decodeString(from: container, keys: ["negativeDescription", "negative_description", "overpressureDescription", "overpressure_description"])
        positiveImageURL = Self.decodeString(from: container, keys: ["positiveImageURL", "positiveImageUrl", "positive_image_url", "positiveThumbnailURL", "positive_thumbnail_url", "positiveThumnailImageURL", "positive_thumnail_image_url"])
        normalImageURL = Self.decodeString(from: container, keys: ["normalImageURL", "normalImageUrl", "normal_image_url", "normalThumbnailURL", "normal_thumbnail_url", "normalThumnailImageURL", "normal_thumnail_image_url"])
        negativeImageURL = Self.decodeString(from: container, keys: ["negativeImageURL", "negativeImageUrl", "negative_image_url", "negativeThumbnailURL", "negative_thumbnail_url", "negativeThumnailImageURL", "negative_thumnail_image_url"])
    }

    func hrvStatusDescription(hrvStatus: HRVStatus) -> String {
        switch hrvStatus {
        case .fullOfEnergy:
            return positiveDescription ?? hrvStatus.rawValue
        case .normal:
            return normalDescription ?? hrvStatus.rawValue
        case .overpressure:
            return negativeDescription ?? hrvStatus.rawValue
        }
    }

    func hrvStatusThumnailImageURL(hrvStatus: HRVStatus) -> String? {
        switch hrvStatus {
        case .fullOfEnergy:
            return positiveImageURL
        case .normal:
            return normalImageURL
        case .overpressure:
            return negativeImageURL
        }
    }
}

extension String {
    var url: URL? {
        URL(string: self)
    }

    var int: Int? {
        Int(self)
    }
}

extension Int {
    var string: String {
        String(self)
    }

    var double: Double {
        Double(self)
    }
}

extension Double {
    var int: Int {
        Int(self)
    }
}

extension Array where Element == Double {
    func average() -> Double {
        guard !isEmpty else { return 0 }
        return reduce(0, +) / Double(count)
    }
}

extension Text {
    func wenyiheiFont(size: CGFloat) -> Text {
        font(.custom("WenYue-XinQingNianTi-NC-W8", size: size))
    }
}

struct SleepStage {
    let stage: HKCategoryValueSleepAnalysis
    let startTime: Date
    let endTime: Date
    let duration: TimeInterval
}

struct SleepData {
    let startTime: Date
    let endTime: Date
    let duration: TimeInterval
    let sleepStages: [SleepStage]
    let totalSleepTime: TimeInterval
    let sleepEfficiency: Double?
}

enum HealthDataType: Int {
    case hrv = 1
    case heartRate = 2
    case spo2 = 3
    case move = 4
    case exercise = 5
    case stand = 6
    case steps = 7
    case walkingHeartRate = 8
    case restingHeartRate = 9
    case sleepingHeartRate = 10
    case sleepingWristTemperature = 11
    case respiratoryRate = 12
    case irregularHeartRhythm = 13
}

private struct DynamicCodingKey: CodingKey {
    var stringValue: String
    var intValue: Int?

    init?(stringValue: String) {
        self.stringValue = stringValue
    }

    init?(intValue: Int) {
        self.stringValue = "\(intValue)"
        self.intValue = intValue
    }
}

private extension UserInfo {
    static func decodeString(from container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> String? {
        for key in keys {
            if let codingKey = DynamicCodingKey(stringValue: key),
               let value = try? container.decodeIfPresent(String.self, forKey: codingKey) {
                return value
            }
        }
        return nil
    }

    static func decodeInt(from container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> Int? {
        for key in keys {
            guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
            if let value = try? container.decodeIfPresent(Int.self, forKey: codingKey) {
                return value
            }
            if let value = try? container.decodeIfPresent(String.self, forKey: codingKey), let intValue = Int(value) {
                return intValue
            }
        }
        return nil
    }
}

extension UserInfo {
    var isPaired: Bool {
        pairId != nil
    }
}

private extension WatchThemeModel {
    static func decodeString(from container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> String? {
        for key in keys {
            if let codingKey = DynamicCodingKey(stringValue: key),
               let value = try? container.decodeIfPresent(String.self, forKey: codingKey) {
                return value
            }
        }
        return nil
    }

    static func decodeBool(from container: KeyedDecodingContainer<DynamicCodingKey>, keys: [String]) -> Bool? {
        for key in keys {
            guard let codingKey = DynamicCodingKey(stringValue: key) else { continue }
            if let value = try? container.decodeIfPresent(Bool.self, forKey: codingKey) {
                return value
            }
            if let value = try? container.decodeIfPresent(Int.self, forKey: codingKey) {
                return value != 0
            }
            if let value = try? container.decodeIfPresent(String.self, forKey: codingKey) {
                return ["1", "true", "yes"].contains(value.lowercased())
            }
        }
        return nil
    }
}