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