// // StatusWidget.swift // StatusWidget // // Created by lolli on 2023/7/15. // import WidgetKit import SwiftUI import Intents import AppIntents import Foundation let demoStatus = Status(name: "Server", cpu: "31.7%", mem: "1.3g / 1.9g", disk: "7.1g / 30.0g", net: "712.3k / 1.2m") let domain = "com.lollipopkit.toolbox" let bgColor = DynamicColor(dark: UIColor.black, light: UIColor.white) // Widget-specific constants enum WidgetConstants { enum Dimensions { static let smallGauge: CGFloat = 56 static let mediumGauge: CGFloat = 64 static let largeGauge: CGFloat = 76 static let refreshIconSmall: CGFloat = 12 static let refreshIconLarge: CGFloat = 14 static let cornerRadius: CGFloat = 12 static let shadowRadius: CGFloat = 2 } enum Thresholds { static let warningThreshold: Double = 0.6 static let criticalThreshold: Double = 0.85 } enum Spacing { static let tight: CGFloat = 4 static let normal: CGFloat = 8 static let loose: CGFloat = 12 static let extraLoose: CGFloat = 16 } enum Colors { static let cardBackground = Color(.systemBackground) static let secondaryText = Color(.secondaryLabel) static let success = Color(.systemGreen) static let warning = Color(.systemOrange) static let critical = Color(.systemRed) static let accent = Color(.systemBlue) } static let appGroupId = "group.com.lollipopkit.toolbox" } // Performance optimization: cache parsed values struct ParseCache { private static var percentCache: [String: Double] = [:] private static var usagePercentCache: [String: Double] = [:] static func parsePercent(_ text: String) -> Double { if let cached = percentCache[text] { return cached } let trimmed = text.trimmingCharacters(in: CharacterSet(charactersIn: "% ")) let result = Double(trimmed).map { max(0, min(1, $0 / 100.0)) } ?? 0 percentCache[text] = result return result } static func parseUsagePercent(_ text: String) -> Double { if let cached = usagePercentCache[text] { return cached } let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) } guard parts.count == 2 else { return 0 } let used = PerformanceUtils.parseSizeToBytes(parts[0]) let total = PerformanceUtils.parseSizeToBytes(parts[1]) let result = total <= 0 ? 0 : max(0, min(1, used / total)) usagePercentCache[text] = result return result } static func parseNetworkTotal(_ text: String) -> (totalBytes: Double, displayText: String) { let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) } guard parts.count == 2 else { return (0, "0 B") } let upload = PerformanceUtils.parseSizeToBytes(parts[0]) let download = PerformanceUtils.parseSizeToBytes(parts[1]) let total = upload + download let displayText = PerformanceUtils.formatSize(total) return (total, displayText) } static func parseNetworkPercent(_ text: String) -> Double { let parts = text.split(separator: "/").map { String($0).trimmingCharacters(in: .whitespaces) } guard parts.count == 2 else { return 0 } let upload = PerformanceUtils.parseSizeToBytes(parts[0]) let download = PerformanceUtils.parseSizeToBytes(parts[1]) let total = upload + download // Return upload percentage of total traffic return total <= 0 ? 0 : max(0, min(1, upload / total)) } } struct PerformanceUtils { // Precomputed multipliers for performance private static let sizeMultipliers: [Character: Double] = [ "k": 1024, "m": pow(1024, 2), "g": pow(1024, 3), "t": pow(1024, 4), "p": pow(1024, 5) ] static func parseSizeToBytes(_ text: String) -> Double { let lower = text.lowercased().replacingOccurrences(of: "b", with: "") let unitChar = lower.trimmingCharacters(in: .whitespaces).last let numberPart: String let multiplier: Double if let u = unitChar, let mult = sizeMultipliers[u] { multiplier = mult numberPart = String(lower.dropLast()) } else { multiplier = 1.0 numberPart = lower } let value = Double(numberPart.trimmingCharacters(in: .whitespaces)) ?? 0 return value * multiplier } static func percentStr(_ value: Double) -> String { let pct = max(0, min(1, value)) * 100 let rounded = (pct * 10).rounded() / 10 return rounded.truncatingRemainder(dividingBy: 1) == 0 ? String(format: "%.0f%%", rounded) : String(format: "%.1f%%", rounded) } static func thresholdColor(_ value: Double) -> Color { let v = max(0, min(1, value)) switch v { case .. String { let units = ["B", "KB", "MB", "GB", "TB"] var size = bytes var unitIndex = 0 while size >= 1024 && unitIndex < units.count - 1 { size /= 1024 unitIndex += 1 } return String(format: "%.1f %@", size, units[unitIndex]) } } struct Provider: IntentTimelineProvider { func placeholder(in context: Context) -> SimpleEntry { SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus)) } func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) { let entry = SimpleEntry(date: Date(), configuration: configuration, state: .normal(demoStatus)) completion(entry) } func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline) -> ()) { var url = configuration.url let family = context.family #if os(iOS) if #available(iOSApplicationExtension 16.0, *) { if family == .accessoryInline || family == .accessoryRectangular { url = UserDefaults(suiteName: WidgetConstants.appGroupId)?.string(forKey: "accessory_widget_url") } } #endif let currentDate = Date() let refreshDate = Calendar.current.date(byAdding: .minute, value: 15, to: currentDate)! fetch(url: url) { result in let entry: SimpleEntry = SimpleEntry( date: currentDate, configuration: configuration, state: result ) let timeline = Timeline(entries: [entry], policy: .after(refreshDate)) completion(timeline) } } func fetch(url: String?, completion: @escaping (ContentState) -> Void) { guard let url = url, url.count >= 12 else { completion(.error(.url("url is nil OR len < 12"))) return } guard let url = URL(string: url) else { completion(.error(.url("parse url failed"))) return } let task = URLSession.shared.dataTask(with: url) { (data, response, error) in if error != nil { completion(.error(.http(error?.localizedDescription ?? "unknown http err"))) return } guard let data = data else { completion(.error(.http("empty http data"))) return } let jsonAll = try! JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] ?? [:] let code = jsonAll["code"] as? Int ?? 1 if (code != 0) { let msg = jsonAll["msg"] as? String ?? "Empty err" completion(.error(.http(msg))) return } let json = jsonAll["data"] as? [String: Any] ?? [:] let name = json["name"] as? String ?? "" let disk = json["disk"] as? String ?? "" let cpu = json["cpu"] as? String ?? "" let mem = json["mem"] as? String ?? "" let net = json["net"] as? String ?? "" completion(.normal(Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net))) } task.resume() } } struct SimpleEntry: TimelineEntry { let date: Date let configuration: ConfigurationIntent let state: ContentState } struct StatusWidgetEntryView : View { var entry: Provider.Entry @Environment(\.widgetFamily) var family: WidgetFamily var body: some View { switch entry.state { case .loading: Text("Loading").widgetBackground() case .error(let err): switch err { case .http(let description): VStack(alignment: .center) { Text(description) if #available(iOS 17.0, *) { Button(intent: RefreshIntent()) { Image(systemName: "arrow.clockwise") .resizable() .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27) }.tint(.gray) } } .widgetBackground() case .url(_): Link("Open wiki ⬅️", destination: helpUrl) .widgetBackground() } case .normal(let data): let sumColor: Color = .primary.opacity(0.7) switch family { case .systemMedium: VStack(alignment: .leading, spacing: WidgetConstants.Spacing.normal) { // Title + refresh if #available(iOS 17.0, *) { HStack { Text(data.name).font(.system(.title3, design: .monospaced)) Spacer() Button(intent: RefreshIntent()) { Image(systemName: "arrow.clockwise") .resizable() .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27) }.tint(.gray) } } else { Text(data.name).font(.system(.title3, design: .monospaced)) } Spacer(minLength: WidgetConstants.Spacing.normal) // Gauges row HStack(spacing: WidgetConstants.Spacing.tight) { GaugeTile(label: "CPU", value: ParseCache.parsePercent(data.cpu), display: data.cpu, diameter: WidgetConstants.Dimensions.smallGauge) GaugeTile(label: "MEM", value: ParseCache.parseUsagePercent(data.mem), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.mem)), diameter: WidgetConstants.Dimensions.smallGauge) GaugeTile(label: "DISK", value: ParseCache.parseUsagePercent(data.disk), display: PerformanceUtils.percentStr(ParseCache.parseUsagePercent(data.disk)), diameter: WidgetConstants.Dimensions.smallGauge) GaugeTile(label: "NET", value: ParseCache.parseNetworkPercent(data.net), display: ParseCache.parseNetworkTotal(data.net).displayText, diameter: WidgetConstants.Dimensions.smallGauge) } .frame(maxWidth: .infinity, alignment: .leading) .padding(.bottom, 3) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .autoPadding() .widgetBackground() #if os(iOS) case .accessoryRectangular: VStack(alignment: .leading, spacing: 2) { HStack { Text(data.name) .font(.system(size: 15, weight: .semibold, design: .monospaced)) Spacer() CirclePercent(percent: data.cpu) .padding(.top, 3) .padding(.trailing, 3) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) Spacer() DetailItem(icon: "memorychip", text: data.mem, color: sumColor) DetailItem(icon: "externaldrive", text: data.disk, color: sumColor) DetailItem(icon: "network", text: data.net, color: sumColor) } .widgetBackground() case .accessoryInline: Text("\(data.name) \(data.cpu)").widgetBackground() #endif default: VStack(alignment: .leading, spacing: 3.7) { if #available(iOS 17.0, *) { HStack { Text(data.name).font(.system(.title3, design: .monospaced)) Spacer() Button(intent: RefreshIntent()) { Image(systemName: "arrow.clockwise") .resizable() .frame(width: WidgetConstants.Dimensions.refreshIconSmall, height: WidgetConstants.Dimensions.refreshIconSmall * 1.27) }.tint(.gray) } } else { Text(data.name).font(.system(.title3, design: .monospaced)) } Spacer() DetailItem(icon: "cpu", text: data.cpu, color: sumColor) DetailItem(icon: "memorychip", text: data.mem, color: sumColor) DetailItem(icon: "externaldrive", text: data.disk, color: sumColor) DetailItem(icon: "network", text: data.net, color: sumColor) } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .autoPadding() .widgetBackground() } } } } extension View { @ViewBuilder func widgetBackground() -> some View { // Modern card-style background with subtle effects let backgroundView = LinearGradient( gradient: Gradient(colors: [ Color(bgColor.resolve()), Color(bgColor.resolve()).opacity(0.95) ]), startPoint: .topLeading, endPoint: .bottomTrailing ) if #available(iOS 17.0, *) { containerBackground(for: .widget) { backgroundView } } else { background(backgroundView) } } // Enhanced padding with improved spacing func autoPadding() -> some View { if #available(iOS 17.0, *) { return self.padding(.all, WidgetConstants.Spacing.tight) } else { return self.padding(.all, WidgetConstants.Spacing.extraLoose + 1) } } // Modern card container with shadow and rounded corners func modernCard(cornerRadius: CGFloat = WidgetConstants.Dimensions.cornerRadius) -> some View { self .background( RoundedRectangle(cornerRadius: cornerRadius) .fill(WidgetConstants.Colors.cardBackground) .shadow( color: .black.opacity(0.08), radius: WidgetConstants.Dimensions.shadowRadius, x: 0, y: 1 ) ) } } struct StatusWidget: Widget { let kind: String = "StatusWidget" var body: some WidgetConfiguration { let cfg = IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in StatusWidgetEntryView(entry: entry) } .configurationDisplayName("Status") .description("Status of your servers.") #if os(iOS) if #available(iOSApplicationExtension 16.0, *) { return cfg.supportedFamilies([.systemSmall, .systemMedium, .accessoryRectangular, .accessoryInline]) } else { return cfg.supportedFamilies([.systemSmall, .systemMedium]) } #else return cfg.supportedFamilies([.systemSmall, .systemMedium]) #endif } } struct StatusWidget_Previews: PreviewProvider { static var previews: some View { StatusWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), state: .normal(demoStatus))) .previewContext(WidgetPreviewContext(family: .systemSmall)) } } struct DetailItem: View { let icon: String let text: String let color: Color var body: some View { HStack(spacing: WidgetConstants.Spacing.normal) { Image(systemName: icon) .resizable() .aspectRatio(contentMode: .fit) .foregroundColor(color.opacity(0.8)) .frame(width: 12, height: 12) .background( Circle() .fill(color.opacity(0.1)) .frame(width: 20, height: 20) ) Text(text) .font(.system(size: 12, weight: .medium, design: .rounded)) .foregroundColor(color) .lineLimit(1) .minimumScaleFactor(0.8) } .padding(.horizontal, WidgetConstants.Spacing.tight) .padding(.vertical, 2) } } // Enhanced circular progress indicator struct CirclePercent: View { // eg: 31.7% let percent: String @State private var animatedProgress: Double = 0 var body: some View { let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: "%"))) let progress = (percentD ?? 0) / 100 ZStack { // Background circle Circle() .stroke(Color.primary.opacity(0.15), lineWidth: 2.5) // Progress circle with gradient Circle() .trim(from: 0, to: CGFloat(max(0, min(1, animatedProgress)))) .stroke( AngularGradient( gradient: Gradient(colors: [ PerformanceUtils.thresholdColor(progress).opacity(0.7), PerformanceUtils.thresholdColor(progress) ]), center: .center ), style: StrokeStyle(lineWidth: 3, lineCap: .round) ) .rotationEffect(.degrees(-90)) // Percentage text Text(percent) .font(.system(size: 8, weight: .bold, design: .rounded)) .foregroundColor(.primary.opacity(0.8)) } .frame(width: 24, height: 24) .onAppear { withAnimation(.easeOut(duration: 0.8).delay(0.2)) { animatedProgress = progress } } .onChange(of: progress) { newProgress in withAnimation(.easeInOut(duration: 0.5)) { animatedProgress = newProgress } } } } // Modern gauge tile with enhanced visual design struct GaugeTile: View { let label: String // 0..1 let value: Double // eg: "31.7%" let display: String let diameter: CGFloat @State private var animatedValue: Double = 0 var body: some View { VStack(spacing: WidgetConstants.Spacing.normal) { ZStack { // Background circle with subtle shadow effect Circle() .stroke(Color.primary.opacity(0.1), lineWidth: 4) .background( Circle() .fill(WidgetConstants.Colors.cardBackground) .shadow(color: .black.opacity(0.05), radius: WidgetConstants.Dimensions.shadowRadius, x: 0, y: 1) ) // Progress arc with gradient effect Circle() .trim(from: 0, to: CGFloat(max(0, min(1, animatedValue)))) .stroke( AngularGradient( gradient: Gradient(colors: [ PerformanceUtils.thresholdColor(value).opacity(0.8), PerformanceUtils.thresholdColor(value) ]), center: .center, startAngle: .degrees(-90), endAngle: .degrees(270) ), style: StrokeStyle(lineWidth: 5, lineCap: .round) ) .rotationEffect(.degrees(-90)) // Center value text with improved typography Text(display) .font(.system(size: diameter < 60 ? 11 : 13, weight: .bold, design: .rounded)) .foregroundColor(.primary) .minimumScaleFactor(0.8) .lineLimit(1) } .frame(width: diameter, height: diameter) .onAppear { withAnimation(.easeOut(duration: 0.8).delay(0.1)) { animatedValue = value } } .onChange(of: value) { newValue in withAnimation(.easeInOut(duration: 0.6)) { animatedValue = newValue } } // Label with enhanced styling if #available(iOS 16.0, *) { Text(label) .font(.system(size: 11, weight: .medium, design: .rounded)) .foregroundColor(WidgetConstants.Colors.secondaryText) .textCase(.uppercase) .tracking(0.5) } else { Text(label) .font(.system(size: 11, weight: .medium, design: .rounded)) .foregroundColor(WidgetConstants.Colors.secondaryText) .textCase(.uppercase) } } .frame(maxWidth: .infinity) } } // Legacy functions maintained for compatibility - now delegate to optimized versions func parsePercent(_ text: String) -> Double { return ParseCache.parsePercent(text) } func parseUsagePercent(_ text: String) -> Double { return ParseCache.parseUsagePercent(text) } func parseSizeToBytes(_ text: String) -> Double { return PerformanceUtils.parseSizeToBytes(text) } func percentStr(_ value: Double) -> String { return PerformanceUtils.percentStr(value) } func thresholdColor(_ value: Double) -> Color { return PerformanceUtils.thresholdColor(value) } struct DynamicColor { let dark: UIColor let light: UIColor func resolve() -> UIColor { if #available(iOS 13, *) { // 版本号大于等于13 return UIColor { (traitCollection: UITraitCollection) -> UIColor in return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ? self.dark : self.light } } return self.light } } @available(iOS 16.0, macOS 13.0, watchOS 9.0, tvOS 16.0, *) struct RefreshIntent: AppIntent { static var title: LocalizedStringResource = "Refresh" static var description = IntentDescription("Refresh status.") func perform() async throws -> some IntentResult { return .result() } }