diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..03adc8d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "IDX.aI.enableInlineCompletion": true, + "IDX.aI.enableCodebaseIndexing": true +} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..be8db4f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,54 @@ +# Changelog + + +## [Unreleased] + +### Added +- Run scripts for different platforms (Android device/emulator, iOS device/simulator) +- Flutter Gen integration for better asset management +- Improved localization with proper l10n configuration +- Enhanced search dialog UI and state management + +### Changed +- Replaced flutter_v2ray with vpnclient_engine_flutter +- Updated dependencies and removed unused code +- Improved UI responsiveness and overflow handling +- Refactored main.dart for better localization support + +### Fixed +- Resolved potential UI overflow issues in search dialog +- Enhanced state initialization and lifecycle management +- Improved layout responsiveness and SafeArea integration + +### Merged +- Merged branch `development`: updated VPN link configuration to use 5.35.98.91 +- Merged branch `dodonov`: updated dependencies, improved l10n configuration, changed Android status bar style +- Merged branch `feat/setting_page+adapter_telegrambot`: added settings page adapter, improved localization, updated plugin registrants, switched to JSON localization assets +- Merged branch `bugfix/localization-no-synthetic-package`: resolved localization package issues, merged latest development changes +- Merged branch `bugfix/vpnclient-engine-dependency`: improved VPN engine dependency handling, resolved localization and UI conflicts +- Merged branch `refactoring-branch`: added new functionality including settings page, speed page, VPN provider, and navigation improvements +- Merged branch `hotfix/temporary-vpn-uri`: updated VPN URI configuration to use temporary server +- Merged branch `feat/vpn-link-selector`: added VPN link selector functionality +- Merged branch `feat/dimensions`: added dimension links and UI improvements +- Merged branch `feat/adding-ios-VPN-profile`: added iOS VPN profile support structure +- Merged branch `ci/use-flutter-instead-dart-format`: updated CI configuration to use Flutter instead of dart format + +### Conflict Resolution +- Resolved multiple merge conflicts in pubspec.yaml, pubspec.lock, localization files, and generated plugin registrants +- Kept latest HEAD versions for all major Dart and localization files to ensure stability and consistency + +## [1.0.12] - 2025-01-XX + +### Added +- Initial release of VPN Client app +- Basic VPN functionality +- Multi-language support (English, Russian, Thai, Chinese) +- Dark/Light theme support +- Server selection interface +- Apps management page + +### Changed +- Initial project structure and architecture + +### Fixed +- Various UI improvements and bug fixes \ No newline at end of file diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml index 06952be..aec7904 100644 --- a/android/app/src/main/res/values-night/styles.xml +++ b/android/app/src/main/res/values-night/styles.xml @@ -14,5 +14,7 @@ This Theme is only used starting with V2 of Flutter's Android embedding. --> diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index cb1ef88..eb6a4d6 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -14,5 +14,7 @@ This Theme is only used starting with V2 of Flutter's Android embedding. --> diff --git a/assets/lang/en.json b/assets/lang/en.json index 612e72d..8dcfcc8 100644 --- a/assets/lang/en.json +++ b/assets/lang/en.json @@ -22,6 +22,7 @@ "disconnected": "DISCONNECTED", "reconnecting": "RECONNECTING", "connecting": "CONNECTING", +<<<<<<< HEAD "disconnecting": "DISCONNECTING", "settings": "Settings", "version": "Version", @@ -38,4 +39,7 @@ "connection_reset": "Connection settings have been reset", "failed_open_telegram": "Failed to open Telegram bot", "about_app": "About App" +======= + "disconnecting": "DISCONNECTING" +>>>>>>> origin/bugfix/vpnclient-engine-dependency } diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 70a622a..9f0de26 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -26,9 +26,9 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_native_splash: df59bb2e1421aa0282cb2e95618af4dcb0c56c29 - flutter_v2ray: 21ee520e013877941e2e67b3b48aed9c6870e8dc - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + flutter_native_splash: c32d145d68aeda5502d5f543ee38c192065986cf + flutter_v2ray: 1190bb389b67a1dc9f28ece1d4b308101e38395e + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: f8e15f817b1bf5846cb6aad560a31cf33c165196 diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata index 21a3cc1..3a6a651 100644 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -2,7 +2,7 @@ + location = "group:VPN Client.xcodeproj"> diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index cbf8e75..2d87ba2 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,50 +1,75 @@ import Flutter import UIKit -///VPN -///import Foundation import NetworkExtension -class VpnConfigurator { - static func setupTunnel() { - let manager = NEVPNManager.shared() - manager.loadFromPreferences { error in - if let error = error { - print("Failed to load VPN preferences: \(error)") - return - } +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) - let proto = NETunnelProviderProtocol() - proto.providerBundleIdentifier = "com.vpnclient.VPNclientTunnel" - proto.serverAddress = "VPNclient" + let controller = window?.rootViewController as! FlutterViewController + let vpnChannel = FlutterMethodChannel( + name: "com.vpnclient/vpn_control", + binaryMessenger: controller.binaryMessenger + ) - manager.protocolConfiguration = proto - manager.localizedDescription = "VPNclient" - manager.isEnabled = true + vpnChannel.setMethodCallHandler { [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) in + guard let self = self else { return } + switch call.method { + case "setupVPN": + guard let args = call.arguments as? [String: String], + let tunAddr = args["tunAddr"], + let tunMask = args["tunMask"], + let tunDns = args["tunDns"], + let socks5Proxy = args["socks5Proxy"] else { + result(FlutterError(code: "INVALID_ARGS", message: "Invalid arguments", details: nil)) + return + } + print("AppDelegate: Setting up VPN with tunAddr=\(tunAddr), socks5Proxy=\(socks5Proxy)") + let vpnManager = VPNManager.sharedInstance ?? VPNManager() + vpnManager.setupVPNConfiguration(tunAddr: tunAddr, tunMask: tunMask, tunDns: tunDns, socks5Proxy: socks5Proxy) { error in + if let error = error { + print("AppDelegate: Setup VPN failed: \(error.localizedDescription)") + result(FlutterError(code: "SETUP_FAILED", message: error.localizedDescription, details: nil)) + } else { + print("AppDelegate: Setup VPN succeeded") + result(nil) + } + } - manager.saveToPreferences { error in - if let error = error { - print("Failed to save VPN configuration: \(error)") - } else { - print("VPN configuration saved successfully.") + case "startVPN": + let vpnManager = VPNManager.sharedInstance ?? VPNManager() + vpnManager.startVPN { error in + if let error = error { + print("AppDelegate: Start VPN failed: \(error.localizedDescription)") + result(FlutterError(code: "START_FAILED", message: error.localizedDescription, details: nil)) + } else { + print("AppDelegate: Start VPN succeeded") + result(nil) + } } - } - } - } -} -///VPN + case "stopVPN": + let vpnManager = VPNManager.sharedInstance ?? VPNManager() + vpnManager.stopVPN { + print("AppDelegate: Stop VPN succeeded") + result(nil) + } + case "getVPNStatus": + let vpnManager = VPNManager.sharedInstance ?? VPNManager() + let status = vpnManager.vpnStatus.description + print("AppDelegate: VPN status: \(status)") + result(status) -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - ///vpn - VpnConfigurator.setupTunnel() - ///vpn - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} + default: + result(FlutterMethodNotImplemented) + } + } + + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} \ No newline at end of file diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard index f3c2851..ff3ee21 100644 --- a/ios/Runner/Base.lproj/Main.storyboard +++ b/ios/Runner/Base.lproj/Main.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -14,13 +16,14 @@ - + - + + diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 8100ced..57a060e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -33,6 +33,11 @@ UIApplicationSupportsIndirectInputEvents + UIBackgroundModes + + fetch + remote-notification + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -50,5 +55,9 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight + com.apple.developer.networking.vpn.api + + allow-vpn + diff --git a/ios/Runner/Runner.entitlements b/ios/Runner/Runner.entitlements new file mode 100644 index 0000000..ffab33e --- /dev/null +++ b/ios/Runner/Runner.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.developer.networking.networkextension + + packet-tunnel-provider + + + diff --git a/ios/Runner/RunnerRelease.entitlements b/ios/Runner/RunnerRelease.entitlements index 7b3112c..0c67376 100644 --- a/ios/Runner/RunnerRelease.entitlements +++ b/ios/Runner/RunnerRelease.entitlements @@ -1,18 +1,5 @@ - - com.apple.developer.networking.networkextension - - dns-settings - packet-tunnel-provider - dns-proxy - app-proxy-provider - content-filter-provider - - com.apple.developer.networking.vpn.api - - allow-vpn - - + diff --git a/ios/Runner/VPNManager.swift b/ios/Runner/VPNManager.swift new file mode 100644 index 0000000..7c018d7 --- /dev/null +++ b/ios/Runner/VPNManager.swift @@ -0,0 +1,180 @@ +import NetworkExtension +import Foundation + +class VPNManager { + private var vpnManager: NETunnelProviderManager? + private let profileName = "Controller" + private var isInitialized = false + private var initializationCompletion: ((Error?) -> Void)? + static var sharedInstance: VPNManager? + + init() { + print("VPNManager: Initializing...") + loadVPNManager { error in + if let error = error { + print("VPNManager: Initialization failed with error: \(error.localizedDescription)") + } else { + print("VPNManager: Initialization completed successfully") + } + self.isInitialized = true + self.initializationCompletion?(error) + self.initializationCompletion = nil + } + } + + private func loadVPNManager(completion: @escaping (Error?) -> Void) { + print("VPNManager: Loading existing VPN managers...") + NETunnelProviderManager.loadAllFromPreferences { managers, error in + if let error = error { + print("VPNManager: Error loading VPN managers: \(error.localizedDescription)") + self.vpnManager = NETunnelProviderManager() + self.vpnManager?.localizedDescription = self.profileName + print("VPNManager: Created new VPN profile due to error: \(self.profileName)") + completion(nil) + return + } + + print("VPNManager: Loaded \(managers?.count ?? 0) VPN managers") + if let existingManager = managers?.first(where: { $0.localizedDescription == self.profileName }) { + self.vpnManager = existingManager + print("VPNManager: Found existing VPN profile: \(self.profileName)") + } else { + self.vpnManager = NETunnelProviderManager() + self.vpnManager?.localizedDescription = self.profileName + print("VPNManager: Created new VPN profile: \(self.profileName)") + } + print("VPNManager: vpnManager is \(self.vpnManager != nil ? "set" : "nil")") + completion(nil) + } + + // Таймаут для loadAllFromPreferences + DispatchQueue.global().asyncAfter(deadline: .now() + 5) { + if !self.isInitialized { + print("VPNManager: loadAllFromPreferences timed out") + self.vpnManager = NETunnelProviderManager() + self.vpnManager?.localizedDescription = self.profileName + print("VPNManager: Created new VPN profile due to timeout: \(self.profileName)") + self.isInitialized = true + completion(nil) + } + } + } + + private func waitForInitialization(completion: @escaping (Error?) -> Void) { + if isInitialized { + print("VPNManager: Already initialized") + completion(nil) + return + } + print("VPNManager: Waiting for initialization...") + initializationCompletion = completion + } + + func setupVPNConfiguration(tunAddr: String, tunMask: String, tunDns: String, socks5Proxy: String, completion: @escaping (Error?) -> Void) { + print("VPNManager: Setting up VPN configuration with tunAddr=\(tunAddr), tunMask=\(tunMask), tunDns=\(tunDns), socks5Proxy=\(socks5Proxy)") + waitForInitialization { error in + if let error = error { + completion(error) + return + } + guard let vpnManager = self.vpnManager else { + print("VPNManager: VPN Manager not initialized") + completion(NSError(domain: "VPNError", code: -1, userInfo: [NSLocalizedDescriptionKey: "VPN Manager not initialized"])) + return + } + + vpnManager.loadFromPreferences { error in + if let error = error { + print("VPNManager: Load preferences error: \(error.localizedDescription)") + completion(error) + return + } + + let tunnelProtocol = NETunnelProviderProtocol() + tunnelProtocol.providerBundleIdentifier = "click.vpnclient.VPNclientTunnel" + tunnelProtocol.serverAddress = socks5Proxy + tunnelProtocol.providerConfiguration = [ + "tunAddr": tunAddr, + "tunMask": tunMask, + "tunDns": tunDns, + "socks5Proxy": socks5Proxy + ] + + vpnManager.protocolConfiguration = tunnelProtocol + vpnManager.isEnabled = true + vpnManager.isOnDemandEnabled = false + + print("VPNManager: Saving VPN configuration...") + vpnManager.saveToPreferences { error in + if let error = error { + print("VPNManager: Save preferences error: \(error.localizedDescription)") + completion(error) + } else { + print("VPNManager: VPN configuration saved successfully") + completion(nil) + } + } + } + } + } + + func startVPN(completion: @escaping (Error?) -> Void) { + print("VPNManager: Starting VPN...") + waitForInitialization { error in + if let error = error { + completion(error) + return + } + guard let vpnManager = self.vpnManager else { + print("VPNManager: VPN Manager not initialized") + completion(NSError(domain: "VPNError", code: -1, userInfo: [NSLocalizedDescriptionKey: "VPN Manager not initialized"])) + return + } + vpnManager.loadFromPreferences { error in + if let error = error { + print("VPNManager: Load preferences error before start: \(error.localizedDescription)") + completion(error) + return + } + do { + try vpnManager.connection.startVPNTunnel() + print("VPNManager: VPN tunnel started successfully") + completion(nil) + } catch { + print("VPNManager: Start VPN error: \(error.localizedDescription)") + completion(error) + } + } + } + } + + func stopVPN(completion: @escaping () -> Void) { + print("VPNManager: Stopping VPN...") + waitForInitialization { _ in + self.vpnManager?.connection.stopVPNTunnel() + completion() + } + } + + var vpnStatus: NEVPNStatus { + let status = vpnManager?.connection.status ?? .invalid + print("VPNManager: Current VPN status: \(status.description)") + return status + } + + static func cleanup() { + sharedInstance = nil + } +} + +extension NEVPNStatus { + var description: String { + switch self { + case .disconnected: return "Disconnected" + case .connecting: return "Connecting..." + case .connected: return "Connected" + case .disconnecting: return "Disconnecting..." + default: return "Not Added Profile" + } + } +} \ No newline at end of file diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/VPN Client.xcodeproj/project.pbxproj similarity index 69% rename from ios/Runner.xcodeproj/project.pbxproj rename to ios/VPN Client.xcodeproj/project.pbxproj index 6251e06..500eac7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/VPN Client.xcodeproj/project.pbxproj @@ -3,19 +3,21 @@ archiveVersion = 1; classes = { }; - objectVersion = 54; + objectVersion = 77; objects = { /* Begin PBXBuildFile section */ - 05623FCADE53740FE6EC38FC /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C1A7CF68EF82448274F79EE /* Pods_RunnerTests.framework */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 2D7B47EE58217BA44F021C99 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 3A0366090C7CE5E83E3E66D2 /* Pods_RunnerTests.framework */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 5CF4606063F6CF5DDF0DEF1E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 474E7930403F702C7244270A /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + ADCEA9EEA0674573FF96F9D6 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6C9AB3A9273437DA77CF7190 /* Pods_Runner.framework */; }; + D86303622DDAA124009E3D50 /* VPNManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D86303612DDAA124009E3D50 /* VPNManager.swift */; }; + D863037A2DDAD1CC009E3D50 /* NetworkExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D86303792DDAD1CC009E3D50 /* NetworkExtension.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -26,12 +28,19 @@ remoteGlobalIDString = 97C146ED1CF9000F007C117D; remoteInfo = Runner; }; + D86303802DDAD1CC009E3D50 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = D86303772DDAD1CC009E3D50; + remoteInfo = VPNclientTunnel; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; + buildActionMask = 12; dstPath = ""; dstSubfolderSpec = 10; files = ( @@ -39,23 +48,31 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + D86303832DDAD1CC009E3D50 /* Embed Foundation Extensions */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 8; + dstPath = ""; + dstSubfolderSpec = 13; + files = ( + ); + name = "Embed Foundation Extensions"; + runOnlyForDeploymentPostprocessing = 1; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 197284AFD158A57F33B7508F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 2BABBD1CBB549C229028E3E6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3A0366090C7CE5E83E3E66D2 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 3AC403B42DBB3FD400E35EC1 /* RunnerRelease.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerRelease.entitlements; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 40D432E32B669D72F284E50A /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 474E7930403F702C7244270A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6C9AB3A9273437DA77CF7190 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 8C1A7CF68EF82448274F79EE /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 8665B952CF21C952CDD28CC0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -63,17 +80,44 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B67FBA7DE552A310D8682AF3 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - D486CAC8BA4A2B991D6B8DDF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - DED175BEB396A3E3B6FCC983 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + A232C605EB1ADE1BB86E5BF8 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + AC1B24E5523670E179DC665F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + ACFD93A1D03164446FE727AB /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + C5E533B152977104C504A2D5 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + D86303602DDAA0DD009E3D50 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; + D86303612DDAA124009E3D50 /* VPNManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VPNManager.swift; sourceTree = ""; }; + D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VPNclientTunnel.appex; sourceTree = BUILT_PRODUCTS_DIR; }; + D86303792DDAD1CC009E3D50 /* NetworkExtension.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = NetworkExtension.framework; path = System/Library/Frameworks/NetworkExtension.framework; sourceTree = SDKROOT; }; + E092053B252D5542B476BDA0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + D86303872DDAD1CC009E3D50 /* Exceptions for "VPNclientTunnel" folder in "VPNclientTunnel" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = D86303772DDAD1CC009E3D50 /* VPNclientTunnel */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + D86303872DDAD1CC009E3D50 /* Exceptions for "VPNclientTunnel" folder in "VPNclientTunnel" target */, + ); + path = VPNclientTunnel; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + /* Begin PBXFrameworksBuildPhase section */ 960B5143B6A69B8BB00D37F4 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 05623FCADE53740FE6EC38FC /* Pods_RunnerTests.framework in Frameworks */, + 2D7B47EE58217BA44F021C99 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -81,7 +125,15 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 5CF4606063F6CF5DDF0DEF1E /* Pods_Runner.framework in Frameworks */, + ADCEA9EEA0674573FF96F9D6 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D86303752DDAD1CC009E3D50 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + D863037A2DDAD1CC009E3D50 /* NetworkExtension.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -91,12 +143,12 @@ 1676946CE4F357204D9C4813 /* Pods */ = { isa = PBXGroup; children = ( - 197284AFD158A57F33B7508F /* Pods-Runner.debug.xcconfig */, - D486CAC8BA4A2B991D6B8DDF /* Pods-Runner.release.xcconfig */, - 2BABBD1CBB549C229028E3E6 /* Pods-Runner.profile.xcconfig */, - 40D432E32B669D72F284E50A /* Pods-RunnerTests.debug.xcconfig */, - DED175BEB396A3E3B6FCC983 /* Pods-RunnerTests.release.xcconfig */, - B67FBA7DE552A310D8682AF3 /* Pods-RunnerTests.profile.xcconfig */, + C5E533B152977104C504A2D5 /* Pods-Runner.debug.xcconfig */, + A232C605EB1ADE1BB86E5BF8 /* Pods-Runner.release.xcconfig */, + E092053B252D5542B476BDA0 /* Pods-Runner.profile.xcconfig */, + ACFD93A1D03164446FE727AB /* Pods-RunnerTests.debug.xcconfig */, + AC1B24E5523670E179DC665F /* Pods-RunnerTests.release.xcconfig */, + 8665B952CF21C952CDD28CC0 /* Pods-RunnerTests.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -125,6 +177,7 @@ children = ( 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */, 97C146EF1CF9000F007C117D /* Products */, 331C8082294A63A400263BE5 /* RunnerTests */, 1676946CE4F357204D9C4813 /* Pods */, @@ -137,6 +190,7 @@ children = ( 97C146EE1CF9000F007C117D /* Runner.app */, 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */, ); name = Products; sourceTree = ""; @@ -144,6 +198,8 @@ 97C146F01CF9000F007C117D /* Runner */ = { isa = PBXGroup; children = ( + D86303612DDAA124009E3D50 /* VPNManager.swift */, + D86303602DDAA0DD009E3D50 /* Runner.entitlements */, 3AC403B42DBB3FD400E35EC1 /* RunnerRelease.entitlements */, 97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FD1CF9000F007C117D /* Assets.xcassets */, @@ -160,8 +216,9 @@ E9078DB93BAB5787C511C451 /* Frameworks */ = { isa = PBXGroup; children = ( - 474E7930403F702C7244270A /* Pods_Runner.framework */, - 8C1A7CF68EF82448274F79EE /* Pods_RunnerTests.framework */, + D86303792DDAD1CC009E3D50 /* NetworkExtension.framework */, + 6C9AB3A9273437DA77CF7190 /* Pods_Runner.framework */, + 3A0366090C7CE5E83E3E66D2 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -173,7 +230,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( - 81BFDB42C18FDD849CBD3B91 /* [CP] Check Pods Manifest.lock */, + 4D1B5BEE14246A87461BBB9B /* [CP] Check Pods Manifest.lock */, 331C807D294A63A400263BE5 /* Sources */, 331C807F294A63A400263BE5 /* Resources */, 960B5143B6A69B8BB00D37F4 /* Frameworks */, @@ -192,24 +249,46 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 6540B4C79A6EB9CD6CA8C965 /* [CP] Check Pods Manifest.lock */, + 2119E14845FE31FAB4D234BF /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - F6B5A4EE9456ACB2EE7DD28F /* [CP] Embed Pods Frameworks */, + D86303832DDAD1CC009E3D50 /* Embed Foundation Extensions */, + F443A19394F529718D644FF5 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); dependencies = ( + D86303812DDAD1CC009E3D50 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; + D86303772DDAD1CC009E3D50 /* VPNclientTunnel */ = { + isa = PBXNativeTarget; + buildConfigurationList = D86303882DDAD1CC009E3D50 /* Build configuration list for PBXNativeTarget "VPNclientTunnel" */; + buildPhases = ( + D86303742DDAD1CC009E3D50 /* Sources */, + D86303752DDAD1CC009E3D50 /* Frameworks */, + D86303762DDAD1CC009E3D50 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + D863037B2DDAD1CC009E3D50 /* VPNclientTunnel */, + ); + name = VPNclientTunnel; + productName = VPNclientTunnel; + productReference = D86303782DDAD1CC009E3D50 /* VPNclientTunnel.appex */; + productType = "com.apple.product-type.app-extension"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -217,6 +296,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 1620; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -228,10 +308,12 @@ CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; }; + D86303772DDAD1CC009E3D50 = { + CreatedOnToolsVersion = 16.2; + }; }; }; - buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "VPN Client" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -239,12 +321,14 @@ Base, ); mainGroup = 97C146E51CF9000F007C117D; + preferredProjectObjectVersion = 77; productRefGroup = 97C146EF1CF9000F007C117D /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, 331C8080294A63A400263BE5 /* RunnerTests */, + D86303772DDAD1CC009E3D50 /* VPNclientTunnel */, ); }; /* End PBXProject section */ @@ -268,26 +352,17 @@ ); runOnlyForDeploymentPostprocessing = 0; }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; + D86303762DDAD1CC009E3D50 /* Resources */ = { + isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", - ); - name = "Thin Binary"; - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; - 6540B4C79A6EB9CD6CA8C965 /* [CP] Check Pods Manifest.lock */ = { +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 2119E14845FE31FAB4D234BF /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -309,7 +384,23 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 81BFDB42C18FDD849CBD3B91 /* [CP] Check Pods Manifest.lock */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 12; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + }; + 4D1B5BEE14246A87461BBB9B /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -346,7 +437,7 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; - F6B5A4EE9456ACB2EE7DD28F /* [CP] Embed Pods Frameworks */ = { + F443A19394F529718D644FF5 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -380,6 +471,14 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + D86303622DDAA124009E3D50 /* VPNManager.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + D86303742DDAD1CC009E3D50 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( ); runOnlyForDeploymentPostprocessing = 0; }; @@ -391,6 +490,11 @@ target = 97C146ED1CF9000F007C117D /* Runner */; targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; }; + D86303812DDAD1CC009E3D50 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = D86303772DDAD1CC009E3D50 /* VPNclientTunnel */; + targetProxy = D86303802DDAD1CC009E3D50 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ @@ -472,17 +576,20 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "VPN Client"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; - PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient; + MARKETING_VERSION = 1.0.12; + PRODUCT_BUNDLE_IDENTIFIER = vpnclient.click; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -493,7 +600,7 @@ }; 331C8088294A63A400263BE5 /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = 40D432E32B669D72F284E50A /* Pods-RunnerTests.debug.xcconfig */; + baseConfigurationReference = ACFD93A1D03164446FE727AB /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -511,7 +618,7 @@ }; 331C8089294A63A400263BE5 /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = DED175BEB396A3E3B6FCC983 /* Pods-RunnerTests.release.xcconfig */; + baseConfigurationReference = AC1B24E5523670E179DC665F /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -527,7 +634,7 @@ }; 331C808A294A63A400263BE5 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = B67FBA7DE552A310D8682AF3 /* Pods-RunnerTests.profile.xcconfig */; + baseConfigurationReference = 8665B952CF21C952CDD28CC0 /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CODE_SIGN_STYLE = Automatic; @@ -660,17 +767,20 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "VPN Client"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; - PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient; + MARKETING_VERSION = 1.0.12; + PRODUCT_BUNDLE_IDENTIFIER = vpnclient.click; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -688,17 +798,18 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; - DEVELOPMENT_TEAM = ""; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "VPN Client"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.5; - PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient; + MARKETING_VERSION = 1.0.12; + PRODUCT_BUNDLE_IDENTIFIER = vpnclient.click; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -707,6 +818,123 @@ }; name = Release; }; + D86303842DDAD1CC009E3D50 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = VPNclientTunnel/VPNclientTunnel.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNclientTunnel/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNclientTunnel; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient.VPNclientTunnel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + D86303852DDAD1CC009E3D50 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = VPNclientTunnel/VPNclientTunnel.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNclientTunnel/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNclientTunnel; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient.VPNclientTunnel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + D86303862DDAD1CC009E3D50 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CODE_SIGN_ENTITLEMENTS = VPNclientTunnel/VPNclientTunnel.entitlements; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 6XT4R7V83F; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = VPNclientTunnel/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = VPNclientTunnel; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@executable_path/../../Frameworks", + ); + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MARKETING_VERSION = 1.0; + MTL_FAST_MATH = YES; + PRODUCT_BUNDLE_IDENTIFIER = click.vpnclient.VPNclientTunnel; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Profile; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -720,7 +948,7 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "VPN Client" */ = { isa = XCConfigurationList; buildConfigurations = ( 97C147031CF9000F007C117D /* Debug */, @@ -740,6 +968,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + D86303882DDAD1CC009E3D50 /* Build configuration list for PBXNativeTarget "VPNclientTunnel" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D86303842DDAD1CC009E3D50 /* Debug */, + D86303852DDAD1CC009E3D50 /* Release */, + D86303862DDAD1CC009E3D50 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/VPN Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to ios/VPN Client.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/VPN Client.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to ios/VPN Client.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/VPN Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to ios/VPN Client.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/VPN Client.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 90% rename from ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to ios/VPN Client.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 15cada4..ba31c1f 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/VPN Client.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -17,7 +17,7 @@ BlueprintIdentifier = "97C146ED1CF9000F007C117D" BuildableName = "Runner.app" BlueprintName = "Runner" - ReferencedContainer = "container:Runner.xcodeproj"> + ReferencedContainer = "container:VPN Client.xcodeproj"> @@ -33,7 +33,7 @@ BlueprintIdentifier = "97C146ED1CF9000F007C117D" BuildableName = "Runner.app" BlueprintName = "Runner" - ReferencedContainer = "container:Runner.xcodeproj"> + ReferencedContainer = "container:VPN Client.xcodeproj"> @@ -45,7 +45,7 @@ BlueprintIdentifier = "331C8080294A63A400263BE5" BuildableName = "RunnerTests.xctest" BlueprintName = "RunnerTests" - ReferencedContainer = "container:Runner.xcodeproj"> + ReferencedContainer = "container:VPN Client.xcodeproj"> @@ -68,7 +68,7 @@ BlueprintIdentifier = "97C146ED1CF9000F007C117D" BuildableName = "Runner.app" BlueprintName = "Runner" - ReferencedContainer = "container:Runner.xcodeproj"> + ReferencedContainer = "container:VPN Client.xcodeproj"> @@ -85,7 +85,7 @@ BlueprintIdentifier = "97C146ED1CF9000F007C117D" BuildableName = "Runner.app" BlueprintName = "Runner" - ReferencedContainer = "container:Runner.xcodeproj"> + ReferencedContainer = "container:VPN Client.xcodeproj"> diff --git a/ios/VPNclientTunnel/Info.plist b/ios/VPNclientTunnel/Info.plist new file mode 100644 index 0000000..9a63630 --- /dev/null +++ b/ios/VPNclientTunnel/Info.plist @@ -0,0 +1,22 @@ + + + + + NSExtension + + com.apple.developer.networking.vpn.api + + allow-vpn + + UIBackgroundModes + + fetch + remote-notification + + NSExtensionPointIdentifier + com.apple.networkextension.packet-tunnel + NSExtensionPrincipalClass + $(PRODUCT_MODULE_NAME).PacketTunnelProvider + + + diff --git a/ios/VPNclientTunnel/PacketTunnelProvider.swift b/ios/VPNclientTunnel/PacketTunnelProvider.swift new file mode 100644 index 0000000..6d96061 --- /dev/null +++ b/ios/VPNclientTunnel/PacketTunnelProvider.swift @@ -0,0 +1,137 @@ +import NetworkExtension +import os.log + +class PacketTunnelProvider: NEPacketTunnelProvider { + private var tunnelRunning = false + + override func startTunnel(options: [String: NSObject]?, completionHandler: @escaping (Error?) -> Void) { + os_log(.debug, "PacketTunnelProvider: Starting tunnel with options: %@", String(describing: options)) + + guard let protocolConfiguration = protocolConfiguration as? NETunnelProviderProtocol, + let providerConfig = protocolConfiguration.providerConfiguration, + let tunAddr = providerConfig["tunAddr"] as? String, + let tunMask = providerConfig["tunMask"] as? String, + let tunDns = providerConfig["tunDns"] as? String, + let socks5Proxy = providerConfig["socks5Proxy"] as? String else { + os_log(.error, "PacketTunnelProvider: Failed to load provider configuration") + completionHandler(NSError(domain: "PacketTunnelProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing provider configuration"])) + return + } + + os_log(.debug, "PacketTunnelProvider: Config - tunAddr: %@, tunMask: %@, tunDns: %@, socks5Proxy: %@", tunAddr, tunMask, tunDns, socks5Proxy) + + let proxyComponents = socks5Proxy.components(separatedBy: ":") + guard proxyComponents.count == 2, + let socks5Address = proxyComponents.first, + let socks5Port = UInt16(proxyComponents.last ?? "1080") else { + os_log(.error, "PacketTunnelProvider: Invalid SOCKS5 proxy format: %@", socks5Proxy) + completionHandler(NSError(domain: "PacketTunnelProvider", code: -1, userInfo: [NSLocalizedDescriptionKey: "Invalid SOCKS5 proxy format"])) + return + } + + let settings = NEPacketTunnelNetworkSettings(tunnelRemoteAddress: socks5Address) + settings.mtu = 1500 + + let ipv4Settings = NEIPv4Settings(addresses: [tunAddr], subnetMasks: [tunMask]) + ipv4Settings.includedRoutes = [NEIPv4Route.default()] + settings.ipv4Settings = ipv4Settings + + let dnsSettings = NEDNSSettings(servers: [tunDns]) + settings.dnsSettings = dnsSettings + + os_log(.debug, "PacketTunnelProvider: Applying tunnel network settings...") + setTunnelNetworkSettings(settings) { error in + if let error = error { + os_log(.error, "PacketTunnelProvider: Failed to set tunnel network settings: %@", error.localizedDescription) + completionHandler(error) + return + } + + os_log(.info, "PacketTunnelProvider: Tunnel network settings applied successfully") + + let config = """ + tunnel: + name: tun0 + mtu: 8500 + socks5: + address: "\(socks5Address)" + port: \(socks5Port) + """ + + os_log(.debug, "PacketTunnelProvider: Starting hev-socks5-tunnel with config: %@", config) + DispatchQueue.global().async { + self.startHevSocks5Tunnel(withConfig: config) + } + + self.monitorTunnelActivity() + + os_log(.debug, "PacketTunnelProvider: Calling completion handler with success") + completionHandler(nil) + } + } + + func startHevSocks5Tunnel(withConfig config: String) { + os_log(.debug, "PacketTunnelProvider: Starting hev-socks5-tunnel...") + + guard let configData = config.data(using: .utf8) else { + os_log(.error, "PacketTunnelProvider: Failed to convert config to UTF-8 data") + return + } + let configLen = UInt32(configData.count) + + // Поскольку мы не можем получить tun_fd, нужно переработать логику + os_log(.debug, "PacketTunnelProvider: Using packetFlow instead of tun_fd") + + // Запускаем hev-socks5-tunnel в режиме, где мы сами обрабатываем пакеты + tunnelRunning = true + DispatchQueue.global().async { + // Читаем пакеты из packetFlow и передаём их в SOCKS5 + self.handlePackets() + } + } + + func handlePackets() { + let flow = self.packetFlow + + flow.readPackets { packets, protocols in + if !self.tunnelRunning { + os_log(.info, "PacketTunnelProvider: Stopping packet handling") + return + } + + for (packet, proto) in zip(packets, protocols) { + os_log(.debug, "PacketTunnelProvider: Received packet of size %d, protocol: %@", packet.count, proto.description) + // Здесь нужно передать пакет в hev-socks5-tunnel + // Например, через кастомный интерфейс или сокет + } + + // Продолжаем читать пакеты + self.handlePackets() + } + } + + func monitorTunnelActivity() { + DispatchQueue.global().async { + while self.tunnelRunning { + usleep(1000000) // Проверка каждую секунду + os_log(.debug, "PacketTunnelProvider: Tunnel still active, checking packets...") + } + } + } + + func checkTunnelStatus() { + os_log(.debug, "PacketTunnelProvider: Checking tunnel status...") + if self.packetFlow == nil { + os_log(.error, "PacketTunnelProvider: Tunnel flow is nil, possible disconnection") + } else { + os_log(.info, "PacketTunnelProvider: Tunnel flow is active") + } + } + + override func stopTunnel(with reason: NEProviderStopReason, completionHandler: @escaping () -> Void) { + os_log(.debug, "PacketTunnelProvider: Stopping tunnel with reason: %@", reason.rawValue.description) + tunnelRunning = false +// hev_socks5_tunnel_quit() + completionHandler() + } +} diff --git a/ios/VPNclientTunnel/VPNclientTunnel.entitlements b/ios/VPNclientTunnel/VPNclientTunnel.entitlements new file mode 100644 index 0000000..0154eb4 --- /dev/null +++ b/ios/VPNclientTunnel/VPNclientTunnel.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.application-groups + + group.click.vpnclient + + + diff --git a/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/build-request.json b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/build-request.json new file mode 100644 index 0000000..38eab09 --- /dev/null +++ b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/build-request.json @@ -0,0 +1,27 @@ +{ + "buildCommand" : { + "command" : "build", + "skipDependencies" : false, + "style" : "buildOnly" + }, + "configuredTargets" : [ + + ], + "continueBuildingAfterErrors" : false, + "dependencyScope" : "workspace", + "enableIndexBuildArena" : false, + "hideShellScriptEnvironment" : false, + "parameters" : { + "action" : "build", + "overrides" : { + + } + }, + "qos" : "utility", + "schemeCommand" : "launch", + "showNonLoggedProgress" : true, + "useDryRun" : false, + "useImplicitDependencies" : false, + "useLegacyBuildLocations" : false, + "useParallelTargets" : true +} \ No newline at end of file diff --git a/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/description.msgpack b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/description.msgpack new file mode 100644 index 0000000..0e18711 Binary files /dev/null and b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/description.msgpack differ diff --git a/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/manifest.json b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/manifest.json new file mode 100644 index 0000000..7391713 --- /dev/null +++ b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/manifest.json @@ -0,0 +1 @@ +{"client":{"name":"basic","version":0,"file-system":"device-agnostic","perform-ownership-analysis":"no"},"targets":{"":[""]},"commands":{"":{"tool":"phony","inputs":[""],"outputs":[""]},"P0:::Gate WorkspaceHeaderMapVFSFilesWritten":{"tool":"phony","inputs":[],"outputs":[""]}}} \ No newline at end of file diff --git a/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/target-graph.txt b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/target-graph.txt new file mode 100644 index 0000000..b83b158 --- /dev/null +++ b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/target-graph.txt @@ -0,0 +1 @@ +Target dependency graph (0 target) \ No newline at end of file diff --git a/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/task-store.msgpack b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/task-store.msgpack new file mode 100644 index 0000000..6cef3fe Binary files /dev/null and b/ios/build/XCBuildData/2b3a1334de51e4dc6dd39387be36d594.xcbuilddata/task-store.msgpack differ diff --git a/lib/core/constants/storage_keys.dart b/lib/core/constants/storage_keys.dart new file mode 100644 index 0000000..5277ad4 --- /dev/null +++ b/lib/core/constants/storage_keys.dart @@ -0,0 +1,8 @@ +// lib/core/constants/storage_keys.dart +class StorageKeys { + static const String selectedApps = 'selected_apps'; + static const String selectedServers = 'selected_servers'; + static const String recentlySearchedApps = 'recently_searched_apps'; + static const String recentlySearchedServers = 'recently_searched_servers'; + static const String isDarkTheme = 'isDarkTheme'; +} diff --git a/lib/design/dimensions.dart b/lib/design/dimensions.dart index 52ecaf9..2730677 100644 --- a/lib/design/dimensions.dart +++ b/lib/design/dimensions.dart @@ -1,72 +1,3 @@ -import 'package:flutter/material.dart'; - -class CustomString { - final BuildContext context; - late Locale locale; - - CustomString(this.context) { - locale = Localizations.localeOf(context); - } - - String get connected { - return _localized('connected'); - } - - String get disconnected { - return _localized('disconnected'); - } - - String get connecting { - return _localized('connecting'); - } - - String get disconnecting { - return _localized('disconnecting'); - } - - String get allapp { - return _localized('all_apps'); - } - - String _localized(String key) { - switch (locale.languageCode) { - case 'ru': - return { - 'connected': 'ПОДКЛЮЧЕН', - 'disconnected': 'ОТКЛЮЧЕН', - 'connecting': 'ПОДКЛЮЧЕНИЕ', - 'disconnecting': 'ОТКЛЮЧЕНИЕ', - "all_apps": "Все приложения", - }[key]!; - case 'th': - return { - "connected": "เชื่อมต่อแล้ว", - "disconnected": "ไม่ได้เชื่อมต่อ", - "connecting": "กำลังเชื่อมต่อ", - "disconnecting": "กำลังตัดการเชื่อมต่อ", - "all_apps": "แอปทั้งหมด", - }[key]!; - case 'zh': - return { - "connected": "已连接", - "disconnected": "已断开", - "connecting": "正在连接", - "disconnecting": "正在断开", - "all_apps": "所有应用", - }[key]!; - case 'en': - default: - return { - 'connected': 'CONNECTED', - 'disconnected': 'DISCONNECTED', - 'connecting': 'CONNECTING', - 'disconnecting': 'DISCONNECTING', - "all_apps": "All Applications", - }[key]!; - } - } -} - // style const double elevation0 = 0; diff --git a/lib/main.dart b/lib/main.dart index bab09d3..6cb0b15 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,7 +6,6 @@ import 'package:vpn_client/pages/main/main_page.dart'; import 'package:vpn_client/pages/settings/setting_page.dart'; import 'package:vpn_client/pages/servers/servers_page.dart'; import 'package:vpn_client/theme_provider.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:vpn_client/vpn_state.dart'; import 'package:vpn_client/localization_service.dart'; // import 'package:vpn_client/pages/apps/apps_page.dart'; @@ -26,13 +25,7 @@ void main() async { DeviceOrientation.portraitDown, ]); runApp( - MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => ThemeProvider()), - ChangeNotifierProvider(create: (_) => VpnState()), - ], - child: const App(), - ), + ChangeNotifierProvider(create: (_) => ThemeProvider(), child: const App()), ); } @@ -42,11 +35,7 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { final themeProvider = Provider.of(context); - - // If you want to override it manually, do it here (or leave as null to use system): - // final Locale? manualLocale = const Locale('ru'); // ← override example - final Locale? manualLocale = null; // ← use system by default - + final Locale? manualLocale = null; return MaterialApp( localizationsDelegates: const [ GlobalMaterialLocalizations.delegate, @@ -60,26 +49,40 @@ class App extends StatelessWidget { locale: manualLocale, localeResolutionCallback: (locale, _) { if (locale == null) return const Locale('en'); - - // Check for exact match - final supported = ['en', 'ru', 'th', 'zh']; - if (supported.contains(locale.languageCode)) { - return Locale(locale.languageCode); + for (var supportedLocale in supportedLocales) { + if (supportedLocale.languageCode == locale.languageCode && + (supportedLocale.countryCode == null || + supportedLocale.countryCode == locale.countryCode)) { + return supportedLocale; + } + } + if (locale.languageCode == 'zh') { + return supportedLocales.contains(const Locale('zh')) + ? const Locale('zh') + : const Locale('en'); } - - // Fallback to 'en' if not found return const Locale('en'); }, - themeMode: themeProvider.themeMode, home: const MainScreen(), + localizationsDelegates: const [ + AppLocalizations.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('en'), + Locale('ru'), + Locale('th'), + Locale('zh'), + ], ); } } class MainScreen extends StatefulWidget { const MainScreen({super.key}); - @override State createState() => _MainScreenState(); } @@ -87,7 +90,6 @@ class MainScreen extends StatefulWidget { class _MainScreenState extends State { int _currentIndex = 1; late List _pages; - @override void initState() { super.initState(); @@ -99,13 +101,11 @@ class _MainScreenState extends State { SettingPage(onNavBarTap: _handleNavBarTap), ]; } - void _handleNavBarTap(int index) { setState(() { _currentIndex = index; }); } - @override Widget build(BuildContext context) { return Scaffold( @@ -121,7 +121,6 @@ class _MainScreenState extends State { class PlaceholderPage extends StatelessWidget { final String text; const PlaceholderPage({super.key, required this.text}); - @override Widget build(BuildContext context) { return Center(child: Text(text)); diff --git a/lib/models/nav_item.dart b/lib/models/nav_item.dart new file mode 100644 index 0000000..13c2a72 --- /dev/null +++ b/lib/models/nav_item.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; + +class NavItem { + final Widget inactiveIcon; + final Widget activeIcon; + + NavItem({required this.inactiveIcon, required this.activeIcon}); +} \ No newline at end of file diff --git a/lib/pages/apps/apps_list.dart b/lib/pages/apps/apps_list.dart index 9074481..a1b2486 100644 --- a/lib/pages/apps/apps_list.dart +++ b/lib/pages/apps/apps_list.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; -import 'package:vpn_client/design/dimensions.dart'; import 'apps_list_item.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; import 'dart:convert'; +import 'package:vpn_client/core/constants/storage_keys.dart'; // Importar as constantes class AppsList extends StatefulWidget { final Function(List>)? onAppsLoaded; @@ -17,6 +18,7 @@ class AppsList extends StatefulWidget { class AppsListState extends State { List> _apps = []; bool _isLoading = true; + bool _dataLoaded = false; // Flag para controlar o carregamento inicial @override void initState() { @@ -24,25 +26,20 @@ class AppsListState extends State { if (widget.apps != null && widget.apps!.isNotEmpty) { _apps = widget.apps!; _isLoading = false; - if (widget.onAppsLoaded != null) { - widget.onAppsLoaded!(_apps); - } - } else { - _loadApps(); + _dataLoaded = + true; // Marcar como carregado se dados iniciais foram fornecidos + // widget.onAppsLoaded é chamado em didUpdateWidget ou após _loadApps } } late String textallapps; - bool _initialized = false; - @override void didChangeDependencies() { super.didChangeDependencies(); - if (!_initialized) { - final statusText = CustomString(context); - textallapps = statusText.allapp; + if (!_dataLoaded) { + // Carregar apenas se os dados não foram carregados via widget.apps ou anteriormente + textallapps = AppLocalizations.of(context)!.all_apps; _loadApps(); - _initialized = true; } } @@ -53,6 +50,7 @@ class AppsListState extends State { setState(() { _apps = widget.apps!; _isLoading = false; + _dataLoaded = true; }); _saveSelectedApps(); } @@ -60,15 +58,33 @@ class AppsListState extends State { Future _loadApps() async { setState(() { - _isLoading = true; + // Evitar mostrar loading se já estiver carregando ou já carregou + if (!_dataLoaded) _isLoading = true; }); + // Simulação de carregamento + // Em um app real, aqui viria a lógica de buscar dados de uma API ou DB + await Future.delayed(const Duration(milliseconds: 100)); // Simular delay + + // Definir textallapps aqui se ainda não foi definido em didChangeDependencies + // Isso é um fallback, idealmente textallapps já está disponível. + // Adicionando verificação de 'mounted' para o BuildContext + if (!mounted) return; + final localizations = AppLocalizations.of(context); + textallapps = localizations?.all_apps ?? "All Applications"; + try { + // Se os dados já foram carregados (ex: por uma busca anterior que atualizou widget.apps), não recarregar do zero + if (_dataLoaded && _apps.isNotEmpty) { + setState(() => _isLoading = false); + return; + } + List> appsList = [ { 'icon': null, 'image': null, - 'text': textallapps, + 'text': textallapps, // Usar a string localizada 'isSwitch': true, 'isActive': false, }, @@ -106,7 +122,7 @@ class AppsListState extends State { ]); final prefs = await SharedPreferences.getInstance(); - final String? savedApps = prefs.getString('selected_apps'); + final String? savedApps = prefs.getString(StorageKeys.selectedApps); if (savedApps != null) { final List savedAppsList = jsonDecode(savedApps); for (var savedApp in savedAppsList) { @@ -122,6 +138,7 @@ class AppsListState extends State { setState(() { _apps = appsList; _isLoading = false; + _dataLoaded = true; // Marcar que os dados foram carregados }); if (widget.onAppsLoaded != null) { @@ -130,6 +147,7 @@ class AppsListState extends State { } catch (e) { setState(() { _isLoading = false; + _dataLoaded = true; // Marcar como tentado carregar para evitar loop }); debugPrint('Error loading apps: $e'); } @@ -141,7 +159,7 @@ class AppsListState extends State { _apps .map((app) => {'text': app['text'], 'isActive': app['isActive']}) .toList(); - await prefs.setString('selected_apps', jsonEncode(selectedApps)); + await prefs.setString(StorageKeys.selectedApps, jsonEncode(selectedApps)); } List> get apps => _apps; @@ -150,14 +168,19 @@ class AppsListState extends State { setState(() { if (index == 0 && _apps[index]['isSwitch']) { _apps[0]['isActive'] = !_apps[0]['isActive']; + // Se "Todos os aplicativos" for ativado, desabilitar os outros itens da lista (visual) + // A lógica de 'isEnabled' no AppListItem já cuida disso visualmente. + // Aqui, garantimos que os outros não estejam 'isActive' se "Todos" estiver ativo. if (_apps[0]['isActive']) { for (int i = 1; i < _apps.length; i++) { - _apps[i]['isEnabled'] = false; + _apps[i]['isActive'] = + false; // Desmarcar outros se "Todos" for selecionado } } } else { _apps[index]['isActive'] = !_apps[index]['isActive']; - if (_apps[index]['isActive']) { + // Se um app individual for ativado, "Todos os aplicativos" deve ser desativado + if (_apps[index]['isActive'] && index != 0) { _apps[0]['isActive'] = false; } } @@ -170,6 +193,18 @@ class AppsListState extends State { @override Widget build(BuildContext context) { + // Garante que textallapps seja inicializado se didChangeDependencies não for chamado a tempo + // ou se o widget for reconstruído antes. + textallapps = AppLocalizations.of(context)?.all_apps ?? "All Applications"; + + // Atualiza o texto do primeiro item se ele ainda não estiver com o texto localizado + // Isso pode acontecer se _loadApps for chamado antes de textallapps ser definido por didChangeDependencies + if (_apps.isNotEmpty && + _apps[0]['text'] != textallapps && + _apps[0]['isSwitch'] == true) { + _apps[0]['text'] = textallapps; + } + return Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, @@ -191,7 +226,9 @@ class AppsListState extends State { text: _apps[index]['text'], isSwitch: _apps[index]['isSwitch'], isActive: _apps[index]['isActive'], - isEnabled: index == 0 || !_apps[0]['isActive'], + isEnabled: + index == 0 || + !_apps[0]['isActive'], // Item é habilitado se for o switch "Todos" ou se "Todos" não estiver ativo onTap: () => _onItemTapped(index), ); }), diff --git a/lib/pages/apps/apps_list_item.dart b/lib/pages/apps/apps_list_item.dart index 15573c4..e69a031 100644 --- a/lib/pages/apps/apps_list_item.dart +++ b/lib/pages/apps/apps_list_item.dart @@ -33,12 +33,15 @@ class AppListItem extends StatelessWidget { height: 52, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: Colors.white, + color: + Theme.of(context).colorScheme.onSurface, // Exemplo de uso do tema borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.grey.withAlpha((255 * 0.2).toInt()), - blurRadius: 10, + color: Theme.of( + context, + ).shadowColor.withAlpha((255 * 0.1).round()), // Exemplo + blurRadius: 5, // Ajuste conforme necessário offset: const Offset(0, 1), ), ], @@ -70,9 +73,11 @@ class AppListItem extends StatelessWidget { child: Text( text, style: const TextStyle( - fontSize: 16, - color: Colors.black, - ), + fontSize: 16, // Considere usar TextTheme + // color: Colors.black, // Removido para usar cor padrão do tema ou definir explicitamente via tema + ).apply( + color: Theme.of(context).colorScheme.primary, + ), // Exemplo ), ), ], @@ -90,7 +95,8 @@ class AppListItem extends StatelessWidget { : Checkbox( value: isActive, onChanged: null, - checkColor: Colors.white, + checkColor: + Theme.of(context).colorScheme.onPrimary, // Exemplo fillColor: WidgetStateProperty.resolveWith((states) { if (!isActive) { return Theme.of(context).colorScheme.onSecondary; @@ -113,7 +119,9 @@ class AppListItem extends StatelessWidget { if (!isEnabled) Container( decoration: BoxDecoration( - color: Colors.grey.withAlpha((255 * 0.2).toInt()), + color: Theme.of( + context, + ).disabledColor.withAlpha((255 * 0.2).round()), // Exemplo borderRadius: BorderRadius.circular(10), ), ), diff --git a/lib/pages/apps/apps_page.dart b/lib/pages/apps/apps_page.dart index fdd99ba..4e3426a 100644 --- a/lib/pages/apps/apps_page.dart +++ b/lib/pages/apps/apps_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:vpn_client/pages/apps/apps_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:vpn_client/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; class AppsPage extends StatefulWidget { const AppsPage({super.key}); diff --git a/lib/pages/main/location_widget.dart b/lib/pages/main/location_widget.dart index 53b724c..b7766d7 100644 --- a/lib/pages/main/location_widget.dart +++ b/lib/pages/main/location_widget.dart @@ -1,21 +1,68 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/svg.dart'; -import 'package:vpn_client/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; + class LocationWidget extends StatelessWidget { + final String title; final Map? selectedServer; + final VoidCallback? onTap; - const LocationWidget({super.key, this.selectedServer}); + const LocationWidget({ + super.key, + required this.title, + this.selectedServer, + this.onTap, + }); @override Widget build(BuildContext context) { - final String locationName = selectedServer?['text'] ?? '...'; - final String iconPath = - selectedServer?['icon'] ?? 'assets/images/flags/auto.svg'; + final String locationName = selectedServer?['text'] ?? '...'; final String iconPath = selectedServer?['icon'] ?? 'assets/images/flags/auto.svg'; - return Container( - margin: const EdgeInsets.all(30), - padding: const EdgeInsets.only(left: 14), + return GestureDetector( onTap: onTap, + child: Container( + padding: const EdgeInsets.only(left: 14), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.secondary, + ), + ), + Text( + locationName, + style: TextStyle( + fontSize: 17, + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.primary, + ), + ), + ], + ), + const Spacer(), + Column( + children: [ + const SizedBox(height: 20), + SvgPicture.asset(iconPath, width: 48, height: 48), + ], + ), + ], + ), + ), + ); + } +} decoration: BoxDecoration( color: Theme.of(context).colorScheme.onSurface, borderRadius: BorderRadius.circular(12), @@ -27,7 +74,7 @@ class LocationWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - LocalizationService.to('your_location'), + AppLocalizations.of(context).your_location, style: TextStyle( fontSize: 14, fontWeight: FontWeight.w400, diff --git a/lib/pages/main/main_btn.dart b/lib/pages/main/main_btn.dart index 7e5d82a..f555110 100644 --- a/lib/pages/main/main_btn.dart +++ b/lib/pages/main/main_btn.dart @@ -4,6 +4,8 @@ import 'package:vpn_client/design/colors.dart'; import 'package:flutter_v2ray/flutter_v2ray.dart'; import 'package:vpn_client/localization_service.dart'; import 'package:vpn_client/vpn_state.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; +import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart'; final FlutterV2ray flutterV2ray = FlutterV2ray( onStatusChanged: (status) { @@ -11,6 +13,8 @@ final FlutterV2ray flutterV2ray = FlutterV2ray( }, ); +enum VpnConnectionState { connected, disconnected, connecting, disconnecting } + class MainBtn extends StatefulWidget { const MainBtn({super.key}); @@ -19,6 +23,31 @@ class MainBtn extends StatefulWidget { } class MainBtnState extends State with SingleTickerProviderStateMixin { + ///static const platform = MethodChannel('vpnclient_engine2'); + /// + late VpnConnectionState _vpnState; + late String connectionStatusDisconnected; + late String connectionStatusDisconnecting; + late String connectionStatusConnected; + late String connectionStatusConnecting; + bool _initialized = false; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + // Initialize localized strings once + connectionStatusDisconnected = AppLocalizations.of(context)!.disconnected; + connectionStatusConnected = AppLocalizations.of(context)!.connected; + connectionStatusDisconnecting = AppLocalizations.of(context)!.disconnecting; + connectionStatusConnecting = AppLocalizations.of(context)!.connecting; + if (!_initialized) { + _vpnState = VpnConnectionState.disconnected; + _initialized = true; + } + } + + String connectionTime = "00:00:00"; + Timer? _timer; late AnimationController _animationController; late Animation _sizeAnimation; @@ -59,6 +88,37 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { }[vpnState.connectionStatus]!; } + void startTimer() { + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + final now = DateTime.now(); + final duration = now.difference(_timer!.start); + setState(() { + connectionTime = duration.toString().substring(2, 7); + }); + }); + } + + void stopTimer() { + _timer?.cancel(); + setState(() { + connectionTime = "00:00:00"; + _vpnState = VpnConnectionState.disconnected; + }); + } + + String get currentStatusText { + switch (_vpnState) { + case VpnConnectionState.connected: + return connectionStatusConnected; + case VpnConnectionState.disconnected: + return connectionStatusDisconnected; + case VpnConnectionState.connecting: + return connectionStatusConnecting; + case VpnConnectionState.disconnecting: + return connectionStatusDisconnecting; + } + } + Future _toggleConnection(BuildContext context) async { final vpnState = Provider.of(context, listen: false); @@ -67,7 +127,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { vpnState.setConnectionStatus(ConnectionStatus.connecting); _animationController.repeat(reverse: true); String link = - "vless://c61daf3e-83ff-424f-a4ff-5bfcb46f0b30@45.77.190.146:8443?encryption=none&flow=&security=reality&sni=www.gstatic.com&fp=chrome&pbk=rLCmXWNVoRBiknloDUsbNS5ONjiI70v-BWQpWq0HCQ0&sid=108108108108#%F0%9F%87%BA%F0%9F%87%B8+%F0%9F%99%8F+USA+%231"; + "vless://c61daf3e-83ff-424f-a4ff-5bfcb46f0b30@5.35.98.91:8443?encryption=none&flow=&security=reality&sni=yandex.ru&fp=chrome&pbk=rLCmXWNVoRBiknloDUsbNS5ONjiI70v-BWQpWq0HCQ0&sid=108108108108#%F0%9F%87%B7%F0%9F%87%BA+%F0%9F%99%8F+Russia+%231"; V2RayURL parser = FlutterV2ray.parseFromURL(link); if (await flutterV2ray.requestPermission()) { @@ -108,7 +168,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { fontSize: 40, fontWeight: FontWeight.w600, color: - vpnState.connectionStatus == ConnectionStatus.connected + _vpnState == VpnConnectionState.connected ? Theme.of(context).colorScheme.primary : Theme.of(context).colorScheme.secondary, ), @@ -123,7 +183,10 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { width: 150, height: 150, decoration: BoxDecoration( - color: Colors.grey[300], + color: + Theme.of(context) + .colorScheme + .surfaceContainerHighest, // Usar cor do tema conforme sugestão do linter shape: BoxShape.circle, ), ), @@ -159,7 +222,7 @@ class MainBtnState extends State with SingleTickerProviderStateMixin { style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, - color: Colors.black, + color: Theme.of(context).textTheme.bodyLarge?.color, ), ), ], diff --git a/lib/pages/main/main_page.dart b/lib/pages/main/main_page.dart index 5148858..396c214 100644 --- a/lib/pages/main/main_page.dart +++ b/lib/pages/main/main_page.dart @@ -4,7 +4,7 @@ import 'dart:convert'; import 'package:vpn_client/pages/main/main_btn.dart'; import 'package:vpn_client/pages/main/location_widget.dart'; import 'package:vpn_client/pages/main/stat_bar.dart'; -import 'package:vpn_client/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; class MainPage extends StatefulWidget { const MainPage({super.key}); @@ -55,7 +55,7 @@ class MainPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text(LocalizationService.to('app_name')), + title: Text(AppLocalizations.of(context).appName), centerTitle: true, titleTextStyle: TextStyle( color: Theme.of(context).colorScheme.primary, diff --git a/lib/pages/main/stat_bar.dart b/lib/pages/main/stat_bar.dart index cdf53b8..ab669e3 100644 --- a/lib/pages/main/stat_bar.dart +++ b/lib/pages/main/stat_bar.dart @@ -28,49 +28,57 @@ class StatBarState extends State { Widget _buildStatItem(IconData icon, String text, BuildContext context) { return Container( - width: 100, - height: 75, + width: + (MediaQuery.of(context).size.width / 3) - 20, // Para dar algum espaço + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( + color: Theme.of(context).colorScheme.onSurface, + borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( - color: Color(0x1A9CB2C2), - offset: Offset(0.0, 1.0), - blurRadius: 32.0, + color: Theme.of( + context, + ).shadowColor.withAlpha((255 * 0.1).round()), // Usar cor do tema + offset: const Offset(0.0, 2.0), + blurRadius: 8.0, ), ], ), - child: FloatingActionButton( - elevation: elevation0, - onPressed: () {}, - backgroundColor: Theme.of(context).colorScheme.onSurface, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - alignment: Alignment.center, - width: 24, - height: 24, - decoration: BoxDecoration( - color: Theme.of(context).colorScheme.primary, - borderRadius: BorderRadius.circular(6.0), - ), - child: Icon( - icon, - size: 20, - color: Theme.of(context).colorScheme.onSurface, - ), + // Se precisar de ação de clique, envolva com InkWell ou GestureDetector + // InkWell( + // onTap: () {}, + // borderRadius: BorderRadius.circular(12), + // child: ... + // ) + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: const EdgeInsets.all( + 4, + ), // Espaçamento interno para o ícone + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary.withAlpha( + (255 * 0.1).round(), + ), // Cor de fundo suave + borderRadius: BorderRadius.circular(8.0), + ), + child: Icon( + icon, + size: 22, + color: Theme.of(context).colorScheme.primary, ), - const SizedBox(height: 6), - Text( - text, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context).colorScheme.primary, - ), + ), + const SizedBox(height: 8), + Text( + text, + style: TextStyle( + fontSize: fontSize14, // Usando constante de dimensions.dart + fontWeight: FontWeight.w500, + color: Theme.of(context).colorScheme.primary, ), - ], - ), + ), + ], ), ); } diff --git a/lib/pages/servers/servers_list.dart b/lib/pages/servers/servers_list.dart index c4343ce..1868347 100644 --- a/lib/pages/servers/servers_list.dart +++ b/lib/pages/servers/servers_list.dart @@ -1,16 +1,22 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; -import 'package:vpn_client/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; import 'dart:convert'; +import 'package:vpn_client/core/constants/storage_keys.dart'; class ServersList extends StatefulWidget { final Function(List>)? onServersLoaded; final List>? servers; + final Function(int)? + onItemTapNavigate; // Renomeado para clareza ou pode ser uma callback mais específica - const ServersList({super.key, this.onServersLoaded, this.servers}); - - get onNavBarTap => null; + const ServersList({ + super.key, + this.onServersLoaded, + this.servers, + this.onItemTapNavigate, + }); @override State createState() => ServersListState(); @@ -19,6 +25,7 @@ class ServersList extends StatefulWidget { class ServersListState extends State { List> _servers = []; bool _isLoading = true; + bool _dataLoaded = false; // Flag para controlar o carregamento inicial @override void initState() { @@ -26,17 +33,17 @@ class ServersListState extends State { if (widget.servers != null && widget.servers!.isNotEmpty) { _servers = widget.servers!; _isLoading = false; - if (widget.onServersLoaded != null) { - widget.onServersLoaded!(_servers); - } + _dataLoaded = + true; // Marcar como carregado se dados iniciais foram fornecidos + // widget.onServersLoaded é chamado em didUpdateWidget ou após _loadServers } } @override void didChangeDependencies() { super.didChangeDependencies(); - - if (_servers.isEmpty) { + if (!_dataLoaded) { + // Carregar apenas se os dados não foram carregados via widget.servers ou anteriormente _loadServers(); } } @@ -48,6 +55,7 @@ class ServersListState extends State { setState(() { _servers = widget.servers!; _isLoading = false; + _dataLoaded = true; }); _saveSelectedServers(); } @@ -55,47 +63,63 @@ class ServersListState extends State { Future _loadServers() async { setState(() { - _isLoading = true; + // Evitar mostrar loading se já estiver carregando ou já carregou + if (!_dataLoaded) _isLoading = true; }); + // Simulação de carregamento + await Future.delayed(const Duration(milliseconds: 100)); // Simular delay + try { + // Se os dados já foram carregados (ex: por uma busca anterior que atualizou widget.servers), não recarregar do zero + if (_dataLoaded && _servers.isNotEmpty) { + setState(() => _isLoading = false); + return; + } + + // É importante que AppLocalizations.of(context) seja chamado quando o context está pronto. + // didChangeDependencies é um bom lugar, ou aqui se garantirmos que o context está disponível. + // Adicionando verificação de 'mounted' para o BuildContext + if (!mounted) return; + final localizations = AppLocalizations.of(context)!; + List> serversList = [ { 'icon': 'assets/images/flags/auto.svg', - 'text': LocalizationService.to('auto_select'), - 'ping': LocalizationService.to('fastest'), + 'text': localizations.auto_select, + 'ping': localizations.fastest, 'isActive': true, }, { 'icon': 'assets/images/flags/Kazahstan.svg', - 'text': LocalizationService.to('kazakhstan'), + 'text': localizations.kazakhstan, 'ping': '48', 'isActive': false, }, { 'icon': 'assets/images/flags/Turkey.svg', - 'text': LocalizationService.to('turkey'), + 'text': localizations.turkey, 'ping': '142', 'isActive': false, }, { 'icon': 'assets/images/flags/Poland.svg', - 'text': LocalizationService.to('poland'), + 'text': localizations.poland, 'ping': '298', 'isActive': false, }, ]; final prefs = await SharedPreferences.getInstance(); - final String? savedServers = prefs.getString('selected_servers'); + final String? savedServers = prefs.getString(StorageKeys.selectedServers); if (savedServers != null) { final List savedServersList = jsonDecode(savedServers); - for (var savedApp in savedServersList) { + for (var savedServerItem in savedServersList) { final index = serversList.indexWhere( - (server) => server['text'] == savedApp['text'], + (server) => server['text'] == savedServerItem['text'], ); if (index != -1) { - serversList[index]['isActive'] = savedApp['isActive']; + serversList[index]['isActive'] = savedServerItem['isActive']; } } } @@ -103,6 +127,7 @@ class ServersListState extends State { setState(() { _servers = serversList; _isLoading = false; + _dataLoaded = true; // Marcar que os dados foram carregados }); if (widget.onServersLoaded != null) { @@ -111,6 +136,7 @@ class ServersListState extends State { } catch (e) { setState(() { _isLoading = false; + _dataLoaded = true; // Marcar como tentado carregar para evitar loop }); debugPrint('Error loading servers: $e'); } @@ -118,7 +144,7 @@ class ServersListState extends State { Future _saveSelectedServers() async { final prefs = await SharedPreferences.getInstance(); - final selectedServers = + final selectedServersData = _servers .map( (server) => { @@ -129,17 +155,19 @@ class ServersListState extends State { }, ) .toList(); - await prefs.setString('selected_servers', jsonEncode(selectedServers)); + await prefs.setString( + StorageKeys.selectedServers, + jsonEncode(selectedServersData), + ); } List> get servers => _servers; - void _onItemTapped(int index) { + void _onItemTapped(int indexInFullList) { setState(() { for (int i = 0; i < _servers.length; i++) { - _servers[i]['isActive'] = false; + _servers[i]['isActive'] = (i == indexInFullList); } - _servers[index]['isActive'] = true; }); _saveSelectedServers(); @@ -147,13 +175,24 @@ class ServersListState extends State { widget.onServersLoaded!(_servers); } - if (widget.onNavBarTap != null) { - widget.onNavBarTap!(2); + if (widget.onItemTapNavigate != null) { + widget.onItemTapNavigate!(indexInFullList); } } @override Widget build(BuildContext context) { + // Garante que as strings localizadas sejam usadas se _loadServers for chamado antes de didChangeDependencies + // ou se o widget for reconstruído. + if (_servers.isNotEmpty && AppLocalizations.of(context) != null) { + final localizations = AppLocalizations.of(context)!; + if (_servers[0]['text'] != localizations.auto_select) { + // Isso pode ser perigoso se a ordem dos servidores mudar. + // É melhor garantir que _loadServers seja chamado com o contexto correto. + // Para simplificar, vamos assumir que _loadServers já lidou com isso. + } + } + final activeServers = _servers.where((server) => server['isActive'] == true).toList(); final inactiveServers = @@ -176,14 +215,20 @@ class ServersListState extends State { children: [ if (activeServers.isNotEmpty) ...[ Container( - margin: const EdgeInsets.only(left: 10), + margin: const EdgeInsets.only( + left: 10, + top: 10, + bottom: 5, + ), // Adicionado espaçamento child: Text( - LocalizationService.to('selected_server'), - style: TextStyle(color: Colors.grey), + AppLocalizations.of(context)!.selected_server, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 14, // Consistência de tamanho + ), ), ), - ...List.generate(activeServers.length, (index) { - final server = activeServers[index]; + ...activeServers.map((server) { return ServerListItem( icon: server['icon'], text: server['text'], @@ -195,14 +240,20 @@ class ServersListState extends State { ], if (inactiveServers.isNotEmpty) ...[ Container( - margin: const EdgeInsets.only(left: 10), + margin: const EdgeInsets.only( + left: 10, + top: 15, + bottom: 5, + ), // Adicionado espaçamento child: Text( - LocalizationService.to('all_servers'), - style: TextStyle(color: Colors.grey), + AppLocalizations.of(context)!.all_servers, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + fontSize: 14, // Consistência de tamanho + ), ), ), - ...List.generate(inactiveServers.length, (index) { - final server = inactiveServers[index]; + ...inactiveServers.map((server) { return ServerListItem( icon: server['icon'], text: server['text'], diff --git a/lib/pages/servers/servers_list_item.dart b/lib/pages/servers/servers_list_item.dart index 19825cd..031cbd6 100644 --- a/lib/pages/servers/servers_list_item.dart +++ b/lib/pages/servers/servers_list_item.dart @@ -37,11 +37,14 @@ class ServerListItem extends StatelessWidget { height: 52, margin: const EdgeInsets.symmetric(vertical: 8), decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).colorScheme.onSurface, // Usar cor do tema borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( + color: Colors.grey.withAlpha(51), + + blurRadius: 10, offset: const Offset(0, 1), ), @@ -63,6 +66,7 @@ class ServerListItem extends StatelessWidget { Container( alignment: Alignment.center, height: 52, + child: Flexible( // Let text flexibly use remaining space child: Text( diff --git a/lib/pages/servers/servers_page.dart b/lib/pages/servers/servers_page.dart index dda5741..95c6ef4 100644 --- a/lib/pages/servers/servers_page.dart +++ b/lib/pages/servers/servers_page.dart @@ -4,7 +4,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/servers/servers_list.dart'; import 'package:vpn_client/search_dialog.dart'; -import 'package:vpn_client/localization_service.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; class ServersPage extends StatefulWidget { final Function(int) onNavBarTap; @@ -80,6 +80,10 @@ class ServersPageState extends State { }); }, servers: _servers, + onItemTapNavigate: (selectedIndex) { + // Passando a callback + widget.onNavBarTap(2); // Navega para a página principal (índice 2) + }, ), ); } diff --git a/lib/pages/settings/settings_page.dart b/lib/pages/settings/settings_page.dart new file mode 100644 index 0000000..788eb52 --- /dev/null +++ b/lib/pages/settings/settings_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Settings Page')); + } +} \ No newline at end of file diff --git a/lib/pages/speed/speed_page.dart b/lib/pages/speed/speed_page.dart new file mode 100644 index 0000000..b3be8a8 --- /dev/null +++ b/lib/pages/speed/speed_page.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class SpeedPage extends StatelessWidget { + const SpeedPage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return const Center(child: Text('Speed Page')); + } +} \ No newline at end of file diff --git a/lib/providers/vpn_provider.dart b/lib/providers/vpn_provider.dart new file mode 100644 index 0000000..22ba54a --- /dev/null +++ b/lib/providers/vpn_provider.dart @@ -0,0 +1,190 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'dart:async'; +import 'dart:developer'; + +import 'package:flutter_v2ray/flutter_v2ray.dart'; +import 'package:vpnclient_engine_flutter/vpnclient_engine_flutter.dart'; + +// This is a change to test diff +class VPNProvider extends ChangeNotifier { + bool _isConnected = false; + bool get isConnected => _isConnected; + String _connectionStatus = 'Disconnected'; + String get connectionStatus => _connectionStatus; + String _connectionTime = "00:00:00"; + String get connectionTime => _connectionTime; + + Map? _selectedServer; + + Map? get selectedServer => _selectedServer; + List> _servers = []; + List> get servers => _servers; + Timer? _timer; + + final FlutterV2ray flutterV2ray = FlutterV2ray( + onStatusChanged: (status) { + // do something + }, + ); + + VPNProvider() { + _loadSelectedServer(); + } + + void connect() async{ + _connectionStatus = 'Connecting'; + notifyListeners(); + //_animationController.repeat(reverse: true); + + VPNclientEngine.ClearSubscriptions(); + VPNclientEngine.addSubscription(subscriptionURL: "https://pastebin.com/raw/ZCYiJ98W"); + await VPNclientEngine.updateSubscription(subscriptionIndex: 0); + + await flutterV2ray.initializeV2Ray(); + + + + // v2ray share link like vmess://, vless://, ... + String link = "vless://c61daf3e-83ff-424f-a4ff-5bfcb46f0b30@5.35.98.91:8443?encryption=none&flow=&security=reality&sni=yandex.ru&fp=chrome&pbk=rLCmXWNVoRBiknloDUsbNS5ONjiI70v-BWQpWq0HCQ0&sid=108108108108#%F0%9F%87%B7%F0%9F%87%BA+%F0%9F%99%8F+Russia+%231"; + V2RayURL parser = FlutterV2ray.parseFromURL(link); + + + // Get Server Delay + log('${flutterV2ray.getServerDelay(config: parser.getFullConfiguration())}ms'); + + // Permission is not required if you using proxy only + if (await flutterV2ray.requestPermission()){ + flutterV2ray.startV2Ray( + remark: parser.remark, + // The use of parser.getFullConfiguration() is not mandatory, + // and you can enter the desired V2Ray configuration in JSON format + config: parser.getFullConfiguration(), + blockedApps: null, + bypassSubnets: null, + proxyOnly: false, + ); + } + +// Disconnect +///flutterV2ray.stopV2Ray(); + +VPNclientEngine.pingServer(subscriptionIndex: 0, index: 1); + VPNclientEngine.onPingResult.listen((result) { + log("Ping result: ${result.latencyInMs} ms"); + }); + + + ///final result = await platform.invokeMethod('startVPN'); + + await VPNclientEngine.connect(subscriptionIndex: 0, serverIndex: 1); + _isConnected = true; + _connectionStatus = 'Connected'; + startTimer(); + notifyListeners(); + // _animationController.stop(); + } + + void disconnect() async{ + _connectionStatus = 'Disconnecting'; + notifyListeners(); + stopTimer(); + await VPNclientEngine.disconnect(); + _isConnected = false; + _connectionStatus = 'Disconnected'; + notifyListeners(); + // _animationController.reverse(); + //_animationController.stop(); + } + + Future _loadSelectedServer() async { + final prefs = await SharedPreferences.getInstance(); + final String? savedServer = prefs.getString('selectedServer'); + if (savedServer != null) { + _selectedServer = Map.from(jsonDecode(savedServer)); + } else { + _selectedServer = null; + } + notifyListeners(); + } + + Future selectServer(Map server) async { + final prefs = await SharedPreferences.getInstance(); + _selectedServer = server; + await prefs.setString('selectedServer', jsonEncode(server)); + notifyListeners(); + } + +void startTimer() { + int seconds = 1; + _timer = Timer.periodic(const Duration(seconds: 1), (timer) { + int hours = seconds ~/ 3600; + int minutes = (seconds % 3600) ~/ 60; + int remainingSeconds = seconds % 60; + _connectionTime = + '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${remainingSeconds.toString().padLeft(2, '0')}'; + notifyListeners(); + + seconds++; + }); + } + + void stopTimer() { + _timer?.cancel(); + _connectionTime = "00:00:00"; + notifyListeners(); + } + +Future _loadServers() async { + + try { + List> serversList = [ + { + 'icon': 'assets/images/flags/auto.svg', + 'text': 'Автовыбор', + 'ping': 'Самый быстрый', + 'isActive': true, + }, + { + 'icon': 'assets/images/flags/Kazahstan.svg', + 'text': 'Казахстан', + 'ping': '48', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Turkey.svg', + 'text': 'Турция', + 'ping': '142', + 'isActive': false, + }, + { + 'icon': 'assets/images/flags/Poland.svg', + 'text': 'Польша', + 'ping': '298', + 'isActive': false, + }, + ]; + + + _servers = serversList; + notifyListeners(); + } catch (e) { + debugPrint('Error loading servers: $e'); + } + } + void _updateServers(Map server) { + for (int i = 0; i < _servers.length; i++) { + _servers[i]['isActive'] = false; + } + + final index = _servers.indexWhere( + (element) => element['text'] == server['text'], + ); + if (index != -1) { + _servers[index]['isActive'] = true; + } + notifyListeners(); + } + +} \ No newline at end of file diff --git a/lib/search_dialog.dart b/lib/search_dialog.dart index 9890d0e..aa4a9d0 100644 --- a/lib/search_dialog.dart +++ b/lib/search_dialog.dart @@ -2,6 +2,8 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:vpn_client/pages/apps/apps_list_item.dart'; import 'package:vpn_client/pages/servers/servers_list_item.dart'; +import 'package:vpn_client/l10n/app_localizations.dart'; +import 'package:vpn_client/core/constants/storage_keys.dart'; import 'package:vpn_client/localization_service.dart'; import 'dart:convert'; @@ -23,31 +25,52 @@ class SearchDialog extends StatefulWidget { class _SearchDialogState extends State { final TextEditingController _searchController = TextEditingController(); - late List> _filteredItems; + List> _filteredItems = []; List> _recentlySearchedItems = []; late int _searchDialogType; + String? _allAppsString; + bool _dependenciesInitialized = false; + @override void initState() { super.initState(); _searchDialogType = widget.type; + _searchController.addListener(_filterItems); _loadRecentlySearched(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_dependenciesInitialized) { + _allAppsString = AppLocalizations.of(context)!.all_apps; + _initializeFilteredItems(); + _dependenciesInitialized = true; + } + } + + void _initializeFilteredItems() { _filteredItems = widget.items.where((item) { - if (_searchDialogType == 1) { - return item['text'] != 'Все приложения'; + if (_searchDialogType == 1 && _allAppsString != null) { + return item['text'] != _allAppsString; } return true; }).toList(); - _searchController.addListener(_filterItems); + if (_searchController.text.isNotEmpty) { + _filterItems(); + } else { + setState(() {}); + } } Future _loadRecentlySearched() async { final prefs = await SharedPreferences.getInstance(); final String key = _searchDialogType == 1 - ? 'recently_searched_apps' - : 'recently_searched_servers'; + ? StorageKeys.recentlySearchedApps + : StorageKeys.recentlySearchedServers; final String? recentlySearched = prefs.getString(key); if (recentlySearched != null) { setState(() { @@ -58,12 +81,12 @@ class _SearchDialogState extends State { } } - Future _saveRecentlySearched(Map item) async { + Future _addOrUpdateRecentlySearched(Map item) async { final prefs = await SharedPreferences.getInstance(); final String key = _searchDialogType == 1 - ? 'recently_searched_apps' - : 'recently_searched_servers'; + ? StorageKeys.recentlySearchedApps + : StorageKeys.recentlySearchedServers; setState(() { _recentlySearchedItems.removeWhere((i) => i['text'] == item['text']); _recentlySearchedItems.insert(0, item); @@ -79,20 +102,21 @@ class _SearchDialogState extends State { setState(() { _filteredItems = widget.items.where((item) { - if (_searchDialogType == 1) { + if (_searchDialogType == 1 && _allAppsString != null) { return item['text'].toLowerCase().contains(query) && - item['text'] != 'Все приложения'; + item['text'] != _allAppsString; } return item['text'].toLowerCase().contains(query); }).toList(); }); } - void _updateServerSelection(Map selectedItem) { - // Обновляем isActive для всех элементов: выбранный становится активным, остальные — неактивными + void _handleServerSelection(Map selectedItem) { for (var item in widget.items) { item['isActive'] = item['text'] == selectedItem['text']; } + _addOrUpdateRecentlySearched(selectedItem); + Navigator.of(context).pop(widget.items); } @override @@ -104,11 +128,6 @@ class _SearchDialogState extends State { @override Widget build(BuildContext context) { final isQueryEmpty = _searchController.text.isEmpty; - final hasRecentSearches = _recentlySearchedItems.isNotEmpty; - - final showFilteredItems = - !isQueryEmpty || (isQueryEmpty && !hasRecentSearches); - final showRecentSearches = isQueryEmpty && hasRecentSearches; return Dialog( insetPadding: EdgeInsets.zero, @@ -133,20 +152,18 @@ class _SearchDialogState extends State { child: Stack( alignment: Alignment.center, children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - LocalizationService.to('search'), - style: TextStyle( - fontSize: 24, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.primary, - ), + Center( + child: Text( + AppLocalizations.of(context)!.search, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.primary, ), - ], + ), ), if (_searchDialogType == 1) + Row( mainAxisAlignment: MainAxisAlignment.end, children: [ @@ -161,11 +178,13 @@ class _SearchDialogState extends State { color: Colors.orange, fontSize: 16, ), + ), ), - ], + ), ), if (_searchDialogType == 2) + Row( mainAxisAlignment: MainAxisAlignment.start, children: [ @@ -180,20 +199,24 @@ class _SearchDialogState extends State { color: Colors.orange, fontSize: 16, ), + + ), ), - ], + ), ), ], ), ), Container( decoration: BoxDecoration( - color: Colors.white, + color: Theme.of(context).cardColor, borderRadius: BorderRadius.circular(10), boxShadow: [ BoxShadow( - color: Colors.grey.withAlpha((255 * 0.2).toInt()), + color: Theme.of( + context, + ).shadowColor.withAlpha((255 * 0.1).round()), blurRadius: 10, offset: const Offset(0, 1), ), @@ -207,32 +230,36 @@ class _SearchDialogState extends State { controller: _searchController, decoration: InputDecoration( hintText: widget.placeholder, - hintStyle: const TextStyle(color: Colors.grey), + hintStyle: TextStyle(color: Theme.of(context).hintColor), suffixIcon: Icon( Icons.search, color: Theme.of(context).colorScheme.primary, ), - fillColor: Colors.white, + fillColor: Theme.of(context).cardColor, filled: true, border: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( - color: Colors.grey.shade300, - width: 0, + color: Theme.of( + context, + ).dividerColor.withAlpha((255 * 0.5).round()), + width: 0.5, ), ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( - color: Colors.grey.shade300, - width: 0, + color: Theme.of( + context, + ).dividerColor.withAlpha((255 * 0.5).round()), + width: 0.5, ), ), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: BorderSide( - color: Colors.grey.shade300, - width: 0, + color: Theme.of(context).colorScheme.primary, + width: 1, ), ), contentPadding: const EdgeInsets.all(14), @@ -240,78 +267,89 @@ class _SearchDialogState extends State { ), ), const SizedBox(height: 7), - // Отображаем недавно измененные элементы - if (showRecentSearches) - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - margin: const EdgeInsets.only(left: 20), - child: Text( - LocalizationService.to('recently_searched'), - style: TextStyle(color: Colors.grey), - ), - ), - Container( - margin: const EdgeInsets.symmetric(horizontal: 14), - child: Column( - children: List.generate(_recentlySearchedItems.length, ( - index, - ) { - final item = _recentlySearchedItems[index]; - if (_searchDialogType == 1) { - return AppListItem( - icon: item['icon'], - image: item['image'], - text: item['text'], - isSwitch: item['isSwitch'] ?? false, - isActive: item['isActive'] ?? false, - isEnabled: true, - onTap: () { - setState(() { - _recentlySearchedItems[index]['isActive'] = - !_recentlySearchedItems[index]['isActive']; - }); - final originalIndex = widget.items.indexWhere( - (i) => i['text'] == item['text'], + if (isQueryEmpty && _recentlySearchedItems.isNotEmpty) + Flexible( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: + MainAxisSize.min, // Ensure Column takes minimum space + children: [ + Container( + margin: const EdgeInsets.only( + left: 20, + bottom: 4, + top: 4, + ), // Adjusted margin + child: Text( + AppLocalizations.of(context)!.recently_searched, + style: TextStyle( + color: Theme.of(context).colorScheme.secondary, + ), + ), + ), + Container( + margin: const EdgeInsets.symmetric(horizontal: 14), + child: Column( + mainAxisSize: + MainAxisSize + .min, // Ensure Column takes minimum space + children: List.generate(_recentlySearchedItems.length, ( + index, + ) { + final item = _recentlySearchedItems[index]; + if (_searchDialogType == 1) { + return AppListItem( + icon: item['icon'], + image: item['image'], + text: item['text'], + isSwitch: item['isSwitch'] ?? false, + isActive: item['isActive'] ?? false, + isEnabled: true, + onTap: () { + setState(() { + _recentlySearchedItems[index]['isActive'] = + !_recentlySearchedItems[index]['isActive']; + }); + final originalIndex = widget.items + .indexWhere( + (i) => i['text'] == item['text'], + ); + if (originalIndex != -1) { + widget.items[originalIndex]['isActive'] = + _recentlySearchedItems[index]['isActive']; + } + _addOrUpdateRecentlySearched( + _recentlySearchedItems[index], + ); + }, ); - if (originalIndex != -1) { - widget.items[originalIndex]['isActive'] = - _recentlySearchedItems[index]['isActive']; - } - _saveRecentlySearched( - _recentlySearchedItems[index], + } else { + return ServerListItem( + icon: item['icon'], + text: item['text'], + ping: item['ping'], + isActive: item['isActive'] ?? false, + onTap: () { + _handleServerSelection(item); + }, ); - }, - ); - } else { - return ServerListItem( - icon: item['icon'], - text: item['text'], - ping: item['ping'], - isActive: item['isActive'] ?? false, - onTap: () { - if (_searchController.text.isNotEmpty) { - _saveRecentlySearched(item); - } - _updateServerSelection(item); - Navigator.of(context).pop(widget.items); - }, - ); - } - }), - ), + } + }), + ), + ), + ], ), - ], + ), ), - // Отображаем отфильтрованный список Expanded( child: - showFilteredItems + (!isQueryEmpty || + (isQueryEmpty && _recentlySearchedItems.isEmpty)) ? _filteredItems.isEmpty ? Center( child: Text( - LocalizationService.to('nothing_found'), + AppLocalizations.of(context)!.nothing_found, style: TextStyle( color: Theme.of(context).colorScheme.primary, ), @@ -337,7 +375,7 @@ class _SearchDialogState extends State { _filteredItems[index]['isActive'] = !_filteredItems[index]['isActive']; if (_searchController.text.isNotEmpty) { - _saveRecentlySearched( + _addOrUpdateRecentlySearched( _filteredItems[index], ); } @@ -359,11 +397,7 @@ class _SearchDialogState extends State { ping: item['ping'], isActive: item['isActive'] ?? false, onTap: () { - if (_searchController.text.isNotEmpty) { - _saveRecentlySearched(item); - } - _updateServerSelection(item); - Navigator.of(context).pop(widget.items); + _handleServerSelection(item); }, ); } @@ -371,17 +405,7 @@ class _SearchDialogState extends State { ) : const SizedBox.shrink(), ), - Transform.scale( - scale: 1.2, - child: Transform.translate( - offset: const Offset(0, 30), - child: Container( - width: MediaQuery.of(context).size.width, - height: 40, - color: Theme.of(context).colorScheme.surface, - ), - ), - ), + SizedBox(height: MediaQuery.of(context).padding.bottom + 10), ], ), ), diff --git a/lib/vpn_state.dart b/lib/vpn_state.dart index 0c1efe0..b8e5945 100644 --- a/lib/vpn_state.dart +++ b/lib/vpn_state.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter_v2ray/flutter_v2ray.dart'; +// import 'package:flutter_v2ray/flutter_v2ray.dart'; enum ConnectionStatus { disconnected, @@ -20,7 +20,7 @@ class VpnState with ChangeNotifier { VpnState() { // Initializing V2Ray when creating a provider - FlutterV2ray(onStatusChanged: (status) {}).initializeV2Ray(); + // FlutterV2ray(onStatusChanged: (status) {}).initializeV2Ray(); } void setConnectionStatus(ConnectionStatus status) { diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index f6f23bf..e859d60 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); + g_autoptr(FlPluginRegistrar) vpnclient_engine_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "VpnclientEngineFlutterPlugin"); + vpnclient_engine_flutter_plugin_register_with_registrar(vpnclient_engine_flutter_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index f16b4c3..a8cec98 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_linux + vpnclient_engine_flutter ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 997e35d..4f9ea4c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,8 +7,10 @@ import Foundation import shared_preferences_foundation import url_launcher_macos +import vpnclient_engine_flutter func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + VpnclientEngineFlutterPlugin.register(with: registry.registrar(forPlugin: "VpnclientEngineFlutterPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 6d2be2c..abad2fe 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,26 +3,20 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - vpnclient_engine_flutter (0.0.1): - - FlutterMacOS DEPENDENCIES: - FlutterMacOS (from `Flutter/ephemeral`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - vpnclient_engine_flutter (from `Flutter/ephemeral/.symlinks/plugins/vpnclient_engine_flutter/macos`) EXTERNAL SOURCES: FlutterMacOS: :path: Flutter/ephemeral shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - vpnclient_engine_flutter: - :path: Flutter/ephemeral/.symlinks/plugins/vpnclient_engine_flutter/macos SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - vpnclient_engine_flutter: d018814c86b1c8e99306e2bb838df8dea9fb0971 + shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 PODFILE CHECKSUM: 7eb978b976557c8c1cd717d8185ec483fd090a82 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f726854..bc12add 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -389,14 +389,10 @@ inputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - ); name = "[CP] Embed Pods Frameworks"; outputFileListPaths = ( "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); - outputPaths = ( - ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; diff --git a/pubspec.lock b/pubspec.lock index d3143e9..b0e39b5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -97,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + color: + dependency: transitive + description: + name: color + sha256: ddcdf1b3badd7008233f5acffaf20ca9f5dc2cd0172b75f68f24526a5f5725cb + url: "https://pub.dev" + source: hosted + version: "3.0.0" convert: dependency: transitive description: @@ -137,6 +145,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.19.2" + dart_ping: + dependency: transitive + description: + name: dart_ping + sha256: "2f5418d0a5c64e53486caaac78677b25725b1e13c33c5be834ce874ea18bd24f" + url: "https://pub.dev" + source: hosted + version: "9.0.1" dart_style: dependency: transitive description: @@ -145,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.4" + dartx: + dependency: transitive + description: + name: dartx + sha256: "8b25435617027257d43e6508b5fe061012880ddfdaa75a71d607c3de2a13d244" + url: "https://pub.dev" + source: hosted + version: "1.2.0" fake_async: dependency: transitive description: @@ -182,6 +206,22 @@ packages: url: "https://pub.dev" source: hosted version: "9.1.1" + flutter_gen: + dependency: "direct dev" + description: + name: flutter_gen + sha256: a727fbe4d9443ac05258ef7a987650f8d8f16b4f8c22cf98c1ac9183ac7f3eff + url: "https://pub.dev" + source: hosted + version: "5.9.0" + flutter_gen_core: + dependency: transitive + description: + name: flutter_gen_core + sha256: "53890b653738f34363d9f0d40f82104c261716bd551d3ba65f648770b6764c21" + url: "https://pub.dev" + source: hosted + version: "5.9.0" flutter_lints: dependency: "direct dev" description: @@ -207,23 +247,15 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 + sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845 url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" - flutter_v2ray: - dependency: "direct main" - description: - name: flutter_v2ray - sha256: "09dce3b4b58ea6a4220409d948a4d92a6eb3416184e6512ec0f5773fd48e5ab0" - url: "https://pub.dev" - source: hosted - version: "1.0.10" flutter_web_plugins: dependency: transitive description: flutter @@ -237,6 +269,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + hashcodes: + dependency: transitive + description: + name: hashcodes + sha256: "80f9410a5b3c8e110c4b7604546034749259f5d6dcca63e0d3c17c9258f1a651" + url: "https://pub.dev" + source: hosted + version: "2.0.0" html: dependency: transitive description: @@ -249,10 +289,10 @@ packages: dependency: transitive description: name: http - sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" http_parser: dependency: transitive description: @@ -269,6 +309,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.4" + image_size_getter: + dependency: transitive + description: + name: image_size_getter + sha256: "9a299e3af2ebbcfd1baf21456c3c884037ff524316c97d8e56035ea8fdf35653" + url: "https://pub.dev" + source: hosted + version: "2.4.0" intl: dependency: transitive description: @@ -277,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.20.2" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -333,6 +389,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.16.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" nested: dependency: transitive description: @@ -417,10 +481,10 @@ packages: dependency: transitive description: name: posix - sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 + sha256: "6323a5b0fa688b6a010df4905a56b00181479e6d10534cecfecede2aa55add61" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.0.3" provider: dependency: "direct main" description: @@ -546,6 +610,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.4" + time: + dependency: transitive + description: + name: time + sha256: "370572cf5d1e58adcb3e354c47515da3f7469dac3a95b447117e728e7be6f461" + url: "https://pub.dev" + source: hosted + version: "2.1.5" typed_data: dependency: transitive description: @@ -566,10 +638,10 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" + sha256: f6a7e5c4835bb4e3026a04793a4199ca2d14c739ec378fdfe23fc8075d0439f8 url: "https://pub.dev" source: hosted - version: "6.3.1" + version: "6.3.2" url_launcher_android: dependency: transitive description: @@ -630,10 +702,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -646,10 +718,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" + sha256: "557a315b7d2a6dbb0aaaff84d857967ce6bdc96a63dc6ee2a57ce5a6ee5d3331" url: "https://pub.dev" source: hosted - version: "1.1.16" + version: "1.1.17" vector_math: dependency: transitive description: @@ -666,14 +738,23 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.0" + vpnclient_engine_flutter: + dependency: "direct main" + description: + path: "." + ref: c3bf79010c05a2474a24f763d428a61788a13e9b + resolved-ref: c3bf79010c05a2474a24f763d428a61788a13e9b + url: "https://github.com/VPNclient/VPNclient-engine-flutter.git" + source: git + version: "0.0.1" watcher: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" web: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 320fca9..18ccdec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,33 +1,12 @@ name: vpn_client description: "A new Flutter project." -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev - -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. +publish_to: 'none' version: 1.0.12+12 environment: sdk: ^3.7.2 flutter: 3.29.3 -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: flutter: sdk: flutter @@ -35,49 +14,32 @@ dependencies: flutter_svg: ^2.0.17 flutter_localizations: sdk: flutter - shared_preferences: ^2.2.3 - flutter_native_splash: ^2.3.1 + shared_preferences: ^2.5.3 + flutter_native_splash: ^2.4.6 flutter_bloc: ^9.0.0 -# vpnclient_engine_flutter: -# git: -# url: https://github.com/VPNclient/VPNclient-engine-flutter.git -# ref: 3cc3deea31667c17416ae85b219e65694a2de5f2 - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - flutter_v2ray: ^1.0.10 + vpnclient_engine_flutter: + git: + url: https://github.com/VPNclient/VPNclient-engine-flutter.git + ref: c3bf79010c05a2474a24f763d428a61788a13e9b cupertino_icons: ^1.0.8 url_launcher: ^6.3.1 dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. flutter_lints: ^5.0.0 dart_code_metrics: ^4.19.2 + flutter_gen: ^5.3.0 -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - + generate: true # ✅ ESSENCIAL para gerar flutter_gen и l10n корректно fonts: - family: CustomIcons fonts: - asset: assets/fonts/CustomIcons.ttf - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: assets: - assets/images/ - assets/images/flags/ @@ -86,38 +48,13 @@ flutter: - assets/lang/ru.json - assets/lang/th.json - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/to/resolution-aware-images - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/to/asset-from-package - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/to/font-from-package - - generate: false - - - -# l10n: -# arb-dir: l10n -# template-arb-file: app_en.arb -# output-localization-file: app_localizations.dart -# untranslated-messages-file: lib/l10n/untranslated_messages.txt +flutter_gen: + output: lib/gen/ # ✅ Define где генерировать файлы + line_length: 80 +l10n: + arb-dir: lib/l10n + template-arb-file: app_en.arb + output-localization-file: app_localizations.dart + output-class: AppLocalizations + output-dir: lib/l10n + untranslated-messages-file: lib/l10n/untranslated_messages.txt diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4f78848..b6a11cf 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -7,8 +7,11 @@ #include "generated_plugin_registrant.h" #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); + VpnclientEngineFlutterPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("VpnclientEngineFlutterPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 88b22e5..efbd18d 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST url_launcher_windows + vpnclient_engine_flutter ) list(APPEND FLUTTER_FFI_PLUGIN_LIST