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

@@ -9,22 +9,62 @@ import SwiftUI
struct ContentView: View {
@ObservedObject var _mgr = PhoneConnMgr()
@State private var selection: Int = 0
@State private var refreshAllCounter: Int = 0
var body: some View {
let _count = _mgr.urls.count == 0 ? 1 : _mgr.urls.count
TabView {
ForEach(0 ..< _count, id:\.self) { index in
let url = _count == 1 && _mgr.urls.count == 0 ? nil : _mgr.urls[index]
PageView(url: url, state: .loading)
let hasServers = !_mgr.urls.isEmpty
let pagesCount = hasServers ? _mgr.urls.count : 1
TabView(selection: $selection) {
ForEach(0 ..< pagesCount, id:\.self) { index in
let url = hasServers ? _mgr.urls[index] : nil
PageView(
url: url,
state: .loading,
refreshAllCounter: refreshAllCounter,
onRefreshAll: { refreshAllCounter += 1 }
)
.tag(index)
}
}
.tabViewStyle(PageTabViewStyle())
// URL
.onChange(of: _mgr.urls) { newValue in
let newCount = newValue.count
//
if newCount == 0 {
selection = 0
} else if selection >= newCount {
//
selection = max(0, newCount - 1)
}
}
// Widget 使
.onChange(of: selection) { newIndex in
let appGroupId = "group.com.lollipopkit.toolbox"
if let defaults = UserDefaults(suiteName: appGroupId) {
defaults.set(newIndex, forKey: "watch_shared_selected_index")
}
}
.onAppear {
//
let appGroupId = "group.com.lollipopkit.toolbox"
let saved = UserDefaults(suiteName: appGroupId)?.integer(forKey: "watch_shared_selected_index") ?? 0
if !_mgr.urls.isEmpty {
selection = min(max(0, saved), _mgr.urls.count - 1)
} else {
selection = 0
}
}
}
}
struct PageView: View {
let url: String?
@State var state: ContentState
//
let refreshAllCounter: Int
let onRefreshAll: () -> Void
var body: some View {
if url == nil {
@@ -36,35 +76,50 @@ struct PageView: View {
Spacer()
}
} else {
switch state {
case .loading:
ProgressView().padding().onAppear {
getStatus(url: url!)
}
case .error(let err):
Group {
switch state {
case .loading:
ProgressView().padding().onAppear {
getStatus(url: url!)
}
case .error(let err):
switch err {
case .http(let description):
VStack(alignment: .center) {
Text(description)
Button(action: {
state = .loading
}){
Image(systemName: "arrow.clockwise")
}.buttonStyle(.plain)
HStack(spacing: 10) {
Button(action: {
state = .loading
}){
Image(systemName: "arrow.clockwise")
}.buttonStyle(.plain)
Button(action: {
onRefreshAll()
}){
Image(systemName: "arrow.triangle.2.circlepath")
}.buttonStyle(.plain)
}
}
case .url(_):
Link("View help", destination: helpUrl)
}
case .normal(let status):
VStack(alignment: .leading) {
case .normal(let status):
VStack(alignment: .leading) {
HStack {
Text(status.name).font(.system(.title, design: .monospaced))
Spacer()
Button(action: {
state = .loading
}){
Image(systemName: "arrow.clockwise")
}.buttonStyle(.plain)
HStack(spacing: 10) {
Button(action: {
state = .loading
}){
Image(systemName: "arrow.clockwise")
}.buttonStyle(.plain)
Button(action: {
onRefreshAll()
}){
Image(systemName: "arrow.triangle.2.circlepath")
}.buttonStyle(.plain)
}
}
Spacer()
DetailItem(icon: "cpu", text: status.cpu)
@@ -72,6 +127,12 @@ struct PageView: View {
DetailItem(icon: "externaldrive", text: status.disk)
DetailItem(icon: "network", text: status.net)
}.frame(maxWidth: .infinity, maxHeight: .infinity).padding([.horizontal], 11)
}
}
.onChange(of: refreshAllCounter) { _ in
if let url = url {
getStatus(url: url)
}
}
}
}
@@ -87,25 +148,32 @@ struct PageView: View {
return
}
let task = URLSession.shared.dataTask(with: url) { (data, response, error) in
guard error == nil else {
state = .error(.http(error!.localizedDescription))
// UI 线 TabView
func setStateOnMain(_ newState: ContentState) {
DispatchQueue.main.async {
self.state = newState
}
}
if let error = error {
setStateOnMain(.error(.http(error.localizedDescription)))
return
}
guard let data = data else {
state = .error(.http("empty data"))
setStateOnMain(.error(.http("empty data")))
return
}
guard let jsonAll = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
state = .error(.http("json parse fail"))
setStateOnMain(.error(.http("json parse fail")))
return
}
guard let code = jsonAll["code"] as? Int else {
state = .error(.http("code is nil"))
setStateOnMain(.error(.http("code is nil")))
return
}
if (code != 0) {
let msg = jsonAll["msg"] as? String ?? ""
state = .error(.http(msg))
setStateOnMain(.error(.http(msg)))
return
}
@@ -115,10 +183,35 @@ struct PageView: View {
let cpu = json["cpu"] as? String ?? ""
let mem = json["mem"] as? String ?? ""
let net = json["net"] as? String ?? ""
state = .normal(Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net))
let status = Status(name: name, cpu: cpu, mem: mem, disk: disk, net: net)
setStateOnMain(.normal(status))
// App Group/ Widget 使
let appGroupId = "group.com.lollipopkit.toolbox"
if let defaults = UserDefaults(suiteName: appGroupId) {
var statusMap = (defaults.dictionary(forKey: "watch_shared_status_by_url") as? [String: [String: String]]) ?? [:]
statusMap[url.absoluteString] = [
"name": status.name,
"cpu": status.cpu,
"mem": status.mem,
"disk": status.disk,
"net": status.net
]
defaults.set(statusMap, forKey: "watch_shared_status_by_url")
}
}
task.resume()
}
//
@ViewBuilder
var _onRefreshAllHook: some View {
EmptyView()
.onChange(of: refreshAllCounter) { _ in
if let url = url {
getStatus(url: url)
}
}
}
}
struct ContentView_Previews: PreviewProvider {