#26 new: ios home widget

This commit is contained in:
lollipopkit
2023-07-16 22:46:32 +08:00
parent 814e9c853a
commit 0ee55d4873
12 changed files with 683 additions and 99 deletions

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>INEnums</key>
<array/>
<key>INIntentDefinitionModelVersion</key>
<string>1.2</string>
<key>INIntentDefinitionNamespace</key>
<string>88xZPY</string>
<key>INIntentDefinitionSystemVersion</key>
<string>22F82</string>
<key>INIntentDefinitionToolsBuildVersion</key>
<string>14E222b</string>
<key>INIntentDefinitionToolsVersion</key>
<string>14.3</string>
<key>INIntents</key>
<array>
<dict>
<key>INIntentCategory</key>
<string>information</string>
<key>INIntentDescription</key>
<string>Config for home status widget</string>
<key>INIntentDescriptionID</key>
<string>tVvJ9c</string>
<key>INIntentEligibleForWidgets</key>
<true/>
<key>INIntentIneligibleForSuggestions</key>
<true/>
<key>INIntentLastParameterTag</key>
<integer>2</integer>
<key>INIntentName</key>
<string>Configuration</string>
<key>INIntentParameters</key>
<array>
<dict>
<key>INIntentParameterConfigurable</key>
<true/>
<key>INIntentParameterDisplayName</key>
<string>Url</string>
<key>INIntentParameterDisplayNameID</key>
<string>5QiOUM</string>
<key>INIntentParameterDisplayPriority</key>
<integer>1</integer>
<key>INIntentParameterMetadata</key>
<dict>
<key>INIntentParameterMetadataCapitalization</key>
<string>Characters</string>
<key>INIntentParameterMetadataDefaultValue</key>
<string>https://push.lolli.tech</string>
<key>INIntentParameterMetadataDefaultValueID</key>
<string>wwvqC2</string>
<key>INIntentParameterMetadataDisableAutocorrect</key>
<true/>
<key>INIntentParameterMetadataDisableSmartDashes</key>
<true/>
<key>INIntentParameterMetadataDisableSmartQuotes</key>
<true/>
</dict>
<key>INIntentParameterName</key>
<string>url</string>
<key>INIntentParameterPromptDialogs</key>
<array>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogType</key>
<string>Configuration</string>
</dict>
<dict>
<key>INIntentParameterPromptDialogCustom</key>
<true/>
<key>INIntentParameterPromptDialogFormatString</key>
<string>Server monitor url</string>
<key>INIntentParameterPromptDialogFormatStringID</key>
<string>j41owf</string>
<key>INIntentParameterPromptDialogType</key>
<string>Primary</string>
</dict>
</array>
<key>INIntentParameterSupportsResolution</key>
<true/>
<key>INIntentParameterTag</key>
<integer>1</integer>
<key>INIntentParameterType</key>
<string>String</string>
</dict>
</array>
<key>INIntentResponse</key>
<dict>
<key>INIntentResponseCodes</key>
<array>
<dict>
<key>INIntentResponseCodeName</key>
<string>success</string>
<key>INIntentResponseCodeSuccess</key>
<true/>
</dict>
<dict>
<key>INIntentResponseCodeName</key>
<string>failure</string>
</dict>
</array>
</dict>
<key>INIntentTitle</key>
<string>StatusWidgetConf</string>
<key>INIntentTitleID</key>
<string>gpCwrM</string>
<key>INIntentType</key>
<string>Custom</string>
<key>INIntentVerb</key>
<string>View</string>
</dict>
</array>
<key>INTypes</key>
<array/>
</dict>
</plist>

View File

@@ -0,0 +1,207 @@
//
// StatusWidget.swift
// StatusWidget
//
// Created by lolli on 2023/7/15.
//
import WidgetKit
import SwiftUI
import Intents
struct Status {
let name: String
let cpu: String
let mem: String
let disk: String
let net: String
}
let demoStatus = Status(name: "Server Name", cpu: "31.7%", mem: "1.3g / 1.9g", disk: "7.1g / 30.0g", net: "712.3k / 1.2m")
let domain = "com.lollipopkit.toolbox"
var url: String?
struct Provider: IntentTimelineProvider {
func placeholder(in context: Context) -> SimpleEntry {
SimpleEntry(date: Date(), configuration: ConfigurationIntent(), data: demoStatus, state: .normal)
}
func getSnapshot(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (SimpleEntry) -> ()) {
let entry = SimpleEntry(date: Date(), configuration: configuration, data: demoStatus, state: .normal)
completion(entry)
}
func getTimeline(for configuration: ConfigurationIntent, in context: Context, completion: @escaping (Timeline<Entry>) -> ()) {
url = configuration.url
let currentDate = Date()
let refreshDate = Calendar.current.date(byAdding: .minute, value: 30, to: currentDate)!
StatusLoader.fetch { result in
let entry: SimpleEntry
switch result {
case .success(let status):
entry = SimpleEntry(
date: currentDate,
configuration: configuration,
data: status,
state: .normal
)
case .failure(let err):
entry = SimpleEntry(date: currentDate, configuration: configuration, data: demoStatus, state: .error(err.localizedDescription))
}
let timeline = Timeline(entries: [entry], policy: .after(refreshDate))
completion(timeline)
}
}
}
struct SimpleEntry: TimelineEntry {
let date: Date
let configuration: ConfigurationIntent
let data: Status
let state: ContentState
}
struct StatusWidgetEntryView : View {
var entry: Provider.Entry
var body: some View {
switch entry.state {
case .loading:
ProgressView().padding()
case .error(let descriotion):
Text(descriotion)
case .normal:
VStack(alignment: .leading, spacing: 5.7) {
Text(entry.data.name).font(.system(.title3))
Spacer()
DetailItem(icon: "cpu", text: entry.data.cpu, color: .primary.opacity(0.7))
DetailItem(icon: "memorychip", text: entry.data.mem, color: .primary.opacity(0.7))
DetailItem(icon: "externaldrive", text: entry.data.disk, color: .primary.opacity(0.7))
Spacer()
DetailItem(icon: "clock", text: date2String(entry.date, dateFormat: "HH:mm"), color: .primary.opacity(0.7))
}.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
.padding()
}
}
}
struct StatusWidget: Widget {
let kind: String = "StatusWidget"
var body: some WidgetConfiguration {
IntentConfiguration(kind: kind, intent: ConfigurationIntent.self, provider: Provider()) { entry in
StatusWidgetEntryView(entry: entry)
}
.configurationDisplayName("Status")
.description("Status of your servers.")
.supportedFamilies([.systemSmall])
}
}
struct StatusWidget_Previews: PreviewProvider {
static var previews: some View {
StatusWidgetEntryView(entry: SimpleEntry(date: Date(), configuration: ConfigurationIntent(), data: demoStatus, state: .normal))
.previewContext(WidgetPreviewContext(family: .systemSmall))
}
}
struct StatusLoader {
static func fetch(completion: @escaping (Result<Status, Error>) -> Void) {
if url == nil || url == "" {
completion(.failure(NSError(domain: domain, code: 1, userInfo: [NSLocalizedDescriptionKey: "Please longpress it to config first."])))
return
}
let URL = URL(string: url!)!
let task = URLSession.shared.dataTask(with: URL) { (data, response, error) in
guard error == nil else {
completion(.failure(error!))
return
}
guard data != nil else {
completion(.failure(NSError(domain: domain, code: 2, userInfo: [NSLocalizedDescriptionKey: "empty network data."])))
return
}
let result = getStatus(fromData: data!)
switch result {
case .success(let status):
completion(.success(status))
case .failure(let error):
completion(.failure(error))
}
}
task.resume()
}
static func getStatus(fromData data: Foundation.Data) -> Result<Status, Error> {
let jsonAll = try! JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
let code = jsonAll["code"] as! Int
if (code != 0) {
switch (code) {
default:
let msg = jsonAll["msg"] as! String? ?? ""
return .failure(NSError(domain: domain, code: code, userInfo: [NSLocalizedDescriptionKey: msg]))
}
}
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
return .success(Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net))
}
}
private func dynamicUIColor(color: DynamicColor) -> UIColor {
if #available(iOS 13, *) { // 13
return UIColor { (traitCollection: UITraitCollection) -> UIColor in
return traitCollection.userInterfaceStyle == UIUserInterfaceStyle.dark ?
color.dark : color.light
}
}
return color.light
}
struct DynamicColor {
let dark: UIColor
let light: UIColor
}
let bgColor = DynamicColor(dark: UIColor(.black), light: UIColor(.white))
let textColor = DynamicColor(dark: UIColor(.white), light: UIColor(.black))
private func dynamicColor(color: DynamicColor) -> Color {
return Color.init(dynamicUIColor(color: color))
}
struct DetailItem: View {
let icon: String
let text: String
let color: Color
var body: some View {
HStack(spacing: 5.7) {
Image(systemName: icon).resizable().foregroundColor(color).frame(width: 11, height: 11, alignment: .center)
Text(text)
.font(.system(.caption2))
.foregroundColor(color)
}
}
}
func date2String(_ date:Date, dateFormat:String = "yyyy-MM-dd HH:mm:ss") -> String {
let formatter = DateFormatter()
formatter.locale = Locale.init(identifier: "zh_CN")
formatter.dateFormat = dateFormat
let date = formatter.string(from: date)
return date
}
enum ContentState {
case loading
case error(String)
case normal
}

View File

@@ -0,0 +1,16 @@
//
// StatusWidgetBundle.swift
// StatusWidget
//
// Created by lolli on 2023/7/15.
//
import WidgetKit
import SwiftUI
@main
struct StatusWidgetBundle: WidgetBundle {
var body: some Widget {
StatusWidget()
}
}