// // Home.swift // Home // // Created by Balaji on 12/09/21. // import SwiftUI import ApplicationLibrary import Libbox import Library import SwiftUI import Crisp struct HomeView: View { @Environment(\.scenePhase) private var scenePhase @EnvironmentObject private var environments: ExtensionEnvironments @State private var alert: Alert? @State private var selection = NavigationPage.dashboard @State private var importProfile: LibboxProfileContent? @State private var importRemoteProfile: LibboxImportRemoteProfile? @State private var isUpdating = false @State private var profileList: [ProfilePreview] = [] @State private var isUrlTesting = false @AppStorage("serverData") private var serverData: String = "" // 用于记录上次更新的时间 @AppStorage("lastFetchTime") private var lastFetchTime: Double = 0.0 // = Date(timeIntervalSince1970: 0).timeIntervalSince1970 @Binding var isLoggedIn: Bool @State private var isNodesLoading = true @StateObject private var commandClient = CommandClient(.groups) @State private var groups: [OutboundGroup] = [] @State private var groups_firsttime: [OutboundGroup] = [] @State var isConnected = false // Current Server... @State var servers: [nodereponseData] = [] @State var serverstixignfufeiTest: [nodereponseData] = [nodereponseData( type: "", name: "香港1🇭🇰高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "香港2🇭🇰高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "香港3🇭🇰高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "香港4🇭🇰高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "美国🇺🇸高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "日本🇯🇵高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "法国🇫🇷高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "韩国🇰🇷高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡1🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡2🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡3🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡4🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡5🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: ""),nodereponseData( type: "", name: "新加坡6🇸🇬高速节点", rate: "", id2: 1, isOnline: 1, cacheKey: "")] @State var currentServer: nodereponseData = nodereponseData( type: "", name: "", rate: "", id2:0, isOnline: 0 , cacheKey: "") @State var changeServer = false @State private var isUserViewActive = false @State private var isSubscriptionActive = false @State private var isInviteActive = false @State private var isKefuActive = false @State private var isErrorViewActive = false //@State private var currentNode = UserDefaults.standard.string(forKey: "selectedNode") ?? "自动选择" @AppStorage("selectedNode") private var currentNode = "自动选择" @AppStorage("goGrouptag") private var goGrouptag = "" @AppStorage("paymentURLKey") private var paymentURLKey = "" @State private var islogined = false @State private var loadingNodes = false @State private var urltestinggoGrouptag = "" @State private var isConfiging = false @AppStorage("baseDYURL") private var baseDYURL = "" @State private var xufeiNotify = false let adimages = ["11-53-53", "12-11-56", "12-14-11"] // 替换为你广告图片的名字 // 当前页的索引 @State private var currentIndex = 0 // 自动轮播计时器 let timer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() var body: some View { VStack{ HStack{ Button { withAnimation{ isUserViewActive.toggle() } } label: { Image(systemName: "circle.grid.cross") .font(.title2) .padding(5) .foregroundColor(.white) .background( RoundedRectangle(cornerRadius: 10) .strokeBorder(.white.opacity(0.25),lineWidth: 1) ) } Spacer() /* Button { withAnimation{ isSubscriptionActive.toggle() } } label: { Image(systemName: "slider.horizontal.3") .font(.title2) .padding(12) .background( RoundedRectangle(cornerRadius: 10) .strokeBorder(.white.opacity(0.25),lineWidth: 1) ) } */ if (paymentURLKey.count > 3){ // Text Bubble Button { withAnimation{ isSubscriptionActive.toggle() } } label: { Text("超值折扣") .font(.footnote) .foregroundColor(.white) .padding(.horizontal, 10) .padding(.vertical, 6) .background( RoundedRectangle(cornerRadius: 4) .fill(Color.orange) ) .overlay( TriangleShape() .fill(Color.orange) .frame(width: 10, height: 10) .offset(x: 10) , alignment: .trailing ) // Panda Image with VIP Tag HStack(spacing: 10){ Text(verbatim: "") Image("applogo") // Replace with actual image asset .resizable() .frame(width: 30, height: 30) .cornerRadius(15) // Text("VIP") // .font(.caption) // .fontWeight(.bold) // .foregroundColor(.yellow) // .padding(4) // .background(Color.orange) // .cornerRadius(4) } } .padding() } Button { withAnimation{ isKefuActive.toggle() } } label: { Image(systemName: "person.fill.questionmark") .font(.title2) .padding(5) .foregroundColor(.white) .background( RoundedRectangle(cornerRadius: 10) .strokeBorder(.white.opacity(0.25),lineWidth: 1) ) Text("客服").foregroundColor(.white) } } // .overlay( // // // Attributed Text... // Text(getTitle()) // ) // .foregroundColor(.white) // end navigationbar // DashboardView() // discount view // Image("11-53-53").resizable().scaledToFill() // .frame(width: UIScreen.main.bounds.width * 0.8,height: 150) // .cornerRadius(10) // 图片轮播 TabView(selection: $currentIndex) { ForEach(0.. 3){ withAnimation{ isSubscriptionActive.toggle() } } } if index == 1 { if (paymentURLKey.count > 3){ withAnimation{ isInviteActive.toggle() } } } } label: { Image(adimages[index]) .resizable() .scaledToFill() .frame(width: UIScreen.main.bounds.width * 0.8,height: 150) .cornerRadius(10) .tag(index) } } } .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) // 隐藏系统自带的指示器 .frame(height: 150) .onReceive(timer) { _ in // 自动切换广告 withAnimation { currentIndex = (currentIndex + 1) % adimages.count } } // 自定义指示器 HStack(spacing: 8) { ForEach(0.. \(islogined)") //await environments.reload() environments.postReload() Task { await doReload() await getConfigCache() // await doReloadSystemProxy() } } .fullScreenCover(isPresented: $isUserViewActive, content: { SideMenuView(isPresented: $isUserViewActive, isLoggedIn: $isLoggedIn) }).transition(.slide) .fullScreenCover(isPresented: $isSubscriptionActive, content: { SubscriptionView(isPresented: $isSubscriptionActive) }).transition(.slide) .fullScreenCover(isPresented: $isKefuActive, content: { SupportView() }) .onReceive(environments.openProfileGetSuccess, perform: { _ in Task{ await referGetLocalNodes() } }) .onReceive(environments.opentixingSubnodes, perform: { _ in // print("onReceive opentixingSubnodes") isSubscriptionActive = true }) .fullScreenCover(isPresented: $isInviteActive, content: { InviteListView(isPresented: $isInviteActive) }) .popover(isPresented: $isErrorViewActive, content: { QuestionView() }) .alertBinding($alert) .onChangeCompat(of: scenePhase) { newValue in if newValue == .active { environments.postReload() } } .onChangeCompat(of: selection) { newValue in print("onChangeCompat: \(newValue)" ) } .onReceive(commandClient.$groups, perform: { groups in if let groups { print("节点刷新成功 groups: \(String(describing: groups.count))") setGroups(groups) } }) .environment(\.selection, $selection) .environment(\.importProfile, $importProfile) .environment(\.importRemoteProfile, $importRemoteProfile) .handlesExternalEvents(preferring: [], allowing: ["*"]) } private func handleXufeiTixingButtonError(){ alert = Alert(title: Text("续费提醒"), message: Text("您的账户已经过期,请续费后继续体验畅快感受;如果您刚购买完请耐心等待,会员时长会在1分钟内到账。"),dismissButton:.default(Text("续费")) { withAnimation{ isSubscriptionActive.toggle() } }) } private func handleProfileButtonError(_ error: Error?) { if let err = error { alert = Alert(errorMessage: err.localizedDescription, { }) } else { Task { await environments.reload() } } } private var ConnectErrorView: some View{ VStack { // Spacer() HStack { Button { isErrorViewActive.toggle() } label: { Image(systemName: "exclamationmark.triangle.fill") .foregroundColor(.red) Text("无法连接? 查看解决方案 >>") .foregroundColor(.red) .font(.system(size: 16)) } } .padding() .frame(width: UIScreen.main.bounds.width * 0.8) // 占据屏幕宽度的 90% .background(Color.red.opacity(0.2)) .cornerRadius(20) //Spacer() } } private func installProfile() async { do { try await ExtensionProfile.install() environments.postReload() } catch { alert = Alert(error) } } private func setGroups(_ goGroups: [LibboxOutboundGroup]) { var groups = [OutboundGroup]() if let goGroup = goGroups.first { goGroup.isExpand = true var items = [OutboundGroupItem]() let itemIterator = goGroup.getItems()! while itemIterator.hasNext() { let goItem = itemIterator.next()! // OutboundGroupItem items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay))) } groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items)) urltestinggoGrouptag = goGroup.tag //不要给默认值 //UserDefaults.standard.setValue("\(goGroup.tag)", forKey: "goGrouptag") } /* 原有的全部显示逻辑 var groups = [OutboundGroup]() for goGroup in goGroups { print(goGroup.tag) //默认设置为开启 goGroup.isExpand = true var items = [OutboundGroupItem]() let itemIterator = goGroup.getItems()! while itemIterator.hasNext() { let goItem = itemIterator.next()! items.append(OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: Double(goItem.urlTestTime)), urlTestDelay: UInt16(goItem.urlTestDelay))) } groups.append(OutboundGroup(tag: goGroup.tag, type: goGroup.type, selected: goGroup.selected, selectable: goGroup.selectable, isExpand: goGroup.isExpand, items: items)) }*/ self.groups = groups isNodesLoading = false } private func referGetLocalNodes() async{ do { profileList = try await ProfileManager.list().map { ProfilePreview($0) } if let profilePrview = profileList.first { guard let profile = try await ProfileManager.get(profilePrview.id) else { throw NSError(domain: "Profile missing", code: 0) } let profileContent = try profile.read() // MARK: - Datum struct localDataum: Codable { let outbounds: [Outbound]? } struct Outbound: Codable { let outboundDefault: String? let outbounds: [String]? let tag, type: String let method, password, server: String? let serverPort: Int? enum CodingKeys: String, CodingKey { case outboundDefault = "default" case outbounds, tag, type, method, password, server case serverPort = "server_port" } } if let data = profileContent.data(using: String.Encoding.utf8), let reponsesubdata = try? JSONDecoder().decode(localDataum.self, from: data){ if let itemsOuts = reponsesubdata.outbounds { var groups = [OutboundGroup]() var items = [OutboundGroupItem]() var index = 0 var goItemindex = false var tagname = "" var tagtype = "" itemsOuts.forEach { goItem in if goItem.type == "urltest"{ goItemindex = true tagname = goItem.tag tagtype = goItem.type print(goItem.tag + " " + goItem.type) } if goItemindex { if let method = goItem.method, let server = goItem.server { // let numberThree: Int = 100 + Int(arc4random_uniform(400)) var sss = OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: 12), urlTestDelay: UInt16(0)) sss.server = server sss.method = method sss.password = goItem.password sss.server_port = goItem.serverPort items.append(sss) }else{ let sss = OutboundGroupItem(tag: goItem.tag, type: goItem.type, urlTestTime: Date(timeIntervalSince1970: 12), urlTestDelay: UInt16(0)) // sss.server = server // sss.method = method // sss.password = goItem.password // sss.server_port = goItem.serverPort items.append(sss) } } index = index + 1 } //groups.append(OutboundGroup(tag: "自动选择", type: "selector", selected: goGrouptag, selectable:true, isExpand: true, items: items)) groups.append(OutboundGroup(tag: tagname, type:tagtype, selected: goGrouptag, selectable:true, isExpand: true, items: items)) await MainActor.run { self.groups_firsttime = groups print("第一次加载 nodes 列表 \(groups_firsttime.count)") } } }else{ print("本地 JSON 节点解析失败") } // self.profile = profile // self.profileContent = profileContent } } catch { alert = Alert(error) return } } private func doReload() async { do { profileList = try await ProfileManager.list().map { ProfilePreview($0) } } catch { alert = Alert(error) return } // environments.emptyProfiles = profileList.isEmpty } public func reloadnotice() async { let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/notice/fetch")! var request = URLRequest(url: userInfoUrl) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization") let task = URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let _ = error { return } guard let data = data else { return } // MARK: - Welcome struct NoticeResponse: Codable { let data: [NoticeResponseDatum]? let total: Int? } // MARK: - Datum struct NoticeResponseDatum: Codable { let title, content: String let show: Int } // Parse the user info response if let reponsesubdata = try? JSONDecoder().decode(NoticeResponse.self, from: data), let total = reponsesubdata.total { if ( total >= 1 ){ if let _ = reponsesubdata.data?.first { // alert = Alert(title: data.title,okMessage: " \(data.content)") } } } } } task.resume() } public func reloadSubscribe() async { let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/getSubscribe")! var request = URLRequest(url: userInfoUrl) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization") withAnimation { islogined = true } let task = URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let err = error { print(err.localizedDescription) Task{ try await Task.sleep(nanoseconds:2_000_000_000) await reloadSubscribe() } return } guard let data = data else { return } // if let jsonString = String(data: data, encoding: .utf8) { // print("Response data: \(jsonString)") // } // Parse the user info response if let reponsesubdata = try? JSONDecoder().decode(SubscribeReponse.self, from: data), let subscript_json = reponsesubdata.data { // withAnimation { // islogined = true // } print("服务器返回最新的地址:"+subscript_json.subscribeURL + " \n本地AppStorage:" + UserManager.shared.getSuburlData()) var localProfile:ProfilePreview? if profileList.count > 1 { //直接删除全部节点 Task { for (_, profile) in profileList.enumerated() { do { _ = try await ProfileManager.delete(profile.origin) } catch { print(error) } } } }else{ if profileList.count == 1 { localProfile = profileList.first }else{ print("第一次准备存储subscribe 地址,首先检查是否付费") // >>> UserManager.shared.storeSuburlData(data: subscript_json.subscribeURL) print("服务器返回最新的地址:"+subscript_json.subscribeURL + " \n本地AppStorage:" + UserManager.shared.getSuburlData()) Task{ //通知更新 Profile environments.profileUpdate.send() } // Task{ // try await checkingProfileBackground(remoteURL: subscript_json.subscribeURL) // } } } if let local = localProfile , let remoteURL = local.remoteURL { print("存在本地目前订阅的URL: - > \(remoteURL) \n 服务器最新地址:\(subscript_json.subscribeURL)") // Task{ // try await checkingProfileBackground(remoteURL: subscript_json.subscribeURL) // } //>> UserManager.shared.storeSuburlData(data: subscript_json.subscribeURL) if remoteURL != subscript_json.subscribeURL{ //切换了用户,删掉之前的 profile 数据然后重新 下载 Task { do { _ = try await ProfileManager.delete(local.origin) } catch { print(error) } environments.profileUpdate.send() } print("Log: 切换了用户,清空本地订阅数据,然后重新更新订阅地址.") // commandClient.connect() }else{ //更新 profile print("Log: 用户不变,可以考虑每次更新订阅地址.") // commandClient.connect() Task { environments.openProfileGetSuccess.send() } /*Task { do { _ = try await local.origin.updateRemoteProfile() } catch { print(error) } }*/ } } }else{ print("LOG: 节点接口JSON解析失败,请注意. \(String(describing: error?.localizedDescription)) ") //DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1) { //Task{ // await reloadSubscribe() //} // } if UserManager.shared.getSuburlData().count > 5 { // withAnimation { // islogined = true // // } //commandClient.connect() } //alert = Alert(errorMessage: "访问失败") } } } task.resume() } //检查订阅地址是否付费nonisolated private func checkingProfileBackground( remoteURL: String) async throws { //let nextProfileID = try await ProfileManager.nextID() let remoteContent = try HTTPClient().getString(remoteURL) print("HomeView remoteContent : \(remoteContent.count) paymentURLKey:\( paymentURLKey)") if(remoteContent.count < 3){ await MainActor.run { //进入正常逻辑 if (UserManager.shared.paymentURL().count > 3){ xufeiNotify = true UserManager.shared.storeSuburlData(data:remoteURL) // environments.openProfileGetSuccess.send() environments.profileUpdate.send() }else{ //如果没有续费的情况下,判断是否是 Apple review xufeiNotify = false environments.profileUpdate.send() } } }else{ //存储到本地 DB UserManager.shared.storeSuburlData(data:remoteURL) await MainActor.run { environments.profileUpdate.send() } // await MainActor.run { // environments.openProfileGetSuccess.send() // } } } @MainActor public func reloadNodes() async { loadingNodes = true let userInfoUrl = URL(string: "\(UserManager.shared.baseURL())user/server/fetch")! var request = URLRequest(url: userInfoUrl) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(UserManager.shared.getAutoData(), forHTTPHeaderField: "Authorization") let task = URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { loadingNodes = false if let error = error { alert = Alert(error) return } guard let data = data else { return } // Parse the user info response if let nodes = try? JSONDecoder().decode(nodereponse.self, from: data) { if let sss = nodes.data{ servers = sss } } } } task.resume() } // MARK: - Networking Logic func getConfigCache() async { print("serverData: \(serverData)") print("lastFetchTime: \(lastFetchTime)") // let dictionary = UserDefaults.standard.dictionaryRepresentation() // dictionary.keys.forEach { key in // print("key: \(key) : \(String(describing: UserDefaults.standard.object(forKey: key)))") // } let datesss = Date(timeIntervalSince1970: lastFetchTime) let dayTimePeriodFormatter = DateFormatter() dayTimePeriodFormatter.dateFormat = "MMM dd YYYY HH:mm a" let dateString = dayTimePeriodFormatter.string(from: datesss) print( "上次getconfig请求的 时间: \(dateString)") let currentTime = Date() let timeIntervalSinceLastFetch = currentTime.timeIntervalSince1970//Double // 如果超过 10 分钟(600 秒),则从服务器获取最新数据 if (timeIntervalSinceLastFetch - lastFetchTime) > 600 && lastFetchTime > 10 { // print("超过 10 分钟(600 秒),则从服务器获取最新数据") await getConfig() }else{ if serverData.count > 10 && lastFetchTime > 10 { print("取getConfig 缓存data ") withAnimation { islogined = true } isConfiging = false Task { environments.openProfileGetSuccess.send() } await MainActor.run { print("crispTokenData:"+UserManager.shared.crispTokenData()) CrispSDK.configure(websiteID: UserManager.shared.crispTokenData()) } }else{ await getConfig() } } } func getConfig() async { isConfiging = true // 读取登录状态和用户信息 let isLoggedIn = UserManager.shared.isUserLoggedIn() let userInfo = UserManager.shared.getUserInfo() let autoData = UserManager.shared.getAutoData() print("Logged in: \(isLoggedIn)") print("本地信息Email: \(userInfo.email), avator: \(userInfo.avator)") print("本地信息Auto Data: \(autoData)") // Parse the login response and get the authorization token struct configReponse: Codable { let baseURL, mainregisterURL,paymentURL,crisptoken: String let telegramurl, kefuurl, websiteURL,baseDYURL: String let message: String let code: Int } let loginUrl = URL(string: UserManager.shared.configURL)! var request = URLRequest(url: loginUrl) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue(Bundle.main.bundleIdentifier ?? "", forHTTPHeaderField: "bid") request.addValue(UserManager.shared.appversion, forHTTPHeaderField: "appver") request.addValue(UIDevice.current.model, forHTTPHeaderField: "model") dump(request.allHTTPHeaderFields ?? [:] ) let task = URLSession.shared.dataTask(with: request) { data, response, error in DispatchQueue.main.async { if let _ = error { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { Task{ await getConfig() } } return } guard let data = data else { DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { Task{ await getConfig() } } return } if let jsonString = String(data: data, encoding: .utf8) { print("Response data: \(jsonString)") serverData = jsonString lastFetchTime = Date().timeIntervalSince1970 print("更新缓存信息") } else { print("Failed to convert data to string.") } if let jsonResponse = try? JSONDecoder().decode(configReponse.self, from: data){ if jsonResponse.code == 1 { //save data UserManager.shared.storebaseURLData(data: jsonResponse.baseURL) UserManager.shared.storemainregisterURLData(data: jsonResponse.mainregisterURL) UserManager.shared.storepaymentURLData(data: jsonResponse.paymentURL) UserManager.shared.storetelegramUrlData(data: jsonResponse.telegramurl) UserManager.shared.storekefuUrlData(data: jsonResponse.kefuurl) UserManager.shared.storewebsiteURLData(data: jsonResponse.websiteURL) UserManager.shared.storeCrispTokenData(data: jsonResponse.crisptoken) CrispSDK.configure(websiteID: jsonResponse.crisptoken) baseDYURL = jsonResponse.baseDYURL isConfiging = false //获取订阅信息 //after 2s DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 1.2) { Task{ await reloadSubscribe() //获取配置通知 //await reloadnotice() } } } }else{ DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) { Task{ await getConfig() } } } } } task.resume() } func from(_ data: Data) throws -> LibboxProfileContent { var error: NSError? let content = LibboxDecodeProfileContent(data, &error) if let error { throw error } return content! } // Bottom Sheet... @ViewBuilder func BottomSheet()->some View{ if islogined { VStack(spacing: 0){ // Current Server... HStack{ VStack(alignment: .leading, spacing: 6) { Text("节点选择") .font(.headline) //Text(currentServer.name).font(.subheadline) Text(currentNode).font(.subheadline) } Spacer(minLength: 10) if changeServer && groups.count>0{ Button { Task { isUrlTesting = true await doURLTest() DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 10) { isUrlTesting = false } } } label: { Image(systemName: "bolt.fill") if (isUrlTesting) { ProgressView() Text(" ").font(.callout) }else{ Text("测速").font(.callout) .fontWeight(.semibold) .foregroundColor(.white) } }.disableWithOpacity(isUrlTesting) } // Change Server Button... Button { if !changeServer { if groups.count > 0 { } Task{ try await Task.sleep(nanoseconds: 1_000_000_000) // commandClient.disconnect() commandClient.connect() } } withAnimation{ changeServer.toggle() if servers.isEmpty{ if changeServer { Task{ // await reloadNodes() } } } } } label: { Text(changeServer ? "收起" : "展开") .font(.callout) .fontWeight(.semibold) .frame(width: 110,height: 45) .background( RoundedRectangle(cornerRadius: 10) .strokeBorder(.white.opacity(0.25),lineWidth: 2) ) .foregroundColor(.white) } } .frame(height: 50) .padding(.horizontal) Divider() .padding(.top) ScrollView(.vertical, showsIndicators: false) { VStack { if loadingNodes { ProgressView() Text("节点数据加载中...") //网络加载节点 Spacer().frame(height: 20) } }.padding() // GroupListView() ScrollView { VStack { //groups首先查看是否第一次开启,直接本地读取列表 if groups.count > 0 { ForEach(groups, id: \.newhashValue) { it in GroupView(it) } }else{ ForEach(groups_firsttime, id: \.newhashValue) { it in GroupView(it) } } /*if (groups_firsttime.count == 0 && groups_firsttime.count == 0 ) { VStack { ProgressView() Spacer().frame(height: 20) Text("节点数据加载中...") //本地 json 加载节点 }.padding() }*/ }.padding() //.background(Color.gray.opacity(0.1)) }.padding().padding(.bottom,getSafeArea().bottom) if (groups_firsttime.count == 0 && groups_firsttime.count == 0 ) { fufeiNodes() } //GroupListView().padding().padding(.bottom,getSafeArea().bottom) } .opacity(changeServer ? 1 : 0) } .frame(maxWidth: .infinity) // Max Height... .frame(height: changeServer ? getRect().height/1.2 : getRect().height / 2.5,alignment: .top) .padding() .background( Color("BottomSheet") .clipShape(CustomCorners(radius: 35, corners: [.topLeft,.topRight])) ) // Safe Area wont show on previews... // showing only 50 pixels of height... .offset(y: changeServer ? 0 : (getRect().height / 2.5)-50) //(getRect().height / 2.5) - (20 + getSafeArea().bottom) }else{ EmptyView() } } func fufeiNodes()->some View{ VStack(alignment: .leading, spacing: 18) { // Filtered servers... // Not showing selected One... ForEach(serverstixignfufeiTest) { server in Button(action: { withAnimation { changeServer.toggle() //提醒付费 if (paymentURLKey.count > 3){ isSubscriptionActive.toggle() } } }, label: { VStack(spacing: 4) { HStack { VStack(alignment: .leading, spacing: 8) { HStack { // Image(server.name) // .resizable() // .aspectRatio(contentMode: .fit) // .frame(width: 20, height: 20) Text(server.name) .font(.subheadline) .fontWeight(.semibold).foregroundStyle(.white) } Label { Text(server.isOnline == 1 ? "在线可用" : "不可用") } icon: { Image(systemName: server.isOnline == 1 ? "checkmark" : "xmark") } .foregroundColor(server.isOnline == 1 ?.green : .red) .font(.caption2) } Spacer(minLength: 10) Image(systemName: "lock.fill").foregroundColor(.red) } .frame(height: 50) .padding(.horizontal) Divider() } }) } } .padding(.top, 25) .padding(.bottom, getSafeArea().bottom) } private nonisolated func doURLTest() async { print("doURLTest: \(await urltestinggoGrouptag) ") do { if await urltestinggoGrouptag.count > 0 { try await LibboxNewStandaloneCommandClient()!.urlTest(urltestinggoGrouptag) } } catch { print("doURLTest: \(error)") // await MainActor.run { // alert = Alert(error) // } } } @ViewBuilder func Background()->some View{ // VStack(alignment: .trailing, content: { // Spacer() // Text("Placeholder") // LottieView(animationFileName: "1d2a0fe5", loopMode: .loop) // .frame(width: 100 ,height: 100 ) // }) ZStack{ BackgroundBg() // Little Planet and little stars.... Image("mars") .resizable() .aspectRatio(contentMode: .fill) .frame(width: 30, height: 30) .scaleEffect(getRect().height < 750 ? 0.8 : 1) // not using offset... // using postiton.. // this will position the object using screen basis... .position(x: 50, y: getRect().height < 750 ? 200 : 220) .opacity(0.7) // Sample star points.... let stars: [CGPoint] = [ CGPoint(x: 15, y: 190), CGPoint(x: 25, y: 250), CGPoint(x: 20, y: 350), CGPoint(x: getRect().width - 30, y: 240), ] ForEach(stars,id: \.x){star in Circle() .fill(.white.opacity(0.3)) .frame(width: 5, height: 5) .position(star) .offset(y: getRect().height < 750 ? -20 : 0) } } .ignoresSafeArea() } private nonisolated func serviceReload() async throws { try LibboxNewStandaloneCommandClient()!.serviceReload() } private nonisolated func setSystemProxyEnabled(_ isEnabled: Bool) async { do { try LibboxNewStandaloneCommandClient()!.setSystemProxyEnabled(isEnabled) await SharedPreferences.systemProxyEnabled.set(isEnabled) } catch { await MainActor.run { alert = Alert(error) } } } func getTitle()->AttributedString{ var str = AttributedString("sss") if let range = str.range(of: "Lite"){ str[range].font = .system(size: 24, weight: .light) } if let range = str.range(of: "VPN"){ str[range].font = .system(size: 24, weight: .black) } return str } } //struct HomeView_Previews: PreviewProvider { // // static var previews: some View { // HomeView() // } //}/ Triangle shape for the small tail in the speech bubble