diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/ApplicationLibrary.swift b/iOS-SwiftUI-Code/ApplicationLibrary/ApplicationLibrary.swift new file mode 100644 index 0000000..b8246da --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/ApplicationLibrary.swift @@ -0,0 +1,6 @@ +import Foundation + +public class ApplicationLibrary { + public static let bundle = Bundle(for: ApplicationLibrary.self) + public static let inPreview = false +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/Contents.json b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json new file mode 100644 index 0000000..51af668 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/Contents.json @@ -0,0 +1,12 @@ +{ + "info": { + "author": "xcode", + "version": 1 + }, + "symbols": [ + { + "filename": "save-save_symbol.svg", + "idiom": "universal" + } + ] +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg new file mode 100644 index 0000000..2b67790 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Assets.xcassets/save.symbolset/save-save_symbol.svg @@ -0,0 +1,127 @@ + + + + + + Weight/Scale Variations + Ultralight + Thin + Light + Regular + Medium + Semibold + Bold + Heavy + Black + Template v.1.0 + + Small + Medium + Large + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Service/ProfileUpdateTask.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Service/ProfileUpdateTask.swift new file mode 100644 index 0000000..65ce3d7 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Service/ProfileUpdateTask.swift @@ -0,0 +1,76 @@ +import Foundation +import Library + +public enum ProfileUpdateTask { + static let minUpdateInterval: TimeInterval = 15 * 60 + static let defaultUpdateInterval: TimeInterval = 60 * 60 + + private static var timer: Timer? + + public static func configure() async throws { + timer?.invalidate() + timer = nil + let profiles = try await ProfileManager.listAutoUpdateEnabled() + if profiles.isEmpty { + return + } + var updateInterval = profiles.map { it in + it.autoUpdateIntervalOrDefault + }.min()! + if updateInterval < minUpdateInterval { + updateInterval = minUpdateInterval + } + timer = Timer(fire: calculateEarliestBeginDate(profiles), interval: updateInterval, repeats: true) { _ in + Task { + await getAndupdateProfiles() + } + } + } + + static func calculateEarliestBeginDate(_ profiles: [Profile]) -> Date { + let nowTime = Date.now + var earliestBeginDate = profiles.map { it in + it.lastUpdated!.addingTimeInterval(it.autoUpdateIntervalOrDefault) + }.min()! + if earliestBeginDate <= nowTime { + earliestBeginDate = nowTime + } + return earliestBeginDate + } + + private nonisolated static func getAndupdateProfiles() async { + do { + _ = try await updateProfiles(ProfileManager.listAutoUpdateEnabled()) + NSLog("profile update task succeed") + } catch { + NSLog("profile update task failed: \(error.localizedDescription)") + } + } + + static func updateProfiles(_ profiles: [Profile]) async -> Bool { + var success = true + for profile in profiles { + if profile.lastUpdated! > Date(timeIntervalSinceNow: -profile.autoUpdateIntervalOrDefault) { + continue + } + do { + try await profile.updateRemoteProfile() + NSLog("Updated profile \(profile.name)") + } catch { + NSLog("Update profile \(profile.name) failed: \(error.localizedDescription)") + success = false + } + } + return success + } +} + +extension Profile { + var autoUpdateIntervalOrDefault: TimeInterval { + if autoUpdateInterval > 0 { + return TimeInterval(autoUpdateInterval * 60) + } else { + return ProfileUpdateTask.defaultUpdateInterval + } + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Service/StoreManager.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Service/StoreManager.swift new file mode 100644 index 0000000..dce0e0d --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Service/StoreManager.swift @@ -0,0 +1,139 @@ +// +// File.swift +// LoginKit +// +// Created by Mac on 2024/10/12. +// + +import Foundation +import Foundation + +//MACOS +// 创建一个全局的 UserManager 类,负责从 UserDefaults 中读取和存储用户信息及登录状态 +class StoreManager { + static let shared = StoreManager() + + + public let configURL = "https://api.gooapis.com/api/vpnconfig.php" + + // UserDefaults keys + private let loginStatusKey = "isLoggedIn" + private let userEmailKey = "userEmail" + private let avatarurlKey = "avatar_url" + private let autodataKey = "autodata" + private let subURLDataKey = "subURLData" + private let baseURLKey = "baseURLKey" + + + // UserDefaults 引用 + private let defaults = UserDefaults.standard + + func baseURL()-> String{ + return UserDefaults.standard.string(forKey: "baseURLKey") ?? "" + } + + func mainregisterURL()-> String{ + UserDefaults.standard.string(forKey: "mainregisterURLKey") ?? "" + } + + func paymentURL()-> String{ + UserDefaults.standard.string(forKey: "paymentURLKey") ?? "" + } + func telegramUrl()-> String{ + UserDefaults.standard.string(forKey: "telegramUrlKey") ?? "" + } + func kefuUrl()-> String{ + UserDefaults.standard.string(forKey: "kefuUrlKey") ?? "" + } + func websiteURL()-> String{ + UserDefaults.standard.string(forKey: "websiteURLKey") ?? "" + } + + //存储 URL 相关信息 + func storebaseURLData(data: String) { + defaults.set(data, forKey: "baseURLKey") + defaults.synchronize() + } + + func storemainregisterURLData(data: String) { + defaults.set(data, forKey: "mainregisterURLKey") + defaults.synchronize() + } + + func storepaymentURLData(data: String) { + defaults.set(data, forKey: "paymentURLKey") + defaults.synchronize() + } + + func storetelegramUrlData(data: String) { + defaults.set(data, forKey: "telegramUrlKey") + defaults.synchronize() + } + + func storekefuUrlData(data: String) { + defaults.set(data, forKey: "kefuUrlKey") + defaults.synchronize() + } + func storewebsiteURLData(data: String) { + defaults.set(data, forKey: "websiteURLKey") + defaults.synchronize() + } + + + // 更新登录状态 + func updateLoginStatus(_ loggedIn: Bool) { + defaults.set(loggedIn, forKey: loginStatusKey) + defaults.synchronize() + } + + + // 检查是否已登录 + func isUserLoggedIn() -> Bool { + return defaults.bool(forKey: loginStatusKey) + } + + // 存储用户信息 + func storeUserInfo(email: String, avator: String) { + defaults.set(email, forKey: userEmailKey) + defaults.set(avator, forKey: avatarurlKey) // 假设 info 是 JSON 字符串 + defaults.synchronize() + } + + // 读取用户信息 + func getUserInfo() -> (email: String, avator: String) { + let email = defaults.string(forKey: userEmailKey) ?? "" + let avator = defaults.string(forKey: avatarurlKey) ?? "" + return (email, avator) + } + + // 存储 autodata + func storeAutoData(data: String) { + defaults.set(data, forKey: autodataKey) + defaults.synchronize() + } + + // 读取 autodata + func getSuburlData() -> String { + return defaults.string(forKey: subURLDataKey) ?? "" + } + + // 存储 autodata + func storeSuburlData(data: String) { + defaults.set(data, forKey: subURLDataKey) + defaults.synchronize() + } + + // 读取 autodata + func getAutoData() -> String { + return defaults.string(forKey: autodataKey) ?? "" + } + + // 清除所有数据(例如在登出时) + func clearUserData() { + defaults.removeObject(forKey: loginStatusKey) + defaults.removeObject(forKey: userEmailKey) + defaults.removeObject(forKey: avatarurlKey) + defaults.removeObject(forKey: autodataKey) + defaults.removeObject(forKey: subURLDataKey) + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Service/UIProfileUpdateTask.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Service/UIProfileUpdateTask.swift new file mode 100644 index 0000000..4fde2a7 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Service/UIProfileUpdateTask.swift @@ -0,0 +1,79 @@ +import BackgroundTasks +import Foundation +import Library +#if canImport(UIKit) + import UIKit +#endif + +#if os(iOS) || os(tvOS) + public class UIProfileUpdateTask: BGAppRefreshTask { + private static let taskSchedulerPermittedIdentifier = "\(FilePath.packageName).update_profiles" + + private static var registered = false + public static func configure() throws { + if !registered { + let success = BGTaskScheduler.shared.register(forTaskWithIdentifier: taskSchedulerPermittedIdentifier, using: nil) { task in + NSLog("profile update task started") + Task { + await UIProfileUpdateTask.getAndUpdateProfiles(task) + } + } + if !success { + throw NSError(domain: "register task failed", code: 0) + } + registered = true + } + Task { + BGTaskScheduler.shared.cancelAllTaskRequests() + let profiles = try await ProfileManager.listAutoUpdateEnabled() + if profiles.isEmpty { + return + } + try scheduleUpdate(ProfileUpdateTask.calculateEarliestBeginDate(profiles)) + } + Task { + if await UIApplication.shared.backgroundRefreshStatus != .available { + await updateOnce() + } + } + } + + private nonisolated static func updateOnce() async { + NSLog("update profiles at start since background refresh unavailable") + let profiles: [Profile] + do { + profiles = try await ProfileManager.listAutoUpdateEnabled() + } catch { + return + } + if profiles.isEmpty { + return + } + _ = await ProfileUpdateTask.updateProfiles(profiles) + } + + private nonisolated static func getAndUpdateProfiles(_ task: BGTask) async { + let profiles: [Profile] + do { + profiles = try await ProfileManager.listAutoUpdateEnabled() + } catch { + return + } + if profiles.isEmpty { + return + } + let success = await ProfileUpdateTask.updateProfiles(profiles) + try? scheduleUpdate(ProfileUpdateTask.calculateEarliestBeginDate(profiles)) + task.setTaskCompleted(success: success) + task.expirationHandler = { + try? scheduleUpdate(nil) + } + } + + private static func scheduleUpdate(_ earliestBeginDate: Date?) throws { + let request = BGAppRefreshTaskRequest(identifier: taskSchedulerPermittedIdentifier) + request.earliestBeginDate = earliestBeginDate + try BGTaskScheduler.shared.submit(request) + } + } +#endif diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Alert.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Alert.swift new file mode 100644 index 0000000..7de1feb --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Alert.swift @@ -0,0 +1,74 @@ +import Foundation +import SwiftUI + +public extension Alert { + init(_ error: Error, _ dismissAction: (() -> Void)? = nil) { + self.init( + errorMessage: error.localizedDescription, + dismissAction + ) + } + + init(errorMessage: String, _ dismissAction: (() -> Void)? = nil) { + self.init( + title: Text("错误提示"), + message: Text(errorMessage), + dismissButton: .default(Text("确定")) { + dismissAction?() + } + ) + } + + init(title: String = "提示", okMessage: String, _ dismissAction: (() -> Void)? = nil) { + self.init( + title: Text(title), + message: Text(okMessage), + dismissButton: .default(Text("确定")) { + dismissAction?() + } + ) + } + + init(OKORNOtitle: String = "提示", YesOrNOMessage: String, _ primaryAction: (() -> Void)? = nil) { + self.init( + title: Text(OKORNOtitle), + message: Text(YesOrNOMessage), + primaryButton: .default(Text("确定"), action: { + primaryAction?() + }), + secondaryButton: .cancel(Text("取消"), action: { + }) + ) + } + +} + +public extension View { + func alertBinding(_ binding: Binding) -> some View { + + alert(isPresented: Binding(get: { + binding.wrappedValue != nil + }, set: { newValue, _ in + if !newValue { + binding.wrappedValue = nil + } + })) { + binding.wrappedValue! + + } + + + } + + func alertBinding(_ binding: Binding, _ isLoading: Binding) -> some View { + alert(isPresented: Binding(get: { + binding.wrappedValue != nil + }, set: { newValue, _ in + if !newValue, !isLoading.wrappedValue { + binding.wrappedValue = nil + } + })) { + binding.wrappedValue! + } + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/BackButton.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/BackButton.swift new file mode 100644 index 0000000..afcbdeb --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/BackButton.swift @@ -0,0 +1,14 @@ +import Foundation +import SwiftUI + +public struct BackButton: View { + @Environment(\.dismiss) private var dismiss + + public var body: some View { + Button { + dismiss() + } label: { + Image(systemName: "chevron.backward") + } + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Setter.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Setter.swift new file mode 100644 index 0000000..395f4fe --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Setter.swift @@ -0,0 +1,12 @@ +import Foundation +import SwiftUI + +public extension Binding { + func withSetter(_ setter: @escaping (Value) -> Void) -> Binding { + Binding { + wrappedValue + } set: { [setter] newValue, _ in + setter(newValue) + } + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift new file mode 100644 index 0000000..25bf0fd --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/Binding+Unwrap.swift @@ -0,0 +1,29 @@ +import SwiftUI + +public extension Binding { + func unwrapped(_ defaultValue: T) -> Binding where Value == T? { + Binding(get: { + wrappedValue ?? defaultValue + }, set: { newValue in + wrappedValue = newValue + }) + } +} + +public extension Binding where Value == Int32 { + func stringBinding(defaultValue: Int32) -> Binding { + Binding { + var intValue = wrappedValue + if intValue == 0 { + intValue = defaultValue + } + return String(intValue) + } set: { newValue in + var newIntValue = Int32(newValue) ?? defaultValue + if newIntValue == 0 { + newIntValue = defaultValue + } + wrappedValue = newIntValue + } + } +} diff --git a/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/DeleteButton.swift b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/DeleteButton.swift new file mode 100644 index 0000000..746f8c1 --- /dev/null +++ b/iOS-SwiftUI-Code/ApplicationLibrary/Views/Abstract/DeleteButton.swift @@ -0,0 +1,46 @@ +import Foundation +import SwiftUI + +public struct DeleteButton