// // MiniMaxServiceUsage.swift // CodexBarCore // // Created by Sisyphus on 2026-03-25. // import Foundation /// Represents the usage information for a specific MiniMax service. /// /// This struct encapsulates all the relevant details about how much of a particular /// MiniMax service has been used within its quota window, including reset timing /// and localized display strings. public struct MiniMaxServiceUsage: Sendable { /// The service identifier (e.g., "text-generation", "text-to-speech", "image") public let serviceType: String /// The type of time window for the quota (e.g., "5 hours" or "Today") /// This should be a localized string. public let windowType: String /// The specific time range for the current quota window. /// For hourly quotas: "10:00-15:00(UTC+8)" /// For daily quotas: full date range string public let timeRange: String /// The amount of quota that has been used. public let usage: Int /// The total quota limit for this service in the current window public let limit: Int /// The percentage of quota used (0-100) public let percent: Double /// Whether this quota window is explicitly unlimited. public let isUnlimited: Bool /// The timestamp when the quota will reset, if available public let resetsAt: Date? /// A localized description of when the quota resets (e.g., "Resets in 2 hours 30 minutes") public let resetDescription: String /// The remaining quota available (limit - usage) public var remaining: Int { max(0, self.limit - self.usage) } /// The display name for this service public var displayName: String { let normalized = self.serviceType.lowercased() return switch normalized { case "general": "General" case "video": "Video" case "text-generation": "Text Generation" case "text-to-speech": "Text to Speech" case "image": "Image" case "text generation": "Text Generation" case "text to speech": "Text to Speech" case "image generation": "Image Generation" case "text to video": "Text to Video" case "image to video": "Image to Video" case "music generation": "Music Generation" case "music generation · v2.6": "Music Generation · v2.6" case "music cover": "Music Cover" case "lyrics generation": "Lyrics Generation" case "image understanding": "Image Understanding" default: self.serviceType } } public var isPrimaryTextQuotaLane: Bool { let normalized = self.serviceType.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() return normalized == "general" || self.displayName == "Text Generation" } /// Creates a new MiniMaxServiceUsage instance. /// /// - Parameters: /// - serviceType: The service identifier /// - windowType: The type of time window (localized) /// - timeRange: The specific time range string /// - usage: The amount of quota used /// - limit: The total quota limit /// - percent: The percentage used (0-100) /// - resetsAt: Optional reset timestamp /// - resetDescription: Localized reset description public init( serviceType: String, windowType: String, timeRange: String, usage: Int, limit: Int, percent: Double, isUnlimited: Bool = false, resetsAt: Date?, resetDescription: String) { self.serviceType = serviceType self.windowType = windowType self.timeRange = timeRange self.usage = usage self.limit = limit self.percent = percent self.isUnlimited = isUnlimited self.resetsAt = resetsAt self.resetDescription = resetDescription } } extension MiniMaxServiceUsage { public static func parseWindowType(_ windowType: String) -> (windowType: String, windowMinutes: Int?) { switch windowType.lowercased() { case "5 hours", "5 小时": return ("5 hours", 300) case "today", "今日": return ("Today", 1440) default: // Try to extract hours from string like "X hours" if let hours = Int(windowType.components(separatedBy: .whitespaces).first ?? "") { return (windowType, hours * 60) } return (windowType, nil) } } public static func parseTimeRange(_ timeRange: String, now: Date) -> Date? { let calendar = Calendar.current // Handle "10:00-15:00(UTC+8)" format if timeRange.contains("-"), timeRange.contains("("), timeRange.contains(")") { // Extract the time part before the timezone let components = timeRange.split(separator: "(") guard components.count >= 1 else { return nil } let timePart = String(components[0]).trimmingCharacters(in: .whitespaces) // Split by "-" to get start and end times let timeComponents = timePart.split(separator: "-") guard timeComponents.count == 2 else { return nil } let endTimeStr = String(timeComponents[1]).trimmingCharacters(in: .whitespaces) // Parse end time (HH:mm format) let timeFormatter = DateFormatter() timeFormatter.dateFormat = "HH:mm" timeFormatter.timeZone = TimeZone.current guard let endTime = timeFormatter.date(from: endTimeStr) else { return nil } // Get today's date components let nowComponents = calendar.dateComponents([.year, .month, .day], from: now) let endTimeComponents = calendar.dateComponents([.hour, .minute], from: endTime) // Combine today's date with end time var combinedComponents = DateComponents() combinedComponents.year = nowComponents.year combinedComponents.month = nowComponents.month combinedComponents.day = nowComponents.day combinedComponents.hour = endTimeComponents.hour combinedComponents.minute = endTimeComponents.minute guard let resultDate = calendar.date(from: combinedComponents) else { return nil } // If the result date is in the past (before now), add one day if resultDate < now { return calendar.date(byAdding: .day, value: 1, to: resultDate) } return resultDate } // Handle "2026/03/25 00:00 - 2026/03/26 00:00" format if timeRange.contains(" - ") { let dateComponents = timeRange.split(separator: " - ") guard dateComponents.count == 2 else { return nil } let endDateStr = String(dateComponents[1]).trimmingCharacters(in: .whitespaces) let dateFormatter = DateFormatter() dateFormatter.dateFormat = "yyyy/MM/dd HH:mm" dateFormatter.timeZone = TimeZone.current return dateFormatter.date(from: endDateStr) } return nil } public static func generateResetDescription(resetsAt: Date, now: Date = Date()) -> String { let calendar = Calendar.current let components = calendar.dateComponents([.hour, .minute], from: now, to: resetsAt) guard let hours = components.hour, let minutes = components.minute else { return "Resets soon" } if hours > 0, minutes > 0 { return "Resets in \(hours) hours \(minutes) minutes" } else if hours > 0 { return "Resets in \(hours) hour\(hours > 1 ? "s" : "")" } else if minutes > 0 { return "Resets in \(minutes) minute\(minutes > 1 ? "s" : "")" } else { return "Resets now" } } }