This commit is contained in:
mrFq1
2023-04-25 14:51:23 +08:00
commit f3141fbffe
39 changed files with 3793 additions and 0 deletions
@@ -0,0 +1,90 @@
//
// ProxiesView.swift
// ClashX Dashboard
//
//
import SwiftUI
class ProxiesSearchString: ObservableObject, Identifiable {
let id = UUID().uuidString
@Published var string: String = ""
}
struct ProxiesView: View {
@State var proxyInfo: ClashProxyResp?
@State var proxyGroups = [ClashProxy]()
@State var providerInfo: ClashProviderResp?
@State var providers = [ClashProvider]()
// @State var proxyProviderList
@State private var searchString = ProxiesSearchString()
@State private var isGlobalMode = false
@State private var proxyListColumnCount = 3
var body: some View {
List() {
Text("Proxies")
.font(.title)
ForEach(proxyGroups, id: \.id) { group in
ProxyGroupView(columnCount: $proxyListColumnCount, proxyGroup: group, proxyInfo: proxyInfo!)
}
Text("Proxy Provider")
.font(.title)
.padding(.top)
ForEach($providers, id: \.id) { provider in
ProxyProviderGroupView(columnCount: $proxyListColumnCount, providerInfo: provider)
}
}
.background {
GeometryReader { geometry in
Rectangle()
.fill(.clear)
.frame(height: 1)
.onChange(of: geometry.size.width) { newValue in
updateColumnCount(newValue)
}
.onAppear {
updateColumnCount(geometry.size.width)
}
}.padding()
}
.searchable(text: $searchString.string)
.environmentObject(searchString)
.onAppear {
// self.isGlobalMode = ConfigManager.shared.currentConfig?.mode == .global
ApiRequest.getMergedProxyData {
proxyInfo = $0
proxyGroups = ($0?.proxyGroups ?? []).filter {
isGlobalMode ? true : $0.name != "GLOBAL"
}
providerInfo = proxyInfo?.enclosingProviderResp
providers = providerInfo?.providers.map {
$0.value
} ?? []
}
}
}
func updateColumnCount(_ width: Double) {
let v = Int(Int(width) / 200)
let new = v == 0 ? 1 : v
if new != proxyListColumnCount {
proxyListColumnCount = new
}
}
}
//struct ProxiesView_Previews: PreviewProvider {
// static var previews: some View {
// ProxiesView()
// }
//}
@@ -0,0 +1,143 @@
//
// ProxyView.swift
// ClashX Dashboard
//
//
import SwiftUI
struct ProxyGroupView: View {
@Binding var columnCount: Int
@State var proxyGroup: ClashProxy
@State var proxyInfo: ClashProxyResp
@State private var proxyItems: [ProxyItemData]
@State private var currentProxy: ClashProxyName
@State private var isUpdatingSelect = false
@State private var selectable = false
@State private var isListExpanded = false
@State private var isTesting = false
@EnvironmentObject var searchString: ProxiesSearchString
init(columnCount: Binding<Int>,
proxyGroup: ClashProxy,
proxyInfo: ClashProxyResp) {
self._columnCount = columnCount
self.proxyGroup = proxyGroup
self.proxyInfo = proxyInfo
self.currentProxy = proxyGroup.now ?? ""
self.selectable = [.select, .fallback].contains(proxyGroup.type)
self.proxyItems = proxyGroup.all?.compactMap { name in
proxyInfo.proxiesMap[name]
}.map(ProxyItemData.init) ?? []
}
var body: some View {
Section {
proxyListView
.background {
Rectangle()
.frame(width: 2, height: listHeight(columnCount))
.foregroundColor(.clear)
}
.show(isVisible: !isListExpanded)
} header: {
proxyInfoView
}
}
var proxyInfoView: some View {
HStack() {
Text(proxyGroup.name)
.font(.title)
.fontWeight(.medium)
Text(proxyGroup.type.rawValue)
Text("\(proxyGroup.all?.count ?? 0)")
Button() {
isListExpanded = !isListExpanded
} label: {
Image(systemName: isListExpanded ? "chevron.up" : "chevron.down")
}
Button() {
startBenchmark()
} label: {
Image(systemName: "bolt.fill")
}
.disabled(isTesting)
}
}
var proxyListView: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: columnCount)) {
ForEach($proxyItems, id: \.id) { item in
ProxyItemView(
proxy: item,
selectable: selectable
)
.background(currentProxy == item.wrappedValue.name ? Color.teal : Color.white)
.cornerRadius(8)
.onTapGesture {
let item = item.wrappedValue
updateSelect(item.name)
}
.show(isVisible: {
if searchString.string.isEmpty {
return true
} else {
return item.wrappedValue.name.lowercased().contains(searchString.string.lowercased())
}
}())
}
}
}
func listHeight(_ columnCount: Int) -> Double {
let lineCount = ceil(Double(proxyItems.count) / Double(columnCount))
return lineCount * 60 + (lineCount - 1) * 8
}
func startBenchmark() {
isTesting = true
ApiRequest.getGroupDelay(groupName: proxyGroup.name) { delays in
proxyGroup.all?.forEach { proxyName in
var delay = 0
if let d = delays[proxyName], d != 0 {
delay = d
}
proxyItems.first {
$0.name == proxyName
}?.delay = delay
}
isTesting = false
}
}
func updateSelect(_ name: String) {
guard selectable, !isUpdatingSelect else { return }
isUpdatingSelect = true
ApiRequest.updateProxyGroup(group: proxyGroup.name, selectProxy: name) { success in
isUpdatingSelect = false
guard success else { return }
currentProxy = name
}
}
}
//struct ProxyView_Previews: PreviewProvider {
// static var previews: some View {
// ProxyGroupView()
// }
//}
//
@@ -0,0 +1,75 @@
//
// ProxyItemData.swift
// ClashX Dashboard
//
//
import Cocoa
import SwiftUI
class ProxyItemData: NSObject, ObservableObject {
let id: String
@objc let name: ClashProxyName
let type: ClashProxyType
let udpString: String
let tfo: Bool
let all: [ClashProxyName]
var delay: Int {
didSet {
switch delay {
case 0:
delayString = NSLocalizedString("fail", comment: "")
default:
delayString = "\(delay) ms"
}
let httpsTest = true
switch delay {
case 0:
delayColor = .gray
case ..<200 where !httpsTest:
delayColor = .green
case ..<800 where httpsTest:
delayColor = .green
case 200..<500 where !httpsTest:
delayColor = .yellow
case 800..<1500 where httpsTest:
delayColor = .yellow
default:
delayColor = .orange
}
}
}
@Published var delayString = ""
@Published var delayColor = Color.clear
init(clashProxy: ClashProxy) {
id = clashProxy.id
name = clashProxy.name
type = clashProxy.type
tfo = clashProxy.tfo
all = clashProxy.all ?? []
udpString = {
if clashProxy.udp {
return "UDP"
} else if clashProxy.xudp {
return "XUDP"
} else {
return ""
}
}()
delay = 0
super.init()
defer {
delay = clashProxy.history.last?.meanDelay ?? clashProxy.history.last?.delay ?? 0
}
}
}
@@ -0,0 +1,74 @@
//
// ProxyItemView.swift
// ClashX Dashboard
//
//
import SwiftUI
struct ProxyItemView: View {
@Binding var proxy: ProxyItemData
@State var selectable: Bool
init(proxy: Binding<ProxyItemData>, selectable: Bool) {
self._proxy = proxy
self.selectable = selectable
self.isBuiltInProxy = [.pass, .direct, .reject].contains(proxy.wrappedValue.type)
}
@State private var isBuiltInProxy: Bool
@State private var mouseOver = false
var body: some View {
VStack {
HStack(alignment: .center) {
Text(proxy.name)
.truncationMode(.tail)
.lineLimit(1)
Spacer(minLength: 6)
Text(proxy.udpString)
.foregroundColor(.secondary)
.font(.system(size: 11))
.show(isVisible: !isBuiltInProxy)
}
Spacer(minLength: 6)
.show(isVisible: !isBuiltInProxy)
HStack(alignment: .center) {
Text(proxy.type.rawValue)
.foregroundColor(.secondary)
.font(.system(size: 12))
Text("[TFO]")
.font(.system(size: 9))
.show(isVisible: proxy.tfo)
Spacer(minLength: 6)
Text(proxy.delayString)
.foregroundColor(proxy.delayColor)
.font(.system(size: 11))
}
.show(isVisible: !isBuiltInProxy)
}
.onHover {
guard selectable else { return }
mouseOver = $0
}
.frame(height: 36)
.padding(12)
.overlay(
RoundedRectangle(cornerRadius: 6)
.stroke(mouseOver ? .secondary : Color.clear, lineWidth: 2)
.padding(1)
)
}
}
//struct ProxyItemView_Previews: PreviewProvider {
// static var previews: some View {
// ProxyItemView()
// }
//}
@@ -0,0 +1,19 @@
//
// ProxyListView.swift
// ClashX Dashboard
//
//
import SwiftUI
struct ProxyListView: View {
var body: some View {
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
}
}
struct ProxyListView_Previews: PreviewProvider {
static var previews: some View {
ProxyListView()
}
}
@@ -0,0 +1,207 @@
//
// ProxyProviderGroupView.swift
// ClashX Dashboard
//
//
import SwiftUI
struct ProxyProviderGroupView: View {
@Binding var columnCount: Int
@Binding var providerInfo: ClashProvider
@State private var proxyItems: [ProxyItemData]
@State private var trafficInfo: String
@State private var expireDate: String
@State private var updateAt: String
@State private var isListExpanded = false
@State private var isTesting = false
@State private var isUpdating = false
@EnvironmentObject var searchString: ProxiesSearchString
init(columnCount: Binding<Int>,
providerInfo: Binding<ClashProvider>) {
self._columnCount = columnCount
self._providerInfo = providerInfo
let info = providerInfo.wrappedValue
self.proxyItems = info.proxies.map(ProxyItemData.init)
if let info = info.subscriptionInfo {
let used = info.download + info.upload
let total = info.total
let formatter = ByteCountFormatter()
trafficInfo = formatter.string(fromByteCount: used)
+ " / "
+ formatter.string(fromByteCount: total)
+ " ( \(String(format: "%.2f", Double(used)/Double(total/100)))% )"
let expire = info.expire
expireDate = "Expire: "
+ Date(timeIntervalSince1970: TimeInterval(expire))
.formatted()
} else {
trafficInfo = ""
expireDate = ""
}
if let updatedAt = info.updatedAt {
let formatter = RelativeDateTimeFormatter()
self.updateAt = formatter.localizedString(for: updatedAt, relativeTo: .now)
} else {
self.updateAt = ""
}
}
var body: some View {
Section {
providerListView
.background {
Rectangle()
.frame(width: 2, height: listHeight(columnCount))
.foregroundColor(.clear)
}
.show(isVisible: !isListExpanded)
} header: {
providerInfoView
} footer: {
HStack {
Button {
update()
} label: {
Label("Update", systemImage: "arrow.clockwise")
}
.disabled(isUpdating)
Button {
startBenchmark()
} label: {
Label("Benchmark", systemImage: "bolt.fill")
}
.disabled(isTesting)
}
}
}
var providerInfoView: some View {
VStack(alignment: .leading) {
HStack {
Text(providerInfo.name)
.font(.title)
.fontWeight(.medium)
Text(providerInfo.vehicleType.rawValue)
.fontWeight(.regular)
Text("\(providerInfo.proxies.count)")
Button() {
isListExpanded = !isListExpanded
} label: {
Image(systemName: isListExpanded ? "chevron.up" : "chevron.down")
}
Button() {
update()
} label: {
Image(systemName: "arrow.clockwise")
}
.disabled(isUpdating)
Button() {
startBenchmark()
} label: {
Image(systemName: "bolt.fill")
}
.disabled(isTesting)
}
HStack {
if trafficInfo != "" {
Text(trafficInfo)
.fontWeight(.regular)
}
if expireDate != "" {
Text(expireDate)
.fontWeight(.regular)
}
}
if updateAt != "" {
Text("Updated \(updateAt)")
.fontWeight(.regular)
}
}
}
var providerListView: some View {
LazyVGrid(columns: Array(repeating: GridItem(.flexible()),
count: columnCount)) {
ForEach($proxyItems, id: \.id) { item in
ProxyItemView(
proxy: item,
selectable: false
)
.background(.white)
.cornerRadius(8)
// .onTapGesture {
// let item = item.wrappedValue
// updateSelect(item.name)
// }
.show(isVisible: {
if searchString.string.isEmpty {
return true
} else {
return item.wrappedValue.name.lowercased().contains(searchString.string.lowercased())
}
}())
}
}
}
func listHeight(_ columnCount: Int) -> Double {
let lineCount = ceil(Double(providerInfo.proxies.count) / Double(columnCount))
return lineCount * 60 + (lineCount - 1) * 8
}
func startBenchmark() {
isTesting = true
let name = providerInfo.name
ApiRequest.healthCheck(proxy: name) {
ApiRequest.requestProxyProviderList {
isTesting = false
guard let provider = $0.allProviders[name] else { return }
self.proxyItems = provider.proxies.map(ProxyItemData.init)
}
}
}
func update() {
isUpdating = true
let name = providerInfo.name
ApiRequest.updateProvider(for: .proxy, name: name) { _ in
ApiRequest.requestProxyProviderList {
isUpdating = false
guard let provider = $0.allProviders[name] else { return }
self.providerInfo = provider
}
}
}
}
//struct ProviderGroupView_Previews: PreviewProvider {
// static var previews: some View {
// ProviderGroupView()
// }
//}