226 lines
7.8 KiB
Swift
226 lines
7.8 KiB
Swift
//
|
|
// 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"
|
|
}
|
|
}
|
|
}
|