mirror of
https://github.com/yanue/V2rayU.git
synced 2025-12-28 09:02:09 +00:00
优化profile ui
This commit is contained in:
parent
035f2278fb
commit
dadb70a85e
@ -33,7 +33,6 @@
|
||||
662CC41A2D1BED9B006E8450 /* Database.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3D22D1BED9B006E8450 /* Database.swift */; };
|
||||
662CC41C2D1BED9B006E8450 /* ProfileModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3CA2D1BED9B006E8450 /* ProfileModel.swift */; };
|
||||
662CC41D2D1BED9B006E8450 /* Port.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3C32D1BED9B006E8450 /* Port.swift */; };
|
||||
662CC41E2D1BED9B006E8450 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3F12D1BED9B006E8450 /* ProfileView.swift */; };
|
||||
662CC4202D1BED9B006E8450 /* Util.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3C82D1BED9B006E8450 /* Util.swift */; };
|
||||
662CC4212D1BED9B006E8450 /* AppMenuView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3FC2D1BED9B006E8450 /* AppMenuView.swift */; };
|
||||
662CC4222D1BED9B006E8450 /* V2rayInbound.swift in Sources */ = {isa = PBXBuildFile; fileRef = 662CC3E42D1BED9B006E8450 /* V2rayInbound.swift */; };
|
||||
@ -69,6 +68,7 @@
|
||||
663814AF2E01938400F5FCF3 /* SubscriptionForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814AE2E01937700F5FCF3 /* SubscriptionForm.swift */; };
|
||||
663814B12E0195F700F5FCF3 /* SubscriptionSync.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814B02E0195ED00F5FCF3 /* SubscriptionSync.swift */; };
|
||||
663814B32E02E0FD00F5FCF3 /* RoutingForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814B22E02E0F600F5FCF3 /* RoutingForm.swift */; };
|
||||
663814B52E0448DB00F5FCF3 /* ProfilePing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 663814B42E0448DB00F5FCF3 /* ProfilePing.swift */; };
|
||||
66A078BC2D008A3700490469 /* SwiftyBeaver in Frameworks */ = {isa = PBXBuildFile; productRef = 66A078BB2D008A3700490469 /* SwiftyBeaver */; };
|
||||
66B1B5312D282DAB0032DD09 /* V2rayMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B1B5302D282D990032DD09 /* V2rayMetrics.swift */; };
|
||||
66B1B5332D2913000032DD09 /* ProfileStatModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66B1B5322D2913000032DD09 /* ProfileStatModel.swift */; };
|
||||
@ -172,7 +172,6 @@
|
||||
662CC3EC2D1BED9B006E8450 /* StreamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreamView.swift; sourceTree = "<group>"; };
|
||||
662CC3EF2D1BED9B006E8450 /* ProfileForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileForm.swift; sourceTree = "<group>"; };
|
||||
662CC3F02D1BED9B006E8450 /* ProfileShow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileShow.swift; sourceTree = "<group>"; };
|
||||
662CC3F12D1BED9B006E8450 /* ProfileView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
|
||||
662CC3F32D1BED9B006E8450 /* Advance.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Advance.swift; sourceTree = "<group>"; };
|
||||
662CC3F42D1BED9B006E8450 /* General.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = General.swift; sourceTree = "<group>"; };
|
||||
662CC3FC2D1BED9B006E8450 /* AppMenuView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppMenuView.swift; sourceTree = "<group>"; };
|
||||
@ -201,6 +200,7 @@
|
||||
663814AE2E01937700F5FCF3 /* SubscriptionForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionForm.swift; sourceTree = "<group>"; };
|
||||
663814B02E0195ED00F5FCF3 /* SubscriptionSync.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionSync.swift; sourceTree = "<group>"; };
|
||||
663814B22E02E0F600F5FCF3 /* RoutingForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoutingForm.swift; sourceTree = "<group>"; };
|
||||
663814B42E0448DB00F5FCF3 /* ProfilePing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePing.swift; sourceTree = "<group>"; };
|
||||
66B1B5302D282D990032DD09 /* V2rayMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = V2rayMetrics.swift; sourceTree = "<group>"; };
|
||||
66B1B5322D2913000032DD09 /* ProfileStatModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileStatModel.swift; sourceTree = "<group>"; };
|
||||
66B1B54C2D2979060032DD09 /* V2rayUTool */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = V2rayUTool; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
@ -390,7 +390,7 @@
|
||||
662CC3ED2D1BED9B006E8450 /* ProfileForm */,
|
||||
662CC3EF2D1BED9B006E8450 /* ProfileForm.swift */,
|
||||
662CC3F02D1BED9B006E8450 /* ProfileShow.swift */,
|
||||
662CC3F12D1BED9B006E8450 /* ProfileView.swift */,
|
||||
663814B42E0448DB00F5FCF3 /* ProfilePing.swift */,
|
||||
);
|
||||
path = Profile;
|
||||
sourceTree = "<group>";
|
||||
@ -747,6 +747,7 @@
|
||||
662CC4062D1BED9B006E8450 /* UserDefaults.swift in Sources */,
|
||||
663745372D2EC8840093A101 /* MenuSpeed.swift in Sources */,
|
||||
662CC4072D1BED9B006E8450 /* OutboundView.swift in Sources */,
|
||||
663814B52E0448DB00F5FCF3 /* ProfilePing.swift in Sources */,
|
||||
662CC4082D1BED9B006E8450 /* SecurityView.swift in Sources */,
|
||||
662CC40A2D1BED9B006E8450 /* V2rayLaunch.swift in Sources */,
|
||||
662CC40B2D1BED9B006E8450 /* StreamView.swift in Sources */,
|
||||
@ -766,7 +767,6 @@
|
||||
662CC41A2D1BED9B006E8450 /* Database.swift in Sources */,
|
||||
662CC41C2D1BED9B006E8450 /* ProfileModel.swift in Sources */,
|
||||
662CC41D2D1BED9B006E8450 /* Port.swift in Sources */,
|
||||
662CC41E2D1BED9B006E8450 /* ProfileView.swift in Sources */,
|
||||
662CC4202D1BED9B006E8450 /* Util.swift in Sources */,
|
||||
662CC4212D1BED9B006E8450 /* AppMenuView.swift in Sources */,
|
||||
662CC4222D1BED9B006E8450 /* V2rayInbound.swift in Sources */,
|
||||
|
||||
@ -8,6 +8,8 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
let NOTIFY_UPDATE_Ping = Notification.Name(rawValue: "NOTIFY_UPDATE_Ping")
|
||||
|
||||
actor PingAll {
|
||||
static var shared = PingAll()
|
||||
|
||||
@ -18,6 +20,7 @@ actor PingAll {
|
||||
func run() {
|
||||
guard !inPing else {
|
||||
NSLog("Ping is already running.")
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 已经在运行中")
|
||||
return
|
||||
}
|
||||
inPing = true
|
||||
@ -27,13 +30,21 @@ actor PingAll {
|
||||
let items = ProfileViewModel.all()
|
||||
guard !items.isEmpty else {
|
||||
NSLog("No items to ping.")
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "没有可 Ping 的节点")
|
||||
return
|
||||
}
|
||||
|
||||
NSLog("Ping started.")
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "开始 Ping 所有节点")
|
||||
// 开始执行异步任务
|
||||
self.pingTaskGroup(items: items)
|
||||
}
|
||||
|
||||
func pingOne(item: ProfileModel) {
|
||||
// 开始执行异步任务
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "开始 Ping 节点")
|
||||
self.pingTaskGroup(items: [item])
|
||||
}
|
||||
|
||||
private func pingTaskGroup(items: [ProfileModel]) {
|
||||
// 使用 Combine 处理多个异步任务
|
||||
@ -41,9 +52,12 @@ actor PingAll {
|
||||
Future<Void, Error> { promise in
|
||||
Task {
|
||||
do {
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "开始 Ping: \(item.remark)")
|
||||
try await self.pingEachServer(item: item)
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "完成 Ping: \(item.remark)")
|
||||
promise(.success(()))
|
||||
} catch {
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 失败: \(item.remark) - \(error.localizedDescription)")
|
||||
promise(.failure(error))
|
||||
}
|
||||
}
|
||||
@ -53,8 +67,10 @@ actor PingAll {
|
||||
.sink(receiveCompletion: { completion in
|
||||
switch completion {
|
||||
case .finished:
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "所有节点 Ping 完成\n")
|
||||
NSLog("Ping completed")
|
||||
case let .failure(error):
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 批量任务异常: \(error.localizedDescription)")
|
||||
NSLog("Error: \(error)")
|
||||
}
|
||||
self.inPing = false
|
||||
@ -106,6 +122,7 @@ actor PingServer {
|
||||
let ping = Ping()
|
||||
let pingTime = try await ping.doPing(bindPort: self.bindPort)
|
||||
print("Ping success, time: \(pingTime)ms")
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 成功: \(item.remark) - \(pingTime)ms")
|
||||
// 更新 speed
|
||||
ProfileViewModel.update_speed(uuid: self.item.uuid, speed: pingTime)
|
||||
}
|
||||
@ -120,6 +137,7 @@ actor PingServer {
|
||||
}
|
||||
try FileManager.default.removeItem(at: URL(fileURLWithPath: jsonFile))
|
||||
} catch {
|
||||
// 捕获错误并打印
|
||||
NSLog("remove ping config error: \(error)")
|
||||
}
|
||||
}
|
||||
@ -155,4 +173,3 @@ actor PingServer {
|
||||
self.process.launch()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,6 +23,7 @@ actor PingRunning {
|
||||
print("Ping task is already running.")
|
||||
return
|
||||
}
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "开始单节点 Ping: \(item.remark)")
|
||||
// 睡眠
|
||||
try await Task.sleep(nanoseconds: 2 * 1_000_000_000) // Wait for 2 seconds
|
||||
// 替换
|
||||
@ -39,16 +40,20 @@ actor PingRunning {
|
||||
do {
|
||||
let pingTime = try await ping.doPing(bindPort: port)
|
||||
print("Ping success, time: \(pingTime)ms")
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 成功: \(item.remark) - \(pingTime)ms")
|
||||
resetFailureCount()
|
||||
success = true
|
||||
} catch {
|
||||
retries += 1
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "Ping 失败: \(item.remark) - 第\(retries)次: \(error.localizedDescription)")
|
||||
print("Ping failed (\(retries)/\(maxRetries)): \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
if !success {
|
||||
await handleFailure()
|
||||
} else {
|
||||
NotificationCenter.default.post(name: NOTIFY_UPDATE_Ping, object: "完成单节点 Ping: \(item.remark)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -33,7 +33,7 @@ struct ContentView: View {
|
||||
.padding(.vertical,20)
|
||||
|
||||
SidebarButton(tab: .activity, title: "Activity", icon: "camera.filters", selectedTab: $selectedTab)
|
||||
SidebarButton(tab: .server, title: "proxies", icon: "network.badge.shield.half.filled", selectedTab: $selectedTab)
|
||||
SidebarButton(tab: .server, title: "Proxies", icon: "shield.lefthalf.filled", selectedTab: $selectedTab)
|
||||
SidebarButton(tab: .subscription, title: "Subscription", icon: "personalhotspot", selectedTab: $selectedTab)
|
||||
SidebarButton(tab: .routing, title: "Routing", icon: "bonjour", selectedTab: $selectedTab)
|
||||
SidebarButton(tab: .setting, title: "Settings", icon: "gear", selectedTab: $selectedTab)
|
||||
|
||||
@ -8,17 +8,61 @@ import SwiftUI
|
||||
|
||||
struct ConfigFormView: View {
|
||||
@ObservedObject var item: ProfileModel
|
||||
@StateObject private var viewModel = ProfileViewModel()
|
||||
|
||||
var onClose: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack{
|
||||
ConfigServerView(item: item)
|
||||
ConfigStreamView(item: item)
|
||||
ConfigTransportView(item: item)
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .center, spacing: 12) {
|
||||
Image(systemName: "personalhotspot")
|
||||
.resizable()
|
||||
.frame(width: 32, height: 32)
|
||||
.foregroundColor(.accentColor)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Subscription Settings")
|
||||
.font(.headline)
|
||||
Text("Edit your Profile information")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
.padding(.vertical, 18)
|
||||
.padding(.leading, 24)
|
||||
Divider()
|
||||
HStack {
|
||||
VStack{
|
||||
ConfigServerView(item: item)
|
||||
ConfigStreamView(item: item)
|
||||
ConfigTransportView(item: item)
|
||||
}
|
||||
.frame(width: 400) // 左
|
||||
|
||||
Divider().frame(width: 0) // 分隔线,适当调整宽度
|
||||
|
||||
ConfigShowView(item: item) // 右
|
||||
}.padding()
|
||||
Spacer()
|
||||
Divider()
|
||||
HStack {
|
||||
Spacer()
|
||||
Button("Cancel") {
|
||||
onClose()
|
||||
}
|
||||
Button("Save") {
|
||||
viewModel.upsert(item: item)
|
||||
onClose()
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.padding(.horizontal, 24)
|
||||
}
|
||||
.frame(width: 660)
|
||||
.onAppear {
|
||||
print("ConfigView appeared with item: \(item.id)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#Preview {
|
||||
ConfigFormView(item: ProfileModel(remark: "test01", protocol: .trojan, address: "dss", port: 443, password: "aaa", encryption: "auto"))
|
||||
}
|
||||
|
||||
@ -72,10 +72,6 @@ struct ConfigServerView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.border(Color.white, width: 1) // 黑色边框,宽度为 2
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -27,10 +27,6 @@ struct ConfigTransportView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.border(Color.secondary, width: 1) // 黑色边框,宽度为 2
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -49,10 +49,6 @@ struct ConfigStreamView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(20)
|
||||
.border(Color.secondary, width: 1) // 黑色边框,宽度为 2
|
||||
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -13,9 +13,12 @@ struct ProfileListView: View {
|
||||
@State private var sortOrder: [KeyPathComparator<ProfileModel>] = []
|
||||
@State private var selection: Set<ProfileModel.ID> = []
|
||||
@State private var selectedRow: ProfileModel? = nil
|
||||
@State private var pingRow: ProfileModel? = nil
|
||||
@State private var selectGroup: String = ""
|
||||
@State private var searchText = ""
|
||||
@State private var draggedRow: ProfileModel?
|
||||
@State private var selectAll: Bool = false
|
||||
@State private var showPingSheet: Bool = false
|
||||
|
||||
var filteredAndSortedItems: [ProfileModel] {
|
||||
let filtered = viewModel.list.filter { item in
|
||||
@ -31,109 +34,151 @@ struct ProfileListView: View {
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text("Prixies")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
|
||||
Image(systemName: "shield.lefthalf.filled")
|
||||
.resizable()
|
||||
.frame(width: 28, height: 28)
|
||||
.foregroundColor(.accentColor)
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text("Proxies")
|
||||
.font(.title)
|
||||
.fontWeight(.bold)
|
||||
Text("Manage your proxy list")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Picker("选择组", selection: $selectGroup) {
|
||||
// ForEach(viewModel.groups) { group in // 使用 groups 数组并遍历
|
||||
// Text(group).tag(group) // 使用 .tag 来绑定选中的项
|
||||
// }
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle()) // 可根据需要选择不同的 Picker 样式
|
||||
.padding()
|
||||
|
||||
Text("搜索")
|
||||
TextField("Search by Address or Remark", text: $searchText)
|
||||
.textFieldStyle(RoundedBorderTextFieldStyle())
|
||||
|
||||
Button("刷新") {
|
||||
loadData()
|
||||
}
|
||||
Button("Ping") {
|
||||
withAnimation {
|
||||
Text("全部分组").tag("")
|
||||
ForEach(viewModel.groups, id: \.self) { group in
|
||||
Text(group).tag(group)
|
||||
}
|
||||
}
|
||||
|
||||
Button("删除") {
|
||||
withAnimation {
|
||||
// 删数据
|
||||
for selectedID in self.selection {
|
||||
viewModel.delete(uuid: selectedID) // 使用找到的模型的 uuid 字段
|
||||
}
|
||||
// 移除选择
|
||||
selection.removeAll()
|
||||
}
|
||||
.pickerStyle(MenuPickerStyle())
|
||||
.frame(width: 140)
|
||||
HStack(spacing: 8) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.secondary)
|
||||
TextField("Search by Address or Remark", text: $searchText)
|
||||
.frame(width: 200)
|
||||
}
|
||||
.disabled(selection.isEmpty)
|
||||
|
||||
Button("新增") {
|
||||
withAnimation {
|
||||
let newProxy = ProfileModel(remark: "New Remark", protocol: .trojan, address: "newAddress", port: 443, password: UUID().uuidString, encryption: "auto")
|
||||
Button(action: { loadData() }) {
|
||||
Label("刷新", systemImage: "arrow.clockwise")
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 6)
|
||||
VStack {
|
||||
Spacer()
|
||||
HStack {
|
||||
// checkbox
|
||||
Toggle(isOn: $selectAll) {
|
||||
Text("全选")
|
||||
}
|
||||
Spacer()
|
||||
Button(action: { withAnimation {
|
||||
let newProxy = ProfileModel(remark: "", protocol: .trojan, address: "", port: 443, password: UUID().uuidString, encryption: "auto")
|
||||
self.selectedRow = newProxy
|
||||
}}) {
|
||||
Label("新增", systemImage: "plus")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Table(of: ProfileModel.self, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("#") { item in
|
||||
Text("\(item.index + 1)") // 显示 1-based 索引
|
||||
}
|
||||
.width(30)
|
||||
TableColumn("Type", value: \.protocol.rawValue)
|
||||
TableColumn("Remark") { row in
|
||||
// 双击事件
|
||||
Text(row.remark).onTapGesture(count: 2) {
|
||||
selectedRow = row
|
||||
}
|
||||
}
|
||||
TableColumn("Address", value: \.address)
|
||||
TableColumn("Port", value: \.port.description)
|
||||
TableColumn("Network", value: \.network.rawValue)
|
||||
TableColumn("TLS", value: \.security.rawValue)
|
||||
} rows: {
|
||||
ForEach(filteredAndSortedItems) { row in
|
||||
TableRow(row)
|
||||
// 启用拖拽功能
|
||||
.draggable(row)
|
||||
// 右键菜单
|
||||
.contextMenu {
|
||||
contextMenuProvider(item: row)
|
||||
.buttonStyle(.bordered)
|
||||
Button(action: {
|
||||
withAnimation {
|
||||
for selectedID in self.selection {
|
||||
viewModel.delete(uuid: selectedID)
|
||||
}
|
||||
selection.removeAll()
|
||||
}
|
||||
}) {
|
||||
Label("删除", systemImage: "trash")
|
||||
}
|
||||
.disabled(selection.isEmpty)
|
||||
.buttonStyle(.bordered)
|
||||
|
||||
Button(action: { showPingSheet = true }) {
|
||||
Label("PingAll", systemImage: "lasso.badge.sparkles")
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
}.padding(.horizontal, 10)
|
||||
// 表格主体
|
||||
Table(of: ProfileModel.self, selection: $selection, sortOrder: $sortOrder) {
|
||||
TableColumn("#") { item in
|
||||
Text("\(item.index + 1)")
|
||||
.font(.system(size: 12, weight: .bold, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
.onTapGesture(count: 2) { selectedRow = item }
|
||||
}
|
||||
.width(30)
|
||||
TableColumn("Type") { row in
|
||||
Text(row.`protocol` == .shadowsocks ? "ss" : row.`protocol`.rawValue)
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}
|
||||
.width(40)
|
||||
TableColumn("Remark") { row in
|
||||
Text(row.remark)
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}
|
||||
.width(150)
|
||||
TableColumn("Address") { row in
|
||||
Text(row.address)
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}
|
||||
.width(120)
|
||||
TableColumn("Port") { row in
|
||||
Text("\(row.port)")
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}
|
||||
.width(40)
|
||||
TableColumn("Network") { row in
|
||||
Text(row.network.rawValue)
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}.width(50)
|
||||
TableColumn("TLS") { row in
|
||||
Text(row.security.rawValue)
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}.width(40)
|
||||
TableColumn("latency(KB/s)") { row in
|
||||
Text(String(format: "%d", row.speed))
|
||||
.font(.system(size: 13))
|
||||
.onTapGesture(count: 2) { selectedRow = row }
|
||||
}.width(76)
|
||||
} rows: {
|
||||
ForEach(filteredAndSortedItems) { row in
|
||||
TableRow(row)
|
||||
.draggable(row)
|
||||
.contextMenu { contextMenuProvider(item: row) }
|
||||
}
|
||||
.dropDestination(for: ProfileModel.self, action: handleDrop)
|
||||
}
|
||||
// 处理拖动逻辑
|
||||
.dropDestination(for: ProfileModel.self, action: handleDrop)
|
||||
|
||||
}
|
||||
.background(.ultraThinMaterial)
|
||||
.border(Color.gray.opacity(0.1), width: 1)
|
||||
.cornerRadius(8)
|
||||
}
|
||||
.sheet(item: $selectedRow) { row in
|
||||
VStack {
|
||||
ConfigView(item: row)
|
||||
.padding()
|
||||
HStack{
|
||||
Spacer()
|
||||
HStack{
|
||||
Button("Cancel") {
|
||||
// 如果需要关闭 `sheet`,将 `selectedRow` 设置为 `nil`
|
||||
selectedRow = nil
|
||||
}
|
||||
Button("Save") {
|
||||
print("upsert, \(row)")
|
||||
viewModel.upsert(item: row)
|
||||
// 如果需要关闭 `sheet`,将 `selectedRow` 设置为 `nil`
|
||||
selectedRow = nil
|
||||
}
|
||||
.buttonStyle(.borderedProminent) // 蓝色主按钮
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.bottom, 20)
|
||||
}
|
||||
ConfigFormView(item: row) {
|
||||
selectedRow = nil
|
||||
loadData()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
loadData()
|
||||
.sheet(item: $pingRow) { row in
|
||||
ProfilePingView(profile: pingRow, isAll: false) {
|
||||
pingRow = nil
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showPingSheet) {
|
||||
ProfilePingView(profile: nil, isAll: true) {
|
||||
showPingSheet = false
|
||||
}
|
||||
}.task { loadData() }
|
||||
}
|
||||
|
||||
// 处理拖拽排序逻辑:
|
||||
@ -153,13 +198,10 @@ struct ProfileListView: View {
|
||||
Button("Edit") {
|
||||
self.selectedRow = item
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Button("Ping") {
|
||||
// Handle ping action
|
||||
self.pingRow = item
|
||||
}
|
||||
|
||||
Button("Delete") {
|
||||
// Handle another action
|
||||
print("item.uuid", item.id, item.uuid)
|
||||
|
||||
158
V2rayU/Views/Profile/ProfilePing.swift
Normal file
158
V2rayU/Views/Profile/ProfilePing.swift
Normal file
@ -0,0 +1,158 @@
|
||||
//
|
||||
// ConfigList.swift
|
||||
// V2rayU
|
||||
//
|
||||
// Created by yanue on 2024/11/30.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ProfilePingView: View {
|
||||
var profile: ProfileModel?
|
||||
var isAll: Bool
|
||||
var onClose: () -> Void
|
||||
@State private var logs: [String] = []
|
||||
@State private var isPinging: Bool = false
|
||||
@State private var scrollProxy: ScrollViewProxy? = nil
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack {
|
||||
Text(isAll ? "Ping All Proxies" : "Ping Proxy")
|
||||
.font(.title2).bold()
|
||||
Spacer()
|
||||
Button(isPinging ? "Pinging..." : "Ping Now") {
|
||||
isPinging = true
|
||||
logs.removeAll()
|
||||
if isAll {
|
||||
doPingAll()
|
||||
} else if let p = profile {
|
||||
doPingItem(item: p)
|
||||
}
|
||||
}
|
||||
.disabled(isPinging)
|
||||
.buttonStyle(.borderedProminent)
|
||||
|
||||
Button(action: onClose) {
|
||||
Text("Close")
|
||||
}
|
||||
}
|
||||
.padding([.top, .horizontal], 20)
|
||||
.padding(.bottom, 8)
|
||||
|
||||
Divider()
|
||||
|
||||
if let p = profile {
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
Text("Remark: \(p.remark)")
|
||||
.font(.subheadline)
|
||||
Text("Protocol: \(p.protocol.rawValue), Address: \(p.address):\(p.port)")
|
||||
.font(.footnote)
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
Divider().padding(.top, 8)
|
||||
} else if isAll {
|
||||
Text("This will ping all proxies and show the result log.")
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
Divider().padding(.top, 8)
|
||||
}
|
||||
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color(NSColor.textBackgroundColor))
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
if logs.isEmpty {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Image(systemName: "arrow.triangle.2.circlepath")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.gray.opacity(0.2))
|
||||
Spacer()
|
||||
}.padding(.top, 80)
|
||||
Spacer()
|
||||
} else {
|
||||
ForEach(logs.indices, id: \ .self) { idx in
|
||||
Text(logs[idx])
|
||||
.font(.system(size: 13, design: .monospaced))
|
||||
.foregroundColor(.primary)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.id(idx)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
}
|
||||
.onAppear { scrollProxy = proxy }
|
||||
.onChange(of: logs) { _ in
|
||||
if let last = logs.indices.last {
|
||||
DispatchQueue.main.async {
|
||||
withAnimation { proxy.scrollTo(last, anchor: .bottom) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 160, maxHeight: 220)
|
||||
}
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.stroke(Color.gray.opacity(0.15), lineWidth: 1)
|
||||
)
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.top, 8)
|
||||
|
||||
Spacer(minLength: 0)
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 10)
|
||||
}
|
||||
.frame(width: 560, height: 380)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 14)
|
||||
.fill(Color(NSColor.windowBackgroundColor))
|
||||
.shadow(color: Color.black.opacity(0.10), radius: 10, x: 0, y: 2)
|
||||
)
|
||||
.onAppear {
|
||||
logs.removeAll()
|
||||
subscribeNotification()
|
||||
}
|
||||
.onDisappear {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
}
|
||||
|
||||
func subscribeNotification() {
|
||||
NotificationCenter.default.addObserver(forName: NOTIFY_UPDATE_Ping, object: nil, queue: .main) { notif in
|
||||
if let msg = notif.object as? String {
|
||||
DispatchQueue.main.async {
|
||||
logs.append(msg)
|
||||
}
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
isPinging = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doPingItem(item: ProfileModel) {
|
||||
Task {
|
||||
await PingAll.shared.pingOne(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
func doPingAll() {
|
||||
Task {
|
||||
await PingAll.shared.run()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,30 +0,0 @@
|
||||
//
|
||||
// Config.swift
|
||||
// V2rayU
|
||||
//
|
||||
// Created by yanue on 2024/11/23.
|
||||
//
|
||||
import SwiftUI
|
||||
|
||||
struct ConfigView: View {
|
||||
@ObservedObject var item: ProfileModel
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
ConfigFormView(item: item).frame(width: 400) // 左
|
||||
|
||||
Divider().frame(width: 0) // 分隔线,适当调整宽度
|
||||
|
||||
ConfigShowView(item: item) // 右
|
||||
|
||||
}.padding()
|
||||
.frame(width: 660)
|
||||
.onAppear {
|
||||
print("ConfigView appeared with item: \(item.id)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ConfigView(item: ProfileModel(remark: "test01", protocol: .trojan, address: "dss", port: 443, password: "aaa", encryption: "auto"))
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user