优化profile ui

This commit is contained in:
yanue 2025-06-19 23:09:25 +08:00
parent 035f2278fb
commit dadb70a85e
11 changed files with 372 additions and 148 deletions

View File

@ -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 */,

View File

@ -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()
}
}

View File

@ -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)")
}
}

View File

@ -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)

View File

@ -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"))
}

View File

@ -72,10 +72,6 @@ struct ConfigServerView: View {
}
}
}
.padding(20)
.border(Color.white, width: 1) // 2
Spacer()
}
}

View File

@ -27,10 +27,6 @@ struct ConfigTransportView: View {
}
}
}
.padding(20)
.border(Color.secondary, width: 1) // 2
Spacer()
}
}

View File

@ -49,10 +49,6 @@ struct ConfigStreamView: View {
}
}
}
.padding(20)
.border(Color.secondary, width: 1) // 2
Spacer()
}
}

View File

@ -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)

View 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()
}
}
}

View File

@ -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"))
}