opt.: watchOS & iOS widget (#847)

This commit is contained in:
lollipopkit🏳️‍⚧️
2025-08-13 01:44:02 +08:00
committed by GitHub
parent 8c0e0f89d5
commit 13e28675af
5 changed files with 655 additions and 59 deletions

View File

@@ -0,0 +1,141 @@
//
// WatchStatusWidget.swift
// WatchStatusWidget Extension
//
// Created by AI Assistant
//
import WidgetKit
import SwiftUI
import Foundation
// Simple model, independent from Runner target
struct Status: Hashable {
let name: String
let cpu: String
let mem: String
let disk: String
let net: String
}
struct WatchProvider: TimelineProvider {
func placeholder(in context: Context) -> WatchEntry {
WatchEntry(date: Date(), status: Status(name: "Server", cpu: "32%", mem: "1.3g/1.9g", disk: "7.1g/30g", net: "712k/1.2m"))
}
func getSnapshot(in context: Context, completion: @escaping (WatchEntry) -> Void) {
completion(loadEntry())
}
func getTimeline(in context: Context, completion: @escaping (Timeline<WatchEntry>) -> Void) {
let entry = loadEntry()
let next = Calendar.current.date(byAdding: .minute, value: 15, to: Date()) ?? Date().addingTimeInterval(900)
completion(Timeline(entries: [entry], policy: .after(next)))
}
private func loadEntry() -> WatchEntry {
let appGroupId = "group.com.lollipopkit.toolbox"
guard let defaults = UserDefaults(suiteName: appGroupId) else {
return WatchEntry(date: Date(), status: Status(name: "Server", cpu: "--%", mem: "-", disk: "-", net: "-"))
}
let urls = (defaults.array(forKey: "watch_shared_urls") as? [String]) ?? []
let idx = defaults.integer(forKey: "watch_shared_selected_index")
var status: Status? = nil
if !urls.isEmpty {
let i = min(max(0, idx), urls.count - 1)
let url = urls[i]
// Load status from shared defaults
if let statusMap = defaults.dictionary(forKey: "watch_shared_status_by_url") as? [String: [String: String]],
let statusDict = statusMap[url] {
status = Status(
name: statusDict["name"] ?? "",
cpu: statusDict["cpu"] ?? "",
mem: statusDict["mem"] ?? "",
disk: statusDict["disk"] ?? "",
net: statusDict["net"] ?? ""
)
}
}
return WatchEntry(
date: Date(),
status: status ?? Status(name: "Server", cpu: "--%", mem: "-", disk: "-", net: "-")
)
}
}
struct WatchEntry: TimelineEntry {
let date: Date
let status: Status
}
struct WatchStatusWidgetEntryView: View {
var entry: WatchProvider.Entry
@Environment(\.widgetFamily) var family
var body: some View {
switch family {
case .accessoryCircular:
ZStack {
Circle().stroke(Color.primary.opacity(0.15), lineWidth: 4)
CirclePercent(percent: entry.status.cpu)
Text(entry.status.cpu.replacingOccurrences(of: "%", with: "")).font(.system(size: 10, weight: .bold, design: .monospaced))
}
.padding(2)
case .accessoryRectangular:
VStack(alignment: .leading, spacing: 2) {
HStack {
Text(entry.status.name).font(.system(size: 12, weight: .semibold, design: .monospaced))
Spacer()
}
HStack(spacing: 6) {
Label(entry.status.cpu, systemImage: "cpu").font(.system(size: 11, design: .monospaced))
}
}
case .accessoryInline:
Text("\(entry.status.name) \(entry.status.cpu)")
default:
VStack {
Text(entry.status.name)
Text(entry.status.cpu)
}
}
}
}
struct WatchStatusWidget: Widget {
let kind: String = "WatchStatusWidget"
var body: some WidgetConfiguration {
StaticConfiguration(kind: kind, provider: WatchProvider()) { entry in
WatchStatusWidgetEntryView(entry: entry)
}
.configurationDisplayName("Server Status")
.description("Shows the selected server status.")
.supportedFamilies([.accessoryCircular, .accessoryRectangular, .accessoryInline])
}
}
struct WatchStatusWidget_Previews: PreviewProvider {
static var previews: some View {
WatchStatusWidgetEntryView(entry: WatchEntry(date: Date(), status: Status(name: "Server", cpu: "37%", mem: "1.3g/1.9g", disk: "7.1g/30g", net: "712k/1.2m")))
.previewContext(WidgetPreviewContext(family: .accessoryRectangular))
}
}
// Helpers reused from iOS widget with lightweight versions
struct CirclePercent: View {
let percent: String
var body: some View {
let percentD = Double(percent.trimmingCharacters(in: .init(charactersIn: "% "))) ?? 0
let p = max(0, min(100, percentD)) / 100.0
Circle()
.trim(from: 0, to: CGFloat(p))
.stroke(Color.primary, style: StrokeStyle(lineWidth: 4, lineCap: .round))
.rotationEffect(.degrees(-90))
}
}