From 7fb4694f238a48a1f15ab58bdf3b9f4f4c379659 Mon Sep 17 00:00:00 2001 From: mrFq1 <1xxbx0il0@mozmail.com> Date: Thu, 18 May 2023 14:39:36 +0800 Subject: [PATCH] feat: provider tab --- ClashX Dashboard.xcodeproj/project.pbxproj | 42 +++- .../Models/DBProviderStorage.swift | 92 ++++++++ .../Providers/ProviderProxiesView.swift | 158 +++++++++++++ .../Providers/ProviderRowView.swift | 51 +++++ .../ContentTabs/Providers/ProvidersView.swift | 68 ++++++ .../Providers/ProxyProviderInfoView.swift | 62 ++++++ .../Providers/ProxyProvidersRowView.swift | 82 +++++++ .../RuleProviderView.swift | 16 +- .../Providers/RuleProvidersRowView.swift | 75 +++++++ .../ContentTabs/Proxies/ProxyGroupView.swift | 1 - .../ContentTabs/Proxies/ProxyListView.swift | 19 -- .../Proxies/ProxyProviderGroupView.swift | 207 ------------------ .../Views/ContentTabs/Rules/RulesView.swift | 22 -- .../Views/SidebarView/SidebarView.swift | 4 + 14 files changed, 639 insertions(+), 260 deletions(-) create mode 100644 ClashX Dashboard/Models/DBProviderStorage.swift create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/ProviderProxiesView.swift create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/ProviderRowView.swift create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/ProvidersView.swift create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/ProxyProviderInfoView.swift create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/ProxyProvidersRowView.swift rename ClashX Dashboard/Views/ContentTabs/{Rules => Providers}/RuleProviderView.swift (64%) create mode 100644 ClashX Dashboard/Views/ContentTabs/Providers/RuleProvidersRowView.swift delete mode 100644 ClashX Dashboard/Views/ContentTabs/Proxies/ProxyListView.swift delete mode 100644 ClashX Dashboard/Views/ContentTabs/Proxies/ProxyProviderGroupView.swift diff --git a/ClashX Dashboard.xcodeproj/project.pbxproj b/ClashX Dashboard.xcodeproj/project.pbxproj index c1fa00a..8163a21 100644 --- a/ClashX Dashboard.xcodeproj/project.pbxproj +++ b/ClashX Dashboard.xcodeproj/project.pbxproj @@ -8,7 +8,10 @@ /* Begin PBXBuildFile section */ 010F693B29ED639A00BAAFB5 /* ClashServerAppStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 010F693A29ED639A00BAAFB5 /* ClashServerAppStorage.swift */; }; - 0140D8F029E6D3C800A515E8 /* ProxyProviderGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0140D8EF29E6D3C800A515E8 /* ProxyProviderGroupView.swift */; }; + 01505C4A2A147B84001ACC4F /* DBProviderStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01505C492A147B84001ACC4F /* DBProviderStorage.swift */; }; + 01505C4C2A14A1A3001ACC4F /* ProviderRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01505C4B2A14A1A3001ACC4F /* ProviderRowView.swift */; }; + 01505C4E2A14AAEB001ACC4F /* ProviderProxiesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01505C4D2A14AAEB001ACC4F /* ProviderProxiesView.swift */; }; + 015278082A15F9FD00516236 /* ProxyProviderInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 015278072A15F9FD00516236 /* ProxyProviderInfoView.swift */; }; 0155D39629F2342F00869830 /* TrafficGraphView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0155D39529F2342F00869830 /* TrafficGraphView.swift */; }; 0155D39829F23BDE00869830 /* OverviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0155D39729F23BDE00869830 /* OverviewView.swift */; }; 0172CB4F29E562960072DDEF /* ClearBackgroundList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0172CB4E29E562960072DDEF /* ClearBackgroundList.swift */; }; @@ -17,6 +20,7 @@ 017DCADD29E83BFD00B9622A /* RuleProviderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */; }; 017F9AAA2A0DFEBD00B81497 /* Introspect in Frameworks */ = {isa = PBXBuildFile; productRef = 017F9AA92A0DFEBD00B81497 /* Introspect */; }; 017F9AAC2A0E0B2300B81497 /* ProxyGroupRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 017F9AAB2A0E0B2300B81497 /* ProxyGroupRowView.swift */; }; + 018003B12A136DDB0070226E /* ProvidersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018003B02A136DDB0070226E /* ProvidersView.swift */; }; 018A61BD29E9A2ED008608C0 /* APISettingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018A61BC29E9A2ED008608C0 /* APISettingView.swift */; }; 018C836C29E17505006366D3 /* ClashApiDatasStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 018C836B29E17505006366D3 /* ClashApiDatasStorage.swift */; }; 0192315F29DD4DCF00539EDD /* ClashX_DashboardApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0192315E29DD4DCF00539EDD /* ClashX_DashboardApp.swift */; }; @@ -54,6 +58,8 @@ 01A351A929DD9CB00054894E /* Connections.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A351A829DD9CB00054894E /* Connections.swift */; }; 01A3EF042A120103003038B5 /* DBProxyStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01A3EF032A120103003038B5 /* DBProxyStorage.swift */; }; 01CD0A9229E93ABB00F4C17E /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = 01CD0A9129E93ABB00F4C17E /* DifferenceKit */; }; + 01DCEFB12A150E8B00DBBDB3 /* RuleProvidersRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DCEFB02A150E8B00DBBDB3 /* RuleProvidersRowView.swift */; }; + 01DCEFB32A150FB300DBBDB3 /* ProxyProvidersRowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01DCEFB22A150FB300DBBDB3 /* ProxyProvidersRowView.swift */; }; 01F885CF29DFD8DF008241EB /* CollectionsTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F885CE29DFD8DF008241EB /* CollectionsTableView.swift */; }; 01F885D129E03F20008241EB /* CollectionTableCellView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 01F885D029E03F20008241EB /* CollectionTableCellView.xib */; }; 01F885D329E04E21008241EB /* ProxyGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 01F885D229E04E21008241EB /* ProxyGroupView.swift */; }; @@ -62,7 +68,10 @@ /* Begin PBXFileReference section */ 010F693A29ED639A00BAAFB5 /* ClashServerAppStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashServerAppStorage.swift; sourceTree = ""; }; - 0140D8EF29E6D3C800A515E8 /* ProxyProviderGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyProviderGroupView.swift; sourceTree = ""; }; + 01505C492A147B84001ACC4F /* DBProviderStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBProviderStorage.swift; sourceTree = ""; }; + 01505C4B2A14A1A3001ACC4F /* ProviderRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderRowView.swift; sourceTree = ""; }; + 01505C4D2A14AAEB001ACC4F /* ProviderProxiesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProviderProxiesView.swift; sourceTree = ""; }; + 015278072A15F9FD00516236 /* ProxyProviderInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyProviderInfoView.swift; sourceTree = ""; }; 0155D39529F2342F00869830 /* TrafficGraphView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrafficGraphView.swift; sourceTree = ""; }; 0155D39729F23BDE00869830 /* OverviewView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OverviewView.swift; sourceTree = ""; }; 0172CB4E29E562960072DDEF /* ClearBackgroundList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearBackgroundList.swift; sourceTree = ""; }; @@ -70,6 +79,7 @@ 017753BF29EF7FB1006999DB /* APIServerItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APIServerItem.swift; sourceTree = ""; }; 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleProviderView.swift; sourceTree = ""; }; 017F9AAB2A0E0B2300B81497 /* ProxyGroupRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupRowView.swift; sourceTree = ""; }; + 018003B02A136DDB0070226E /* ProvidersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProvidersView.swift; sourceTree = ""; }; 018A61BC29E9A2ED008608C0 /* APISettingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APISettingView.swift; sourceTree = ""; }; 018C836B29E17505006366D3 /* ClashApiDatasStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClashApiDatasStorage.swift; sourceTree = ""; }; 0192315B29DD4DCF00539EDD /* ClashX Dashboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "ClashX Dashboard.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -103,6 +113,8 @@ 01A351A129DD8F440054894E /* RuleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleItemView.swift; sourceTree = ""; }; 01A351A829DD9CB00054894E /* Connections.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Connections.swift; sourceTree = ""; }; 01A3EF032A120103003038B5 /* DBProxyStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DBProxyStorage.swift; sourceTree = ""; }; + 01DCEFB02A150E8B00DBBDB3 /* RuleProvidersRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleProvidersRowView.swift; sourceTree = ""; }; + 01DCEFB22A150FB300DBBDB3 /* ProxyProvidersRowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyProvidersRowView.swift; sourceTree = ""; }; 01F885CE29DFD8DF008241EB /* CollectionsTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionsTableView.swift; sourceTree = ""; }; 01F885D029E03F20008241EB /* CollectionTableCellView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = CollectionTableCellView.xib; sourceTree = ""; }; 01F885D229E04E21008241EB /* ProxyGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProxyGroupView.swift; sourceTree = ""; }; @@ -137,6 +149,20 @@ path = APISetting; sourceTree = ""; }; + 018003AF2A136D3E0070226E /* Providers */ = { + isa = PBXGroup; + children = ( + 018003B02A136DDB0070226E /* ProvidersView.swift */, + 01DCEFB22A150FB300DBBDB3 /* ProxyProvidersRowView.swift */, + 01DCEFB02A150E8B00DBBDB3 /* RuleProvidersRowView.swift */, + 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */, + 01505C4B2A14A1A3001ACC4F /* ProviderRowView.swift */, + 01505C4D2A14AAEB001ACC4F /* ProviderProxiesView.swift */, + 015278072A15F9FD00516236 /* ProxyProviderInfoView.swift */, + ); + path = Providers; + sourceTree = ""; + }; 018C836929E1703D006366D3 /* Logs */ = { isa = PBXGroup; children = ( @@ -203,6 +229,7 @@ children = ( 01A351A529DD9B2D0054894E /* Overview */, 01A351A629DD9BB50054894E /* Proxies */, + 018003AF2A136D3E0070226E /* Providers */, 01A351A029DD8F210054894E /* Rules */, 01A351A729DD9C7F0054894E /* Connections */, 01A7335F29E2CBD600205699 /* Config */, @@ -279,7 +306,6 @@ isa = PBXGroup; children = ( 0192317929DD5DB000539EDD /* RulesView.swift */, - 017DCADC29E83BFD00B9622A /* RuleProviderView.swift */, 01A351A129DD8F440054894E /* RuleItemView.swift */, ); path = Rules; @@ -301,7 +327,6 @@ 0192317729DD5DA500539EDD /* ProxiesView.swift */, 017F9AAB2A0E0B2300B81497 /* ProxyGroupRowView.swift */, 01F885D229E04E21008241EB /* ProxyGroupView.swift */, - 0140D8EF29E6D3C800A515E8 /* ProxyProviderGroupView.swift */, 01F885D429E053DE008241EB /* ProxyItemView.swift */, ); path = Proxies; @@ -322,6 +347,7 @@ isa = PBXGroup; children = ( 01A3EF032A120103003038B5 /* DBProxyStorage.swift */, + 01505C492A147B84001ACC4F /* DBProviderStorage.swift */, ); path = Models; sourceTree = ""; @@ -425,15 +451,17 @@ files = ( 0192317A29DD5DB000539EDD /* RulesView.swift in Sources */, 019D6A8729F015DF00A6AC02 /* ArrayExtensions.swift in Sources */, + 01505C4C2A14A1A3001ACC4F /* ProviderRowView.swift in Sources */, 0192316129DD4DCF00539EDD /* ContentView.swift in Sources */, + 01DCEFB32A150FB300DBBDB3 /* ProxyProvidersRowView.swift in Sources */, 0192318729DD83FF00539EDD /* OverviewTopItemView.swift in Sources */, 0172CB4F29E562960072DDEF /* ClearBackgroundList.swift in Sources */, 0192318029DD5E0B00539EDD /* LogsView.swift in Sources */, + 01505C4E2A14AAEB001ACC4F /* ProviderProxiesView.swift in Sources */, 0192318529DD7DCD00539EDD /* SidebarItemView.swift in Sources */, 0192317E29DD5E0100539EDD /* ConfigView.swift in Sources */, 0155D39629F2342F00869830 /* TrafficGraphView.swift in Sources */, 01A3EF042A120103003038B5 /* DBProxyStorage.swift in Sources */, - 0140D8F029E6D3C800A515E8 /* ProxyProviderGroupView.swift in Sources */, 017F9AAC2A0E0B2300B81497 /* ProxyGroupRowView.swift in Sources */, 017DCADD29E83BFD00B9622A /* RuleProviderView.swift in Sources */, 0192318329DD70B400539EDD /* SidebarItem.swift in Sources */, @@ -446,20 +474,24 @@ 010F693B29ED639A00BAAFB5 /* ClashServerAppStorage.swift in Sources */, 01F885D329E04E21008241EB /* ProxyGroupView.swift in Sources */, 0192315F29DD4DCF00539EDD /* ClashX_DashboardApp.swift in Sources */, + 018003B12A136DDB0070226E /* ProvidersView.swift in Sources */, 0192B5CD29DE5151002CDBF3 /* ClashProxy.swift in Sources */, 0192B61129DE5292002CDBF3 /* Array+Safe.swift in Sources */, 01A351A229DD8F440054894E /* RuleItemView.swift in Sources */, 01F885D529E053DE008241EB /* ProxyItemView.swift in Sources */, 0192B5D029DE5151002CDBF3 /* ClashRuleProvider.swift in Sources */, + 01DCEFB12A150E8B00DBBDB3 /* RuleProvidersRowView.swift in Sources */, 0192317129DD566000539EDD /* SidebarView.swift in Sources */, 0192B5D129DE5151002CDBF3 /* ClashRule.swift in Sources */, 0192317C29DD5DF200539EDD /* ConnectionsView.swift in Sources */, 017753C029EF7FB2006999DB /* APIServerItem.swift in Sources */, + 015278082A15F9FD00516236 /* ProxyProviderInfoView.swift in Sources */, 0192B61429DE5292002CDBF3 /* String+Encode.swift in Sources */, 0192317829DD5DA500539EDD /* ProxiesView.swift in Sources */, 0192B5F629DE5262002CDBF3 /* ConfigManager.swift in Sources */, 0192B61A29DE5292002CDBF3 /* DateFormatter+.swift in Sources */, 01F885CF29DFD8DF008241EB /* CollectionsTableView.swift in Sources */, + 01505C4A2A147B84001ACC4F /* DBProviderStorage.swift in Sources */, 0192B5CA29DE5151002CDBF3 /* ClashConfig.swift in Sources */, 0172CB5129E5AE670072DDEF /* SwiftUIViewExtensions.swift in Sources */, 01A351A929DD9CB00054894E /* Connections.swift in Sources */, diff --git a/ClashX Dashboard/Models/DBProviderStorage.swift b/ClashX Dashboard/Models/DBProviderStorage.swift new file mode 100644 index 0000000..8cb2fa2 --- /dev/null +++ b/ClashX Dashboard/Models/DBProviderStorage.swift @@ -0,0 +1,92 @@ +// +// DBProviderStorage.swift +// ClashX Dashboard +// +// + +import Cocoa +import SwiftUI + +class DBProviderStorage: ObservableObject { + @Published var proxyProviders = [DBProxyProvider]() + @Published var ruleProviders = [DBRuleProvider]() + + init() {} + +} + +class DBProxyProvider: ObservableObject, Identifiable { + let id: String + + @Published var name: ClashProviderName + @Published var proxies: [DBProxy] + @Published var type: ClashProvider.ProviderType + @Published var vehicleType: ClashProvider.ProviderVehicleType + + @Published var trafficInfo: String + @Published var trafficPercentage: String + @Published var expireDate: String + @Published var updatedAt: String + + init(provider: ClashProvider) { + id = provider.id + + name = provider.name + proxies = provider.proxies.map(DBProxy.init) + type = provider.type + vehicleType = provider.vehicleType + + if let info = provider.subscriptionInfo { + let used = info.download + info.upload + let total = info.total + + let trafficRate = "\(String(format: "%.2f", Double(used)/Double(total/100)))%" + + let formatter = ByteCountFormatter() + + trafficInfo = formatter.string(fromByteCount: used) + + " / " + + formatter.string(fromByteCount: total) + + " ( \(trafficRate) )" + + let expire = info.expire + + expireDate = "Expire: " + + Date(timeIntervalSince1970: TimeInterval(expire)) + .formatted() + self.trafficPercentage = trafficRate + } else { + trafficInfo = "" + expireDate = "" + trafficPercentage = "0.0%" + } + + if let updatedAt = provider.updatedAt { + let formatter = RelativeDateTimeFormatter() + formatter.unitsStyle = .abbreviated + self.updatedAt = formatter.localizedString(for: updatedAt, relativeTo: .now) + } else { + self.updatedAt = "" + } + } +} + +class DBRuleProvider: ObservableObject, Identifiable { + let id: String + + @Published var name: ClashProviderName + @Published var ruleCount: Int + @Published var behavior: String + @Published var type: String + @Published var updatedAt: Date? + + init(provider: ClashRuleProvider) { + id = UUID().uuidString + + name = provider.name + ruleCount = provider.ruleCount + behavior = provider.behavior + type = provider.type + updatedAt = provider.updatedAt + } +} diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/ProviderProxiesView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/ProviderProxiesView.swift new file mode 100644 index 0000000..be19c55 --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/ProviderProxiesView.swift @@ -0,0 +1,158 @@ +// +// ProviderProxiesView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct ProviderProxiesView: View { + + @ObservedObject var provider: DBProxyProvider + @EnvironmentObject var hideProxyNames: HideProxyNames + @EnvironmentObject var searchString: ProxiesSearchString + + @State private var columnCount: Int = 3 + @State private var isTesting = false + @State private var isUpdating = false + + var body: some View { + ScrollView { + Section { + proxyListView + } header: { + HStack { + ProxyProviderInfoView(provider: provider) + buttonsView + } + } + .padding() + } + .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() + } + } + + func updateColumnCount(_ width: Double) { + let v = Int(Int(width) / 180) + let new = v == 0 ? 1 : v + + if new != columnCount { + columnCount = new + } + } + + var proxyListView: some View { + LazyVGrid(columns: Array(repeating: GridItem(.flexible()), + count: columnCount)) { + ForEach($provider.proxies, id: \.id) { proxy in + ProxyItemView( + proxy: proxy, + selectable: false + ) + .background(.white) + .cornerRadius(8) + + .show(isVisible: { + if searchString.string.isEmpty { + return true + } else { + return proxy.wrappedValue.name.lowercased().contains(searchString.string.lowercased()) + } + }()) + } + } + } + + + + var buttonsView: some View { + VStack { + Button() { + startHealthCheck() + } label: { + HStack { + if isTesting { + ProgressView() + .controlSize(.small) + .frame(width: 12) + } else { + Image(systemName: "bolt.fill") + .frame(width: 12) + } + Text(isTesting ? "Testing" : "Health Check") + .frame(width: 90) + } + .foregroundColor(isTesting ? .gray : .blue) + } + .disabled(isTesting) + + Button() { + startUpdate() + } label: { + HStack { + if isUpdating { + ProgressView() + .controlSize(.small) + .frame(width: 12) + } else { + Image(systemName: "arrow.clockwise") + .frame(width: 12) + } + Text(isUpdating ? "Updating" : "Update") + .frame(width: 90) + } + .foregroundColor(isUpdating ? .gray : .blue) + } + .disabled(isTesting) + } + } + + func startHealthCheck() { + isTesting = true + ApiRequest.healthCheck(proxy: provider.name) { + updateProvider { + isTesting = false + } + } + } + + func startUpdate() { + isUpdating = true + ApiRequest.updateProvider(for: .proxy, name: provider.name) { _ in + updateProvider { + isUpdating = false + } + } + } + + func updateProvider(_ completeHandler: (() -> Void)? = nil) { + ApiRequest.requestProxyProviderList { resp in + if let p = resp.allProviders[provider.name] { + let new = DBProxyProvider(provider: p) + provider.proxies = new.proxies + provider.updatedAt = new.updatedAt + provider.expireDate = new.expireDate + provider.trafficInfo = new.trafficInfo + provider.trafficPercentage = new.trafficPercentage + } + completeHandler?() + } + } +} + +//struct ProviderProxiesView_Previews: PreviewProvider { +// static var previews: some View { +// ProviderProxiesView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/ProviderRowView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/ProviderRowView.swift new file mode 100644 index 0000000..1fe907c --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/ProviderRowView.swift @@ -0,0 +1,51 @@ +// +// ProviderRowView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct ProviderRowView: View { + + @ObservedObject var proxyProvider: DBProxyProvider + @EnvironmentObject var hideProxyNames: HideProxyNames + + var body: some View { + NavigationLink { + ProviderProxiesView(provider: proxyProvider) + } label: { + labelView + } + } + + var labelView: some View { + VStack(spacing: 2) { + HStack(alignment: .center) { + Text(hideProxyNames.hide + ? String(proxyProvider.id.prefix(8)) + : proxyProvider.name) + .font(.system(size: 15)) + Spacer() + Text(proxyProvider.trafficPercentage) + .font(.system(size: 12)) + .foregroundColor(.secondary) + } + + HStack { + Text(proxyProvider.vehicleType.rawValue) + Spacer() + Text(proxyProvider.updatedAt) + } + .font(.system(size: 11)) + .foregroundColor(.secondary) + } + .padding(EdgeInsets(top: 1, leading: 4, bottom: 1, trailing: 4)) + } +} + +//struct ProviderRowView_Previews: PreviewProvider { +// static var previews: some View { +// ProviderRowView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/ProvidersView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/ProvidersView.swift new file mode 100644 index 0000000..26bdb07 --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/ProvidersView.swift @@ -0,0 +1,68 @@ +// +// ProvidersView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct ProvidersView: View { + @ObservedObject var providerStorage = DBProviderStorage() + @EnvironmentObject var hideProxyNames: HideProxyNames + @State private var searchString = ProxiesSearchString() + + var body: some View { + + NavigationView { + List { + Section("Providers") { + ProxyProvidersRowView(providerStorage: providerStorage) + RuleProvidersRowView(providerStorage: providerStorage) + } + + Text("") + + Section("Proxy Provider") { + ForEach(providerStorage.proxyProviders,id: \.id) { + ProviderRowView(proxyProvider: $0) + } + } + } + .introspectTableView { + $0.refusesFirstResponder = true + $0.doubleAction = nil + } + .listStyle(.plain) + EmptyView() + } + .searchable(text: $searchString.string) + .environmentObject(searchString) + .onAppear { + loadProviders() + } + } + + func loadProviders() { + ApiRequest.requestProxyProviderList { resp in + providerStorage.proxyProviders = resp.allProviders.values.filter { + $0.vehicleType == .HTTP + }.sorted { + $0.name < $1.name + } + .map(DBProxyProvider.init) + } + ApiRequest.requestRuleProviderList { resp in + providerStorage.ruleProviders = resp.allProviders.values.sorted { + $0.name < $1.name + } + .map(DBRuleProvider.init) + } + } + +} + +//struct ProvidersView_Previews: PreviewProvider { +// static var previews: some View { +// ProvidersView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProviderInfoView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProviderInfoView.swift new file mode 100644 index 0000000..86463c2 --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProviderInfoView.swift @@ -0,0 +1,62 @@ +// +// ProxyProviderInfoView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct ProxyProviderInfoView: View { + + @ObservedObject var provider: DBProxyProvider + @EnvironmentObject var hideProxyNames: HideProxyNames + + var body: some View { + VStack { + header + content + + } + } + + var header: some View { + HStack() { + Text(hideProxyNames.hide + ? String(provider.id.prefix(8)) + : provider.name) + .font(.system(size: 17)) + Text(provider.vehicleType.rawValue) + .font(.system(size: 13)) + .foregroundColor(.secondary) + Text("\(provider.proxies.count)") + .font(.system(size: 11)) + .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) + .background(Color.gray.opacity(0.5)) + .cornerRadius(4) + + Spacer() + } + } + + var content: some View { + VStack { + HStack(spacing: 20) { + Text(provider.trafficInfo) + Text(provider.expireDate) + Spacer() + } + HStack { + Text("Updated \(provider.updatedAt)") + Spacer() + } + } + .font(.system(size: 12)) + .foregroundColor(.secondary) + } +} + +//struct ProxyProviderInfoView_Previews: PreviewProvider { +// static var previews: some View { +// ProxyProviderInfoView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProvidersRowView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProvidersRowView.swift new file mode 100644 index 0000000..f7d65bd --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/ProxyProvidersRowView.swift @@ -0,0 +1,82 @@ +// +// ProxyProvidersRowView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct ProxyProvidersRowView: View { + + @ObservedObject var providerStorage: DBProviderStorage + + @State private var isUpdating = false + + var body: some View { + NavigationLink { + contentView + } label: { + Text("Proxy") + .font(.system(size: 15)) + .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) + } + } + + var contentView: some View { + ScrollView { + Section { + VStack(spacing: 16) { + listView + } + } header: { + Button { + updateAll() + } label: { + HStack { + if isUpdating { + ProgressView() + .controlSize(.small) + .frame(width: 12) + } else { + Image(systemName: "arrow.clockwise") + .frame(width: 12) + } + Text(isUpdating ? "Updating" : "Update All") + .frame(width: 80) + } + .foregroundColor(isUpdating ? .gray : .blue) + } + .disabled(isUpdating) + } + .padding() + } + } + + var listView: some View { + ForEach(providerStorage.proxyProviders, id: \.id) { + ProxyProviderInfoView(provider: $0) + } + } + + func updateAll() { + isUpdating = true + + ApiRequest.updateAllProviders(for: .proxy) { _ in + ApiRequest.requestProxyProviderList { resp in + providerStorage.proxyProviders = resp.allProviders.values.filter { + $0.vehicleType == .HTTP + }.sorted { + $0.name < $1.name + } + .map(DBProxyProvider.init) + isUpdating = false + } + } + } +} + +//struct AllProvidersRowView_Previews: PreviewProvider { +// static var previews: some View { +// ProxyProvidersRowView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Rules/RuleProviderView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/RuleProviderView.swift similarity index 64% rename from ClashX Dashboard/Views/ContentTabs/Rules/RuleProviderView.swift rename to ClashX Dashboard/Views/ContentTabs/Providers/RuleProviderView.swift index 5afadba..d686234 100644 --- a/ClashX Dashboard/Views/ContentTabs/Rules/RuleProviderView.swift +++ b/ClashX Dashboard/Views/ContentTabs/Providers/RuleProviderView.swift @@ -8,25 +8,29 @@ import SwiftUI struct RuleProviderView: View { - @State var ruleProvider: ClashRuleProvider + @State var provider: DBRuleProvider var body: some View { VStack(alignment: .leading) { HStack { - Text(ruleProvider.name) + Text(provider.name) .font(.title) .fontWeight(.medium) - Text(ruleProvider.type) - Text(ruleProvider.behavior) + Text(provider.type) + Text(provider.behavior) + Spacer() } HStack { - Text("\(ruleProvider.ruleCount) rules") - if let date = ruleProvider.updatedAt { + Text("\(provider.ruleCount) rules") + if let date = provider.updatedAt { Text("Updated \(RelativeDateTimeFormatter().localizedString(for: date, relativeTo: .now))") } + Spacer() } + .font(.system(size: 12)) + .foregroundColor(.secondary) } } } diff --git a/ClashX Dashboard/Views/ContentTabs/Providers/RuleProvidersRowView.swift b/ClashX Dashboard/Views/ContentTabs/Providers/RuleProvidersRowView.swift new file mode 100644 index 0000000..515e43e --- /dev/null +++ b/ClashX Dashboard/Views/ContentTabs/Providers/RuleProvidersRowView.swift @@ -0,0 +1,75 @@ +// +// RuleProvidersRowView.swift +// ClashX Dashboard +// +// + +import SwiftUI + +struct RuleProvidersRowView: View { + + @ObservedObject var providerStorage: DBProviderStorage + + @State private var isUpdating = false + + var body: some View { + NavigationLink { + contentView + } label: { + Text("Rule") + .font(.system(size: 15)) + .padding(EdgeInsets(top: 2, leading: 4, bottom: 2, trailing: 4)) + } + } + + var contentView: some View { + ScrollView { + Section { + VStack(spacing: 12) { + ForEach(providerStorage.ruleProviders, id: \.id) { + RuleProviderView(provider: $0) + } + } + } header: { + Button { + updateAll() + } label: { + HStack { + if isUpdating { + ProgressView() + .controlSize(.small) + .frame(width: 12) + } else { + Image(systemName: "arrow.clockwise") + .frame(width: 12) + } + Text(isUpdating ? "Updating" : "Update All") + .frame(width: 80) + } + .foregroundColor(isUpdating ? .gray : .blue) + } + .disabled(isUpdating) + } + .padding() + } + } + + func updateAll() { + isUpdating = true + ApiRequest.updateAllProviders(for: .rule) { _ in + ApiRequest.requestRuleProviderList { resp in + providerStorage.ruleProviders = resp.allProviders.values.sorted { + $0.name < $1.name + } + .map(DBRuleProvider.init) + isUpdating = false + } + } + } +} + +//struct ProxyProvidersRowView_Previews: PreviewProvider { +// static var previews: some View { +// RuleProvidersRowView() +// } +//} diff --git a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyGroupView.swift b/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyGroupView.swift index 3a7ee5f..6b62d16 100644 --- a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyGroupView.swift +++ b/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyGroupView.swift @@ -27,7 +27,6 @@ struct ProxyGroupView: View { } .padding() } - .background { GeometryReader { geometry in Rectangle() diff --git a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyListView.swift b/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyListView.swift deleted file mode 100644 index d8760f7..0000000 --- a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyListView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// 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() - } -} diff --git a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyProviderGroupView.swift b/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyProviderGroupView.swift deleted file mode 100644 index 9f32a6d..0000000 --- a/ClashX Dashboard/Views/ContentTabs/Proxies/ProxyProviderGroupView.swift +++ /dev/null @@ -1,207 +0,0 @@ -// -// 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, -// providerInfo: Binding) { -// 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() -// } -//} diff --git a/ClashX Dashboard/Views/ContentTabs/Rules/RulesView.swift b/ClashX Dashboard/Views/ContentTabs/Rules/RulesView.swift index 9234b0b..c122ae0 100644 --- a/ClashX Dashboard/Views/ContentTabs/Rules/RulesView.swift +++ b/ClashX Dashboard/Views/ContentTabs/Rules/RulesView.swift @@ -8,21 +8,11 @@ import SwiftUI struct RulesView: View { - @State var ruleProviders = [ClashRuleProvider]() - @State var ruleItems = [ClashRule]() @State private var searchString: String = "" - var providers: [ClashRuleProvider] { - if searchString.isEmpty { - return ruleProviders - } else { - return ruleProviders.filtered(searchString, for: ["name", "behavior", "type"]) - } - } - var rules: [EnumeratedSequence<[ClashRule]>.Element] { if searchString.isEmpty { return Array(ruleItems.enumerated()) @@ -34,10 +24,6 @@ struct RulesView: View { var body: some View { List { - ForEach(providers, id: \.self) { - RuleProviderView(ruleProvider: $0) - } - ForEach(rules, id: \.element.id) { RuleItemView(index: $0.offset, rule: $0.element) } @@ -48,14 +34,6 @@ struct RulesView: View { ApiRequest.getRules { ruleItems = $0 } - - ApiRequest.requestRuleProviderList { - ruleProviders = $0.allProviders.map { - $0.value - }.sorted { - $0.name < $1.name - } - } } } } diff --git a/ClashX Dashboard/Views/SidebarView/SidebarView.swift b/ClashX Dashboard/Views/SidebarView/SidebarView.swift index ffc17d7..3f633db 100644 --- a/ClashX Dashboard/Views/SidebarView/SidebarView.swift +++ b/ClashX Dashboard/Views/SidebarView/SidebarView.swift @@ -24,6 +24,10 @@ struct SidebarView: View { icon: "globe.asia.australia", view: AnyView(ProxiesView())), + SidebarItem(name: "Providers", + icon: "link.icloud", + view: AnyView(ProvidersView())), + SidebarItem(name: "Rules", icon: "waveform.and.magnifyingglass", view: AnyView(RulesView())),