mirror of
https://github.com/lollipopkit/flutter_server_box.git
synced 2025-12-17 07:14:28 +01:00
opt.: watchOS & iOS widget (#847)
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user